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