UNPKG

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
"use strict"; 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;