'use strict'; /** * @typedef {import('css-tree').Rule} CsstreeRule * @typedef {import('./types').Specificity} Specificity * @typedef {import('./types').Stylesheet} Stylesheet * @typedef {import('./types').StylesheetRule} StylesheetRule * @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration * @typedef {import('./types').ComputedStyles} ComputedStyles * @typedef {import('./types').XastRoot} XastRoot * @typedef {import('./types').XastElement} XastElement * @typedef {import('./types').XastParent} XastParent * @typedef {import('./types').XastChild} XastChild */ const stable = require('stable'); const csstree = require('css-tree'); // @ts-ignore not defined in @types/csso const specificity = require('csso/lib/restructure/prepare/specificity'); const { visit, matches } = require('./xast.js'); const { attrsGroups, inheritableAttrs, presentationNonInheritableGroupAttrs, } = require('../plugins/_collections.js'); // @ts-ignore not defined in @types/csstree const csstreeWalkSkip = csstree.walk.skip; /** * @type {(ruleNode: CsstreeRule, dynamic: boolean) => StylesheetRule} */ const parseRule = (ruleNode, dynamic) => { let selectors; let selectorsSpecificity; /** * @type {Array} */ const declarations = []; csstree.walk(ruleNode, (cssNode) => { if (cssNode.type === 'SelectorList') { // compute specificity from original node to consider pseudo classes selectorsSpecificity = specificity(cssNode); const newSelectorsNode = csstree.clone(cssNode); csstree.walk(newSelectorsNode, (pseudoClassNode, item, list) => { if (pseudoClassNode.type === 'PseudoClassSelector') { dynamic = true; list.remove(item); } }); selectors = csstree.generate(newSelectorsNode); return csstreeWalkSkip; } if (cssNode.type === 'Declaration') { declarations.push({ name: cssNode.property, value: csstree.generate(cssNode.value), important: cssNode.important === true, }); return csstreeWalkSkip; } }); if (selectors == null || selectorsSpecificity == null) { throw Error('assert'); } return { dynamic, selectors, specificity: selectorsSpecificity, declarations, }; }; /** * @type {(css: string, dynamic: boolean) => Array} */ const parseStylesheet = (css, dynamic) => { /** * @type {Array} */ const rules = []; const ast = csstree.parse(css, { parseValue: false, parseAtrulePrelude: false, }); csstree.walk(ast, (cssNode) => { if (cssNode.type === 'Rule') { rules.push(parseRule(cssNode, dynamic || false)); return csstreeWalkSkip; } if (cssNode.type === 'Atrule') { if (cssNode.name === 'keyframes') { return csstreeWalkSkip; } csstree.walk(cssNode, (ruleNode) => { if (ruleNode.type === 'Rule') { rules.push(parseRule(ruleNode, dynamic || true)); return csstreeWalkSkip; } }); return csstreeWalkSkip; } }); return rules; }; /** * @type {(css: string) => Array} */ const parseStyleDeclarations = (css) => { /** * @type {Array} */ const declarations = []; const ast = csstree.parse(css, { context: 'declarationList', parseValue: false, }); csstree.walk(ast, (cssNode) => { if (cssNode.type === 'Declaration') { declarations.push({ name: cssNode.property, value: csstree.generate(cssNode.value), important: cssNode.important === true, }); } }); return declarations; }; /** * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles} */ const computeOwnStyle = (stylesheet, node) => { /** * @type {ComputedStyles} */ const computedStyle = {}; const importantStyles = new Map(); // collect attributes for (const [name, value] of Object.entries(node.attributes)) { if (attrsGroups.presentation.includes(name)) { computedStyle[name] = { type: 'static', inherited: false, value }; importantStyles.set(name, false); } } // collect matching rules for (const { selectors, declarations, dynamic } of stylesheet.rules) { if (matches(node, selectors)) { for (const { name, value, important } of declarations) { const computed = computedStyle[name]; if (computed && computed.type === 'dynamic') { continue; } if (dynamic) { computedStyle[name] = { type: 'dynamic', inherited: false }; continue; } if ( computed == null || important === true || importantStyles.get(name) === false ) { computedStyle[name] = { type: 'static', inherited: false, value }; importantStyles.set(name, important); } } } } // collect inline styles const styleDeclarations = node.attributes.style == null ? [] : parseStyleDeclarations(node.attributes.style); for (const { name, value, important } of styleDeclarations) { const computed = computedStyle[name]; if (computed && computed.type === 'dynamic') { continue; } if ( computed == null || important === true || importantStyles.get(name) === false ) { computedStyle[name] = { type: 'static', inherited: false, value }; importantStyles.set(name, important); } } return computedStyle; }; /** * Compares two selector specificities. * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211 * * @type {(a: Specificity, b: Specificity) => number} */ const compareSpecificity = (a, b) => { for (var i = 0; i < 4; i += 1) { if (a[i] < b[i]) { return -1; } else if (a[i] > b[i]) { return 1; } } return 0; }; /** * @type {(root: XastRoot) => Stylesheet} */ const collectStylesheet = (root) => { /** * @type {Array} */ const rules = []; /** * @type {Map} */ const parents = new Map(); visit(root, { element: { enter: (node, parentNode) => { // store parents parents.set(node, parentNode); // find and parse all styles if (node.name === 'style') { const dynamic = node.attributes.media != null && node.attributes.media !== 'all'; if ( node.attributes.type == null || node.attributes.type === '' || node.attributes.type === 'text/css' ) { const children = node.children; for (const child of children) { if (child.type === 'text' || child.type === 'cdata') { rules.push(...parseStylesheet(child.value, dynamic)); } } } } }, }, }); // sort by selectors specificity stable.inplace(rules, (a, b) => compareSpecificity(a.specificity, b.specificity) ); return { rules, parents }; }; exports.collectStylesheet = collectStylesheet; /** * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles} */ const computeStyle = (stylesheet, node) => { const { parents } = stylesheet; // collect inherited styles const computedStyles = computeOwnStyle(stylesheet, node); let parent = parents.get(node); while (parent != null && parent.type !== 'root') { const inheritedStyles = computeOwnStyle(stylesheet, parent); for (const [name, computed] of Object.entries(inheritedStyles)) { if ( computedStyles[name] == null && // ignore not inheritable styles inheritableAttrs.includes(name) === true && presentationNonInheritableGroupAttrs.includes(name) === false ) { computedStyles[name] = { ...computed, inherited: true }; } } parent = parents.get(parent); } return computedStyles; }; exports.computeStyle = computeStyle;