style.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. 'use strict';
  2. /**
  3. * @typedef {import('css-tree').Rule} CsstreeRule
  4. * @typedef {import('./types').Specificity} Specificity
  5. * @typedef {import('./types').Stylesheet} Stylesheet
  6. * @typedef {import('./types').StylesheetRule} StylesheetRule
  7. * @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration
  8. * @typedef {import('./types').ComputedStyles} ComputedStyles
  9. * @typedef {import('./types').XastRoot} XastRoot
  10. * @typedef {import('./types').XastElement} XastElement
  11. * @typedef {import('./types').XastParent} XastParent
  12. * @typedef {import('./types').XastChild} XastChild
  13. */
  14. const stable = require('stable');
  15. const csstree = require('css-tree');
  16. // @ts-ignore not defined in @types/csso
  17. const specificity = require('csso/lib/restructure/prepare/specificity');
  18. const { visit, matches } = require('./xast.js');
  19. const {
  20. attrsGroups,
  21. inheritableAttrs,
  22. presentationNonInheritableGroupAttrs,
  23. } = require('../plugins/_collections.js');
  24. // @ts-ignore not defined in @types/csstree
  25. const csstreeWalkSkip = csstree.walk.skip;
  26. /**
  27. * @type {(ruleNode: CsstreeRule, dynamic: boolean) => StylesheetRule}
  28. */
  29. const parseRule = (ruleNode, dynamic) => {
  30. let selectors;
  31. let selectorsSpecificity;
  32. /**
  33. * @type {Array<StylesheetDeclaration>}
  34. */
  35. const declarations = [];
  36. csstree.walk(ruleNode, (cssNode) => {
  37. if (cssNode.type === 'SelectorList') {
  38. // compute specificity from original node to consider pseudo classes
  39. selectorsSpecificity = specificity(cssNode);
  40. const newSelectorsNode = csstree.clone(cssNode);
  41. csstree.walk(newSelectorsNode, (pseudoClassNode, item, list) => {
  42. if (pseudoClassNode.type === 'PseudoClassSelector') {
  43. dynamic = true;
  44. list.remove(item);
  45. }
  46. });
  47. selectors = csstree.generate(newSelectorsNode);
  48. return csstreeWalkSkip;
  49. }
  50. if (cssNode.type === 'Declaration') {
  51. declarations.push({
  52. name: cssNode.property,
  53. value: csstree.generate(cssNode.value),
  54. important: cssNode.important === true,
  55. });
  56. return csstreeWalkSkip;
  57. }
  58. });
  59. if (selectors == null || selectorsSpecificity == null) {
  60. throw Error('assert');
  61. }
  62. return {
  63. dynamic,
  64. selectors,
  65. specificity: selectorsSpecificity,
  66. declarations,
  67. };
  68. };
  69. /**
  70. * @type {(css: string, dynamic: boolean) => Array<StylesheetRule>}
  71. */
  72. const parseStylesheet = (css, dynamic) => {
  73. /**
  74. * @type {Array<StylesheetRule>}
  75. */
  76. const rules = [];
  77. const ast = csstree.parse(css, {
  78. parseValue: false,
  79. parseAtrulePrelude: false,
  80. });
  81. csstree.walk(ast, (cssNode) => {
  82. if (cssNode.type === 'Rule') {
  83. rules.push(parseRule(cssNode, dynamic || false));
  84. return csstreeWalkSkip;
  85. }
  86. if (cssNode.type === 'Atrule') {
  87. if (cssNode.name === 'keyframes') {
  88. return csstreeWalkSkip;
  89. }
  90. csstree.walk(cssNode, (ruleNode) => {
  91. if (ruleNode.type === 'Rule') {
  92. rules.push(parseRule(ruleNode, dynamic || true));
  93. return csstreeWalkSkip;
  94. }
  95. });
  96. return csstreeWalkSkip;
  97. }
  98. });
  99. return rules;
  100. };
  101. /**
  102. * @type {(css: string) => Array<StylesheetDeclaration>}
  103. */
  104. const parseStyleDeclarations = (css) => {
  105. /**
  106. * @type {Array<StylesheetDeclaration>}
  107. */
  108. const declarations = [];
  109. const ast = csstree.parse(css, {
  110. context: 'declarationList',
  111. parseValue: false,
  112. });
  113. csstree.walk(ast, (cssNode) => {
  114. if (cssNode.type === 'Declaration') {
  115. declarations.push({
  116. name: cssNode.property,
  117. value: csstree.generate(cssNode.value),
  118. important: cssNode.important === true,
  119. });
  120. }
  121. });
  122. return declarations;
  123. };
  124. /**
  125. * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles}
  126. */
  127. const computeOwnStyle = (stylesheet, node) => {
  128. /**
  129. * @type {ComputedStyles}
  130. */
  131. const computedStyle = {};
  132. const importantStyles = new Map();
  133. // collect attributes
  134. for (const [name, value] of Object.entries(node.attributes)) {
  135. if (attrsGroups.presentation.includes(name)) {
  136. computedStyle[name] = { type: 'static', inherited: false, value };
  137. importantStyles.set(name, false);
  138. }
  139. }
  140. // collect matching rules
  141. for (const { selectors, declarations, dynamic } of stylesheet.rules) {
  142. if (matches(node, selectors)) {
  143. for (const { name, value, important } of declarations) {
  144. const computed = computedStyle[name];
  145. if (computed && computed.type === 'dynamic') {
  146. continue;
  147. }
  148. if (dynamic) {
  149. computedStyle[name] = { type: 'dynamic', inherited: false };
  150. continue;
  151. }
  152. if (
  153. computed == null ||
  154. important === true ||
  155. importantStyles.get(name) === false
  156. ) {
  157. computedStyle[name] = { type: 'static', inherited: false, value };
  158. importantStyles.set(name, important);
  159. }
  160. }
  161. }
  162. }
  163. // collect inline styles
  164. const styleDeclarations =
  165. node.attributes.style == null
  166. ? []
  167. : parseStyleDeclarations(node.attributes.style);
  168. for (const { name, value, important } of styleDeclarations) {
  169. const computed = computedStyle[name];
  170. if (computed && computed.type === 'dynamic') {
  171. continue;
  172. }
  173. if (
  174. computed == null ||
  175. important === true ||
  176. importantStyles.get(name) === false
  177. ) {
  178. computedStyle[name] = { type: 'static', inherited: false, value };
  179. importantStyles.set(name, important);
  180. }
  181. }
  182. return computedStyle;
  183. };
  184. /**
  185. * Compares two selector specificities.
  186. * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
  187. *
  188. * @type {(a: Specificity, b: Specificity) => number}
  189. */
  190. const compareSpecificity = (a, b) => {
  191. for (var i = 0; i < 4; i += 1) {
  192. if (a[i] < b[i]) {
  193. return -1;
  194. } else if (a[i] > b[i]) {
  195. return 1;
  196. }
  197. }
  198. return 0;
  199. };
  200. /**
  201. * @type {(root: XastRoot) => Stylesheet}
  202. */
  203. const collectStylesheet = (root) => {
  204. /**
  205. * @type {Array<StylesheetRule>}
  206. */
  207. const rules = [];
  208. /**
  209. * @type {Map<XastElement, XastParent>}
  210. */
  211. const parents = new Map();
  212. visit(root, {
  213. element: {
  214. enter: (node, parentNode) => {
  215. // store parents
  216. parents.set(node, parentNode);
  217. // find and parse all styles
  218. if (node.name === 'style') {
  219. const dynamic =
  220. node.attributes.media != null && node.attributes.media !== 'all';
  221. if (
  222. node.attributes.type == null ||
  223. node.attributes.type === '' ||
  224. node.attributes.type === 'text/css'
  225. ) {
  226. const children = node.children;
  227. for (const child of children) {
  228. if (child.type === 'text' || child.type === 'cdata') {
  229. rules.push(...parseStylesheet(child.value, dynamic));
  230. }
  231. }
  232. }
  233. }
  234. },
  235. },
  236. });
  237. // sort by selectors specificity
  238. stable.inplace(rules, (a, b) =>
  239. compareSpecificity(a.specificity, b.specificity)
  240. );
  241. return { rules, parents };
  242. };
  243. exports.collectStylesheet = collectStylesheet;
  244. /**
  245. * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles}
  246. */
  247. const computeStyle = (stylesheet, node) => {
  248. const { parents } = stylesheet;
  249. // collect inherited styles
  250. const computedStyles = computeOwnStyle(stylesheet, node);
  251. let parent = parents.get(node);
  252. while (parent != null && parent.type !== 'root') {
  253. const inheritedStyles = computeOwnStyle(stylesheet, parent);
  254. for (const [name, computed] of Object.entries(inheritedStyles)) {
  255. if (
  256. computedStyles[name] == null &&
  257. // ignore not inheritable styles
  258. inheritableAttrs.includes(name) === true &&
  259. presentationNonInheritableGroupAttrs.includes(name) === false
  260. ) {
  261. computedStyles[name] = { ...computed, inherited: true };
  262. }
  263. }
  264. parent = parents.get(parent);
  265. }
  266. return computedStyles;
  267. };
  268. exports.computeStyle = computeStyle;