123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- '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<StylesheetDeclaration>}
- */
- 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<StylesheetRule>}
- */
- const parseStylesheet = (css, dynamic) => {
- /**
- * @type {Array<StylesheetRule>}
- */
- 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<StylesheetDeclaration>}
- */
- const parseStyleDeclarations = (css) => {
- /**
- * @type {Array<StylesheetDeclaration>}
- */
- 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<StylesheetRule>}
- */
- const rules = [];
- /**
- * @type {Map<XastElement, XastParent>}
- */
- 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;
|