UNPKG

@wix/css-property-parser

Version:

A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance

204 lines (203 loc) 6.37 kB
// CSS text-shadow property parser // Handles parsing of CSS text-shadow property according to MDN specification // https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow import { parse as parseLength, toCSSValue as lengthToCSSValue } from './length.js'; import { parse as parseColor, toCSSValue as colorToCSSValue } from './color.js'; import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js'; import { isCssVariable, isGlobalKeyword, tokenize } from '../utils/shared-utils.js'; /** * Parses a single text shadow value like "2px 2px 4px red" or "red 2px 2px 4px" */ export function parseSingleShadow(shadowStr) { if (!shadowStr || typeof shadowStr !== 'string') { return null; } const trimmed = shadowStr.trim(); if (trimmed === '') { return null; } // Use shared tokenizer for consistent parsing const tokens = tokenize(trimmed); if (tokens.length < 2) { return null; // Need at least offset-x and offset-y } // Separate lengths from colors const lengthTokens = []; const colorTokens = []; for (const token of tokens) { const lengthResult = parseLength(token); if (lengthResult) { lengthTokens.push(lengthResult); } else { const colorResult = parseColor(token); if (colorResult) { colorTokens.push(token); } else { // Invalid token return null; } } } // Need exactly 2 or 3 length values (offset-x, offset-y, optional blur) if (lengthTokens.length < 2 || lengthTokens.length > 3) { return null; } // Can have at most 1 color if (colorTokens.length > 1) { return null; } const result = { offsetX: lengthTokens[0], offsetY: lengthTokens[1] }; // Third length value is blur radius if (lengthTokens.length === 3) { result.blurRadius = lengthTokens[2]; } // Parse color if present if (colorTokens.length === 1) { const colorResult = parseColor(colorTokens[0]); if (colorResult) { result.color = colorResult; } } return result; } /** * Parses a CSS text-shadow property string into structured components * Follows MDN specification: https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow */ export function parse(value) { if (!value || typeof value !== 'string') { return null; } const trimmed = value.trim(); if (trimmed === '') { return null; } // CSS variables - parse and return directly if (isCssVariable(trimmed)) { return parseCSSVariable(trimmed); } // Handle global keywords if (isGlobalKeyword(trimmed)) { return { type: 'keyword', keyword: trimmed.toLowerCase() }; } // Handle 'none' keyword if (trimmed.toLowerCase() === 'none') { return { type: 'keyword', keyword: 'none' }; } // Check for trailing comma or leading comma if (trimmed.endsWith(',') || trimmed.startsWith(',')) { return null; } // Check for double commas if (trimmed.includes(',,')) { return null; } // Parse multiple shadows separated by commas const shadowStrings = []; let current = ''; let parenDepth = 0; let inString = false; let stringChar = ''; for (let i = 0; i < trimmed.length; i++) { const char = trimmed[i]; if (!inString && (char === '"' || char === "'")) { inString = true; stringChar = char; current += char; } else if (inString && char === stringChar) { inString = false; stringChar = ''; current += char; } else if (!inString && char === '(') { parenDepth++; current += char; } else if (!inString && char === ')') { parenDepth--; current += char; } else if (!inString && parenDepth === 0 && char === ',') { if (current.trim()) { shadowStrings.push(current.trim()); current = ''; } else { return null; // Empty shadow between commas } } else { current += char; } } if (current.trim()) { shadowStrings.push(current.trim()); } if (shadowStrings.length === 0) { return null; } // Parse each shadow component const shadows = []; for (const shadowStr of shadowStrings) { const shadow = parseSingleShadow(shadowStr); if (!shadow) { return null; // Invalid shadow component } shadows.push(shadow); } return { shadows }; } /** * Converts a parsed text-shadow value back to CSS string representation */ export function toCSSValue(parsed) { if (!parsed) { return null; } // Handle CSS variables if ('CSSvariable' in parsed) { return cssVariableToCSSValue(parsed); } if ('keyword' in parsed) { return parsed.keyword; } if ('shadows' in parsed && parsed.shadows.length > 0) { const shadowStrings = parsed.shadows.map(shadow => { const parts = []; // Add offsets (required) const offsetX = lengthToCSSValue(shadow.offsetX); const offsetY = lengthToCSSValue(shadow.offsetY); if (!offsetX || !offsetY) { return null; } parts.push(offsetX, offsetY); // Add blur radius (optional) if (shadow.blurRadius) { const blurValue = lengthToCSSValue(shadow.blurRadius); if (blurValue) { parts.push(blurValue); } } // Add color (optional) if (shadow.color) { const colorValue = colorToCSSValue(shadow.color); if (colorValue) { parts.push(colorValue); } } return parts.join(' '); }); // Check if any shadow failed to serialize if (shadowStrings.some(s => s === null)) { return null; } return shadowStrings.join(', '); } return null; }