From 44d21097b290e46579d2f5539119c51eb2d44864 Mon Sep 17 00:00:00 2001 From: johnkenny54 <45182853+johnkenny54@users.noreply.github.com> Date: Thu, 14 Nov 2024 06:54:14 -0800 Subject: [PATCH] feat(cleanupStyleAttributes): minify numeric values (#82) --- lib/attvalue.js | 55 ------------ lib/length.js | 53 ++++++++++++ lib/lengthOrPct.js | 22 +++++ lib/numericvalue.js | 83 +++++++++++++++++++ lib/opacity.js | 55 ++++++++++++ lib/stop-offset.js | 82 ++---------------- lib/svg-parse-att.js | 1 - plugins/cleanupStyleAttributes.js | 17 +++- plugins/round.js | 2 +- test/lib/length.test.js | 18 ++++ .../plugins/cleanupStyleAttributes.16.svg.txt | 15 ++++ .../plugins/cleanupStyleAttributes.17.svg.txt | 19 +++++ 12 files changed, 288 insertions(+), 134 deletions(-) create mode 100644 lib/lengthOrPct.js create mode 100644 lib/numericvalue.js create mode 100644 lib/opacity.js create mode 100644 test/lib/length.test.js create mode 100644 test/plugins/cleanupStyleAttributes.16.svg.txt create mode 100644 test/plugins/cleanupStyleAttributes.17.svg.txt diff --git a/lib/attvalue.js b/lib/attvalue.js index 6559fd9..33b6a18 100644 --- a/lib/attvalue.js +++ b/lib/attvalue.js @@ -1,5 +1,3 @@ -import { minifyNumber } from './svgo/tools.js'; - export class AttValue { #strVal; /** @@ -26,56 +24,3 @@ export class AttValue { return this.#strVal; } } - -export class OpacityValue extends AttValue { - /** @type {string|undefined} */ - #strVal; - /** @type {number|undefined} */ - #opacity; - - /** - * @param {string|undefined} strVal - * @param {number} [opacity] - */ - constructor(strVal, opacity) { - super(strVal); - this.#opacity = opacity; - } - - /** - * @returns {number} - */ - getOpacity() { - if (this.#opacity === undefined) { - // If opacity is not set, set it from the original string. - this.#opacity = parseFloat(super.toString()); - } - return this.#opacity; - } - - /** - * @param {import('./types.js').SVGAttValue} value - * @returns {OpacityValue} - */ - static getOpacityObj(value) { - if (typeof value === 'string') { - return new OpacityValue(value); - } - if (value instanceof OpacityValue) { - return value; - } - throw value; - } - - /** - * Override parent method to insure string is minified. - * @returns {string} - */ - toString() { - if (this.#strVal === undefined) { - // Minified string hasn't been generated yet. - this.#strVal = minifyNumber(this.getOpacity()); - } - return this.#strVal; - } -} diff --git a/lib/length.js b/lib/length.js index 65154b0..0688572 100644 --- a/lib/length.js +++ b/lib/length.js @@ -1,4 +1,5 @@ import { AttValue } from './attvalue.js'; +import { ExactNum } from './exactnum.js'; import { isDigit, minifyNumber, toFixed } from './svgo/tools.js'; export class LengthValue extends AttValue { @@ -20,6 +21,18 @@ export class LengthValue extends AttValue { if (isDigit(lastChar) || lastChar === '.') { return new PixelLengthValue(value, undefined); } + let units = ''; + for (let index = value.length - 1; index >= 0; index--) { + const char = value[index]; + if (isDigit(char) || char === '.') { + const num = value.substring(0, index + 1); + if (units === 'px') { + return new PixelLengthValue(num, undefined); + } + return new UnitLengthValue(num, units); + } + units = char + units; + } } return new LengthValue(value); } @@ -38,6 +51,13 @@ export class LengthValue extends AttValue { throw value; } + /** + * @returns {LengthValue} + */ + getMinifiedValue() { + return this; + } + /** * @returns {number|null} */ @@ -74,6 +94,17 @@ class PixelLengthValue extends LengthValue { return minifyNumber(this.#pixels); } + /** + * @returns {LengthValue} + */ + getMinifiedValue() { + const pixels = this.getPixels(); + if (pixels === null) { + throw new Error(); + } + return new PixelLengthValue(minifyNumber(pixels), this.#pixels); + } + /** * @returns {number|null} */ @@ -96,3 +127,25 @@ class PixelLengthValue extends LengthValue { return new PixelLengthValue(undefined, toFixed(pixels, digits)); } } + +class UnitLengthValue extends LengthValue { + #value; + #units; + + /** + * @param {string} value + * @param {string} units + */ + constructor(value, units) { + super(value + units); + this.#value = new ExactNum(value); + this.#units = units; + } + + /** + * @returns {LengthValue} + */ + getMinifiedValue() { + return new UnitLengthValue(this.#value.getMinifiedString(), this.#units); + } +} diff --git a/lib/lengthOrPct.js b/lib/lengthOrPct.js new file mode 100644 index 0000000..5c62619 --- /dev/null +++ b/lib/lengthOrPct.js @@ -0,0 +1,22 @@ +import { LengthValue } from './length.js'; +import { PctValue } from './numericvalue.js'; + +export class LengthOrPctValue { + /** + * @param {import('./types.js').SVGAttValue} value + * @returns {LengthValue|PctValue} + */ + static getLengthOrPctObj(value) { + if (typeof value === 'string') { + const v = value.trim(); + if (v.endsWith('%')) { + const pct = PctValue.createPctValue(v); + if (pct) { + return pct; + } + } + } + + return LengthValue.getLengthObj(value); + } +} diff --git a/lib/numericvalue.js b/lib/numericvalue.js new file mode 100644 index 0000000..7ba7c48 --- /dev/null +++ b/lib/numericvalue.js @@ -0,0 +1,83 @@ +import { AttValue } from './attvalue.js'; +import { ExactNum } from './exactnum.js'; +import { isNumber, toFixed } from './svgo/tools.js'; + +export class NumericValue extends AttValue { + #n; + + /** + * @param {ExactNum} n + */ + constructor(n) { + super(undefined); + this.#n = n; + } + + generateString() { + return this.#n.getMinifiedString(); + } + + getMinifiedValue() { + const value = this.#n.getValue(); + // Use % if it can be represented as a single digit. + if (value >= 0.01 && value <= 0.09 && this.#n.getNumberOfDigits() === 2) { + return new PctValue(new ExactNum(value * 100)); + } + return this; + } + + /** + * @param {number} digits + * @returns {AttValue} + */ + round(digits) { + const value = toFixed(this.#n.getValue(), digits); + return new NumericValue(new ExactNum(value)).getMinifiedValue(); + } +} + +export class PctValue extends AttValue { + #n; + + /** + * @param {ExactNum} n + */ + constructor(n) { + super(undefined); + this.#n = n; + } + + /** + * @param {string} value + * @returns {PctValue|undefined} + */ + static createPctValue(value) { + const pct = value.substring(0, value.length - 1); + if (isNumber(pct)) { + return new PctValue(new ExactNum(pct)); + } + } + + generateString() { + return this.#n.getMinifiedString() + '%'; + } + + getMinifiedValue() { + const pct = this.#n.getValue(); + // Use % if it can be represented as a single digit. + if (pct >= 1 && pct <= 9 && Number.isInteger(pct)) { + return this; + } + return new NumericValue(new ExactNum(pct / 100)); + } + + /** + * @param {number} digits + * @returns {AttValue} + */ + round(digits) { + return new NumericValue(new ExactNum(this.#n.getValue() / 100)).round( + digits, + ); + } +} diff --git a/lib/opacity.js b/lib/opacity.js new file mode 100644 index 0000000..9463d5e --- /dev/null +++ b/lib/opacity.js @@ -0,0 +1,55 @@ +import { AttValue } from './attvalue.js'; +import { minifyNumber } from './svgo/tools.js'; + +export class OpacityValue extends AttValue { + /** @type {string|undefined} */ + #strVal; + /** @type {number|undefined} */ + #opacity; + + /** + * @param {string|undefined} strVal + * @param {number} [opacity] + */ + constructor(strVal, opacity) { + super(strVal); + this.#opacity = opacity; + } + + /** + * @returns {number} + */ + getOpacity() { + if (this.#opacity === undefined) { + // If opacity is not set, set it from the original string. + this.#opacity = parseFloat(super.toString()); + } + return this.#opacity; + } + + /** + * @param {import('./types.js').SVGAttValue} value + * @returns {OpacityValue} + */ + static getOpacityObj(value) { + if (typeof value === 'string') { + return new OpacityValue(value); + } + if (value instanceof OpacityValue) { + return value; + } + throw value; + } + + /** + * Override parent method to insure string is minified. + * @returns {string} + */ + toString() { + if (this.#strVal === undefined) { + // Minified string hasn't been generated yet. + this.#strVal = minifyNumber(this.getOpacity()); + } + return this.#strVal; + } +} diff --git a/lib/stop-offset.js b/lib/stop-offset.js index b345f77..2274e8c 100644 --- a/lib/stop-offset.js +++ b/lib/stop-offset.js @@ -1,6 +1,7 @@ import { AttValue } from './attvalue.js'; import { ExactNum } from './exactnum.js'; -import { isNumber, toFixed } from './svgo/tools.js'; +import { NumericValue, PctValue } from './numericvalue.js'; +import { isNumber } from './svgo/tools.js'; export class StopOffsetValue extends AttValue { /** @@ -17,13 +18,13 @@ export class StopOffsetValue extends AttValue { static #createStopOffsetObj(value) { value = value.trim(); if (value.endsWith('%')) { - const pct = value.substring(0, value.length - 1); - if (isNumber(pct)) { - return new PctStopOffsetValue(new ExactNum(pct)); + const pct = PctValue.createPctValue(value); + if (pct) { + return pct; } } else { if (isNumber(value)) { - return new NumericStopOffsetValue(new ExactNum(value)); + return new NumericValue(new ExactNum(value)); } } return new StopOffsetValue(value); @@ -52,79 +53,10 @@ export class StopOffsetValue extends AttValue { /** * @param {number} digits - * @returns {StopOffsetValue} + * @returns {AttValue} */ // eslint-disable-next-line no-unused-vars round(digits) { return this; } } - -class NumericStopOffsetValue extends StopOffsetValue { - #n; - - /** - * @param {ExactNum} n - */ - constructor(n) { - super(undefined); - this.#n = n; - } - - generateString() { - return this.#n.getMinifiedString(); - } - - getMinifiedValue() { - const value = this.#n.getValue(); - // Use % if it can be represented as a single digit. - if (value >= 0.01 && value <= 0.09 && this.#n.getNumberOfDigits() === 2) { - return new PctStopOffsetValue(new ExactNum(value * 100)); - } - return this; - } - - /** - * @param {number} digits - * @returns {StopOffsetValue} - */ - round(digits) { - const value = toFixed(this.#n.getValue(), digits); - return new NumericStopOffsetValue(new ExactNum(value)).getMinifiedValue(); - } -} - -class PctStopOffsetValue extends StopOffsetValue { - #n; - - /** - * @param {ExactNum} n - */ - constructor(n) { - super(undefined); - this.#n = n; - } - - generateString() { - return this.#n.getMinifiedString() + '%'; - } - - getMinifiedValue() { - const pct = this.#n.getValue(); - // Use % if it can be represented as a single digit. - if (pct >= 1 && pct <= 9 && Number.isInteger(pct)) { - return this; - } - return new NumericStopOffsetValue(new ExactNum(pct / 100)); - } - - /** - * @param {number} digits - * @returns {StopOffsetValue} - */ - round(digits) { - return new NumericStopOffsetValue( - new ExactNum(this.#n.getValue() / 100), - ).round(digits); - } -} diff --git a/lib/svg-parse-att.js b/lib/svg-parse-att.js index 37999ed..990e141 100644 --- a/lib/svg-parse-att.js +++ b/lib/svg-parse-att.js @@ -67,7 +67,6 @@ export function svgParseTransform(str) { } /** - * * @param {import('./types.js').XastElement} element * @param {string} attName * @param {import('./types.js').SVGAttValue} attValue diff --git a/plugins/cleanupStyleAttributes.js b/plugins/cleanupStyleAttributes.js index 38d0fab..9a31e57 100644 --- a/plugins/cleanupStyleAttributes.js +++ b/plugins/cleanupStyleAttributes.js @@ -1,5 +1,6 @@ import { getStyleDeclarations } from '../lib/css-tools.js'; import { writeStyleAttribute } from '../lib/css.js'; +import { LengthOrPctValue } from '../lib/lengthOrPct.js'; import { visitSkip } from '../lib/xast.js'; import { elemsGroups, @@ -31,7 +32,7 @@ export const fn = (root, params, info) => { return; } - const classes = classStr.split(CLASS_SPLITTER); + const classes = classStr.toString().split(CLASS_SPLITTER); const newClasses = classes.filter((c) => styleData.hasClassReference(c)); if (newClasses.length === 0) { delete element.attributes.class; @@ -118,7 +119,19 @@ export const fn = (root, params, info) => { if (isShapeGroup && uselessShapeProperties.has(p)) { continue; } - newProperties.set(p, v); + let newValue = v; + switch (p) { + case 'font-size': + case 'stroke-dashoffset': + case 'stroke-width': + { + const parsedValue = LengthOrPctValue.getLengthOrPctObj(v.value); + const minified = parsedValue.getMinifiedValue(); + newValue.value = minified; + } + break; + } + newProperties.set(p, newValue); } writeStyleAttribute(node, newProperties); }, diff --git a/plugins/round.js b/plugins/round.js index 6a57f95..4481b85 100644 --- a/plugins/round.js +++ b/plugins/round.js @@ -1,8 +1,8 @@ -import { OpacityValue } from '../lib/attvalue.js'; import { ColorValue } from '../lib/color.js'; import { getStyleDeclarations } from '../lib/css-tools.js'; import { writeStyleAttribute } from '../lib/css.js'; import { LengthValue } from '../lib/length.js'; +import { OpacityValue } from '../lib/opacity.js'; import { parsePathCommands, stringifyPathCommands } from '../lib/pathutils.js'; import { StopOffsetValue } from '../lib/stop-offset.js'; import { diff --git a/test/lib/length.test.js b/test/lib/length.test.js new file mode 100644 index 0000000..6b70363 --- /dev/null +++ b/test/lib/length.test.js @@ -0,0 +1,18 @@ +import { LengthOrPctValue } from '../../lib/lengthOrPct.js'; + +describe('test parsing and minifying', () => { + /** @type {{in:string,minified?:string}[]} */ + const testCases = [ + { in: '.12', minified: '.12' }, + { in: '.12px', minified: '.12' }, + { in: '0.90em', minified: '.9em' }, + { in: 'xx', minified: 'xx' }, + ]; + for (const testCase of testCases) { + it(`${testCase.in}`, () => { + const attValue = LengthOrPctValue.getLengthOrPctObj(testCase.in); + const minified = attValue.getMinifiedValue(); + expect(minified.toString()).toBe(testCase.minified ?? testCase); + }); + } +}); diff --git a/test/plugins/cleanupStyleAttributes.16.svg.txt b/test/plugins/cleanupStyleAttributes.16.svg.txt new file mode 100644 index 0000000..23d4017 --- /dev/null +++ b/test/plugins/cleanupStyleAttributes.16.svg.txt @@ -0,0 +1,15 @@ +Minify stroke-width and stroke-dashoffset. + +=== + + + + + + +@@@ + + + + + diff --git a/test/plugins/cleanupStyleAttributes.17.svg.txt b/test/plugins/cleanupStyleAttributes.17.svg.txt new file mode 100644 index 0000000..5647ecb --- /dev/null +++ b/test/plugins/cleanupStyleAttributes.17.svg.txt @@ -0,0 +1,19 @@ +Minify font-size property. + +=== + + + Test + Test + Test + Test + + +@@@ + + + Test + Test + Test + Test +