@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
335 lines (334 loc) • 12.2 kB
JavaScript
"use strict";
// CSS margin property parser - Phase 3 refactored
// Handles parsing of CSS margin shorthand property according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/margin
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');
// Import and re-export centralized types
const types_1 = require('../types.cjs');
// ========================================
// Utilities
// ========================================
/**
* Checks if a string is a valid margin value component
*/
function isMarginValue(value) {
const trimmed = value.trim();
// CSS variables are valid margin values
if ((0, shared_utils_1.isCssVariable)(trimmed)) {
return true;
}
// anchor-size() function is valid
if (trimmed.startsWith('anchor-size(') && trimmed.endsWith(')') && trimmed.length > 12) {
return true;
}
// Global keywords and auto are valid
if ((0, shared_utils_1.isGlobalKeyword)(trimmed) || trimmed.toLowerCase() === 'auto') {
return true;
}
// Try to parse as percentage
const percentageResult = (0, percentage_1.parse)(trimmed);
if (percentageResult) {
return true; // Margin allows negative percentages
}
// Try to parse as length
const lengthResult = (0, length_1.parse)(trimmed);
if (lengthResult) {
return true; // Margin allows negative lengths
}
return false;
}
/**
* Parses a single margin value into atomic type
*/
function parseMarginValue(value) {
const trimmed = value.trim();
// Handle CSS variables in individual components
if ((0, shared_utils_1.isCssVariable)(trimmed)) {
return (0, css_variable_1.parse)(trimmed);
}
// Handle anchor-size() function
if (trimmed.startsWith('anchor-size(') && trimmed.endsWith(')') && trimmed.length > 12) {
const expression = trimmed.slice(12, -1).trim(); // Remove 'anchor-size(' and ')'
if (expression === '') {
// anchor-size() with no arguments is valid
return {
function: 'anchor-size'
};
}
// Parse arguments: [ <anchor-name> || <anchor-size> ]? , <length-percentage>?
const args = expression.split(',').map(arg => arg.trim());
if (args.length > 2) {
return null; // Too many arguments
}
let anchorName;
let sizeKeyword;
let fallback;
if (args.length === 1) {
// Either anchor-name/size-keyword or fallback
const arg = args[0];
const validKeyword = (0, shared_utils_1.getValidKeyword)(arg, types_1.ANCHOR_SIZE_KEYWORDS);
if (validKeyword) {
sizeKeyword = validKeyword;
}
else if (arg.startsWith('--')) {
anchorName = arg;
}
else {
// Try to parse as length-percentage fallback
const percentageResult = (0, percentage_1.parse)(arg);
if (percentageResult) {
fallback = percentageResult;
}
else {
const lengthResult = (0, length_1.parse)(arg);
if (lengthResult) {
fallback = lengthResult;
}
else {
return null; // Invalid argument
}
}
}
}
else if (args.length === 2) {
// First arg: anchor-name or size-keyword, second arg: fallback
const firstArg = args[0];
const secondArg = args[1];
const validFirstKeyword = (0, shared_utils_1.getValidKeyword)(firstArg, types_1.ANCHOR_SIZE_KEYWORDS);
if (validFirstKeyword) {
sizeKeyword = validFirstKeyword;
}
else if (firstArg.startsWith('--')) {
anchorName = firstArg;
}
else {
return null; // Invalid first argument
}
// Second argument should be fallback
const percentageResult = (0, percentage_1.parse)(secondArg);
if (percentageResult) {
fallback = percentageResult;
}
else {
const lengthResult = (0, length_1.parse)(secondArg);
if (lengthResult) {
fallback = lengthResult;
}
else {
return null; // Invalid fallback
}
}
}
return {
function: 'anchor-size',
anchorName,
sizeKeyword,
fallback
};
}
// First validate the value
if (!isMarginValue(trimmed)) {
return null;
}
// Handle keywords (global keywords + auto)
if ((0, shared_utils_1.isGlobalKeyword)(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
if (trimmed.toLowerCase() === 'auto') {
return { type: 'keyword', keyword: 'auto' };
}
// Try percentage first
const percentageResult = (0, percentage_1.parse)(trimmed);
if (percentageResult) {
return percentageResult;
}
// Try length
const lengthResult = (0, length_1.parse)(trimmed);
if (lengthResult) {
return lengthResult;
}
return null;
}
// ========================================
// Main Functions
// ========================================
/**
* Parses a CSS margin property value into structured components
* @param value - The CSS margin property string
* @returns Parsed margin object with individual side values or null if invalid
*/
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 = (0, css_variable_1.parse)(trimmed);
if (cssVariableResult) {
return cssVariableResult;
}
// Handle single global keyword or auto
if ((0, shared_utils_1.isGlobalKeyword)(trimmed)) {
const keyword = { type: 'keyword', keyword: trimmed.toLowerCase() };
return {
marginTop: keyword,
marginRight: keyword,
marginBottom: keyword,
marginLeft: keyword
};
}
if (trimmed.toLowerCase() === 'auto') {
const keyword = { type: 'keyword', keyword: 'auto' };
return {
marginTop: keyword,
marginRight: keyword,
marginBottom: keyword,
marginLeft: keyword
};
}
// Use shared tokenize utility to handle calc expressions properly
const values = (0, shared_utils_1.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 = parseMarginValue(val);
if (!parsedValue) {
return null;
}
parsedValues.push(parsedValue);
}
// Expand values according to CSS margin shorthand rules using shared utility
const expandedStrings = (0, shared_utils_1.expandShorthandValues)(values);
const [topStr, rightStr, bottomStr, leftStr] = expandedStrings;
// Parse each expanded value
const marginTop = parseMarginValue(topStr);
const marginRight = parseMarginValue(rightStr);
const marginBottom = parseMarginValue(bottomStr);
const marginLeft = parseMarginValue(leftStr);
if (!marginTop || !marginRight || !marginBottom || !marginLeft) {
return null;
}
return {
marginTop,
marginRight,
marginBottom,
marginLeft
};
}
/**
* Converts a parsed margin back to a CSS value string
* @param parsed - The parsed margin object
* @returns CSS value string or null if invalid
*/
function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables for entire shorthand
if ('CSSvariable' in parsed) {
return (0, css_variable_1.toCSSValue)(parsed);
}
// Handle single atomic values (apply to all sides)
if (!('marginTop' in parsed)) {
const singleValue = parsed;
return convertMarginValueToCSSValue(singleValue);
}
// Handle expanded margin (MarginExpanded)
const marginExpanded = parsed;
// Check if we have the required expanded structure
if (!marginExpanded.marginTop || !marginExpanded.marginRight ||
!marginExpanded.marginBottom || !marginExpanded.marginLeft) {
return null;
}
const topValue = convertMarginValueToCSSValue(marginExpanded.marginTop);
const rightValue = convertMarginValueToCSSValue(marginExpanded.marginRight);
const bottomValue = convertMarginValueToCSSValue(marginExpanded.marginBottom);
const leftValue = convertMarginValueToCSSValue(marginExpanded.marginLeft);
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 margin value back to CSS string
*/
function convertMarginValueToCSSValue(marginValue) {
// Handle CSS variables in components
if ('CSSvariable' in marginValue) {
return (0, css_variable_1.toCSSValue)(marginValue);
}
// Handle keywords
if ('keyword' in marginValue) {
return marginValue.keyword;
}
// Handle anchor-size function
if ('function' in marginValue && marginValue.function === 'anchor-size') {
const anchorFunc = marginValue;
const args = [];
if (anchorFunc.anchorName) {
args.push(anchorFunc.anchorName);
}
if (anchorFunc.sizeKeyword) {
args.push(anchorFunc.sizeKeyword);
}
if (anchorFunc.fallback) {
// Convert fallback to CSS string - handle length, percentage, and calc expressions
let fallbackStr = null;
if ('CSSvariable' in anchorFunc.fallback) {
fallbackStr = (0, css_variable_1.toCSSValue)(anchorFunc.fallback);
}
else if ('expression' in anchorFunc.fallback && 'function' in anchorFunc.fallback) {
// Handle calc, min, max, clamp expressions
const calcFunc = anchorFunc.fallback;
fallbackStr = `${calcFunc.function}(${calcFunc.expression})`;
}
else if ('unit' in anchorFunc.fallback && anchorFunc.fallback.unit === '%') {
fallbackStr = (0, percentage_1.toCSSValue)(anchorFunc.fallback);
}
else if ('value' in anchorFunc.fallback && 'unit' in anchorFunc.fallback) {
fallbackStr = (0, length_1.toCSSValue)(anchorFunc.fallback);
}
if (fallbackStr) {
args.push(fallbackStr);
}
}
return `anchor-size(${args.join(', ')})`;
}
// Handle length/percentage by delegating to the appropriate toCSSValue function
const lengthValue = (0, length_1.toCSSValue)(marginValue);
if (lengthValue) {
return lengthValue;
}
const percentageValue = (0, percentage_1.toCSSValue)(marginValue);
if (percentageValue) {
return percentageValue;
}
return null;
}