Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add minifyColors plugin #51

Merged
merged 7 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/builtin.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as convertStyleToAttrs from '../plugins/convertStyleToAttrs.js';
import * as createGroups from '../plugins/createGroups.js';
import * as inlineStyles from '../plugins/inlineStyles.js';
import * as mergePaths from '../plugins/mergePaths.js';
import * as minifyColors from '../plugins/minifyColors.js';
import * as minifyPathData from '../plugins/minifyPathData.js';
import * as minifyStyles from '../plugins/minifyStyles.js';
import * as minifyTransforms from '../plugins/minifyTransforms.js';
Expand Down Expand Up @@ -89,6 +90,7 @@ export const builtin = Object.freeze([
createGroups,
inlineStyles,
mergePaths,
minifyColors,
minifyPathData,
minifyStyles,
minifyTransforms,
Expand Down
251 changes: 251 additions & 0 deletions lib/color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { colorsNames, colorsShortNames } from '../plugins/_collections.js';
import { AttValue } from './attvalue.js';

const REGEX_RGB = /rgb\((.*)\)/i;
const REGEX_RGB_ARGS = /\s|,/;

export class ColorValue extends AttValue {
/**
* @param {string|undefined} strVal
*/
constructor(strVal) {
super(strVal);
}

/**
* @param {string} value
* @returns {ColorValue}
*/
static #createColorObj(value) {
value = value.trim();
const lower = value.toLowerCase();
if (value.startsWith('#')) {
const obj = HexColor.create(value.substring(1));
if (obj) {
return obj;
}
} else if (colorsNames[lower]) {
return new ExtendedColor(value);
} else if (lower.startsWith('rgb(')) {
const obj = value.includes('%')
? RGBPctColor.create(value)
: RGBColor.create(value);
if (obj) {
return obj;
}
}
return new ColorValue(value);
}

/**
* @param {string|AttValue} value
* @returns {ColorValue}
*/
static getColorObj(value) {
if (typeof value === 'string') {
return this.#createColorObj(value);
}
if (value instanceof ColorValue) {
return value;
}
throw value;
}

/**
* @returns {ColorValue}
*/
getMinifiedValue() {
return this;
}
}

class ExtendedColor extends ColorValue {
/**
* @returns {ColorValue}
*/
getMinifiedValue() {
let value = this.toString().toLowerCase();
const hexString = colorsNames[value];
if (hexString.length < value.length) {
return new HexColor(hexString);
}
return new ExtendedColor(value);
}
}

class HexColor extends ColorValue {
/**
* @param {string} hexDigits
* @returns {HexColor|undefined}
*/
static create(hexDigits) {
if (hexDigits.length !== 3 && hexDigits.length !== 6) {
return;
}
for (const char of hexDigits.toLowerCase()) {
switch (char) {
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
break;
default:
return;
}
}

return new HexColor('#' + hexDigits);
}

/**
* @returns {ColorValue}
*/
getMinifiedValue() {
let value = this.toString().toLowerCase();

if (value.length === 7) {
// See if it can be shortened to 3 characters.
if (
value[1] === value[2] &&
value[3] === value[4] &&
value[5] === value[6]
) {
return new HexColor(
'#' + value[1] + value[3] + value[5],
).getMinifiedValue();
}
}
const name = colorsShortNames[value];
if (name && name.length <= value.length) {
return new ExtendedColor(name);
}
return new HexColor(value);
}
}

class RGBColor extends ColorValue {
#rgb;

/**
* @param {string} fn
* @param {[number,number,number]} rgb
*/
constructor(fn, rgb) {
super(fn);
this.#rgb = rgb;
}

/**
* @param {string} fn
* @returns {RGBColor|undefined}
*/
static create(fn) {
const match = REGEX_RGB.exec(fn);
if (!match) {
return;
}
const strArgs = match[1];
if (!strArgs) {
return;
}
const rawArgs = strArgs.split(REGEX_RGB_ARGS);
const args = rawArgs.filter((a) => a !== '');
if (args.length !== 3) {
return;
}
const nums = args.map((str) => {
const n = parseInt(str);
if (n < 0 || n > 255) {
return;
}
if (str === n.toString()) {
return n;
}
});
if (nums.some((n) => n === undefined)) {
return;
}
// @ts-ignore - undefined values excluded above
return new RGBColor(fn, nums);
}

/**
* @returns {ColorValue}
*/
getMinifiedValue() {
return new HexColor(
this.#rgb.reduce((str, val) => {
const hex = val.toString(16);
return str + (hex.length === 1 ? '0' + hex : hex);
}, '#'),
).getMinifiedValue();
}
}

class RGBPctColor extends ColorValue {
#rgb;

/**
* @param {string} fn
* @param {[number,number,number]} rgb
*/
constructor(fn, rgb) {
super(fn);
this.#rgb = rgb;
}

/**
* @param {string} fn
* @returns {RGBColor|undefined}
*/
static create(fn) {
const match = REGEX_RGB.exec(fn);
if (!match) {
return;
}
const strArgs = match[1];
if (!strArgs) {
return;
}
const rawArgs = strArgs.split(REGEX_RGB_ARGS);
const args = rawArgs.filter((a) => a !== '');
if (args.length !== 3) {
return;
}
const nums = args.map((str) => {
if (str.length === 1 || !str.endsWith('%')) {
return;
}
const numStr = str.substring(0, str.length - 1);
const n = parseFloat(numStr);
if (n < 0 || n > 100 || numStr !== n.toString()) {
return;
}
return n;
});
if (nums.some((n) => n === undefined)) {
return;
}
// @ts-ignore - undefined values excluded above
return new RGBPctColor(fn, nums);
}

/**
* @returns {ColorValue}
*/
getMinifiedValue() {
return new ColorValue(`rgb(${this.#rgb.map((n) => n + '%').join(',')})`);
}
}
79 changes: 79 additions & 0 deletions plugins/minifyColors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ColorValue } from '../lib/color.js';
import { getStyleDeclarations } from '../lib/css-tools.js';
import { writeStyleAttribute } from '../lib/css.js';
import { svgSetAttValue } from '../lib/svg-parse-att.js';

export const name = 'minifyColors';
export const description =
'minifies color values used in attributes and style properties';

/**
* @type {import('./plugins-types.js').Plugin<'minifyColors'>};
'>}
*/
export const fn = (root, params, info) => {
const styleData = info.docData.getStyles();
if (
info.docData.hasScripts() ||
styleData === null ||
styleData.hasAttributeSelector()
) {
return;
}

return {
element: {
enter: (element) => {
// Minify attribute values.
for (const [attName, attVal] of Object.entries(element.attributes)) {
switch (attName) {
case 'fill':
case 'flood-color':
case 'lighting-color':
case 'stop-color':
case 'stroke':
{
const value = ColorValue.getColorObj(attVal);
const min = value.getMinifiedValue();
if (min) {
svgSetAttValue(element, attName, min);
}
}
break;
}
}

// Minify style properties.
const props = getStyleDeclarations(element);
if (!props) {
return;
}
let propChanged = false;
for (const [propName, propValue] of props.entries()) {
switch (propName) {
case 'fill':
case 'flood-color':
case 'lighting-color':
case 'stop-color':
case 'stroke':
{
const value = ColorValue.getColorObj(propValue.value);
const min = value.getMinifiedValue();
if (min) {
propChanged = true;
props.set(propName, {
value: min,
important: propValue.important,
});
}
}
break;
}
}
if (propChanged) {
writeStyleAttribute(element, props);
}
},
},
};
};
1 change: 1 addition & 0 deletions plugins/plugins-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type DefaultPlugins = {
floatPrecision?: number;
noSpaceAfterFlags?: boolean;
};
minifyColors: void;
minifyPathData: void;
minifyStyles: void;
minifyTransforms: void;
Expand Down
4 changes: 2 additions & 2 deletions plugins/preset-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import * as cleanupStyleAttributes from './cleanupStyleAttributes.js';
import * as cleanupXlink from './cleanupXlink.js';
import * as collapseGroups from './collapseGroups.js';
import * as combineStyleElements from './combineStyleElements.js';
import * as convertColors from './convertColors.js';
import * as convertEllipseToCircle from './convertEllipseToCircle.js';
import * as convertShapeToPath from './convertShapeToPath.js';
import * as createGroups from './createGroups.js';
import * as inlineStyles from './inlineStyles.js';
import * as minifyColors from './minifyColors.js';
import * as minifyPathData from './minifyPathData.js';
import * as minifyStyles from './minifyStyles.js';
import * as minifyTransforms from './minifyTransforms.js';
Expand Down Expand Up @@ -42,7 +42,7 @@ const presetDefault = createPreset({
inlineStyles,
minifyStyles,
cleanupIds,
convertColors,
minifyColors,
removeUnknownsAndDefaults,
removeNonInheritableGroupAttrs,
removeUselessStrokeAndFill,
Expand Down
4 changes: 2 additions & 2 deletions plugins/preset-next.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import * as cleanupStyleAttributes from './cleanupStyleAttributes.js';
import * as cleanupXlink from './cleanupXlink.js';
import * as collapseGroups from './collapseGroups.js';
import * as combineStyleElements from './combineStyleElements.js';
import * as convertColors from './convertColors.js';
import * as convertEllipseToCircle from './convertEllipseToCircle.js';
import * as convertShapeToPath from './convertShapeToPath.js';
import * as createGroups from './createGroups.js';
import * as inlineStyles from './inlineStyles.js';
import * as minifyColors from './minifyColors.js';
import * as minifyPathData from './minifyPathData.js';
import * as minifyStyles from './minifyStyles.js';
import * as minifyTransforms from './minifyTransforms.js';
Expand Down Expand Up @@ -42,7 +42,7 @@ const presetNext = createPreset({
inlineStyles,
minifyStyles,
cleanupIds,
convertColors,
minifyColors,
removeUnknownsAndDefaults,
removeNonInheritableGroupAttrs,
removeUselessStrokeAndFill,
Expand Down
Loading