@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
173 lines (172 loc) • 6.75 kB
JavaScript
"use strict";
// CSS border-radius property parser - Phase 3 refactored
// Handles parsing of CSS border-radius shorthand property according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius
Object.defineProperty(exports, "__esModule", { value: true });
exports.parse = parse;
exports.toCSSValue = toCSSValue;
const length_1 = require('./length.cjs');
const percentage_1 = require('./percentage.cjs');
const css_variable_1 = require('./css-variable.cjs');
const shared_utils_1 = require('../utils/shared-utils.cjs');
// ========================================
// Property-specific keywords
// ========================================
const BORDER_RADIUS_KEYWORDS = new Set(['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
function isBorderRadiusKeyword(value) {
return BORDER_RADIUS_KEYWORDS.has(value.toLowerCase());
}
// ========================================
// Core parsing functions
// ========================================
function parseRadiusValue(value) {
const trimmed = value.trim();
if ((0, shared_utils_1.isGlobalKeyword)(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
if (isBorderRadiusKeyword(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
// Try percentage first (to handle cases like '50%')
const percentageResult = (0, percentage_1.parse)(trimmed);
if (percentageResult && (0, shared_utils_1.isNonNegative)(percentageResult)) {
return percentageResult;
}
// Try length
const lengthResult = (0, length_1.parse)(trimmed);
if (lengthResult && (0, shared_utils_1.isNonNegative)(lengthResult)) {
return lengthResult;
}
return null;
}
function parseEllipticalSyntax(value) {
// Handle elliptical syntax with slash separator
const ellipticalParts = value.split('/');
if (ellipticalParts.length > 2) {
return null; // Invalid: more than one slash
}
const horizontalPart = ellipticalParts[0];
const verticalPart = ellipticalParts.length === 2 ? ellipticalParts[1] : '';
// Check for empty parts
if (!horizontalPart || !horizontalPart.trim()) {
return null;
}
// If there's a slash, there must be valid vertical values
if (ellipticalParts.length === 2 && (!verticalPart || !verticalPart.trim())) {
return null;
}
const horizontalTokens = (0, shared_utils_1.tokenize)(horizontalPart);
const verticalTokens = ellipticalParts.length === 2 ? (0, shared_utils_1.tokenize)(verticalPart) : [];
if (horizontalTokens.length === 0 || horizontalTokens.length > 4) {
return null;
}
if (ellipticalParts.length === 2 && (verticalTokens.length === 0 || verticalTokens.length > 4)) {
return null;
}
return {
horizontal: horizontalTokens,
vertical: verticalTokens
};
}
// ========================================
// Main parse function
// ========================================
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 ((0, shared_utils_1.isCssVariable)(trimmed)) {
return (0, css_variable_1.parse)(trimmed);
}
// Handle elliptical syntax
const ellipticalResult = parseEllipticalSyntax(trimmed);
if (!ellipticalResult) {
return null;
}
const { horizontal, vertical } = ellipticalResult;
// For elliptical syntax, we need to implement proper type support first
// TODO: Implement full elliptical syntax support with proper types
if (vertical.length > 0) {
// Elliptical syntax not yet fully implemented - requires type system updates
return null;
}
// Parse horizontal values
const parsedValues = [];
for (const token of horizontal) {
const parsed = parseRadiusValue(token);
if (!parsed) {
return null;
}
parsedValues.push(parsed);
}
if (parsedValues.length === 0 || parsedValues.length > 4) {
return null;
}
// Apply CSS shorthand expansion rules (1→4, 2→4, 3→4, 4→4)
const expandedValues = (0, shared_utils_1.expandShorthandValues)(horizontal);
const [topLeft, topRight, bottomRight, bottomLeft] = expandedValues;
// Parse each corner value
const borderTopLeftRadius = parseRadiusValue(topLeft);
const borderTopRightRadius = parseRadiusValue(topRight);
const borderBottomRightRadius = parseRadiusValue(bottomRight);
const borderBottomLeftRadius = parseRadiusValue(bottomLeft);
if (!borderTopLeftRadius || !borderTopRightRadius || !borderBottomRightRadius || !borderBottomLeftRadius) {
return null;
}
return {
borderTopLeftRadius,
borderTopRightRadius,
borderBottomRightRadius,
borderBottomLeftRadius
};
}
// ========================================
// CSS value serialization
// ========================================
function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables
if ('CSSvariable' in parsed) {
return (0, css_variable_1.toCSSValue)(parsed);
}
const topLeft = serializeRadiusValue(parsed.borderTopLeftRadius);
const topRight = serializeRadiusValue(parsed.borderTopRightRadius);
const bottomRight = serializeRadiusValue(parsed.borderBottomRightRadius);
const bottomLeft = serializeRadiusValue(parsed.borderBottomLeftRadius);
if (!topLeft || !topRight || !bottomRight || !bottomLeft) {
return null;
}
// Optimize output by removing redundant values
const values = [topLeft, topRight, bottomRight, bottomLeft];
// Remove trailing values that match earlier ones (CSS shorthand compression)
if (values[3] === values[1])
values.pop(); // Remove bottom-left if same as top-right
if (values.length === 3 && values[2] === values[0])
values.pop(); // Remove bottom-right if same as top-left
if (values.length === 2 && values[1] === values[0])
values.pop(); // Remove top-right if same as top-left
return values.join(' ');
}
function serializeRadiusValue(radiusValue) {
// Handle keywords
if ('keyword' in radiusValue) {
return radiusValue.keyword;
}
// Handle length/percentage by delegating to the appropriate toCSSValue function
const lengthValue = (0, length_1.toCSSValue)(radiusValue);
if (lengthValue) {
return lengthValue;
}
const percentageValue = (0, percentage_1.toCSSValue)(radiusValue);
if (percentageValue) {
return percentageValue;
}
return null;
}