@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
160 lines (159 loc) • 6.7 kB
JavaScript
// Position data type parser
// Handles parsing of CSS position values according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/position_value
// (background-position, object-position, etc.) and reconstruction back to CSS string
import { isCssVariable } from '../utils/shared-utils.js';
import { parse as parseLengthPercentage } from './length-percentage.js';
import { parse as parseCSSVariable } from './css-variable.js';
// Position keywords
const HORIZONTAL_KEYWORDS = ['left', 'center', 'right'];
const VERTICAL_KEYWORDS = ['top', 'center', 'bottom'];
/**
* Parses a CSS position value into structured components
* @param value - The CSS position value
* @returns Parsed position object or null if invalid
*/
export function parse(value) {
if (!value || value.trim() === '') {
return null;
}
const trimmed = value.trim();
// Check for CSS variables - parse them if valid
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Handle single value
if (!trimmed.includes(' ')) {
return parseSingleValue(trimmed);
}
// Handle multiple values
const tokens = trimmed.split(/\s+/);
if (tokens.length === 2) {
return parseTwoValues(tokens[0], tokens[1]);
}
else if (tokens.length === 3) {
return parseThreeValues(tokens[0], tokens[1], tokens[2]);
}
else if (tokens.length === 4) {
return parseFourValues(tokens[0], tokens[1], tokens[2], tokens[3]);
}
return null;
}
/**
* Converts a parsed position back to a CSS value string
* @param parsed - The parsed position object
* @returns CSS value string or null if invalid
*/
export function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables
if ('CSSvariable' in parsed) {
if (parsed.defaultValue) {
return `var(${parsed.CSSvariable}, ${parsed.defaultValue})`;
}
return `var(${parsed.CSSvariable})`;
}
// Handle position values - cast to CSSPositionValue after type guard
const positionValue = parsed;
// Validate the parsed position
if (typeof positionValue.x !== 'string' || typeof positionValue.y !== 'string') {
return null;
}
const parts = [];
parts.push(positionValue.x);
if (positionValue.xOffset)
parts.push(positionValue.xOffset);
parts.push(positionValue.y);
if (positionValue.yOffset)
parts.push(positionValue.yOffset);
return parts.join(' ');
}
// Internal helper functions (not exported)
function parseSingleValue(value) {
if (HORIZONTAL_KEYWORDS.includes(value)) {
return { type: 'position', x: value, y: 'center' };
}
else if (VERTICAL_KEYWORDS.includes(value)) {
return { type: 'position', x: 'center', y: value };
}
else if (parseLengthPercentage(value) !== null) {
return { type: 'position', x: value, y: 'center' };
}
return null;
}
function parseTwoValues(first, second) {
// Handle 'center center' as a special case first
if (first === 'center' && second === 'center') {
return { type: 'position', x: 'center', y: 'center' };
}
// Both horizontal keywords (excluding center)
if (HORIZONTAL_KEYWORDS.includes(first) && HORIZONTAL_KEYWORDS.includes(second) &&
first !== 'center' && second !== 'center') {
return null; // Invalid
}
// Both vertical keywords (excluding center)
if (VERTICAL_KEYWORDS.includes(first) && VERTICAL_KEYWORDS.includes(second) &&
first !== 'center' && second !== 'center') {
return null; // Invalid
}
// Check if both are length/percentage values first
const firstIsLP = parseLengthPercentage(first) !== null;
const secondIsLP = parseLengthPercentage(second) !== null;
// Both are length/percentage
if (firstIsLP && secondIsLP) {
return { type: 'position', x: first, y: second };
}
// First is horizontal keyword, second is vertical keyword or length/percentage
if (HORIZONTAL_KEYWORDS.includes(first) && (VERTICAL_KEYWORDS.includes(second) || secondIsLP)) {
return { type: 'position', x: first, y: second };
}
// First is vertical keyword, second is horizontal keyword or length/percentage
if (VERTICAL_KEYWORDS.includes(first) && (HORIZONTAL_KEYWORDS.includes(second) || secondIsLP)) {
return { type: 'position', x: second, y: first };
}
// First is length/percentage, second is vertical keyword
if (firstIsLP && VERTICAL_KEYWORDS.includes(second)) {
return { type: 'position', x: first, y: second };
}
return null;
}
function parseThreeValues(first, second, third) {
// Reject invalid combinations like 'left right center' upfront
if (HORIZONTAL_KEYWORDS.includes(first) && HORIZONTAL_KEYWORDS.includes(second) &&
first !== 'center' && second !== 'center') {
return null;
}
if (VERTICAL_KEYWORDS.includes(first) && VERTICAL_KEYWORDS.includes(second) &&
first !== 'center' && second !== 'center') {
return null;
}
// Pattern: keyword offset keyword
if (HORIZONTAL_KEYWORDS.includes(first) && parseLengthPercentage(second) !== null && VERTICAL_KEYWORDS.includes(third)) {
return { type: 'position', x: first, y: third, xOffset: second };
}
if (VERTICAL_KEYWORDS.includes(first) && parseLengthPercentage(second) !== null && HORIZONTAL_KEYWORDS.includes(third)) {
return { type: 'position', x: third, y: first, yOffset: second };
}
// Pattern: keyword keyword offset
if (HORIZONTAL_KEYWORDS.includes(first) && VERTICAL_KEYWORDS.includes(second) && parseLengthPercentage(third) !== null) {
return { type: 'position', x: first, y: second, yOffset: third };
}
if (VERTICAL_KEYWORDS.includes(first) && HORIZONTAL_KEYWORDS.includes(second) && parseLengthPercentage(third) !== null) {
return { type: 'position', x: second, y: first, xOffset: third };
}
return null;
}
function parseFourValues(first, second, third, fourth) {
// Pattern: horizontal offset vertical offset
if (HORIZONTAL_KEYWORDS.includes(first) && parseLengthPercentage(second) !== null &&
VERTICAL_KEYWORDS.includes(third) && parseLengthPercentage(fourth) !== null) {
return { type: 'position', x: first, y: third, xOffset: second, yOffset: fourth };
}
if (VERTICAL_KEYWORDS.includes(first) && parseLengthPercentage(second) !== null &&
HORIZONTAL_KEYWORDS.includes(third) && parseLengthPercentage(fourth) !== null) {
return { type: 'position', x: third, y: first, yOffset: second, xOffset: fourth };
}
return null;
}