UNPKG

@wix/css-property-parser

Version:

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

233 lines (232 loc) 7.8 kB
// Border property parser // Handles parsing of CSS border shorthand property according to MDN specification // https://developer.mozilla.org/en-US/docs/Web/CSS/border 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, GLOBAL_KEYWORDS } from '../utils/shared-utils.js'; import { BORDER_WIDTH_KEYWORDS, BORDER_STYLE_KEYWORDS } from '../types.js'; /** * Parse border width value (length or keyword) */ function parseBorderWidth(value) { if (isGlobalKeyword(value)) { return { type: 'keyword', keyword: value.toLowerCase() }; } if (BORDER_WIDTH_KEYWORDS.includes(value.toLowerCase())) { return { type: 'keyword', keyword: value.toLowerCase() }; } return parseLength(value); } /** * Parse border style value */ function parseBorderStyle(value) { if (isGlobalKeyword(value)) { return { type: 'keyword', keyword: value.toLowerCase() }; } if (BORDER_STYLE_KEYWORDS.includes(value.toLowerCase())) { return { type: 'keyword', keyword: value.toLowerCase() }; } return null; } /** * Parse border color value */ function parseBorderColor(value) { if (isGlobalKeyword(value)) { return { type: 'keyword', keyword: value.toLowerCase() }; } if (value.toLowerCase() === 'currentcolor') { return { type: 'keyword', keyword: 'currentcolor' }; } const colorResult = parseColor(value); // Only return non-CSS-variable color results if (colorResult && !('CSSvariable' in colorResult)) { return colorResult; } return null; } /** * Convert border width to CSS string */ function borderWidthToCSSValue(value) { if ('keyword' in value) { return value.keyword; } return lengthToCSSValue(value); } /** * Convert border style to CSS string */ function borderStyleToCSSValue(value) { // Handle CSS variables if ('CSSvariable' in value) { return cssVariableToCSSValue(value); } return value.keyword; } /** * Convert border color to CSS string */ function borderColorToCSSValue(value) { // Handle CSS variables if ('CSSvariable' in value) { return cssVariableToCSSValue(value); } if ('keyword' in value) { return value.keyword; } return colorToCSSValue(value); } /** * Parses a CSS border property string into structured components * @param value - The CSS border property value * @returns Parsed border object with width, style, and color components */ 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 { width: { type: 'keyword', keyword: trimmed.toLowerCase() }, style: { type: 'keyword', keyword: trimmed.toLowerCase() }, color: { type: 'keyword', keyword: trimmed.toLowerCase() } }; } // Handle 'none' keyword - sets style to none if (trimmed.toLowerCase() === 'none') { return { style: { type: 'keyword', keyword: 'none' } }; } // Split the value into tokens const tokens = tokenize(trimmed); if (tokens.length === 0) { return null; } const border = {}; const usedTokenIndices = new Set(); // Parse each token and assign to appropriate property for (let i = 0; i < tokens.length; i++) { if (usedTokenIndices.has(i)) continue; const token = tokens[i]; // Check for CSS variables in individual tokens - return null as we can't resolve mixed values if (isCssVariable(token)) { return null; } // Try border width first if (!border.width) { const widthResult = parseBorderWidth(token); if (widthResult && !('keyword' in widthResult && GLOBAL_KEYWORDS.includes(widthResult.keyword))) { border.width = widthResult; usedTokenIndices.add(i); continue; } } // Try border style if (!border.style) { const styleResult = parseBorderStyle(token); if (styleResult && !('keyword' in styleResult && GLOBAL_KEYWORDS.includes(styleResult.keyword))) { border.style = styleResult; usedTokenIndices.add(i); continue; } } // Try border color if (!border.color) { const colorResult = parseBorderColor(token); if (colorResult && !('keyword' in colorResult && GLOBAL_KEYWORDS.includes(colorResult.keyword))) { border.color = colorResult; usedTokenIndices.add(i); continue; } } // If token doesn't match any category, the border is invalid return null; } // At least one component must be present if (!border.width && !border.style && !border.color) { return null; } return border; } /** * Converts a parsed border value back to a CSS string * @param parsed - The parsed border value * @returns CSS string representation or null if invalid */ export function toCSSValue(parsed) { if (!parsed) { return null; } // Handle CSS variables if ('CSSvariable' in parsed) { return cssVariableToCSSValue(parsed); } // Check if all components have the same global keyword let commonGlobalKeyword = null; if (parsed.width && 'keyword' in parsed.width && GLOBAL_KEYWORDS.includes(parsed.width.keyword)) { commonGlobalKeyword = parsed.width.keyword; } if (parsed.style && 'keyword' in parsed.style && GLOBAL_KEYWORDS.includes(parsed.style.keyword)) { if (commonGlobalKeyword === null) { commonGlobalKeyword = parsed.style.keyword; } else if (commonGlobalKeyword !== parsed.style.keyword) { commonGlobalKeyword = null; // Different global keywords } } else { commonGlobalKeyword = null; // Not all same global keyword } if (parsed.color && 'keyword' in parsed.color && GLOBAL_KEYWORDS.includes(parsed.color.keyword)) { if (commonGlobalKeyword === null) { commonGlobalKeyword = parsed.color.keyword; } else if (commonGlobalKeyword !== parsed.color.keyword) { commonGlobalKeyword = null; // Different global keywords } } else { commonGlobalKeyword = null; // Not all same global keyword } // If all three components have the same global keyword, return just that keyword if (commonGlobalKeyword && parsed.width && parsed.style && parsed.color) { return commonGlobalKeyword; } const parts = []; // Add width if (parsed.width) { const widthString = borderWidthToCSSValue(parsed.width); if (widthString) { parts.push(widthString); } } // Add style if (parsed.style) { const styleString = borderStyleToCSSValue(parsed.style); if (styleString) { parts.push(styleString); } } // Add color if (parsed.color) { const colorString = borderColorToCSSValue(parsed.color); if (colorString) { parts.push(colorString); } } return parts.length > 0 ? parts.join(' ') : null; }