summaryrefslogtreecommitdiff
path: root/node_modules/svgo/plugins/inlineStyles.js
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2023-10-14 15:05:55 +0800
committerlittleblack111 <littleblack11111@gmail.com>2023-10-14 15:05:55 +0800
commitc06f7ee0a40255214c477e13d9b956a7e07c2900 (patch)
treef5982a568ef8fab865d0142e71c7494fea3e4c1b /node_modules/svgo/plugins/inlineStyles.js
parent3fd9400d4dccc25aa29b5702216732e0b7542073 (diff)
postcss,(plugins): nanocss, postcss-preset-env
Diffstat (limited to 'node_modules/svgo/plugins/inlineStyles.js')
-rw-r--r--node_modules/svgo/plugins/inlineStyles.js386
1 files changed, 386 insertions, 0 deletions
diff --git a/node_modules/svgo/plugins/inlineStyles.js b/node_modules/svgo/plugins/inlineStyles.js
new file mode 100644
index 00000000..e931b5f6
--- /dev/null
+++ b/node_modules/svgo/plugins/inlineStyles.js
@@ -0,0 +1,386 @@
+'use strict';
+
+/**
+ * @typedef {import('../lib/types').Specificity} Specificity
+ * @typedef {import('../lib/types').XastElement} XastElement
+ * @typedef {import('../lib/types').XastParent} XastParent
+ */
+
+const csstree = require('css-tree');
+const {
+ // @ts-ignore not defined in @types/csso
+ syntax: { specificity },
+} = require('csso');
+const {
+ visitSkip,
+ querySelectorAll,
+ detachNodeFromParent,
+} = require('../lib/xast.js');
+
+exports.name = 'inlineStyles';
+exports.description = 'inline styles (additional options)';
+
+/**
+ * Compares two selector specificities.
+ * extracted from https://github.com/keeganstreet/specificity/blob/main/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 {(value: any) => any}
+ */
+const toAny = (value) => value;
+
+/**
+ * Moves + merges styles from style elements to element styles
+ *
+ * Options
+ * onlyMatchedOnce (default: true)
+ * inline only selectors that match once
+ *
+ * removeMatchedSelectors (default: true)
+ * clean up matched selectors,
+ * leave selectors that hadn't matched
+ *
+ * useMqs (default: ['', 'screen'])
+ * what media queries to be used
+ * empty string element for styles outside media queries
+ *
+ * usePseudos (default: [''])
+ * what pseudo-classes/-elements to be used
+ * empty string element for all non-pseudo-classes and/or -elements
+ *
+ * @author strarsis <strarsis@gmail.com>
+ *
+ * @type {import('./plugins-types').Plugin<'inlineStyles'>}
+ */
+exports.fn = (root, params) => {
+ const {
+ onlyMatchedOnce = true,
+ removeMatchedSelectors = true,
+ useMqs = ['', 'screen'],
+ usePseudos = [''],
+ } = params;
+
+ /**
+ * @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
+ */
+ const styles = [];
+ /**
+ * @type {Array<{
+ * node: csstree.Selector,
+ * item: csstree.ListItem<csstree.CssNode>,
+ * rule: csstree.Rule,
+ * matchedElements?: Array<XastElement>
+ * }>}
+ */
+ let selectors = [];
+
+ return {
+ element: {
+ enter: (node, parentNode) => {
+ // skip <foreignObject /> content
+ if (node.name === 'foreignObject') {
+ return visitSkip;
+ }
+ // collect only non-empty <style /> elements
+ if (node.name !== 'style' || node.children.length === 0) {
+ return;
+ }
+ // values other than the empty string or text/css are not used
+ if (
+ node.attributes.type != null &&
+ node.attributes.type !== '' &&
+ node.attributes.type !== 'text/css'
+ ) {
+ return;
+ }
+ // parse css in style element
+ let cssText = '';
+ for (const child of node.children) {
+ if (child.type === 'text' || child.type === 'cdata') {
+ cssText += child.value;
+ }
+ }
+ /**
+ * @type {null | csstree.CssNode}
+ */
+ let cssAst = null;
+ try {
+ cssAst = csstree.parse(cssText, {
+ parseValue: false,
+ parseCustomProperty: false,
+ });
+ } catch {
+ return;
+ }
+ if (cssAst.type === 'StyleSheet') {
+ styles.push({ node, parentNode, cssAst });
+ }
+
+ // collect selectors
+ csstree.walk(cssAst, {
+ visit: 'Selector',
+ enter(node, item) {
+ const atrule = this.atrule;
+ const rule = this.rule;
+ if (rule == null) {
+ return;
+ }
+
+ // skip media queries not included into useMqs param
+ let mq = '';
+ if (atrule != null) {
+ mq = atrule.name;
+ if (atrule.prelude != null) {
+ mq += ` ${csstree.generate(atrule.prelude)}`;
+ }
+ }
+ if (useMqs.includes(mq) === false) {
+ return;
+ }
+
+ /**
+ * @type {Array<{
+ * item: csstree.ListItem<csstree.CssNode>,
+ * list: csstree.List<csstree.CssNode>
+ * }>}
+ */
+ const pseudos = [];
+ if (node.type === 'Selector') {
+ node.children.forEach((childNode, childItem, childList) => {
+ if (
+ childNode.type === 'PseudoClassSelector' ||
+ childNode.type === 'PseudoElementSelector'
+ ) {
+ pseudos.push({ item: childItem, list: childList });
+ }
+ });
+ }
+
+ // skip pseudo classes and pseudo elements not includes into usePseudos param
+ const pseudoSelectors = csstree.generate({
+ type: 'Selector',
+ children: new csstree.List().fromArray(
+ pseudos.map((pseudo) => pseudo.item.data)
+ ),
+ });
+ if (usePseudos.includes(pseudoSelectors) === false) {
+ return;
+ }
+
+ // remove pseudo classes and elements to allow querySelector match elements
+ // TODO this is not very accurate since some pseudo classes like first-child
+ // are used for selection
+ for (const pseudo of pseudos) {
+ pseudo.list.remove(pseudo.item);
+ }
+
+ selectors.push({ node, item, rule });
+ },
+ });
+ },
+ },
+
+ root: {
+ exit: () => {
+ if (styles.length === 0) {
+ return;
+ }
+ // stable sort selectors
+ const sortedSelectors = [...selectors]
+ .sort((a, b) => {
+ const aSpecificity = specificity(a.item.data);
+ const bSpecificity = specificity(b.item.data);
+ return compareSpecificity(aSpecificity, bSpecificity);
+ })
+ .reverse();
+
+ for (const selector of sortedSelectors) {
+ // match selectors
+ const selectorText = csstree.generate(selector.item.data);
+ /**
+ * @type {Array<XastElement>}
+ */
+ const matchedElements = [];
+ try {
+ for (const node of querySelectorAll(root, selectorText)) {
+ if (node.type === 'element') {
+ matchedElements.push(node);
+ }
+ }
+ } catch (selectError) {
+ continue;
+ }
+ // nothing selected
+ if (matchedElements.length === 0) {
+ continue;
+ }
+
+ // apply styles to matched elements
+ // skip selectors that match more than once if option onlyMatchedOnce is enabled
+ if (onlyMatchedOnce && matchedElements.length > 1) {
+ continue;
+ }
+
+ // apply <style/> to matched elements
+ for (const selectedEl of matchedElements) {
+ const styleDeclarationList = csstree.parse(
+ selectedEl.attributes.style == null
+ ? ''
+ : selectedEl.attributes.style,
+ {
+ context: 'declarationList',
+ parseValue: false,
+ }
+ );
+ if (styleDeclarationList.type !== 'DeclarationList') {
+ continue;
+ }
+ const styleDeclarationItems = new Map();
+ csstree.walk(styleDeclarationList, {
+ visit: 'Declaration',
+ enter(node, item) {
+ styleDeclarationItems.set(node.property, item);
+ },
+ });
+ // merge declarations
+ csstree.walk(selector.rule, {
+ visit: 'Declaration',
+ enter(ruleDeclaration) {
+ // existing inline styles have higher priority
+ // no inline styles, external styles, external styles used
+ // inline styles, external styles same priority as inline styles, inline styles used
+ // inline styles, external styles higher priority than inline styles, external styles used
+ const matchedItem = styleDeclarationItems.get(
+ ruleDeclaration.property
+ );
+ const ruleDeclarationItem =
+ styleDeclarationList.children.createItem(ruleDeclaration);
+ if (matchedItem == null) {
+ styleDeclarationList.children.append(ruleDeclarationItem);
+ } else if (
+ matchedItem.data.important !== true &&
+ ruleDeclaration.important === true
+ ) {
+ styleDeclarationList.children.replace(
+ matchedItem,
+ ruleDeclarationItem
+ );
+ styleDeclarationItems.set(
+ ruleDeclaration.property,
+ ruleDeclarationItem
+ );
+ }
+ },
+ });
+ selectedEl.attributes.style =
+ csstree.generate(styleDeclarationList);
+ }
+
+ if (
+ removeMatchedSelectors &&
+ matchedElements.length !== 0 &&
+ selector.rule.prelude.type === 'SelectorList'
+ ) {
+ // clean up matching simple selectors if option removeMatchedSelectors is enabled
+ selector.rule.prelude.children.remove(selector.item);
+ }
+ selector.matchedElements = matchedElements;
+ }
+
+ // no further processing required
+ if (removeMatchedSelectors === false) {
+ return;
+ }
+
+ // clean up matched class + ID attribute values
+ for (const selector of sortedSelectors) {
+ if (selector.matchedElements == null) {
+ continue;
+ }
+
+ if (onlyMatchedOnce && selector.matchedElements.length > 1) {
+ // skip selectors that match more than once if option onlyMatchedOnce is enabled
+ continue;
+ }
+
+ for (const selectedEl of selector.matchedElements) {
+ // class
+ const classList = new Set(
+ selectedEl.attributes.class == null
+ ? null
+ : selectedEl.attributes.class.split(' ')
+ );
+ /**
+ * csstree v2 changed this type
+ * @type {csstree.CssNode}
+ */
+ const firstSubSelector = toAny(selector.node.children.first);
+ if (
+ firstSubSelector != null &&
+ firstSubSelector.type === 'ClassSelector'
+ ) {
+ classList.delete(firstSubSelector.name);
+ }
+ if (classList.size === 0) {
+ delete selectedEl.attributes.class;
+ } else {
+ selectedEl.attributes.class = Array.from(classList).join(' ');
+ }
+
+ // ID
+ if (
+ firstSubSelector != null &&
+ firstSubSelector.type === 'IdSelector'
+ ) {
+ if (selectedEl.attributes.id === firstSubSelector.name) {
+ delete selectedEl.attributes.id;
+ }
+ }
+ }
+ }
+
+ for (const style of styles) {
+ csstree.walk(style.cssAst, {
+ visit: 'Rule',
+ enter: function (node, item, list) {
+ // clean up <style/> rulesets without any css selectors left
+ if (
+ node.type === 'Rule' &&
+ node.prelude.type === 'SelectorList' &&
+ // csstree v2 changed this type
+ toAny(node.prelude.children.isEmpty)
+ ) {
+ list.remove(item);
+ }
+ },
+ });
+
+ // csstree v2 changed this type
+ if (toAny(style.cssAst.children.isEmpty)) {
+ // remove emtpy style element
+ detachNodeFromParent(style.node, style.parentNode);
+ } else {
+ // update style element if any styles left
+ const firstChild = style.node.children[0];
+ if (firstChild.type === 'text' || firstChild.type === 'cdata') {
+ firstChild.value = csstree.generate(style.cssAst);
+ }
+ }
+ }
+ },
+ },
+ };
+};