diff --git a/lib/css.js b/lib/css.js index ccb26d3..704f358 100644 --- a/lib/css.js +++ b/lib/css.js @@ -1,5 +1,6 @@ /** * @typedef {import('./types.js').XastElement} XastElement + * @typedef {XastElement&{style?:string,declarations?:import('./types.js').CSSDeclarationMap}} XastElementExtended * @typedef {import('./types.js').CSSFeatures} CSSFeatures * @typedef{{type:'AttributeSelector',name:string,matcher:string|null,value:string|null}} AttributeSelector * @typedef{{type:'ClassSelector',name:string}} ClassSelector @@ -310,6 +311,23 @@ export class CSSParseError extends Error { } } +/** + * @param {XastElementExtended} element + * @returns {import('./types.js').CSSDeclarationMap|undefined} + */ +export function getStyleDeclarations(element) { + const style = element.attributes.style; + if (style === undefined || style === '') { + return; + } + if (element.style !== style) { + element.style = style; + element.declarations = parseStyleDeclarations(style); + } + // Copy cached map in case it is changed by caller. + return new Map(element.declarations); +} + /** * @param {string} css */ @@ -319,9 +337,10 @@ export function _isStyleComplex(css) { /** * @param {string|undefined} css - * @returns {Map} + * @returns {Map} */ export function parseStyleDeclarations(css) { + /** @type {Map} */ const declarations = new Map(); if (css === undefined) { return declarations; @@ -337,9 +356,35 @@ export function parseStyleDeclarations(css) { if (declaration) { const pv = declaration.split(':'); if (pv.length === 2) { - declarations.set(pv[0].trim(), pv[1].trim()); + const dec = pv[1].trim(); + const value = dec.endsWith('!important') + ? { value: dec.substring(0, dec.length - 10).trim(), important: true } + : { value: dec, important: false }; + declarations.set(pv[0].trim(), value); } } } return declarations; } + +/** + * @param {import('../lib/types.js').XastElement} element + * @param {Map} 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; + } else { + delete element.attributes.style; + } +} diff --git a/lib/docdata.js b/lib/docdata.js index f70ee54..e35ddae 100644 --- a/lib/docdata.js +++ b/lib/docdata.js @@ -7,7 +7,7 @@ import { import { parseStylesheet } from './style-css-tree.js'; import { findReferences } from './svgo/tools.js'; import { detachNodeFromParent, visit, visitSkip } from './xast.js'; -import { CSSParseError, parseStyleDeclarations } from './css.js'; +import { CSSParseError, getStyleDeclarations } from './css.js'; /** * @typedef {import('../lib/types.js').XastElement} XastElement @@ -83,6 +83,7 @@ export class StyleData { * @returns {Map} */ computeOwnStyle(node) { + /** @type {Map} */ const computedStyles = new Map(); if (node.type === 'root') { @@ -120,11 +121,14 @@ export class StyleData { } // Override with inline styles. - parseStyleDeclarations(node.attributes.style).forEach((value, name) => { - if (!importantProperties.has(name)) { - computedStyles.set(name, value); - } - }); + const declarations = getStyleDeclarations(node); + if (declarations) { + declarations.forEach((value, name) => { + if (value.important || !importantProperties.has(name)) { + computedStyles.set(name, value.value); + } + }); + } return computedStyles; } diff --git a/lib/style-css-tree-tools.js b/lib/style-css-tree-tools.js index 7320a49..fd21d89 100644 --- a/lib/style-css-tree-tools.js +++ b/lib/style-css-tree-tools.js @@ -2,9 +2,10 @@ import * as csstree from 'css-tree'; /** * @param {string|undefined} css - * @returns {Map} + * @returns {Map} */ export function _parseStyleDeclarations(css) { + /** @type {Map} */ const declarations = new Map(); if (css === undefined) { return declarations; @@ -15,10 +16,10 @@ export function _parseStyleDeclarations(css) { }); csstree.walk(ast, (cssNode) => { if (cssNode.type === 'Declaration') { - declarations.set( - cssNode.property.toLowerCase(), - csstree.generate(cssNode.value), - ); + declarations.set(cssNode.property.toLowerCase(), { + value: csstree.generate(cssNode.value), + important: !!cssNode.important, + }); } }); return declarations; diff --git a/lib/style-css-tree.js b/lib/style-css-tree.js index 16ed780..07a3776 100644 --- a/lib/style-css-tree.js +++ b/lib/style-css-tree.js @@ -209,9 +209,10 @@ function createCSSSelector(node) { /** * @param {csstree.CssNode} ast - * @returns {Map} + * @returns {Map} */ export function parseStyleSheetDeclarations(ast) { + /** @type {Map} */ const declarations = new Map(); csstree.walk(ast, (cssNode) => { switch (cssNode.type) { @@ -221,7 +222,7 @@ export function parseStyleSheetDeclarations(ast) { case 'Declaration': declarations.set(cssNode.property.toLowerCase(), { value: csstree.generate(cssNode.value), - important: cssNode.important, + important: !!cssNode.important, }); break; case 'Raw': diff --git a/lib/style.js b/lib/style.js index 04bc4de..9ac7296 100644 --- a/lib/style.js +++ b/lib/style.js @@ -277,26 +277,3 @@ export const computeStyle = (stylesheet, node) => { } return computedStyles; }; - -/** - * @param {import('../lib/types.js').XastElement} element - * @param {Map} properties - */ -export function writeStyleAttribute(element, properties) { - let style = ''; - for (const [p, v] of properties.entries()) { - if (style !== '') { - style += ';'; - } - if (typeof v === 'string') { - style += `${p}:${v}`; - } else { - style += `${p}:${v.value}`; - } - } - if (style) { - element.attributes.style = style; - } else { - delete element.attributes.style; - } -} diff --git a/lib/types.d.ts b/lib/types.d.ts index d7c7716..1dcc0a7 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -162,6 +162,10 @@ export class CSSRule { matches(element: XastElement): boolean; } +export type CSSPropertyValue = { value: string; important: boolean }; + +export type CSSDeclarationMap = Map; + export type PluginInfo = { path?: string; passNumber: number; diff --git a/plugins/cleanupStyleAttributes.js b/plugins/cleanupStyleAttributes.js index 435f1ba..5c09516 100644 --- a/plugins/cleanupStyleAttributes.js +++ b/plugins/cleanupStyleAttributes.js @@ -1,5 +1,4 @@ -import { parseStyleDeclarations } from '../lib/css.js'; -import { writeStyleAttribute } from '../lib/style.js'; +import { getStyleDeclarations, writeStyleAttribute } from '../lib/css.js'; import { visitSkip } from '../lib/xast.js'; import { elemsGroups, uselessShapeProperties } from './_collections.js'; @@ -170,8 +169,8 @@ export const fn = (root, params, info) => { return; } - const origStyle = node.attributes.style; - if (origStyle === undefined || origStyle === '') { + const origProperties = getStyleDeclarations(node); + if (!origProperties) { return; } @@ -184,7 +183,6 @@ export const fn = (root, params, info) => { } const isShapeGroup = node.name === 'g' && hasOnlyShapeChildren(node); - const origProperties = parseStyleDeclarations(origStyle); for (const [p, v] of origProperties.entries()) { if (!elementCanHaveProperty(node.name, p)) { continue; diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js index 98be0b8..1a32407 100644 --- a/plugins/inlineStyles.js +++ b/plugins/inlineStyles.js @@ -1,11 +1,11 @@ +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/style.js'; - export const name = 'inlineStyles'; export const description = 'Move properties in + + + diff --git a/test/lib/styledata.computestyle.test.js b/test/lib/styledata.computestyle.test.js index 74c9261..7f9d75a 100644 --- a/test/lib/styledata.computestyle.test.js +++ b/test/lib/styledata.computestyle.test.js @@ -183,3 +183,17 @@ test('computeStyle - selector list for tags', () => { expect(getComputed(styleData, treeInfo, 'c', 'fill')).toBe('green'); expect(getComputed(styleData, treeInfo, 'r', 'fill')).toBe('green'); }); + +test('computeStyle - !important', () => { + const data = generateData('./test/lib/docdata/style.computestyle.10.svg'); + const treeInfo = generateTreeData(data.root); + const styleData = data.docData.getStyles(); + + expect(styleData).toBeTruthy(); + if (styleData === null) { + return; + } + + expect(getComputed(styleData, treeInfo, 'y', 'fill')).toBe('yellow'); + expect(getComputed(styleData, treeInfo, 'r', 'fill')).toBe('red'); +}); diff --git a/test/plugins/cleanupStyleAttributes.15.svg.txt b/test/plugins/cleanupStyleAttributes.15.svg.txt new file mode 100644 index 0000000..c025667 --- /dev/null +++ b/test/plugins/cleanupStyleAttributes.15.svg.txt @@ -0,0 +1,23 @@ +Preserve !important on attribute. + +=== + + + + + + + + + +@@@ + + + + + + + + diff --git a/test/plugins/moveElemsStylesToGroup.10.svg.txt b/test/plugins/moveElemsStylesToGroup.10.svg.txt new file mode 100644 index 0000000..6b0b437 --- /dev/null +++ b/test/plugins/moveElemsStylesToGroup.10.svg.txt @@ -0,0 +1,23 @@ +Don't move style if one is !important. + +=== + + + + + + + + + +@@@ + + + + + + + + diff --git a/test/plugins/moveElemsStylesToGroup.11.svg.txt b/test/plugins/moveElemsStylesToGroup.11.svg.txt new file mode 100644 index 0000000..ae47f93 --- /dev/null +++ b/test/plugins/moveElemsStylesToGroup.11.svg.txt @@ -0,0 +1,23 @@ +Don't move style if it is !important. + +=== + + + + + + + + + +@@@ + + + + + + + +