postcss-color-golf
Version:
PostCSS plugin for aggressive minification and optimization of CSS color values. Make every color a hole-in-one for your bundle size!
485 lines (480 loc) • 15.3 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
var import_postcss_value_parser = __toESM(require("postcss-value-parser"), 1);
// src/skip.ts
var import_vuln_regex_detector = require("vuln-regex-detector");
var DEFAULT_SKIP = ["content", "font-family", "counter-reset"];
function throwUnsafeRegex(rule, idx, docsUrl) {
const src = typeof rule === "string" ? rule : `/${rule.source}/`;
throw new Error(
`Unsafe regex in skip rule "${src}" at index ${idx}. Please choose a safe regex. See ${docsUrl}`
);
}
function parseSkipRule(rule, idx, docsUrl) {
if (rule.startsWith("regex:")) {
const match = /^regex:(.*?)(?::([a-z]*))?$/.exec(rule);
if (!match) {
throw new Error(`Invalid regex skip rule:"${rule}" (See ${docsUrl})`);
}
const [, pattern, flags] = match;
if ((0, import_vuln_regex_detector.test)(pattern)) {
throwUnsafeRegex(rule, idx, docsUrl);
}
try {
const regex = new RegExp(pattern, flags || void 0);
if ((0, import_vuln_regex_detector.test)(regex.source)) {
throwUnsafeRegex(regex, idx, docsUrl);
}
return regex;
} catch (e) {
throw new Error(
`Invalid regex syntax in skip rule "${rule}" at index ${idx}:${e.message} (See ${docsUrl})`
);
}
}
return rule;
}
function compileSkipRules(rules, docsUrl = "https://github.com/yourorg/yourplugin#skip-rules") {
return [...DEFAULT_SKIP, ...rules].map((rule, idx) => {
if (typeof rule === "string") {
return parseSkipRule(rule, idx, docsUrl);
} else if (rule instanceof RegExp) {
if ((0, import_vuln_regex_detector.test)(rule.source)) {
throwUnsafeRegex(rule, idx, docsUrl);
}
return rule;
} else {
throw new Error(
`Invalid skip rule at index ${idx}:${String(rule)} (See ${docsUrl})`
);
}
});
}
function shouldSkip(item, skip) {
for (const pat of skip) {
if (typeof pat === "string") {
if (item === pat) return true;
} else if (pat instanceof RegExp) {
if (pat.test(item)) return true;
}
}
return false;
}
// src/color-minify.ts
var import_culori = require("culori");
var APPROXIMATED_SPACES = [
"lab",
// CIELAB
"lch",
// CIELCH
"luv",
// CIELUV
"din99",
// DIN99 Lab
"din99o",
// DIN99o Lab
"din99d",
// DIN99d Lab
"oklab",
"oklch",
"okhsl",
"okhsv",
"jzazbz",
"yiq",
"xyz",
// CIE XYZ
"xyb",
"ictcp",
"display-p3",
"rec2020",
"a98-rgb",
"prophoto-rgb",
"gray",
// CSS gray()
"cubehelix"
];
var namedColors = {
aliceblue: "#f0f8ff",
antiquewhite: "#faebd7",
aqua: "#0ff",
aquamarine: "#7fffd4",
azure: "#f0ffff",
beige: "#f5f5dc",
bisque: "#ffe4c4",
black: "#000",
blanchedalmond: "#ffebcd",
blue: "#00f",
blueviolet: "#8a2be2",
brown: "#a52a2a",
burlywood: "#deb887",
cadetblue: "#5f9ea0",
chartreuse: "#7fff00",
chocolate: "#d2691e",
coral: "#ff7f50",
cornflowerblue: "#6495ed",
cornsilk: "#fff8dc",
crimson: "#dc143c",
cyan: "#0ff",
darkblue: "#00008b",
darkcyan: "#008b8b",
darkgoldenrod: "#b8860b",
darkgray: "#a9a9a9",
darkgreen: "#006400",
darkgrey: "#a9a9a9",
darkkhaki: "#bdb76b",
darkmagenta: "#8b008b",
darkolivegreen: "#556b2f",
darkorange: "#ff8c00",
darkorchid: "#9932cc",
darkred: "#8b0000",
darksalmon: "#e9967a",
darkseagreen: "#8fbc8f",
darkslateblue: "#483d8b",
darkslategray: "#2f4f4f",
darkslategrey: "#2f4f4f",
darkturquoise: "#00ced1",
darkviolet: "#9400d3",
deeppink: "#ff1493",
deepskyblue: "#00bfff",
dimgray: "#696969",
dimgrey: "#696969",
dodgerblue: "#1e90ff",
firebrick: "#b22222",
floralwhite: "#fffaf0",
forestgreen: "#228b22",
fuchsia: "#f0f",
gainsboro: "#dcdcdc",
ghostwhite: "#f8f8ff",
gold: "#ffd700",
goldenrod: "#daa520",
gray: "#808080",
green: "#008000",
greenyellow: "#adff2f",
grey: "#808080",
honeydew: "#f0fff0",
hotpink: "#ff69b4",
indianred: "#cd5c5c",
indigo: "#4b0082",
ivory: "#fffff0",
khaki: "#f0e68c",
lavender: "#e6e6fa",
lime: "#0f0",
linen: "#faf0e6",
magenta: "#f0f",
maroon: "#800000",
mediumaquamarine: "#66cdaa",
mediumblue: "#0000cd",
mediumorchid: "#ba55d3",
mediumpurple: "#9370db",
mediumseagreen: "#3cb371",
mediumslateblue: "#7b68ee",
mediumspringgreen: "#00fa9a",
mediumturquoise: "#48d1cc",
mediumvioletred: "#c71585",
midnightblue: "#191970",
mintcream: "#f5fffa",
mistyrose: "#ffe4e1",
moccasin: "#ffe4b5",
navajowhite: "#ffdead",
navy: "#000080",
oldlace: "#fdf5e6",
olive: "#808000",
olivedrab: "#6b8e23",
orange: "#ffa500",
orangered: "#ff4500",
orchid: "#da70d6",
palegoldenrod: "#eee8aa",
palegreen: "#98fb98",
paleturquoise: "#afeeee",
palevioletred: "#db7093",
papayawhip: "#ffefd5",
peachpuff: "#ffdab9",
peru: "#cd853f",
pink: "#ffc0cb",
plum: "#dda0dd",
powderblue: "#b0e0e6",
purple: "#800080",
rebeccapurple: "#663399",
red: "#f00",
rosybrown: "#bc8f8f",
royalblue: "#4169e1",
saddlebrown: "#8b4513",
salmon: "#fa8072",
sandybrown: "#f4a460",
seagreen: "#2e8b57",
seashell: "#fff5ee",
sienna: "#a0522d",
silver: "#c0c0c0",
skyblue: "#87ceeb",
slateblue: "#6a5acd",
slategray: "#708090",
slategrey: "#708090",
snow: "#fffafa",
springgreen: "#00ff7f",
steelblue: "#4682b4",
tan: "#d2b48c",
teal: "#008080",
thistle: "#d8bfd8",
tomato: "#ff6347",
turquoise: "#40e0d0",
violet: "#ee82ee",
wheat: "#f5deb3",
white: "#fff",
whitesmoke: "#f5f5f5",
yellow: "#ff0",
yellowgreen: "#9acd32",
transparent: "rgba(0,0,0,0)"
};
var ignoreApproximatedSpaces = false;
var ignoredSpaces = [];
var preferHex = true;
var hexToName = {};
for (const [name, hex] of Object.entries(namedColors)) {
const shortHex = shortenHex(hex);
if (!hexToName[shortHex] || name.length < hexToName[shortHex].length) {
hexToName[shortHex] = name;
}
}
function shortenHex(hex) {
hex = hex.toLowerCase();
if (!hex.startsWith("#")) hex = "#" + hex;
if (hex.length === 9) {
const r = hex.slice(1, 3);
const g = hex.slice(3, 5);
const b = hex.slice(5, 7);
const a = hex.slice(7, 9);
if (a === "00") {
return "transparent";
}
if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1] && a[0] === a[1]) {
return `#${r[0]}${g[0]}${b[0]}${a[0]}`;
}
return hex;
}
if (hex.length === 7 && hex.startsWith("#")) {
const r = hex.slice(1, 3);
const g = hex.slice(3, 5);
const b = hex.slice(5, 7);
if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1]) {
return `#${r[0]}${g[0]}${b[0]}`;
}
return hex;
}
if ((hex.length === 4 || hex.length === 5) && hex.startsWith("#")) {
return hex;
}
return hex;
}
function setOpt(o, v) {
switch (o) {
case "preferHex":
if (typeof v === "boolean") preferHex = v;
break;
case "ignoreApproximatedSpaces":
if (typeof v === "boolean") ignoreApproximatedSpaces = v;
break;
case "ignoredSpaces":
if (Array.isArray(v)) ignoredSpaces = v;
break;
default:
console.log("unknown option");
break;
}
}
function getShortestColorFormat(hex) {
if (hex === "#00000000") return "transparent";
const shortHex = shortenHex(hex);
const lowerHex = shortHex.toLowerCase();
const name = hexToName[lowerHex];
if (shortHex === "transparent" || name === "transparent")
return "transparent";
if (name && name.length < lowerHex.length) return name;
if (name && name.length === lowerHex.length) {
return preferHex ? lowerHex : name;
}
return shortHex;
}
function preprocessNonStandardColor(color) {
return color.replace(
/rgba?\(\s*#([a-f0-9]{3,8})\s*,\s*([^,]+)\s*,\s*([^,]+)(?:\s*,\s*([^)]+))?\s*\)/gi,
(match, hex, g, b, a) => {
const parsedHex = (0, import_culori.parse)(`#${hex}`);
if (!parsedHex) return match;
const rgb = (0, import_culori.converter)("rgb")(parsedHex);
if (!rgb) return match;
const r = Math.round((rgb.r || 0) * 255);
return a ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
}
);
}
function normalizeCommasAndSpaces(str) {
let result = str.replace(/,\s+/g, ", ");
result = result.replace(/\(\s+/g, "(").replace(/\s+\)/g, ")");
return result.replace(/\s{2,}/g, " ");
}
function colorToMinified(input) {
if (!input || input.includes("url(") || input.startsWith('"') || input.startsWith("'")) {
return input;
}
if (ignoreApproximatedSpaces && APPROXIMATED_SPACES.some((space) => input.toLowerCase().includes(space)) || Array.isArray(ignoredSpaces) && ignoredSpaces.length > 0 && ignoredSpaces.some((space) => input.toLowerCase().includes(space))) {
return input;
}
const preprocessed = preprocessNonStandardColor(input);
const parsed = (0, import_culori.parse)(preprocessed);
if (!parsed) return input;
if ("alpha" in parsed && parsed.alpha === 0) {
return "transparent";
}
const rgb = (0, import_culori.converter)("rgb")(parsed);
if (!rgb) return input;
const formattedHex = rgb.alpha !== void 0 && rgb.alpha < 1 ? (0, import_culori.formatHex8)(rgb) : (0, import_culori.formatHex)(rgb);
if (!formattedHex) return input;
const shortestHex = shortenHex(formattedHex);
return getShortestColorFormat(shortestHex);
}
// src/index.ts
function processValue(value, opts) {
if (!value) return value;
const parsed = (0, import_postcss_value_parser.default)(value);
opts.preferHex !== void 0 && setOpt("preferHex", opts.preferHex);
opts.ignoreApproximatedSpaces !== void 0 && setOpt("ignoreApproximatedSpaces", opts.ignoreApproximatedSpaces);
opts.ignoredSpaces !== void 0 && setOpt("ignoredSpaces", opts.ignoredSpaces);
parsed.walk((node, index, nodes) => {
if (node.type === "string" || node.type === "function" && node.value === "url" || node.type === "comment") {
return false;
}
if (node.type === "word") {
const parsedColor = colorToMinified(node.value);
if (parsedColor) {
node.value = parsedColor;
}
} else if (node.type === "function") {
const colorFuncNames = [
"rgb",
"rgba",
"hsl",
"hsla",
"lab",
"lch",
"oklab",
"oklch",
"color"
];
if (colorFuncNames.includes(node.value.toLowerCase())) {
const asString = import_postcss_value_parser.default.stringify(node);
const parsedColor = colorToMinified(asString);
if (parsedColor && nodes) {
nodes[index] = {
type: "word",
value: parsedColor,
sourceIndex: node.sourceIndex,
sourceEndIndex: "sourceEndIndex" in node && typeof node.sourceEndIndex === "number" ? node.sourceEndIndex : node.sourceIndex
};
}
} else {
if (node.nodes) {
for (let i = 0; i < node.nodes.length; i++) {
const childNode = node.nodes[i];
if (childNode.type === "word") {
const parsedColor = colorToMinified(childNode.value);
if (parsedColor && parsedColor !== childNode.value) {
childNode.value = parsedColor;
}
} else if (childNode.type === "function") {
const colorFuncNames2 = ["rgb", "rgba", "hsl", "hsla", "lab", "lch", "oklab", "oklch", "color"];
if (colorFuncNames2.includes(childNode.value.toLowerCase())) {
const asString = import_postcss_value_parser.default.stringify(childNode);
const parsedColor = colorToMinified(asString);
if (parsedColor && parsedColor !== asString) {
node.nodes[i] = {
type: "word",
value: parsedColor,
sourceIndex: childNode.sourceIndex,
sourceEndIndex: childNode.sourceEndIndex || childNode.sourceIndex
};
}
} else if (childNode.nodes) {
import_postcss_value_parser.default.walk(childNode.nodes, (nestedNode, nestedIndex, nestedParent) => {
if (nestedNode.type === "word") {
const nestedColor = colorToMinified(nestedNode.value);
if (nestedColor && nestedColor !== nestedNode.value) {
nestedNode.value = nestedColor;
}
} else if (nestedNode.type === "function") {
const nestedString = import_postcss_value_parser.default.stringify(nestedNode);
const nestedResult = colorToMinified(nestedString);
if (nestedResult && nestedResult !== nestedString && nestedParent) {
nestedParent[nestedIndex] = {
type: "word",
value: nestedResult,
sourceIndex: nestedNode.sourceIndex,
sourceEndIndex: nestedNode.sourceEndIndex || nestedNode.sourceIndex
};
}
}
return false;
});
}
}
}
}
}
}
});
return normalizeCommasAndSpaces(parsed.toString());
}
var colorGolfPlugin = (opts = {}) => {
const options = {
preferHex: true,
skip: [],
ignoreApproximatedSpaces: false,
ignoredSpaces: [],
...opts
};
const patterns = compileSkipRules(opts.skip || []);
return {
postcssPlugin: "postcss-color-golf",
Once(root) {
root.walkDecls((decl) => {
if (shouldSkip(decl.prop, patterns)) return;
const orig = decl.value;
const next = processValue(orig, options);
if (next !== orig) decl.value = next;
});
}
};
};
colorGolfPlugin.postcss = true;
var index_default = colorGolfPlugin;