sections-util.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. 'use strict';
  2. /* exported
  3. calcStyleDigest
  4. MozDocMapper
  5. styleCodeEmpty
  6. styleJSONseemsValid
  7. styleSectionGlobal
  8. styleSectionsEqual
  9. */
  10. const MozDocMapper = {
  11. TO_CSS: {
  12. urls: 'url',
  13. urlPrefixes: 'url-prefix',
  14. domains: 'domain',
  15. regexps: 'regexp',
  16. },
  17. FROM_CSS: {
  18. 'url': 'urls',
  19. 'url-prefix': 'urlPrefixes',
  20. 'domain': 'domains',
  21. 'regexp': 'regexps',
  22. },
  23. /**
  24. * @param {Object} section
  25. * @param {function(func:string, value:string)} fn
  26. */
  27. forEachProp(section, fn) {
  28. for (const [propName, func] of Object.entries(MozDocMapper.TO_CSS)) {
  29. const props = section[propName];
  30. if (props) props.forEach(value => fn(func, value));
  31. }
  32. },
  33. /**
  34. * @param {Array<?[type,value]>} funcItems
  35. * @param {?Object} [section]
  36. * @returns {Object} section
  37. */
  38. toSection(funcItems, section = {}) {
  39. for (const item of funcItems) {
  40. const [func, value] = item || [];
  41. const propName = MozDocMapper.FROM_CSS[func];
  42. if (propName) {
  43. const props = section[propName] || (section[propName] = []);
  44. if (Array.isArray(value)) props.push(...value);
  45. else props.push(value);
  46. }
  47. }
  48. return section;
  49. },
  50. /**
  51. * @param {StyleObj} style
  52. * @returns {string}
  53. */
  54. styleToCss(style) {
  55. const res = [];
  56. for (const section of style.sections) {
  57. const funcs = [];
  58. MozDocMapper.forEachProp(section, (type, value) =>
  59. funcs.push(`${type}("${value.replace(/[\\"]/g, '\\$&')}")`));
  60. res.push(funcs.length
  61. ? `@-moz-document ${funcs.join(', ')} {\n${section.code}\n}`
  62. : section.code);
  63. }
  64. return res.join('\n\n');
  65. },
  66. };
  67. function styleCodeEmpty(code) {
  68. if (!code) {
  69. return true;
  70. }
  71. let lastIndex = 0;
  72. const rx = /\s+|\/\*([^*]|\*(?!\/))*(\*\/|$)|@namespace[^;]+;|@charset[^;]+;/giyu;
  73. while (rx.exec(code)) {
  74. lastIndex = rx.lastIndex;
  75. if (lastIndex === code.length) {
  76. return true;
  77. }
  78. }
  79. styleCodeEmpty.lastIndex = lastIndex;
  80. return false;
  81. }
  82. /** Checks if section is global i.e. has no targets at all */
  83. function styleSectionGlobal(section) {
  84. return (!section.regexps || !section.regexps.length) &&
  85. (!section.urlPrefixes || !section.urlPrefixes.length) &&
  86. (!section.urls || !section.urls.length) &&
  87. (!section.domains || !section.domains.length);
  88. }
  89. /**
  90. * The sections are checked in successive order because it matters when many sections
  91. * match the same URL and they have rules with the same CSS specificity
  92. * @param {Object} a - first style object
  93. * @param {Object} b - second style object
  94. * @returns {?boolean}
  95. */
  96. function styleSectionsEqual({sections: a}, {sections: b}) {
  97. const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
  98. return a && b && a.length === b.length && a.every(sameSection);
  99. function sameSection(secA, i) {
  100. return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
  101. targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
  102. }
  103. function equalOrEmpty(a, b, type, comparator) {
  104. const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
  105. const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
  106. return typeA && typeB && comparator(a, b) ||
  107. (a == null || typeA && !a.length) &&
  108. (b == null || typeB && !b.length);
  109. }
  110. function arrayMirrors(a, b) {
  111. return a.length === b.length &&
  112. a.every(el => b.includes(el)) &&
  113. b.every(el => a.includes(el));
  114. }
  115. }
  116. async function calcStyleDigest(style) {
  117. // retain known properties in an arbitrarily predefined order
  118. const src = style.usercssData
  119. ? style.sourceCode
  120. // retain known properties in an arbitrarily predefined order
  121. : JSON.stringify((style.sections || []).map(section => /** @namespace StyleSection */({
  122. code: section.code || '',
  123. urls: section.urls || [],
  124. urlPrefixes: section.urlPrefixes || [],
  125. domains: section.domains || [],
  126. regexps: section.regexps || [],
  127. })));
  128. const srcBytes = new TextEncoder().encode(src);
  129. const res = await crypto.subtle.digest('SHA-1', srcBytes);
  130. return Array.from(new Uint8Array(res), b => (0x100 + b).toString(16).slice(1)).join('');
  131. }
  132. function styleJSONseemsValid(json) {
  133. return json
  134. && typeof json.name == 'string'
  135. && json.name.trim()
  136. && Array.isArray(json.sections)
  137. && typeof (json.sections[0] || {}).code === 'string';
  138. }