UNPKG

@wix/css-property-parser

Version:

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

240 lines (239 loc) 7.64 kB
// CSS box-shadow property parser // Handles parsing of CSS box-shadow property according to MDN specification // https://developer.mozilla.org/en-US/docs/Web/CSS/box-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 box shadow value like "2px 2px 4px 1px red inset" or "inset 2px 2px 4px red" * Supports MDN syntax: offset-x offset-y [blur-radius] [spread-radius] [color] [inset] */ 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 } // Track inset keyword let inset = false; let insetCount = 0; const lengthTokens = []; const colorTokens = []; const nonLengthTokens = []; // First pass: separate tokens by type for (const token of tokens) { if (token.toLowerCase() === 'inset') { inset = true; insetCount++; if (insetCount > 1) { return null; // Multiple inset keywords not allowed } } else { const lengthResult = parseLength(token); if (lengthResult) { lengthTokens.push(lengthResult); } else { const colorResult = parseColor(token); if (colorResult) { colorTokens.push(token); } else { nonLengthTokens.push(token); } } } } // Check for invalid tokens (non-length, non-color, non-inset) if (nonLengthTokens.length > 0) { return null; } // Need exactly 2, 3, or 4 length values (offset-x, offset-y, optional blur, optional spread) if (lengthTokens.length < 2 || lengthTokens.length > 4) { 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]; } // Fourth length value is spread radius if (lengthTokens.length === 4) { result.spreadRadius = lengthTokens[3]; } // Parse color if present if (colorTokens.length === 1) { const colorResult = parseColor(colorTokens[0]); if (colorResult) { result.color = colorResult; } } // Set inset flag if present if (inset) { result.inset = true; } return result; } /** * Parses a CSS box-shadow property string into structured components * Follows MDN specification: https://developer.mozilla.org/en-US/docs/Web/CSS/box-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 box-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 inset keyword first if present if (shadow.inset) { parts.push('inset'); } // 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 spread radius (optional) if (shadow.spreadRadius) { const spreadValue = lengthToCSSValue(shadow.spreadRadius); if (spreadValue) { parts.push(spreadValue); } } // 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; }