@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
197 lines (196 loc) • 6.84 kB
JavaScript
// CSS padding property parser - Phase 3 refactored
// Handles parsing of CSS padding shorthand property according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/padding
import { parse as parseLength, toCSSValue as lengthToCSSValue } from './length.js';
import { parse as parsePercentage, toCSSValue as percentageToCSSValue } from './percentage.js';
import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js';
import { isCssVariable, isGlobalKeyword, tokenize, expandShorthandValues, isNonNegative } from '../utils/shared-utils.js';
// ========================================
// Utilities
// ========================================
/**
* Checks if a string is a valid padding value component
*/
function isPaddingValue(value) {
const trimmed = value.trim();
// CSS variables are valid padding values
if (isCssVariable(trimmed)) {
return true;
}
// Global keywords are valid
if (isGlobalKeyword(trimmed)) {
return true;
}
// Try to parse as percentage (must be non-negative)
const percentageResult = parsePercentage(trimmed);
if (percentageResult) {
return isNonNegative(percentageResult);
}
// Try to parse as length (must be non-negative)
const lengthResult = parseLength(trimmed);
if (lengthResult) {
return isNonNegative(lengthResult);
}
return false;
}
/**
* Parses a single padding value into atomic type
*/
function parsePaddingValue(value) {
const trimmed = value.trim();
// Handle CSS variables in individual components
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// First validate the value
if (!isPaddingValue(trimmed)) {
return null;
}
// Handle keywords
if (isGlobalKeyword(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
// Try percentage first
const percentageResult = parsePercentage(trimmed);
if (percentageResult) {
return percentageResult;
}
// Try length
const lengthResult = parseLength(trimmed);
if (lengthResult) {
return lengthResult;
}
return null;
}
// ========================================
// Main Functions
// ========================================
/**
* Parses a CSS padding property value into structured components
* @param value - The CSS padding property string
* @returns Parsed padding object with individual side values or null if invalid
*/
export function parse(value) {
if (!value || typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
if (trimmed === '') {
return null;
}
// CSS variables can be parsed directly for entire shorthand
// Try to parse as CSS variable first, but only if it's valid
const cssVariableResult = parseCSSVariable(trimmed);
if (cssVariableResult) {
return cssVariableResult;
}
// Handle single global keyword
if (isGlobalKeyword(trimmed)) {
const keyword = { type: 'keyword', keyword: trimmed.toLowerCase() };
return {
type: 'padding-expanded',
paddingTop: keyword,
paddingRight: keyword,
paddingBottom: keyword,
paddingLeft: keyword
};
}
// Use shared tokenize utility to handle calc expressions properly
const values = tokenize(trimmed);
if (values.length === 0 || values.length > 4) {
return null;
}
// Validate and parse each value
const parsedValues = [];
for (const val of values) {
const parsedValue = parsePaddingValue(val);
if (!parsedValue) {
return null;
}
parsedValues.push(parsedValue);
}
// Expand values according to CSS padding shorthand rules using shared utility
const expandedStrings = expandShorthandValues(values);
const [topStr, rightStr, bottomStr, leftStr] = expandedStrings;
// Parse each expanded value
const paddingTop = parsePaddingValue(topStr);
const paddingRight = parsePaddingValue(rightStr);
const paddingBottom = parsePaddingValue(bottomStr);
const paddingLeft = parsePaddingValue(leftStr);
if (!paddingTop || !paddingRight || !paddingBottom || !paddingLeft) {
return null;
}
return {
type: 'padding-expanded',
paddingTop,
paddingRight,
paddingBottom,
paddingLeft
};
}
/**
* Converts a parsed padding back to a CSS value string
* @param parsed - The parsed padding object
* @returns CSS value string or null if invalid
*/
export function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables for entire shorthand
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle single atomic values (apply to all sides)
if (parsed.type !== 'padding-expanded') {
const singleValue = parsed;
return convertPaddingValueToCSSValue(singleValue);
}
// Handle expanded padding (PaddingExpanded)
const paddingExpanded = parsed;
const topValue = convertPaddingValueToCSSValue(paddingExpanded.paddingTop);
const rightValue = convertPaddingValueToCSSValue(paddingExpanded.paddingRight);
const bottomValue = convertPaddingValueToCSSValue(paddingExpanded.paddingBottom);
const leftValue = convertPaddingValueToCSSValue(paddingExpanded.paddingLeft);
if (!topValue || !rightValue || !bottomValue || !leftValue) {
return null;
}
// Try to recreate the most compact form
// All sides same
if (topValue === rightValue && rightValue === bottomValue && bottomValue === leftValue) {
return topValue;
}
// Vertical/horizontal pattern
if (topValue === bottomValue && rightValue === leftValue) {
return `${topValue} ${rightValue}`;
}
// Top + horizontal + bottom pattern
if (rightValue === leftValue) {
return `${topValue} ${rightValue} ${bottomValue}`;
}
// All different
return `${topValue} ${rightValue} ${bottomValue} ${leftValue}`;
}
/**
* Converts a single padding value back to CSS string
*/
function convertPaddingValueToCSSValue(paddingValue) {
// Handle CSS variables in components
if ('CSSvariable' in paddingValue) {
return cssVariableToCSSValue(paddingValue);
}
// Handle keywords
if ('keyword' in paddingValue) {
return paddingValue.keyword;
}
// Handle length/percentage by delegating to the appropriate toCSSValue function
const lengthValue = lengthToCSSValue(paddingValue);
if (lengthValue) {
return lengthValue;
}
const percentageValue = percentageToCSSValue(paddingValue);
if (percentageValue) {
return percentageValue;
}
return null;
}