From d49f65b793761905547483b667927871fe1f84f4 Mon Sep 17 00:00:00 2001 From: eight Date: Sat, 12 Aug 2017 04:15:22 +0800 Subject: [PATCH] Refactor: pull out mozParser --- edit.html | 2 + edit/edit.js | 154 +++++++---------------------------------------- js/moz-parser.js | 147 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 131 deletions(-) create mode 100644 js/moz-parser.js diff --git a/edit.html b/edit.html index 4684b534f5..9e9710c9ec 100644 --- a/edit.html +++ b/edit.html @@ -6,6 +6,8 @@ + + diff --git a/edit/edit.js b/edit/edit.js index ff29c0d59f..8be6748568 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1,5 +1,5 @@ /* eslint brace-style: 0, operator-linebreak: 0 */ -/* global CodeMirror exports parserlib CSSLint */ +/* global CodeMirror exports parserlib CSSLint mozParser */ 'use strict'; let styleId = null; @@ -1530,17 +1530,7 @@ function showMozillaFormat() { } function toMozillaFormat() { - return getSectionsHashes().map(section => { - let cssMds = []; - for (const i in propertyToCss) { - if (section[i]) { - cssMds = cssMds.concat(section[i].map(v => - propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")' - )); - } - } - return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code; - }).join('\n\n'); + return mozParser.format({sections: getSectionsHashes()}); } function fromMozillaFormat() { @@ -1568,117 +1558,8 @@ function fromMozillaFormat() { const replaceOldStyle = this.name === 'import-replace'; popup.querySelector('.dismiss').onclick(); const mozStyle = trimNewLines(popup.codebox.getValue()); - const parser = new parserlib.css.Parser(); - const lines = mozStyle.split('\n'); - const sectionStack = [{code: '', start: {line: 1, col: 1}}]; - const errors = []; - // let oldSectionCount = editors.length; - let firstAddedCM; - - parser.addListener('startdocument', function (e) { - let outerText = getRange(sectionStack.last.start, (--e.col, e)); - const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/); - const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')}; - // move last comment before @-moz-document inside the section - if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) { - section.code = gapComment[1] + '\n'; - outerText = trimNewLines(outerText.substring(0, gapComment.index)); - } - if (outerText.trim()) { - sectionStack.last.code = outerText; - doAddSection(sectionStack.last); - sectionStack.last.code = ''; - } - for (const f of e.functions) { - const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/); - if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) { - errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`); - continue; - } - const aType = CssToProperty[m[1]]; - const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\'); - (section[aType] = section[aType] || []).push(aValue); - } - sectionStack.push(section); - }); - - parser.addListener('enddocument', function () { - const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start'); - const section = sectionStack.pop(); - section.code += getRange(section.start, end); - sectionStack.last.start = (++end.col, end); - doAddSection(section); - }); - - parser.addListener('endstylesheet', () => { - // add nonclosed outer sections (either broken or the last global one) - const endOfText = {line: lines.length, col: lines.last.length + 1}; - sectionStack.last.code += getRange(sectionStack.last.start, endOfText); - sectionStack.forEach(doAddSection); - - delete maximizeCodeHeight.stats; - editors.forEach(cm => { - maximizeCodeHeight(cm.getSection(), cm === editors.last); - }); - - makeSectionVisible(firstAddedCM); - firstAddedCM.focus(); - - if (errors.length) { - showHelp(t('issues'), $element({ - tag: 'pre', - textContent: errors.join('\n'), - })); - } - }); - - parser.addListener('error', e => { - errors.push(e.line + ':' + e.col + ' ' + - e.message.replace(/ at line \d.+$/, '')); - }); - - parser.parse(mozStyle); - function getRange(start, end) { - const L1 = start.line - 1; - const C1 = start.col - 1; - const L2 = end.line - 1; - const C2 = end.col - 1; - if (L1 === L2) { - return lines[L1].substr(C1, C2 - C1 + 1); - } else { - const middle = lines.slice(L1 + 1, L2).join('\n'); - return lines[L1].substr(C1) + '\n' + middle + - (L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2))); - } - } - function doAddSection(section) { - section.code = section.code.trim(); - // don't add empty sections - if ( - !section.code && - !section.urls && - !section.urlPrefixes && - !section.domains && - !section.regexps - ) { - return; - } - if (!firstAddedCM) { - if (!initFirstSection(section)) { - return; - } - } - setCleanItem(addSection(null, section), false); - firstAddedCM = firstAddedCM || editors.last; - } - // do onetime housekeeping as the imported text is confirmed to be a valid style - function initFirstSection(section) { - // skip adding the first global section when there's no code/comments - if (!section.code.replace('@namespace url(http://www.w3.org/1999/xhtml);', '') /* ignore boilerplate NS */ - .replace(/[\s\n]/g, '')) { /* ignore all whitespace including new lines */ - return false; - } + mozParser.parse(mozStyle).then(sections => { if (replaceOldStyle) { editors.slice(0).reverse().forEach(cm => { removeSection({target: cm.getSection().firstElementChild}); @@ -1689,16 +1570,27 @@ function fromMozillaFormat() { removeSection({target: editors.last.getSection()}); } } - return true; - } - } - function backtrackTo(parser, tokenType, startEnd) { - const tokens = parser._tokenStream._lt; - for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) { - if (tokens[i].type === tokenType) { - return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']}; + + const firstSection = sections[0]; + setCleanItem(addSection(null, firstSection), false); + const firstAddedCM = editors.last; + for (const section of sections.slice(1)) { + setCleanItem(addSection(null, section), false); } - } + + delete maximizeCodeHeight.stats; + editors.forEach(cm => { + maximizeCodeHeight(cm.getSection(), cm === editors.last); + }); + + makeSectionVisible(firstAddedCM); + firstAddedCM.focus(); + }, errors => { + showHelp(t('issues'), $element({ + tag: 'pre', + textContent: errors.join('\n'), + })); + }); } function trimNewLines(s) { return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, ''); diff --git a/js/moz-parser.js b/js/moz-parser.js new file mode 100644 index 0000000000..d75a1b783c --- /dev/null +++ b/js/moz-parser.js @@ -0,0 +1,147 @@ +/* globals parserlib, loadScript */ + +'use strict'; + +// eslint-disable-next-line no-var +var mozParser = (function(){ + // direct & reverse mapping of @-moz-document keywords and internal property names + const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; + const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'}; + + function backtrackTo(parser, tokenType, startEnd) { + const tokens = parser._tokenStream._lt; + for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) { + if (tokens[i].type === tokenType) { + return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']}; + } + } + } + + function trimNewLines(s) { + return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, ''); + } + + function parseMozFormat(mozStyle) { + return new Promise((resolve, reject) => { + const parser = new parserlib.css.Parser(); + const lines = mozStyle.split('\n'); + const sectionStack = [{code: '', start: {line: 1, col: 1}}]; + const errors = []; + const sections = []; + + parser.addListener('startdocument', function (e) { + let outerText = getRange(sectionStack.last.start, (--e.col, e)); + const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/); + const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')}; + // move last comment before @-moz-document inside the section + if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) { + section.code = gapComment[1] + '\n'; + outerText = trimNewLines(outerText.substring(0, gapComment.index)); + } + if (outerText.trim()) { + sectionStack.last.code = outerText; + doAddSection(sectionStack.last); + sectionStack.last.code = ''; + } + for (const f of e.functions) { + const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/); + if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) { + errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`); + continue; + } + const aType = CssToProperty[m[1]]; + const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\'); + (section[aType] = section[aType] || []).push(aValue); + } + sectionStack.push(section); + }); + + parser.addListener('enddocument', function () { + const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start'); + const section = sectionStack.pop(); + section.code += getRange(section.start, end); + sectionStack.last.start = (++end.col, end); + doAddSection(section); + }); + + parser.addListener('endstylesheet', () => { + // add nonclosed outer sections (either broken or the last global one) + const endOfText = {line: lines.length, col: lines.last.length + 1}; + sectionStack.last.code += getRange(sectionStack.last.start, endOfText); + sectionStack.forEach(doAddSection); + + + if (errors.length) { + reject(errors); + } else { + resolve(sections); + } + }); + + parser.addListener('error', e => { + errors.push(e.line + ':' + e.col + ' ' + + e.message.replace(/ at line \d.+$/, '')); + }); + + parser.parse(mozStyle); + + function getRange(start, end) { + const L1 = start.line - 1; + const C1 = start.col - 1; + const L2 = end.line - 1; + const C2 = end.col - 1; + if (L1 === L2) { + return lines[L1].substr(C1, C2 - C1 + 1); + } else { + const middle = lines.slice(L1 + 1, L2).join('\n'); + return lines[L1].substr(C1) + '\n' + middle + + (L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2))); + } + } + + function doAddSection(section) { + section.code = section.code.trim(); + // don't add empty sections + if ( + !section.code && + !section.urls && + !section.urlPrefixes && + !section.domains && + !section.regexps + ) { + return; + } + /* ignore boilerplate NS */ + if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') { + return; + } + sections.push(Object.assign({}, section)); + } + }); + } + + return { + // Parse mozilla-format userstyle into sections + parse(text) { + if (typeof parserlib === 'undefined') { + return loadScript('vender/csslint/csslint-worker.js') + .then(() => parseMozFormat(text)); + } + return parseMozFormat(text); + }, + format(style) { + return style.sections.map(section => { + let cssMds = []; + for (const i in propertyToCss) { + if (section[i]) { + cssMds = cssMds.concat(section[i].map(v => + propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")' + )); + } + } + return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code; + }).join('\n\n'); + } + }; +})(); +