diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8a3043b1..2587ab96 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -215,6 +215,10 @@ "message": "Use default", "description": "'Set to default' button in a confirm dialog" }, + "confirmDiscardChanges": { + "message": "Discard the changes?", + "description": "Generic label or title displayed when trying to close something (not a style) with unsaved changes" + }, "confirmSave": { "message": "Save", "description": "'Save' button in a confirm dialog" diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 7eacbf8b..7c00b4c0 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -1,3 +1,6 @@ +.CodeMirror-hints { + z-index: 999; +} .CodeMirror-hint:hover { color: white; background: #08f; diff --git a/edit/edit.js b/edit/edit.js index e2d7bd24..4f3dc0b4 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1718,6 +1718,7 @@ function fromMozillaFormat() { popup.codebox.focus(); popup.codebox.on('changes', cm => { popup.classList.toggle('ready', !cm.isBlank()); + cm.markClean(); }); // overwrite default extraKeys as those are inapplicable in popup context popup.codebox.options.extraKeys = { @@ -1885,15 +1886,18 @@ function showKeyMapHelp() { } } -function showHelp(title, body) { +function showHelp(title = '', body) { const div = $('#help-popup'); div.classList.remove('big'); - $('.contents', div).textContent = ''; - $('.contents', div).appendChild(typeof body === 'string' ? tHTML(body) : body); + const contents = $('.contents', div); + contents.textContent = ''; + if (body) { + contents.appendChild(typeof body === 'string' ? tHTML(body) : body); + } $('.title', div).textContent = title; if (getComputedStyle(div).display === 'none') { - document.addEventListener('keydown', closeHelp); + window.addEventListener('keydown', closeHelp, true); // avoid chaining on multiple showHelp() calls $('.dismiss', div).onclick = closeHelp; } @@ -1902,16 +1906,19 @@ function showHelp(title, body) { return div; function closeHelp(e) { - if ( - !e || - e.type === 'click' || - ((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) - ) { + if (!e || e.type === 'click' || + (e.which === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && + !$('.CodeMirror-hints, #message-box') && !(document.activeElement instanceof HTMLInputElement))) { + if (e && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) { + messageBox.confirm(t('confirmDiscardChanges')).then(ok => ok && closeHelp()); + return; + } div.style.display = ''; - const contents = $('.contents'); contents.textContent = ''; clearTimeout(contents.timer); - document.removeEventListener('keydown', closeHelp); + window.removeEventListener('keydown', closeHelp, true); + window.dispatchEvent(new Event('closeHelp')); + (editors.lastActive || editors[0]).focus(); } } } diff --git a/edit/lint-codemirror-helper.js b/edit/lint-codemirror-helper.js index be0a61ee..04f16450 100644 --- a/edit/lint-codemirror-helper.js +++ b/edit/lint-codemirror-helper.js @@ -1,38 +1,26 @@ -/* global CodeMirror CSSLint parserlib stylelint linterConfig */ +/* global CodeMirror linterConfig */ 'use strict'; -CodeMirror.registerHelper('lint', 'csslint', code => new Promise(resolve => { - CSSLint.onmessage = ({data}) => { - resolve( - data.map(({line, col, message, rule, type}) => line && { - message, - from: {line: line - 1, ch: col - 1}, - to: {line: line - 1, ch: col}, - rule: rule.id, - severity: type - }).filter(Boolean)); - }; - const config = deepCopy(linterConfig.getCurrent('csslint')); - CSSLint.postMessage({action: 'verify', code, config}); -})); +CodeMirror.registerHelper('lint', 'csslint', code => + linterConfig.invokeWorker({code, config: linterConfig.getCurrent()}).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))); CodeMirror.registerHelper('lint', 'stylelint', code => - stylelint.lint({ - code, - config: deepCopy(linterConfig.getCurrent('stylelint')), - }).then(({results}) => { - if (!results[0]) { - return []; - } - return results[0].warnings.map(warning => ({ - from: CodeMirror.Pos(warning.line - 1, warning.column - 1), - to: CodeMirror.Pos(warning.line - 1, warning.column), - message: warning.text + linterConfig.invokeWorker({code, config: linterConfig.getCurrent()}).then(({results}) => + !results[0] && [] || + 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: warning.text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'), - severity : warning.severity - })); - }) -); + rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'), + severity, + })))); diff --git a/edit/lint.js b/edit/lint.js index f3039c00..fb90634c 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -1,5 +1,5 @@ /* global CodeMirror messageBox */ -/* global editors makeSectionVisible showCodeMirrorPopup showHelp */ +/* global editors makeSectionVisible showCodeMirrorPopup showHelp hotkeyRerouter */ /* global loadScript require CSSLint stylelint */ /* global makeLink */ 'use strict'; @@ -20,8 +20,16 @@ var linterConfig = { csslint: 'editorCSSLintConfig', stylelint: 'editorStylelintConfig', }, + worker: { + csslint: {path: '/vendor-overwrites/csslint/csslint-worker.js'}, + stylelint: {path: '/vendor-overwrites/stylelint/stylelint-bundle.min.js'}, + }, + allRuleIds: { + csslint: null, + stylelint: null, + }, - getDefault() { + getName() { // 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') { @@ -30,11 +38,11 @@ var linterConfig = { return linter; }, - getCurrent(linter = linterConfig.getDefault()) { + getCurrent(linter = linterConfig.getName()) { return this.fallbackToDefaults(this[linter] || {}); }, - getForCodeMirror(linter = linterConfig.getDefault()) { + getForCodeMirror(linter = linterConfig.getName()) { return CodeMirror.lint && CodeMirror.lint[linter] ? { getAnnotations: CodeMirror.lint[linter], delay: prefs.get('editor.lintDelay'), @@ -44,7 +52,7 @@ var linterConfig = { } : false; }, - fallbackToDefaults(config, linter = linterConfig.getDefault()) { + 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 @@ -56,33 +64,52 @@ var linterConfig = { } }, - setLinter(linter = linterConfig.getDefault()) { + setLinter(linter = linterConfig.getName()) { linter = linter.toLowerCase(); linter = linter === 'csslint' || linter === 'stylelint' ? linter : ''; - if (linterConfig.getDefault() !== linter) { + if (linterConfig.getName() !== linter) { prefs.set('editor.linter', linter); } return linter; }, - findInvalidRules(config, linter = linterConfig.getDefault()) { - const rules = linter === 'stylelint' ? config.rules : config; + 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 => { - if (linter === 'stylelint') { - resolve(Object.keys(stylelint.rules)); - } else { - CSSLint.onmessage = ({data}) => - resolve(data.map(rule => rule.id)); - CSSLint.postMessage({action: 'getRules'}); + worker.queue.push({message, resolve}); + if (worker.queue.length === 1) { + worker.instance.postMessage(message); } - }).then(allRules => { - allRules = new Set(allRules); - return Object.keys(rules).filter(rule => !allRules.has(rule)); + }); + }, + + 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.getDefault() === 'stylelint') { + if (linterConfig.getName() === 'stylelint') { config.syntax = undefined; } return JSON.stringify(config, null, 2) @@ -91,7 +118,7 @@ var linterConfig = { save(config) { config = this.fallbackToDefaults(config); - const linter = linterConfig.getDefault(); + const linter = linterConfig.getName(); this[linter] = config; BG.chromeSync.setLZValue(this.storageName[linter], config); return config; @@ -155,7 +182,7 @@ function initLint() { prefs.subscribe(['editor.linter'], updateLinter); } -function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) { +function updateLinter({immediately, linter = linterConfig.getName()} = {}) { if (!immediately) { debounce(updateLinter, 0, {immediately: true, linter}); return; @@ -358,17 +385,16 @@ function gotoLintIssue(event) { } function showLintHelp() { - const linter = linterConfig.getDefault(); + 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; 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); + const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID); return rule && $element({tag: 'li', appendChild: [ $element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}), @@ -398,150 +424,202 @@ function showLintHelp() { ); } -function showLinterErrorMessage(title, contents) { +function showLinterErrorMessage(title, contents, popup) { 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) { - showLinterErrorMessage(linter, t('linterJSONError')); - popup.codebox.focus(); - return; - } - linterConfig.findInvalidRules(json, linter).then(invalid => { - 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(); - 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')); - }); + }).then(() => popup && popup.codebox.focus()); } function setupLinterPopup(config) { - const linter = linterConfig.getDefault(); + const linter = linterConfig.getName(); 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 defaultConfig = linterConfig.stringify(linterConfig.defaults[linter] || {}); 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, null, { + lint: false, + extraKeys: {'Ctrl-Enter': save}, + hintOptions: {hint}, }); - 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(); + $('.contents', popup).appendChild(makeFooter()); + + const cm = popup.codebox; + cm.focus(); + cm.setValue(config); + cm.clearHistory(); + cm.markClean(); + cm.on('changes', updateButtonState); + updateButtonState(); + + hotkeyRerouter.setState(false); + window.addEventListener('closeHelp', function _() { + window.removeEventListener('closeHelp', _); + hotkeyRerouter.setState(true); }); - 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'); + cm.setOption('mode', 'application/json'); + cm.setOption('lint', 'json'); }); + + function makeFooter() { + const makeButton = (className, onclick, text, options = {}) => + $element(Object.assign(options, { + className, + onclick, + tag: 'button', + type: 'button', + textContent: t(text), + })); + return $element({ + appendChild: [ + $element({ + tag: 'p', + appendChild: [ + t('linterRulesLink') + ' ', + $element({ + tag: 'a', + target: '_blank', + href: linter === 'stylelint' + ? 'https://stylelint.io/user-guide/rules/' + : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID', + textContent: linterTitle + }), + linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '' + ] + }), + makeButton('save', save, 'styleSaveLabel', {title: 'Ctrl-Enter'}), + makeButton('cancel', cancel, 'confirmClose'), + makeButton('reset', reset, 'genericResetLabel', {title: t('linterResetMessage')}), + $element({ + tag: 'span', + className: 'saved-message', + textContent: t('genericSavedMessage') + }) + ] + }); + } + + function save(event) { + if (event instanceof Event) { + event.preventDefault(); + } + const json = tryJSONparse(cm.getValue()); + if (!json) { + showLinterErrorMessage(linter, t('linterJSONError'), popup); + cm.focus(); + } + linterConfig.findInvalidRules(json, linter).then(invalid => { + if (invalid.length) { + showLinterErrorMessage(linter, [ + t('linterInvalidConfigError'), + $element({tag: 'ul', appendChild: invalid.map(name => + $element({tag: 'li', textContent: name})), + }), + ], popup); + return; + } + linterConfig.setLinter(linter); + linterConfig.save(json); + linterConfig.showSavedMessage(); + cm.markClean(); + cm.focus(); + updateButtonState(); + }); + } + + function reset(event) { + event.preventDefault(); + if (linterConfig.getName() !== linter) { + 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.getDefault()) { - if (!name) { - return Promise.resolve(); - } - return loadLibrary().then(loadAddon); - - function loadLibrary() { - if (name === 'csslint' && !window.CSSLint) { - window.CSSLint = new Worker('/vendor-overwrites/csslint/csslint-worker.js'); - return loadScript([ - '/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([ +function loadLinterAssets(name = linterConfig.getName()) { + const worker = linterConfig.worker[name]; + return !name || !worker || worker.instance ? Promise.resolve() : + loadScript((worker.instance ? [] : [ + (worker.instance = new Worker(worker.path)), + `/edit/lint-defaults-${name}.js`, + ]).concat(CodeMirror.lint ? [] : [ '/vendor/codemirror/addon/lint/lint.css', '/msgbox/msgbox.css', '/vendor/codemirror/addon/lint/lint.js', '/edit/lint-codemirror-helper.js', '/msgbox/msgbox.js' - ]); - } + ])); } diff --git a/edit/source-editor.js b/edit/source-editor.js index e2bcbb59..1c31a0c5 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -70,7 +70,7 @@ function createSourceEditor(style) { update(); function update() { - linterEl.value = linterConfig.getDefault(); + linterEl.value = linterConfig.getName(); const cssLintOption = linterEl.querySelector('[value="csslint"]'); if (cm.getOption('mode') !== 'css') { diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 0562757f..4354d895 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -15,6 +15,7 @@ function messageBox({ if (onshow) { onshow(messageBox.element); } + messageBox.element.focus(); return new Promise(_resolve => { messageBox.resolve = _resolve; }); diff --git a/vendor-overwrites/csslint/csslint-worker.js b/vendor-overwrites/csslint/csslint-worker.js index 679a7667..c4ba86cd 100644 --- a/vendor-overwrites/csslint/csslint-worker.js +++ b/vendor-overwrites/csslint/csslint-worker.js @@ -10950,14 +10950,15 @@ if (!CSSLint.suppressUsoVarError) { }); } -self.onmessage = ({data: {action, code, config}}) => { +self.onmessage = ({data: {action = 'run', code, config}}) => { switch (action) { - case 'getRules': - self.postMessage(CSSLint.getRules()); + case 'getAllRuleIds': + // the functions are non-tranferable and we need only an id + self.postMessage(CSSLint.getRules().map(rule => rule.id)); return; - case 'verify': + case 'run': Object.defineProperty(config, 'errors', {get: () => 0, set: () => 0}); config['uso-vars'] = 1; self.postMessage(CSSLint.verify(code, config).messages.map(m => { diff --git a/vendor-overwrites/stylelint/stylelint-bundle.min.js b/vendor-overwrites/stylelint/stylelint-bundle.min.js index 2db9e4ef..8e4acb41 100644 --- a/vendor-overwrites/stylelint/stylelint-bundle.min.js +++ b/vendor-overwrites/stylelint/stylelint-bundle.min.js @@ -1672,4 +1672,60 @@ N,R,y-N,"inline"])):(K.lastIndex=G+1,K.test(E),y=0===K.lastIndex?E.length-1:K.la {}],609:[function(a,l,g){l.exports=function(a,g,f){if(0===a.length)return a;if(g){f||a.sort(g);f=1;for(var d=a.length,c=a[0],b,h=1;h { + 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; + } +})();