| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- /* 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');
- }
- };
- })();
|