@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
279 lines (278 loc) • 10.3 kB
JavaScript
// Color property parser
// Handles parsing of CSS color values according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
// into structured components and reconstruction back to CSS string
import { isCssVariable, isGlobalKeyword } from '../utils/shared-utils.js';
import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js';
// Named colors (CSS Level 1-4)
const NAMED_COLORS = [
'transparent',
'black', 'white', 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta',
'gray', 'grey', 'silver', 'maroon', 'olive', 'lime', 'aqua', 'teal', 'navy', 'fuchsia', 'purple',
'aliceblue', 'antiquewhite', 'aquamarine', 'azure', 'beige', 'bisque', 'blanchedalmond',
'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral',
'cornflowerblue', 'cornsilk', 'crimson', 'darkblue', 'darkcyan', 'darkgoldenrod',
'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen',
'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue',
'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue',
'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen',
'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'greenyellow', 'honeydew',
'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush',
'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow',
'lightgray', 'lightgrey', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen',
'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow',
'limegreen', 'linen', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple',
'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
'navajowhite', 'oldlace', 'olivedrab', 'orange', 'orangered', 'orchid',
'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip',
'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'rosybrown', 'royalblue',
'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna',
'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen',
'steelblue', 'tan', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat',
'whitesmoke', 'yellowgreen'
];
/**
* Parses a CSS color value into structured components
* @param value - The CSS color value string
* @returns Parsed color object or null if invalid
*/
export function parse(value) {
if (!value || typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
if (trimmed === '')
return null;
// Check for CSS variables - use centralized resolver
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Handle hex colors
if (trimmed.startsWith('#')) {
return parseHexColor(trimmed);
}
// Handle functional colors
if (trimmed.startsWith('rgb(') || trimmed.startsWith('rgba(')) {
return parseRgbColor(trimmed);
}
if (trimmed.startsWith('hsl(') || trimmed.startsWith('hsla(')) {
return parseHslColor(trimmed);
}
// Handle global keywords
if (isGlobalKeyword(trimmed)) {
return {
type: 'color',
format: 'keyword',
values: {
name: trimmed.toLowerCase()
}
};
}
// Handle special values
if (trimmed.toLowerCase() === 'transparent') {
return {
type: 'color',
format: 'transparent',
values: {
name: 'transparent'
}
};
}
if (trimmed.toLowerCase() === 'currentcolor') {
return {
type: 'color',
format: 'currentColor',
values: {
name: 'currentcolor'
}
};
}
// Handle named colors
if (NAMED_COLORS.includes(trimmed.toLowerCase())) {
return {
type: 'color',
format: 'named',
values: {
name: trimmed.toLowerCase()
}
};
}
return null;
}
/**
* Converts a parsed color back to a CSS value string
* @param parsed - The parsed color object
* @returns CSS value string or null if invalid
*/
export function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle regular color values
if ('format' in parsed) {
switch (parsed.format) {
case 'hex':
return reconstructHexColor(parsed);
case 'rgb':
return `rgb(${parsed.values.r}, ${parsed.values.g}, ${parsed.values.b})`;
case 'rgba':
return `rgba(${parsed.values.r}, ${parsed.values.g}, ${parsed.values.b}, ${parsed.values.a})`;
case 'hsl':
return `hsl(${parsed.values.h}, ${parsed.values.s}%, ${parsed.values.l}%)`;
case 'hsla':
return `hsla(${parsed.values.h}, ${parsed.values.s}%, ${parsed.values.l}%, ${parsed.values.a})`;
case 'named':
case 'transparent':
case 'currentColor':
case 'keyword':
return parsed.values.name || '';
default:
return null;
}
}
return null;
}
// Internal helper functions
function parseHexColor(value) {
const hex = value.slice(1); // Remove #
let r, g, b, a = 1;
// Validate hex format
if (!/^[0-9a-fA-F]{3,8}$/.test(hex)) {
return null;
}
try {
if (hex.length === 3) {
// #RGB
r = parseInt(hex[0] + hex[0], 16);
g = parseInt(hex[1] + hex[1], 16);
b = parseInt(hex[2] + hex[2], 16);
}
else if (hex.length === 4) {
// #RGBA
r = parseInt(hex[0] + hex[0], 16);
g = parseInt(hex[1] + hex[1], 16);
b = parseInt(hex[2] + hex[2], 16);
a = parseInt(hex[3] + hex[3], 16) / 255;
}
else if (hex.length === 6) {
// #RRGGBB
r = parseInt(hex.substr(0, 2), 16);
g = parseInt(hex.substr(2, 2), 16);
b = parseInt(hex.substr(4, 2), 16);
}
else if (hex.length === 8) {
// #RRGGBBAA
r = parseInt(hex.substr(0, 2), 16);
g = parseInt(hex.substr(2, 2), 16);
b = parseInt(hex.substr(4, 2), 16);
a = parseInt(hex.substr(6, 2), 16) / 255;
}
else {
return null;
}
return {
type: 'color',
format: 'hex',
values: { r, g, b, a }
};
}
catch {
return null;
}
}
function parseRgbColor(value) {
const isRgba = value.startsWith('rgba(');
const match = value.match(isRgba ? /rgba\(([^)]+)\)/ : /rgb\(([^)]+)\)/);
if (!match)
return null;
const params = match[1].split(',').map(s => s.trim());
if ((!isRgba && params.length !== 3) || (isRgba && params.length !== 4)) {
return null;
}
try {
const r = parseInt(params[0]);
const g = parseInt(params[1]);
const b = parseInt(params[2]);
const a = isRgba ? parseFloat(params[3]) : 1;
if (isNaN(r) || isNaN(g) || isNaN(b) || (isRgba && isNaN(a))) {
return null;
}
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
return null;
}
if (isRgba && (a < 0 || a > 1)) {
return null;
}
return {
type: 'color',
format: isRgba ? 'rgba' : 'rgb',
values: isRgba ? { r, g, b, a } : { r, g, b }
};
}
catch {
return null;
}
}
function parseHslColor(value) {
const isHsla = value.startsWith('hsla(');
const match = value.match(isHsla ? /hsla\(([^)]+)\)/ : /hsl\(([^)]+)\)/);
if (!match)
return null;
const params = match[1].split(',').map(s => s.trim());
if ((!isHsla && params.length !== 3) || (isHsla && params.length !== 4)) {
return null;
}
try {
const h = parseInt(params[0]);
const s = parseInt(params[1].replace('%', ''));
const l = parseInt(params[2].replace('%', ''));
const a = isHsla ? parseFloat(params[3]) : 1;
if (isNaN(h) || isNaN(s) || isNaN(l) || (isHsla && isNaN(a))) {
return null;
}
if (h < 0 || h > 360 || s < 0 || s > 100 || l < 0 || l > 100) {
return null;
}
if (isHsla && (a < 0 || a > 1)) {
return null;
}
return {
type: 'color',
format: isHsla ? 'hsla' : 'hsl',
values: isHsla ? { h, s, l, a } : { h, s, l }
};
}
catch {
return null;
}
}
function reconstructHexColor(parsed) {
const { r, g, b, a } = parsed.values;
if (r === undefined || g === undefined || b === undefined) {
return null;
}
const toHex = (n) => Math.round(n).toString(16).padStart(2, '0');
const toHexShort = (n) => Math.round(n).toString(16);
// Check if we can use short hex format (3 or 4 characters)
// This is when each color component is divisible by 17 (i.e., 0x00, 0x11, 0x22, etc.)
const canUseShort = r % 17 === 0 && g % 17 === 0 && b % 17 === 0 &&
(a === undefined || a === 1 || (a * 255) % 17 === 0);
if (canUseShort) {
if (a !== undefined && a !== 1) {
const alphaHex = Math.round(a * 255 / 17).toString(16);
return `#${toHexShort(r / 17)}${toHexShort(g / 17)}${toHexShort(b / 17)}${alphaHex}`;
}
return `#${toHexShort(r / 17)}${toHexShort(g / 17)}${toHexShort(b / 17)}`;
}
// Use long hex format
if (a !== undefined && a !== 1) {
const alphaHex = Math.round(a * 255).toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaHex}`;
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}