/* global parserlib */ 'use strict'; /** * Extracts @-moz-document blocks into sections and the code between them into global sections. * Puts the global comments into the following section to minimize the amount of global sections. * Doesn't move the comment with ==UserStyle== inside. * @param {string} code * @param {number} styleId - used to preserve parserCache on subsequent runs over the same style * @returns {{sections: Array, errors: Array}} */ function parseMozFormat({code, styleId}) { const CssToProperty = { 'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps', }; const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/; const parser = new parserlib.css.Parser(); const sectionStack = [{code: '', start: 0}]; const errors = []; const sections = []; const mozStyle = code; parser.addListener('startdocument', e => { const lastSection = sectionStack[sectionStack.length - 1]; let outerText = mozStyle.slice(lastSection.start, e.offset); const lastCmt = getLastComment(outerText); const section = { code: '', start: parser._tokenStream._token.offset + 1, }; // move last comment before @-moz-document inside the section if (!lastCmt.includes('AGENT_SHEET') && !lastCmt.includes('==') && !/==userstyle==/iu.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 {name, expr, uri} of e.functions) { const aType = CssToProperty[name.toLowerCase()]; const p0 = expr && expr.parts[0]; if (p0 && aType === 'regexps') { const s = p0.text; if (hasSingleEscapes.test(p0.text)) { const isQuoted = (s.startsWith('"') || s.startsWith("'")) && s.endsWith(s[0]); p0.value = isQuoted ? s.slice(1, -1) : s; } } (section[aType] = section[aType] || []).push(uri || p0 && p0.value || ''); } sectionStack.push(section); }); parser.addListener('enddocument', e => { const section = sectionStack.pop(); const lastSection = sectionStack[sectionStack.length - 1]; section.code += mozStyle.slice(section.start, e.offset); lastSection.start = e.offset + 1; doAddSection(section); }); parser.addListener('endstylesheet', () => { // add nonclosed outer sections (either broken or the last global one) const lastSection = sectionStack[sectionStack.length - 1]; lastSection.code += mozStyle.slice(lastSection.start); sectionStack.forEach(doAddSection); }); parser.addListener('error', e => { errors.push(`${e.line}:${e.col} ${e.message.replace(/ at line \d.+$/, '')}`); }); parser.parse(mozStyle, { reuseCache: !parseMozFormat.styleId || styleId === parseMozFormat.styleId, }); parseMozFormat.styleId = styleId; return {sections, errors}; 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 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 - 2); // then find the real start of current comment // e.g. /* preceding */ /* current /* current /* current */ open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2); } return open ? text.slice(open) : text; } }