Fix code quality violations and exclude Manifest from checks
Document application modes (development/debug/production) Add global file drop handler, order column normalization, SPA hash fix Serve CDN assets via /_vendor/ URLs instead of merging into bundles Add production minification with license preservation Improve JSON formatting for debugging and production optimization Add CDN asset caching with CSS URL inlining for production builds Add three-mode system (development, debug, production) Update Manifest CLAUDE.md to reflect helper class architecture Refactor Manifest.php into helper classes for better organization Pre-manifest-refactor checkpoint: Add app_mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
334
node_modules/svgo/plugins/inlineStyles.js
generated
vendored
334
node_modules/svgo/plugins/inlineStyles.js
generated
vendored
@@ -1,72 +1,48 @@
|
||||
'use strict';
|
||||
import * as csstree from 'css-tree';
|
||||
import { syntax } from 'csso';
|
||||
import { attrsGroups, pseudoClasses } from './_collections.js';
|
||||
import { detachNodeFromParent, querySelectorAll } from '../lib/xast.js';
|
||||
import { visitSkip } from '../lib/util/visit.js';
|
||||
import { compareSpecificity, includesAttrSelector } from '../lib/style.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../lib/types').Specificity} Specificity
|
||||
* @typedef {import('../lib/types').XastElement} XastElement
|
||||
* @typedef {import('../lib/types').XastParent} XastParent
|
||||
* @typedef InlineStylesParams
|
||||
* @property {boolean=} onlyMatchedOnce Inlines selectors that match once only.
|
||||
* @property {boolean=} removeMatchedSelectors
|
||||
* Clean up matched selectors. Unused selects are left as-is.
|
||||
* @property {string[]=} useMqs
|
||||
* Media queries to use. An empty string indicates all selectors outside of
|
||||
* media queries.
|
||||
* @property {string[]=} usePseudos
|
||||
* Pseudo-classes and elements to use. An empty string indicates all
|
||||
* non-pseudo-classes and elements.
|
||||
*/
|
||||
|
||||
const csstree = require('css-tree');
|
||||
// @ts-ignore not defined in @types/csso
|
||||
const specificity = require('csso/lib/restructure/prepare/specificity');
|
||||
const stable = require('stable');
|
||||
const {
|
||||
visitSkip,
|
||||
querySelectorAll,
|
||||
detachNodeFromParent,
|
||||
} = require('../lib/xast.js');
|
||||
|
||||
exports.type = 'visitor';
|
||||
exports.name = 'inlineStyles';
|
||||
exports.active = true;
|
||||
exports.description = 'inline styles (additional options)';
|
||||
export const name = 'inlineStyles';
|
||||
export const description = 'inline styles (additional options)';
|
||||
|
||||
/**
|
||||
* Compares two selector specificities.
|
||||
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
|
||||
* Some pseudo-classes can only be calculated by clients, like :visited,
|
||||
* :future, or :hover, but there are other pseudo-classes that we can evaluate
|
||||
* during optimization.
|
||||
*
|
||||
* @type {(a: Specificity, b: Specificity) => number}
|
||||
* Pseudo-classes that we can evaluate during optimization, and shouldn't be
|
||||
* toggled conditionally through the `usePseudos` parameter.
|
||||
*
|
||||
* @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes
|
||||
*/
|
||||
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;
|
||||
};
|
||||
const preservedPseudos = [
|
||||
...pseudoClasses.functional,
|
||||
...pseudoClasses.treeStructural,
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Merges styles from style nodes into inline styles.
|
||||
*
|
||||
* @type {import('../lib/types.js').Plugin<InlineStylesParams>}
|
||||
* @author strarsis <strarsis@gmail.com>
|
||||
*
|
||||
* @type {import('../lib/types').Plugin<{
|
||||
* onlyMatchedOnce?: boolean,
|
||||
* removeMatchedSelectors?: boolean,
|
||||
* useMqs?: Array<string>,
|
||||
* usePseudos?: Array<string>
|
||||
* }>}
|
||||
*/
|
||||
exports.fn = (root, params) => {
|
||||
export const fn = (root, params) => {
|
||||
const {
|
||||
onlyMatchedOnce = true,
|
||||
removeMatchedSelectors = true,
|
||||
@@ -75,31 +51,32 @@ exports.fn = (root, params) => {
|
||||
} = params;
|
||||
|
||||
/**
|
||||
* @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
|
||||
* @type {{
|
||||
* node: import('../lib/types.js').XastElement,
|
||||
* parentNode: import('../lib/types.js').XastParent,
|
||||
* cssAst: csstree.StyleSheet
|
||||
* }[]}
|
||||
*/
|
||||
const styles = [];
|
||||
/**
|
||||
* @type {Array<{
|
||||
* @type {{
|
||||
* node: csstree.Selector,
|
||||
* item: csstree.ListItem<csstree.CssNode>,
|
||||
* rule: csstree.Rule,
|
||||
* matchedElements?: Array<XastElement>
|
||||
* }>}
|
||||
* matchedElements?: import('../lib/types.js').XastElement[]
|
||||
* }[]}
|
||||
*/
|
||||
let selectors = [];
|
||||
const 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 !== '' &&
|
||||
@@ -107,16 +84,13 @@ exports.fn = (root, params) => {
|
||||
) {
|
||||
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}
|
||||
*/
|
||||
|
||||
const cssText = node.children
|
||||
.filter((child) => child.type === 'text' || child.type === 'cdata')
|
||||
.map((child) => child.value)
|
||||
.join('');
|
||||
|
||||
/** @type {?csstree.CssNode} */
|
||||
let cssAst = null;
|
||||
try {
|
||||
cssAst = csstree.parse(cssText, {
|
||||
@@ -132,63 +106,68 @@ exports.fn = (root, params) => {
|
||||
|
||||
// collect selectors
|
||||
csstree.walk(cssAst, {
|
||||
visit: 'Selector',
|
||||
enter(node, item) {
|
||||
visit: 'Rule',
|
||||
enter(node) {
|
||||
const atrule = this.atrule;
|
||||
const rule = this.rule;
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip media queries not included into useMqs param
|
||||
let mq = '';
|
||||
let mediaQuery = '';
|
||||
if (atrule != null) {
|
||||
mq = atrule.name;
|
||||
mediaQuery = atrule.name;
|
||||
if (atrule.prelude != null) {
|
||||
mq += ` ${csstree.generate(atrule.prelude)}`;
|
||||
mediaQuery += ` ${csstree.generate(atrule.prelude)}`;
|
||||
}
|
||||
}
|
||||
if (useMqs.includes(mq) === false) {
|
||||
if (!useMqs.includes(mediaQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Array<{
|
||||
* item: csstree.ListItem<csstree.CssNode>,
|
||||
* list: csstree.List<csstree.CssNode>
|
||||
* }>}
|
||||
*/
|
||||
const pseudos = [];
|
||||
if (node.type === 'Selector') {
|
||||
node.children.each((childNode, childItem, childList) => {
|
||||
if (
|
||||
childNode.type === 'PseudoClassSelector' ||
|
||||
childNode.type === 'PseudoElementSelector'
|
||||
) {
|
||||
pseudos.push({ item: childItem, list: childList });
|
||||
if (node.prelude.type === 'SelectorList') {
|
||||
node.prelude.children.forEach((childNode, item) => {
|
||||
if (childNode.type === 'Selector') {
|
||||
/**
|
||||
* @type {{
|
||||
* item: csstree.ListItem<csstree.CssNode>,
|
||||
* list: csstree.List<csstree.CssNode>
|
||||
* }[]}
|
||||
*/
|
||||
const pseudos = [];
|
||||
|
||||
childNode.children.forEach(
|
||||
(grandchildNode, grandchildItem, grandchildList) => {
|
||||
const isPseudo =
|
||||
grandchildNode.type === 'PseudoClassSelector' ||
|
||||
grandchildNode.type === 'PseudoElementSelector';
|
||||
|
||||
if (
|
||||
isPseudo &&
|
||||
!preservedPseudos.includes(grandchildNode.name)
|
||||
) {
|
||||
pseudos.push({
|
||||
item: grandchildItem,
|
||||
list: grandchildList,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const pseudoSelectors = csstree.generate({
|
||||
type: 'Selector',
|
||||
children: new csstree.List().fromArray(
|
||||
pseudos.map((pseudo) => pseudo.item.data),
|
||||
),
|
||||
});
|
||||
|
||||
if (usePseudos.includes(pseudoSelectors)) {
|
||||
for (const pseudo of pseudos) {
|
||||
pseudo.list.remove(pseudo.item);
|
||||
}
|
||||
}
|
||||
|
||||
selectors.push({ node: childNode, rule: node, item: item });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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 });
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -199,19 +178,19 @@ exports.fn = (root, params) => {
|
||||
if (styles.length === 0) {
|
||||
return;
|
||||
}
|
||||
// stable sort selectors
|
||||
const sortedSelectors = stable(selectors, (a, b) => {
|
||||
const aSpecificity = specificity(a.item.data);
|
||||
const bSpecificity = specificity(b.item.data);
|
||||
return compareSpecificity(aSpecificity, bSpecificity);
|
||||
}).reverse();
|
||||
const sortedSelectors = selectors
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const aSpecificity = syntax.specificity(a.item.data);
|
||||
const bSpecificity = syntax.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>}
|
||||
*/
|
||||
/** @type {import('../lib/types.js').XastElement[]} */
|
||||
const matchedElements = [];
|
||||
try {
|
||||
for (const node of querySelectorAll(root, selectorText)) {
|
||||
@@ -219,7 +198,7 @@ exports.fn = (root, params) => {
|
||||
matchedElements.push(node);
|
||||
}
|
||||
}
|
||||
} catch (selectError) {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
// nothing selected
|
||||
@@ -236,22 +215,28 @@ exports.fn = (root, params) => {
|
||||
// apply <style/> to matched elements
|
||||
for (const selectedEl of matchedElements) {
|
||||
const styleDeclarationList = csstree.parse(
|
||||
selectedEl.attributes.style == null
|
||||
? ''
|
||||
: selectedEl.attributes.style,
|
||||
selectedEl.attributes.style ?? '',
|
||||
{
|
||||
context: 'declarationList',
|
||||
parseValue: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
if (styleDeclarationList.type !== 'DeclarationList') {
|
||||
continue;
|
||||
}
|
||||
const styleDeclarationItems = new Map();
|
||||
|
||||
/** @type {csstree.ListItem<csstree.CssNode>} */
|
||||
let firstListItem;
|
||||
|
||||
csstree.walk(styleDeclarationList, {
|
||||
visit: 'Declaration',
|
||||
enter(node, item) {
|
||||
styleDeclarationItems.set(node.property, item);
|
||||
if (firstListItem == null) {
|
||||
firstListItem = item;
|
||||
}
|
||||
|
||||
styleDeclarationItems.set(node.property.toLowerCase(), item);
|
||||
},
|
||||
});
|
||||
// merge declarations
|
||||
@@ -262,30 +247,42 @@ exports.fn = (root, params) => {
|
||||
// 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 property = ruleDeclaration.property;
|
||||
|
||||
if (
|
||||
attrsGroups.presentation.has(property) &&
|
||||
!selectors.some((selector) =>
|
||||
includesAttrSelector(selector.item, property),
|
||||
)
|
||||
) {
|
||||
delete selectedEl.attributes[property];
|
||||
}
|
||||
|
||||
const matchedItem = styleDeclarationItems.get(property);
|
||||
const ruleDeclarationItem =
|
||||
styleDeclarationList.children.createItem(ruleDeclaration);
|
||||
if (matchedItem == null) {
|
||||
styleDeclarationList.children.append(ruleDeclarationItem);
|
||||
styleDeclarationList.children.insert(
|
||||
ruleDeclarationItem,
|
||||
firstListItem,
|
||||
);
|
||||
} else if (
|
||||
matchedItem.data.important !== true &&
|
||||
ruleDeclaration.important === true
|
||||
) {
|
||||
styleDeclarationList.children.replace(
|
||||
matchedItem,
|
||||
ruleDeclarationItem
|
||||
);
|
||||
styleDeclarationItems.set(
|
||||
ruleDeclaration.property,
|
||||
ruleDeclarationItem
|
||||
ruleDeclarationItem,
|
||||
);
|
||||
styleDeclarationItems.set(property, ruleDeclarationItem);
|
||||
}
|
||||
},
|
||||
});
|
||||
selectedEl.attributes.style =
|
||||
csstree.generate(styleDeclarationList);
|
||||
|
||||
const newStyles = csstree.generate(styleDeclarationList);
|
||||
if (newStyles.length !== 0) {
|
||||
selectedEl.attributes.style = newStyles;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -300,7 +297,7 @@ exports.fn = (root, params) => {
|
||||
}
|
||||
|
||||
// no further processing required
|
||||
if (removeMatchedSelectors === false) {
|
||||
if (!removeMatchedSelectors) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,15 +317,25 @@ exports.fn = (root, params) => {
|
||||
const classList = new Set(
|
||||
selectedEl.attributes.class == null
|
||||
? null
|
||||
: selectedEl.attributes.class.split(' ')
|
||||
: selectedEl.attributes.class.split(' '),
|
||||
);
|
||||
const firstSubSelector = selector.node.children.first();
|
||||
if (
|
||||
firstSubSelector != null &&
|
||||
firstSubSelector.type === 'ClassSelector'
|
||||
) {
|
||||
classList.delete(firstSubSelector.name);
|
||||
|
||||
for (const child of selector.node.children) {
|
||||
if (
|
||||
child.type === 'ClassSelector' &&
|
||||
!selectors.some((selector) =>
|
||||
includesAttrSelector(
|
||||
selector.item,
|
||||
'class',
|
||||
child.name,
|
||||
true,
|
||||
),
|
||||
)
|
||||
) {
|
||||
classList.delete(child.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (classList.size === 0) {
|
||||
delete selectedEl.attributes.class;
|
||||
} else {
|
||||
@@ -336,13 +343,20 @@ exports.fn = (root, params) => {
|
||||
}
|
||||
|
||||
// ID
|
||||
const firstSubSelector = selector.node.children.first;
|
||||
if (
|
||||
firstSubSelector != null &&
|
||||
firstSubSelector.type === 'IdSelector'
|
||||
firstSubSelector?.type === 'IdSelector' &&
|
||||
selectedEl.attributes.id === firstSubSelector.name &&
|
||||
!selectors.some((selector) =>
|
||||
includesAttrSelector(
|
||||
selector.item,
|
||||
'id',
|
||||
firstSubSelector.name,
|
||||
true,
|
||||
),
|
||||
)
|
||||
) {
|
||||
if (selectedEl.attributes.id === firstSubSelector.name) {
|
||||
delete selectedEl.attributes.id;
|
||||
}
|
||||
delete selectedEl.attributes.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,15 +369,15 @@ exports.fn = (root, params) => {
|
||||
if (
|
||||
node.type === 'Rule' &&
|
||||
node.prelude.type === 'SelectorList' &&
|
||||
node.prelude.children.isEmpty()
|
||||
node.prelude.children.isEmpty
|
||||
) {
|
||||
list.remove(item);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (style.cssAst.children.isEmpty()) {
|
||||
// remove emtpy style element
|
||||
if (style.cssAst.children.isEmpty) {
|
||||
// remove empty style element
|
||||
detachNodeFromParent(style.node, style.parentNode);
|
||||
} else {
|
||||
// update style element if any styles left
|
||||
|
||||
Reference in New Issue
Block a user