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.
+
+===
+
+
+
+@@@
+
+