UNPKG

@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
// 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)}`; }