diff --git a/lib/builtin.js b/lib/builtin.js index 73bb61f..58e361e 100644 --- a/lib/builtin.js +++ b/lib/builtin.js @@ -19,6 +19,7 @@ import * as createGroups from '../plugins/createGroups.js'; import * as inlineStyles from '../plugins/inlineStyles.js'; import * as inlineUse from '../plugins/inlineUse.js'; import * as mergePaths from '../plugins/mergePaths.js'; +import * as mergeGradients from '../plugins/mergeGradients.js'; import * as minifyColors from '../plugins/minifyColors.js'; import * as minifyGradients from '../plugins/minifyGradients.js'; import * as minifyPathData from '../plugins/minifyPathData.js'; @@ -82,6 +83,7 @@ export const builtin = Object.freeze([ createGroups, inlineStyles, inlineUse, + mergeGradients, mergePaths, minifyColors, minifyGradients, diff --git a/lib/css.js b/lib/css.js index 56be31c..c4d52f7 100644 --- a/lib/css.js +++ b/lib/css.js @@ -5,7 +5,6 @@ import { } from './svgo/tools.js'; /** - * @typedef {import('./types.js').XastElement} XastElement * @typedef {import('./types.js').CSSFeatures} CSSFeatures * @typedef{{type:'AttributeSelector',name:string,matcher:string|null,value:string|null,flags:string|null}} AttributeSelector * @typedef{{type:'ClassSelector',name:string}} ClassSelector @@ -15,7 +14,6 @@ import { * @typedef{{type:'TypeSelector',name:string}} TypeSelector * @typedef{AttributeSelector|ClassSelector|IdSelector|PseudoClassSelector|PseudoElementSelector|TypeSelector} SimpleSelector * @typedef {import('./types.js').CSSDeclarationMap} CSSDeclarationMap - * @typedef {XastElement&{style?:string,declarations?:CSSDeclarationMap}} XastElementExtended */ export class CSSRuleSet { @@ -78,6 +76,19 @@ export class CSSRuleSet { return false; } + /** + * @param {string} id + * @returns {boolean} + */ + hasIdSelector(id) { + for (const rule of this.#rules) { + if (rule.hasIdSelector(id)) { + return true; + } + } + return false; + } + #initFeatures() { const features = new Set(); if (this.#atRule) { @@ -108,7 +119,7 @@ export class CSSRule { #specificity; #declarations; #isInMediaQuery; - /** @type {(function(XastElement):boolean)|undefined} */ + /** @type {(function(import('./types.js').XastElement):boolean)|undefined} */ #matcher; /** @@ -205,12 +216,19 @@ export class CSSRule { return this.#selector.hasAttributeSelector(attName); } + /** + * @param {string} id + */ + hasIdSelector(id) { + return this.#selector.hasIdSelector(id); + } + hasPseudos() { return this.#selector.hasPseudos(); } /** - * @returns {(function(XastElement):boolean)|undefined} + * @returns {(function(import('./types.js').XastElement):boolean)|undefined} */ #initMatcher() { const sequences = this.#selector.getSequences(); @@ -241,7 +259,7 @@ export class CSSRule { } /** - * @param {XastElement} element + * @param {import('./types.js').XastElement} element * @return {boolean|null} */ _matches(element) { @@ -252,7 +270,7 @@ export class CSSRule { } /** - * @param {XastElement} element + * @param {import('./types.js').XastElement} element * @return {boolean} */ // eslint-disable-next-line no-unused-vars @@ -352,6 +370,13 @@ export class CSSSelector { return this.#selectorSequences.some((s) => s.hasAttributeSelector(attName)); } + /** + * @param {string} id + */ + hasIdSelector(id) { + return this.#selectorSequences.some((s) => s.hasIdSelector(id)); + } + hasPseudos() { return this.#strWithoutPseudos !== undefined; } @@ -485,6 +510,18 @@ export class CSSSelectorSequence { return false; } + /** + * @param {string} id + */ + hasIdSelector(id) { + for (const selector of this.#simpleSelectors) { + if (selector.type === 'IdSelector') { + return selector.name === id; + } + } + return false; + } + /** * @param {Map} idMap */ @@ -512,29 +549,3 @@ export class CSSParseError extends Error { super(message); } } - -/** - * @param {XastElementExtended} element - * @param {CSSDeclarationMap} properties - */ -export function writeStyleAttribute(element, properties) { - let style = ''; - for (const [p, decValue] of properties.entries()) { - if (style !== '') { - style += ';'; - } - style += `${p}:${decValue.value}`; - if (decValue.important) { - style += '!important'; - } - } - if (style) { - element.attributes.style = style; - element.style = style; - element.declarations = properties; - } else { - delete element.attributes.style; - delete element.style; - delete element.declarations; - } -} diff --git a/lib/docdata.js b/lib/docdata.js index 3ce8290..3cb99e1 100644 --- a/lib/docdata.js +++ b/lib/docdata.js @@ -12,7 +12,6 @@ import { CSSParseError } from './css.js'; import { cssPropToString, getStyleDeclarations } from './css-tools.js'; /** - * @typedef {import('../lib/types.js').XastElement} XastElement * @typedef {import('../lib/types.js').XastParent} XastParent * @typedef {import('../lib/types.js').XastRoot} XastRoot */ @@ -36,7 +35,7 @@ export class StyleData { #referencedClasses; /** - * @param {XastElement[]} styleElements + * @param {import('../lib/types.js').XastElement[]} styleElements * @param {CSSRuleSet[]} ruleSets */ constructor(styleElements, ruleSets) { @@ -178,7 +177,7 @@ export class StyleData { } /** - * @param {XastElement} element + * @param {import('../lib/types.js').XastElement} element * @param {{element:XastParent,styles?:Map}[]} parentInfo * @param {CSSDeclarationMap} [declarations] * @returns {Map} @@ -245,7 +244,7 @@ export class StyleData { } /** - * @param {XastElement} element + * @param {import('../lib/types.js').XastElement} element */ getMatchingRules(element) { const rules = []; @@ -306,6 +305,19 @@ export class StyleData { return this.getReferencedClasses().has(className); } + /** + * @param {string} id + * @returns {boolean} + */ + hasIdSelector(id) { + for (const ruleSet of this.#ruleSets) { + if (ruleSet.hasIdSelector(id)) { + return true; + } + } + return false; + } + /** * @param {CSSFeatures[]} features */ @@ -333,7 +345,7 @@ export class StyleData { mergeStyles() { /** - * @param {XastElement} element + * @param {import('../lib/types.js').XastElement} element */ function gatherCSS(element) { let css = getCSS(element); @@ -486,7 +498,7 @@ class DocData { } /** - * @param {XastElement} styleElement + * @param {import('../lib/types.js').XastElement} styleElement */ function getCSS(styleElement) { let css = ''; @@ -499,7 +511,7 @@ function getCSS(styleElement) { } /** - * @param {XastElement} styleElement + * @param {import('../lib/types.js').XastElement} styleElement */ function getRuleSets(styleElement) { /** @type {CSSRuleSet[]} */ @@ -537,11 +549,11 @@ function getRuleSets(styleElement) { * @param {XastRoot} root */ export const getDocData = (root) => { - /** @type {XastElement[]} */ + /** @type {import('../lib/types.js').XastElement[]} */ const styleElements = []; /** @type {CSSRuleSet[]} */ const ruleSets = []; - /** @type {Map} */ + /** @type {Map} */ const parents = new Map(); let styleError = false; let hasAnimations = false; diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index c3b7178..f9e43e2 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -527,6 +527,45 @@ export function updateReferencedDeclarationIds(decls, idMap) { } } +/** + * @param {import('../types.js').XastElement} element + * @param {string} attName + * @param {Map} idMap + */ +export function updateReferencedId(element, attName, idMap) { + if (attName === 'style') { + updateReferencedStyleId(element, idMap); + return; + } + + const attValue = element.attributes[attName]; + const ids = getReferencedIdsInAttribute(attName, attValue); + if (ids.length !== 1) { + throw new Error(); + } + const newId = idMap.get(ids[0].id); + if (newId === undefined) { + throw new Error(); + } + element.attributes[attName] = attValue.replace( + '#' + ids[0].literalString, + '#' + newId, + ); +} + +/** + * @param {import('../types.js').XastElement} element + * @param {Map} idMap + */ +function updateReferencedStyleId(element, idMap) { + const decls = getStyleDeclarations(element); + if (decls === undefined) { + throw new Error(); + } + updateReferencedDeclarationIds(decls, idMap); + writeStyleAttribute(element, decls); +} + /** * @param {string} attribute * @param {string} value @@ -584,3 +623,29 @@ export const toFixed = (num, precision) => { const pow = 10 ** precision; return Math.round(num * pow) / pow; }; + +/** + * @param {import('../types.js').XastElement&{style?:string,declarations?:import('../types.js').CSSDeclarationMap}} element + * @param {import('../types.js').CSSDeclarationMap} properties + */ +export function writeStyleAttribute(element, properties) { + let style = ''; + for (const [p, decValue] of properties.entries()) { + if (style !== '') { + style += ';'; + } + style += `${p}:${decValue.value}`; + if (decValue.important) { + style += '!important'; + } + } + if (style) { + element.attributes.style = style; + element.style = style; + element.declarations = properties; + } else { + delete element.attributes.style; + delete element.style; + delete element.declarations; + } +} diff --git a/lib/types.d.ts b/lib/types.d.ts index cac0b49..749dbb3 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -131,6 +131,7 @@ export class StyleData { getReferencedIds(): Map; hasAttributeSelector(attName?: string): boolean; hasClassReference(className: string): boolean; + hasIdSelector(id: string): boolean; hasOnlyFeatures(features: CSSFeatures[]): boolean; hasStyles(): boolean; mergeStyles(): void; diff --git a/plugins/cleanupIds.js b/plugins/cleanupIds.js index f3a5717..85d4e8d 100644 --- a/plugins/cleanupIds.js +++ b/plugins/cleanupIds.js @@ -1,11 +1,8 @@ -import { getStyleDeclarations } from '../lib/css-tools.js'; -import { writeStyleAttribute } from '../lib/css.js'; import { generateId, - getReferencedIdsInAttribute, recordReferencedIds, SVGOError, - updateReferencedDeclarationIds, + updateReferencedId, } from '../lib/svgo/tools.js'; import { visitSkip } from '../lib/xast.js'; import { elemsGroups } from './_collections.js'; @@ -13,45 +10,6 @@ import { elemsGroups } from './_collections.js'; export const name = 'cleanupIds'; export const description = 'removes unused IDs and minifies used'; -/** - * @param {import('../lib/types.js').XastElement} element - * @param {string} attName - * @param {Map} idMap - */ -function updateReferencedId(element, attName, idMap) { - if (attName === 'style') { - updateReferencedStyleId(element, idMap); - return; - } - - const attValue = element.attributes[attName]; - const ids = getReferencedIdsInAttribute(attName, attValue); - if (ids.length !== 1) { - throw new Error(); - } - const newId = idMap.get(ids[0].id); - if (newId === undefined) { - throw new Error(); - } - element.attributes[attName] = attValue.replace( - '#' + ids[0].literalString, - '#' + newId, - ); -} - -/** - * @param {import('../lib/types.js').XastElement} element - * @param {Map} idMap - */ -function updateReferencedStyleId(element, idMap) { - const decls = getStyleDeclarations(element); - if (decls === undefined) { - throw new Error(); - } - updateReferencedDeclarationIds(decls, idMap); - writeStyleAttribute(element, decls); -} - /** * Remove unused and minify used IDs * diff --git a/plugins/cleanupStyleAttributes.js b/plugins/cleanupStyleAttributes.js index 9a31e57..59b7817 100644 --- a/plugins/cleanupStyleAttributes.js +++ b/plugins/cleanupStyleAttributes.js @@ -1,6 +1,6 @@ import { getStyleDeclarations } from '../lib/css-tools.js'; -import { writeStyleAttribute } from '../lib/css.js'; import { LengthOrPctValue } from '../lib/lengthOrPct.js'; +import { writeStyleAttribute } from '../lib/svgo/tools.js'; import { visitSkip } from '../lib/xast.js'; import { elemsGroups, diff --git a/plugins/cleanupTextElements.js b/plugins/cleanupTextElements.js index a709ddf..0ba7c61 100644 --- a/plugins/cleanupTextElements.js +++ b/plugins/cleanupTextElements.js @@ -1,5 +1,5 @@ import { getStyleDeclarations } from '../lib/css-tools.js'; -import { writeStyleAttribute } from '../lib/css.js'; +import { writeStyleAttribute } from '../lib/svgo/tools.js'; export const name = 'cleanupTextElements'; export const description = 'simplify elements and content'; diff --git a/plugins/createGroups.js b/plugins/createGroups.js index b3e1665..8ad39e9 100644 --- a/plugins/createGroups.js +++ b/plugins/createGroups.js @@ -1,8 +1,7 @@ import { cssPropToString, getStyleDeclarations } from '../lib/css-tools.js'; -import { writeStyleAttribute } from '../lib/css.js'; import { svgSetAttValue } from '../lib/svg-parse-att.js'; import { cssTransformToSVGAtt } from '../lib/svg-to-css.js'; -import { getHrefId } from '../lib/svgo/tools.js'; +import { getHrefId, writeStyleAttribute } from '../lib/svgo/tools.js'; import { getInheritableProperties } from './_styles.js'; export const name = 'createGroups'; diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js index 1a32407..4749c37 100644 --- a/plugins/inlineStyles.js +++ b/plugins/inlineStyles.js @@ -1,11 +1,10 @@ -import { writeStyleAttribute } from '../lib/css.js'; - /** - * @typedef {import('../lib/types.js').XastElement} XastElement * @typedef {import('../lib/types.js').XastParent} XastParent * @typedef {import('../lib/types.js').CSSRule} CSSRule */ +import { writeStyleAttribute } from '../lib/svgo/tools.js'; + export const name = 'inlineStyles'; export const description = 'Move properties in + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + diff --git a/test/plugins/mergeGradients.03.svg.txt b/test/plugins/mergeGradients.03.svg.txt new file mode 100644 index 0000000..051b10a --- /dev/null +++ b/test/plugins/mergeGradients.03.svg.txt @@ -0,0 +1,33 @@ +Merge gradients that are referenced in a + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + +