/* global parserlib, loadScript */ 'use strict'; // eslint-disable-next-line no-var var mozParser = (() => { // 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 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', e => { const lastSection = sectionStack[sectionStack.length - 1]; let outerText = getRange(lastSection.start, {line: e.line, col: e.col - 1}); const lastCmt = getLastComment(outerText); const {endLine: line, endCol: col} = parser._tokenStream._token; const section = {code: '', start: {line, col}}; // move last comment before @-moz-document inside the section if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) { if (lastCmt) { section.code = lastCmt + '\n'; outerText = outerText.slice(0, -lastCmt.length); } outerText = outerText.match(/^\s*/)[0] + outerText.trim(); } if (outerText.trim()) { lastSection.code = outerText; doAddSection(lastSection); lastSection.code = ''; } for (const f of e.functions) { const m = f && f.match(/^([\w-]*)\((.+?)\)$/); 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 = unquote(aType !== 'regexps' ? m[2] : m[2].replace(/\\\\/g, '\\')); (section[aType] = section[aType] || []).push(aValue); } sectionStack.push(section); }); parser.addListener('enddocument', e => { const section = sectionStack.pop(); const lastSection = sectionStack[sectionStack.length - 1]; const end = {line: e.line, col: e.col - 1}; section.code += getRange(section.start, end); end.col += 2; lastSection.start = end; doAddSection(section); }); parser.addListener('endstylesheet', () => { // add nonclosed outer sections (either broken or the last global one) const lastLine = lines[lines.length - 1]; const endOfText = {line: lines.length, col: lastLine.length + 1}; const lastSection = sectionStack[sectionStack.length - 1]; lastSection.code += getRange(lastSection.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)); } function unquote(s) { const first = s.charAt(0); return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s; } function getLastComment(text) { let open = text.length; let close; while (open) { // at this point we're guaranteed to be outside of a comment close = text.lastIndexOf('*/', open - 2); if (close < 0) { break; } // stop if a non-whitespace precedes and return what we currently have const tailEmpty = !text.substring(close + 2, open).trim(); if (!tailEmpty) { break; } // find a closed preceding comment const prevClose = text.lastIndexOf('*/', close); // then find the real start of current comment // e.g. /* preceding */ /* current /* current /* current */ open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2); } return text.substr(open); } }); } return { // Parse mozilla-format userstyle into sections parse(text) { return Promise.resolve(self.CSSLint || loadScript('/vendor-overwrites/csslint/csslint-worker.js')) .then(() => 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'); } }; })();