moz-parser.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /* global parserlib, loadScript */
  2. 'use strict';
  3. // eslint-disable-next-line no-var
  4. var mozParser = (() => {
  5. // direct & reverse mapping of @-moz-document keywords and internal property names
  6. const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
  7. const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
  8. function parseMozFormat(mozStyle) {
  9. return new Promise((resolve, reject) => {
  10. const parser = new parserlib.css.Parser();
  11. const lines = mozStyle.split('\n');
  12. const sectionStack = [{code: '', start: {line: 1, col: 1}}];
  13. const errors = [];
  14. const sections = [];
  15. parser.addListener('startdocument', e => {
  16. const lastSection = sectionStack[sectionStack.length - 1];
  17. let outerText = getRange(lastSection.start, {line: e.line, col: e.col - 1});
  18. const lastCmt = getLastComment(outerText);
  19. const {endLine: line, endCol: col} = parser._tokenStream._token;
  20. const section = {code: '', start: {line, col}};
  21. // move last comment before @-moz-document inside the section
  22. if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) {
  23. if (lastCmt) {
  24. section.code = lastCmt + '\n';
  25. outerText = outerText.slice(0, -lastCmt.length);
  26. }
  27. outerText = outerText.match(/^\s*/)[0] + outerText.trim();
  28. }
  29. if (outerText.trim()) {
  30. lastSection.code = outerText;
  31. doAddSection(lastSection);
  32. lastSection.code = '';
  33. }
  34. for (const f of e.functions) {
  35. const m = f && f.match(/^([\w-]*)\((.+?)\)$/);
  36. if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
  37. errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
  38. continue;
  39. }
  40. const aType = CssToProperty[m[1]];
  41. const aValue = unquote(aType !== 'regexps' ? m[2] : m[2].replace(/\\\\/g, '\\'));
  42. (section[aType] = section[aType] || []).push(aValue);
  43. }
  44. sectionStack.push(section);
  45. });
  46. parser.addListener('enddocument', e => {
  47. const section = sectionStack.pop();
  48. const lastSection = sectionStack[sectionStack.length - 1];
  49. const end = {line: e.line, col: e.col - 1};
  50. section.code += getRange(section.start, end);
  51. end.col += 2;
  52. lastSection.start = end;
  53. doAddSection(section);
  54. });
  55. parser.addListener('endstylesheet', () => {
  56. // add nonclosed outer sections (either broken or the last global one)
  57. const lastLine = lines[lines.length - 1];
  58. const endOfText = {line: lines.length, col: lastLine.length + 1};
  59. const lastSection = sectionStack[sectionStack.length - 1];
  60. lastSection.code += getRange(lastSection.start, endOfText);
  61. sectionStack.forEach(doAddSection);
  62. if (errors.length) {
  63. reject(errors);
  64. } else {
  65. resolve(sections);
  66. }
  67. });
  68. parser.addListener('error', e => {
  69. errors.push(e.line + ':' + e.col + ' ' +
  70. e.message.replace(/ at line \d.+$/, ''));
  71. });
  72. parser.parse(mozStyle);
  73. function getRange(start, end) {
  74. const L1 = start.line - 1;
  75. const C1 = start.col - 1;
  76. const L2 = end.line - 1;
  77. const C2 = end.col - 1;
  78. if (L1 === L2) {
  79. return lines[L1].substr(C1, C2 - C1 + 1);
  80. } else {
  81. const middle = lines.slice(L1 + 1, L2).join('\n');
  82. return lines[L1].substr(C1) + '\n' + middle +
  83. (L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
  84. }
  85. }
  86. function doAddSection(section) {
  87. section.code = section.code.trim();
  88. // don't add empty sections
  89. if (
  90. !section.code &&
  91. !section.urls &&
  92. !section.urlPrefixes &&
  93. !section.domains &&
  94. !section.regexps
  95. ) {
  96. return;
  97. }
  98. /* ignore boilerplate NS */
  99. if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
  100. return;
  101. }
  102. sections.push(Object.assign({}, section));
  103. }
  104. function unquote(s) {
  105. const first = s.charAt(0);
  106. return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
  107. }
  108. function getLastComment(text) {
  109. let open = text.length;
  110. let close;
  111. while (open) {
  112. // at this point we're guaranteed to be outside of a comment
  113. close = text.lastIndexOf('*/', open - 2);
  114. if (close < 0) {
  115. break;
  116. }
  117. // stop if a non-whitespace precedes and return what we currently have
  118. const tailEmpty = !text.substring(close + 2, open).trim();
  119. if (!tailEmpty) {
  120. break;
  121. }
  122. // find a closed preceding comment
  123. const prevClose = text.lastIndexOf('*/', close);
  124. // then find the real start of current comment
  125. // e.g. /* preceding */ /* current /* current /* current */
  126. open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2);
  127. }
  128. return text.substr(open);
  129. }
  130. });
  131. }
  132. return {
  133. // Parse mozilla-format userstyle into sections
  134. parse(text) {
  135. return Promise.resolve(self.CSSLint || loadScript('/vendor-overwrites/csslint/csslint-worker.js'))
  136. .then(() => parseMozFormat(text));
  137. },
  138. format(style) {
  139. return style.sections.map(section => {
  140. let cssMds = [];
  141. for (const i in propertyToCss) {
  142. if (section[i]) {
  143. cssMds = cssMds.concat(section[i].map(v =>
  144. propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
  145. ));
  146. }
  147. }
  148. return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
  149. }).join('\n\n');
  150. }
  151. };
  152. })();