@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
166 lines (165 loc) • 5.32 kB
JavaScript
// CSS gap property parser - Phase 3 refactored
// Handles parsing of CSS gap property according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/gap
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, isNonNegative } from '../utils/shared-utils.js';
// ========================================
// Utilities
// ========================================
/**
* Checks if a string is a valid gap value component
*/
function isGapValue(value) {
const trimmed = value.trim();
// CSS variables are valid gap values
if (isCssVariable(trimmed)) {
return true;
}
// Global keywords are valid
if (isGlobalKeyword(trimmed)) {
return true;
}
// Normal keyword is valid for gap
if (trimmed.toLowerCase() === 'normal') {
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 gap value into atomic type
*/
function parseGapValue(value) {
const trimmed = value.trim();
// Handle CSS variables in individual components
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// First validate the value
if (!isGapValue(trimmed)) {
return null;
}
// Handle keywords
if (isGlobalKeyword(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
if (trimmed.toLowerCase() === 'normal') {
return { type: 'keyword', keyword: 'normal' };
}
// 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 gap property value into structured components
* @param value - The CSS gap property string
* @returns Parsed gap object with rowGap and columnGap 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
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
if (isGlobalKeyword(trimmed)) {
const keyword = { type: 'keyword', keyword: trimmed.toLowerCase() };
return { rowGap: keyword, columnGap: keyword };
}
// Use shared tokenize utility to handle calc expressions properly
const values = tokenize(trimmed);
if (values.length === 0 || values.length > 2) {
return null;
}
const parsedValues = [];
for (const val of values) {
const parsedValue = parseGapValue(val);
if (!parsedValue) {
return null;
}
parsedValues.push(parsedValue);
}
if (parsedValues.length === 1) {
return { rowGap: parsedValues[0], columnGap: parsedValues[0] };
}
else {
return { rowGap: parsedValues[0], columnGap: parsedValues[1] };
}
}
/**
* Converts a parsed gap back to a CSS value string
* @param parsed - The parsed gap 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 expanded gap (GapExpanded)
const gapExpanded = parsed;
const rowGapValue = convertGapValueToCSSValue(gapExpanded.rowGap);
const columnGapValue = convertGapValueToCSSValue(gapExpanded.columnGap);
if (!rowGapValue || !columnGapValue) {
return null;
}
// If both values are the same, return single value
if (rowGapValue === columnGapValue) {
return rowGapValue;
}
// Return two values
return `${rowGapValue} ${columnGapValue}`;
}
/**
* Converts a single gap value back to CSS string
*/
function convertGapValueToCSSValue(gapValue) {
// Handle CSS variables in components
if ('CSSvariable' in gapValue) {
return cssVariableToCSSValue(gapValue);
}
// Handle keywords
if ('keyword' in gapValue) {
return gapValue.keyword;
}
// Handle length/percentage by delegating to the appropriate toCSSValue function
const lengthValue = lengthToCSSValue(gapValue);
if (lengthValue) {
return lengthValue;
}
const percentageValue = percentageToCSSValue(gapValue);
if (percentageValue) {
return percentageValue;
}
return null;
}