moz-parser.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. 'use strict';
  2. require([
  3. '/js/csslint/parserlib', /* global parserlib */
  4. '/js/sections-util', /* global MozDocMapper */
  5. ]);
  6. /* exported extractSections */
  7. /**
  8. * Extracts @-moz-document blocks into sections and the code between them into global sections.
  9. * Puts the global comments into the following section to minimize the amount of global sections.
  10. * Doesn't move the comment with ==UserStyle== inside.
  11. * @param {Object} _
  12. * @param {string} _.code
  13. * @param {boolean} [_.fast] - uses topDocOnly option to extract sections as text
  14. * @param {number} [_.styleId] - used to preserve parserCache on subsequent runs over the same style
  15. * @returns {{sections: Array, errors: Array}}
  16. * @property {?number} lastStyleId
  17. */
  18. function extractSections({code, styleId, fast = true}) {
  19. const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/;
  20. const parser = new parserlib.css.Parser({
  21. starHack: true,
  22. skipValidation: true,
  23. topDocOnly: fast,
  24. });
  25. const sectionStack = [{code: '', start: 0}];
  26. const errors = [];
  27. const sections = [];
  28. const mozStyle = code.replace(/\r\n?/g, '\n'); // same as parserlib.StringReader
  29. parser.addListener('startdocument', e => {
  30. const lastSection = sectionStack[sectionStack.length - 1];
  31. let outerText = mozStyle.slice(lastSection.start, e.offset);
  32. const lastCmt = getLastComment(outerText);
  33. const section = {
  34. code: '',
  35. start: parser._tokenStream._token.offset + 1,
  36. };
  37. // move last comment before @-moz-document inside the section
  38. if (!lastCmt.includes('AGENT_SHEET') &&
  39. !/==userstyle==/i.test(lastCmt)) {
  40. if (lastCmt) {
  41. section.code = lastCmt + '\n';
  42. outerText = outerText.slice(0, -lastCmt.length);
  43. }
  44. outerText = outerText.match(/^\s*/)[0] + outerText.trim();
  45. }
  46. if (outerText.trim()) {
  47. lastSection.code = outerText;
  48. doAddSection(lastSection);
  49. lastSection.code = '';
  50. }
  51. for (const {name, expr, uri} of e.functions) {
  52. const aType = MozDocMapper.FROM_CSS[name.toLowerCase()];
  53. const p0 = expr && expr.parts[0];
  54. if (p0 && aType === 'regexps') {
  55. const s = p0.text;
  56. if (hasSingleEscapes.test(p0.text)) {
  57. const isQuoted = /^['"]/.test(s) && s.endsWith(s[0]);
  58. p0.value = isQuoted ? s.slice(1, -1) : s;
  59. }
  60. }
  61. (section[aType] = section[aType] || []).push(uri || p0 && p0.value || '');
  62. }
  63. sectionStack.push(section);
  64. });
  65. parser.addListener('enddocument', e => {
  66. const section = sectionStack.pop();
  67. const lastSection = sectionStack[sectionStack.length - 1];
  68. section.code += mozStyle.slice(section.start, e.offset);
  69. lastSection.start = e.offset + 1;
  70. doAddSection(section);
  71. });
  72. parser.addListener('endstylesheet', () => {
  73. // add nonclosed outer sections (either broken or the last global one)
  74. const lastSection = sectionStack[sectionStack.length - 1];
  75. lastSection.code += mozStyle.slice(lastSection.start);
  76. sectionStack.forEach(doAddSection);
  77. });
  78. parser.addListener('error', e => {
  79. errors.push(e);
  80. });
  81. try {
  82. parser.parse(mozStyle, {
  83. reuseCache: !extractSections.lastStyleId || styleId === extractSections.lastStyleId,
  84. });
  85. } catch (e) {
  86. errors.push(e);
  87. }
  88. for (const err of errors) {
  89. for (const [k, v] of Object.entries(err)) {
  90. if (typeof v === 'object') delete err[k];
  91. }
  92. err.message = `${err.line}:${err.col} ${err.message}`;
  93. }
  94. extractSections.lastStyleId = styleId;
  95. return {sections, errors};
  96. function doAddSection(section) {
  97. section.code = section.code.trim();
  98. // don't add empty sections
  99. if (
  100. !section.code &&
  101. !section.urls &&
  102. !section.urlPrefixes &&
  103. !section.domains &&
  104. !section.regexps
  105. ) {
  106. return;
  107. }
  108. /* ignore boilerplate NS */
  109. if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
  110. return;
  111. }
  112. sections.push(Object.assign({}, section));
  113. }
  114. function getLastComment(text) {
  115. let open = text.length;
  116. let close;
  117. while (open) {
  118. // at this point we're guaranteed to be outside of a comment
  119. close = text.lastIndexOf('*/', open - 2);
  120. if (close < 0) {
  121. break;
  122. }
  123. // stop if a non-whitespace precedes and return what we currently have
  124. const tailEmpty = !text.substring(close + 2, open).trim();
  125. if (!tailEmpty) {
  126. break;
  127. }
  128. // find a closed preceding comment
  129. const prevClose = text.lastIndexOf('*/', close - 2);
  130. // then find the real start of current comment
  131. // e.g. /* preceding */ /* current /* current /* current */
  132. open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2);
  133. }
  134. return open ? text.slice(open) : text;
  135. }
  136. }