@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
641 lines (640 loc) • 25.1 kB
JavaScript
// Background and all constituent properties implementation
// Comprehensive implementation following MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/background
/* eslint-disable @typescript-eslint/no-namespace */
import { isCssVariable, tokenize, getValidKeyword } from '../utils/shared-utils.js';
import { parse as parseColor, toCSSValue as colorToCSSValue } from './color.js';
import { parse as parseLength } from './length.js';
import { parse as parsePercentage } from './percentage.js';
import { parse as parsePosition, toCSSValue as positionToCSSValue } from './position.js';
import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js';
// =============================================================================
// BACKGROUND-ATTACHMENT PROPERTY
// =============================================================================
import { BACKGROUND_ATTACHMENT_KEYWORDS, BOX_KEYWORDS as CLIP_KEYWORDS, BOX_KEYWORDS as ORIGIN_KEYWORDS, BACKGROUND_COLOR_KEYWORDS, BACKGROUND_REPEAT_KEYWORDS, BACKGROUND_SIZE_KEYWORDS, BACKGROUND_IMAGE_KEYWORDS } from '../types.js';
export var BackgroundAttachment;
(function (BackgroundAttachment) {
const ATTACHMENT_KEYWORDS = BACKGROUND_ATTACHMENT_KEYWORDS;
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Attachment-specific keywords
const lowerValue = trimmed.toLowerCase();
const attachmentKeyword = getValidKeyword(lowerValue, ATTACHMENT_KEYWORDS);
if (attachmentKeyword) {
return { type: 'keyword', keyword: attachmentKeyword };
}
return null;
}
BackgroundAttachment.parse = parse;
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle keyword values
return parsed.keyword;
}
BackgroundAttachment.toCSSValue = toCSSValue;
})(BackgroundAttachment || (BackgroundAttachment = {}));
// =============================================================================
// BACKGROUND-CLIP PROPERTY
// =============================================================================
export var BackgroundClip;
(function (BackgroundClip) {
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Clip-specific keywords
const lowerValue = trimmed.toLowerCase();
const clipKeyword = getValidKeyword(lowerValue, CLIP_KEYWORDS);
if (clipKeyword) {
return { type: 'keyword', keyword: clipKeyword };
}
return null;
}
BackgroundClip.parse = parse;
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle keyword values
return parsed.keyword;
}
BackgroundClip.toCSSValue = toCSSValue;
})(BackgroundClip || (BackgroundClip = {}));
// =============================================================================
// BACKGROUND-COLOR PROPERTY
// =============================================================================
export var BackgroundColor;
(function (BackgroundColor) {
const COLOR_KEYWORDS = BACKGROUND_COLOR_KEYWORDS;
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Transparent keyword
const lowerValue = trimmed.toLowerCase();
const colorKeyword = getValidKeyword(lowerValue, COLOR_KEYWORDS);
if (colorKeyword) {
return { type: 'keyword', keyword: colorKeyword };
}
// Try to parse as color
const colorResult = parseColor(trimmed);
if (colorResult) {
// Handle CSS variables from color parser -
// CSS variables should be handled at the main background level, not here
if ('CSSvariable' in colorResult) {
return null;
}
return colorResult;
}
return null;
}
BackgroundColor.parse = parse;
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
if ('keyword' in parsed) {
return parsed.keyword;
}
else {
return colorToCSSValue(parsed);
}
}
BackgroundColor.toCSSValue = toCSSValue;
})(BackgroundColor || (BackgroundColor = {}));
// =============================================================================
// BACKGROUND-ORIGIN PROPERTY
// =============================================================================
export var BackgroundOrigin;
(function (BackgroundOrigin) {
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Origin-specific keywords
const lowerValue = trimmed.toLowerCase();
const originKeyword = getValidKeyword(lowerValue, ORIGIN_KEYWORDS);
if (originKeyword) {
return { type: 'keyword', keyword: originKeyword };
}
return null;
}
BackgroundOrigin.parse = parse;
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle keyword values
return parsed.keyword;
}
BackgroundOrigin.toCSSValue = toCSSValue;
})(BackgroundOrigin || (BackgroundOrigin = {}));
// =============================================================================
// BACKGROUND-REPEAT PROPERTY
// =============================================================================
export var BackgroundRepeat;
(function (BackgroundRepeat) {
const REPEAT_KEYWORDS = BACKGROUND_REPEAT_KEYWORDS;
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Check for single or two-value repeat syntax
const lowerValue = trimmed.toLowerCase();
const tokens = lowerValue.split(/\s+/);
if (tokens.length === 1) {
// Single value
const repeatKeyword = getValidKeyword(tokens[0], REPEAT_KEYWORDS);
if (repeatKeyword) {
return { type: 'keyword', keyword: repeatKeyword };
}
}
else if (tokens.length === 2) {
// Two values
const firstKeyword = getValidKeyword(tokens[0], REPEAT_KEYWORDS);
const secondKeyword = getValidKeyword(tokens[1], REPEAT_KEYWORDS);
if (firstKeyword && secondKeyword) {
// Combine keywords for compound repeat values
const combinedKeyword = `${firstKeyword} ${secondKeyword}`;
return { type: 'keyword', keyword: combinedKeyword };
}
}
return null;
}
BackgroundRepeat.parse = parse;
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle keyword values
return parsed.keyword;
}
BackgroundRepeat.toCSSValue = toCSSValue;
})(BackgroundRepeat || (BackgroundRepeat = {}));
// =============================================================================
// BACKGROUND-SIZE PROPERTY
// =============================================================================
export var BackgroundSize;
(function (BackgroundSize) {
const SIZE_KEYWORDS = BACKGROUND_SIZE_KEYWORDS;
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Size keywords (cover, contain, auto)
const lowerValue = trimmed.toLowerCase();
const sizeKeyword = getValidKeyword(lowerValue, SIZE_KEYWORDS);
if (sizeKeyword) {
return { type: 'keyword', keyword: sizeKeyword };
}
// Parse as length/percentage values
const tokens = lowerValue.split(/\s+/);
if (tokens.length === 1) {
// Single value
const sizeValue = parseSizeValue(tokens[0]);
if (sizeValue) {
return { width: sizeValue };
}
}
else if (tokens.length === 2) {
// Two values
const width = parseSizeValue(tokens[0]);
const height = parseSizeValue(tokens[1]);
if (width && height) {
return { width, height };
}
}
return null;
}
BackgroundSize.parse = parse;
function parseSizeValue(value) {
// Try auto keyword first
if (value === 'auto') {
return { type: 'keyword', keyword: 'auto' };
}
// Try length
const lengthResult = parseLength(value);
if (lengthResult) {
return lengthResult;
}
// Try percentage
const percentageResult = parsePercentage(value);
if (percentageResult) {
return percentageResult;
}
return null;
}
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
if ('keyword' in parsed) {
return parsed.keyword;
}
if ('width' in parsed) {
const widthStr = sizeValueToString(parsed.width);
if ('height' in parsed) {
const heightStr = sizeValueToString(parsed.height);
return `${widthStr} ${heightStr}`;
}
return widthStr;
}
return null;
}
BackgroundSize.toCSSValue = toCSSValue;
function sizeValueToString(sizeValue) {
if (typeof sizeValue === 'object' && sizeValue !== null) {
if ('keyword' in sizeValue) {
return sizeValue.keyword;
}
if ('value' in sizeValue && 'unit' in sizeValue) {
const typedValue = sizeValue;
return `${typedValue.value}${typedValue.unit}`;
}
}
return '';
}
})(BackgroundSize || (BackgroundSize = {}));
// =============================================================================
// BACKGROUND-POSITION PROPERTY
// =============================================================================
export var BackgroundPosition;
(function (BackgroundPosition) {
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Use position evaluator for position parsing
const positionResult = parsePosition(trimmed);
if (positionResult) {
// Handle CSS variables from position parser -
// CSS variables should be handled at the main background level, not here
if ('CSSvariable' in positionResult) {
return null;
}
return positionResult;
}
return null;
}
BackgroundPosition.parse = parse;
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
if ('keyword' in parsed) {
return parsed.keyword;
}
return positionToCSSValue(parsed);
}
BackgroundPosition.toCSSValue = toCSSValue;
})(BackgroundPosition || (BackgroundPosition = {}));
// =============================================================================
// BACKGROUND-IMAGE PROPERTY
// =============================================================================
export var BackgroundImage;
(function (BackgroundImage) {
const IMAGE_KEYWORDS = BACKGROUND_IMAGE_KEYWORDS;
const GRADIENT_FUNCTIONS = ['linear-gradient', 'radial-gradient', 'conic-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'repeating-conic-gradient'];
function parse(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables - return proper CSSVariable object
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Image keywords (none)
const lowerValue = trimmed.toLowerCase();
const imageKeyword = getValidKeyword(lowerValue, IMAGE_KEYWORDS);
if (imageKeyword) {
return { type: 'keyword', keyword: imageKeyword };
}
// Check for URL
if (lowerValue.startsWith('url(')) {
return parseUrl(trimmed);
}
// Check for gradients
for (const gradientFunction of GRADIENT_FUNCTIONS) {
if (lowerValue.startsWith(gradientFunction + '(')) {
return parseGradient(trimmed, gradientFunction);
}
}
return null;
}
BackgroundImage.parse = parse;
function parseUrl(value) {
const match = value.match(/^url\(\s*(['"]?)(.*?)\1\s*\)$/);
if (!match) {
return null;
}
const [, quote, url] = match;
return {
type: 'url',
url: url,
quoted: !!quote
};
}
function parseGradient(value, functionName) {
// Simple gradient parsing - just store the full value
if (value.endsWith(')')) {
return {
type: 'gradient',
function: functionName,
value: value
};
}
return null;
}
function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
if ('keyword' in parsed) {
return parsed.keyword;
}
if ('type' in parsed) {
if (parsed.type === 'url') {
if (parsed.quoted) {
return `url("${parsed.url}")`;
}
else {
return `url(${parsed.url})`;
}
}
if (parsed.type === 'gradient') {
return parsed.value;
}
}
return null;
}
BackgroundImage.toCSSValue = toCSSValue;
})(BackgroundImage || (BackgroundImage = {}));
// =============================================================================
// MAIN BACKGROUND SHORTHAND PROPERTY
// =============================================================================
export 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 (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords
const globalKeyword = getValidKeyword(trimmed.toLowerCase(), ['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
if (globalKeyword) {
return { type: 'keyword', keyword: globalKeyword };
}
// Try to parse as expanded background
const expanded = parseExpandedBackground(trimmed);
if (expanded) {
return expanded;
}
// If nothing matches, treat as keyword (fallback behavior for backward compatibility)
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
function parseExpandedBackground(value) {
// Default values for all background properties
const defaults = {
backgroundAttachment: { type: 'keyword', keyword: 'scroll' },
backgroundClip: { type: 'keyword', keyword: 'border-box' },
backgroundColor: { type: 'keyword', keyword: 'transparent' },
backgroundImage: { type: 'keyword', keyword: 'none' },
backgroundOrigin: { type: 'keyword', keyword: 'padding-box' },
backgroundPosition: { type: 'position', x: '0%', y: '0%' },
backgroundRepeat: { type: 'keyword', keyword: 'repeat' },
backgroundSize: { type: 'keyword', keyword: 'auto' }
};
// Parse individual components
const tokens = tokenize(value);
const result = { ...defaults };
let foundMatch = false;
for (const token of tokens) {
// Try each constituent parser
const attachmentResult = BackgroundAttachment.parse(token);
if (attachmentResult) {
result.backgroundAttachment = attachmentResult;
foundMatch = true;
continue;
}
const clipResult = BackgroundClip.parse(token);
if (clipResult) {
result.backgroundClip = clipResult;
foundMatch = true;
continue;
}
const colorResult = BackgroundColor.parse(token);
if (colorResult) {
result.backgroundColor = colorResult;
foundMatch = true;
continue;
}
const imageResult = BackgroundImage.parse(token);
if (imageResult) {
result.backgroundImage = imageResult;
foundMatch = true;
continue;
}
const originResult = BackgroundOrigin.parse(token);
if (originResult) {
result.backgroundOrigin = originResult;
foundMatch = true;
continue;
}
const positionResult = BackgroundPosition.parse(token);
if (positionResult) {
result.backgroundPosition = positionResult;
foundMatch = true;
continue;
}
const repeatResult = BackgroundRepeat.parse(token);
if (repeatResult) {
result.backgroundRepeat = repeatResult;
foundMatch = true;
continue;
}
const sizeResult = BackgroundSize.parse(token);
if (sizeResult) {
result.backgroundSize = sizeResult;
foundMatch = true;
continue;
}
}
return foundMatch ? result : null;
}
export function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
if ('keyword' in parsed) {
return parsed.keyword;
}
if ('layers' in parsed) {
// Multi-layer background
return parsed.layers.map(layer => expandedToCSSValue(layer)).join(', ');
}
// Single expanded background
return expandedToCSSValue(parsed);
}
function expandedToCSSValue(expanded) {
const parts = [];
// Add non-default values
if (expanded.backgroundImage && !('keyword' in expanded.backgroundImage && expanded.backgroundImage.keyword === 'none')) {
const imageValue = BackgroundImage.toCSSValue(expanded.backgroundImage);
if (imageValue)
parts.push(imageValue);
}
if (expanded.backgroundPosition && 'x' in expanded.backgroundPosition && !(expanded.backgroundPosition.x === '0%' && expanded.backgroundPosition.y === '0%')) {
const positionValue = BackgroundPosition.toCSSValue(expanded.backgroundPosition);
if (positionValue)
parts.push(positionValue);
}
if (expanded.backgroundSize && !('keyword' in expanded.backgroundSize && expanded.backgroundSize.keyword === 'auto')) {
const sizeValue = BackgroundSize.toCSSValue(expanded.backgroundSize);
if (sizeValue)
parts.push('/', sizeValue);
}
if (expanded.backgroundRepeat && !('keyword' in expanded.backgroundRepeat && expanded.backgroundRepeat.keyword === 'repeat')) {
const repeatValue = BackgroundRepeat.toCSSValue(expanded.backgroundRepeat);
if (repeatValue)
parts.push(repeatValue);
}
if (expanded.backgroundAttachment && !('keyword' in expanded.backgroundAttachment && expanded.backgroundAttachment.keyword === 'scroll')) {
const attachmentValue = BackgroundAttachment.toCSSValue(expanded.backgroundAttachment);
if (attachmentValue)
parts.push(attachmentValue);
}
if (expanded.backgroundOrigin && !('keyword' in expanded.backgroundOrigin && expanded.backgroundOrigin.keyword === 'padding-box')) {
const originValue = BackgroundOrigin.toCSSValue(expanded.backgroundOrigin);
if (originValue)
parts.push(originValue);
}
if (expanded.backgroundClip && !('keyword' in expanded.backgroundClip && expanded.backgroundClip.keyword === 'border-box')) {
const clipValue = BackgroundClip.toCSSValue(expanded.backgroundClip);
if (clipValue)
parts.push(clipValue);
}
if (expanded.backgroundColor && !('keyword' in expanded.backgroundColor && expanded.backgroundColor.keyword === 'transparent')) {
const colorValue = BackgroundColor.toCSSValue(expanded.backgroundColor);
if (colorValue)
parts.push(colorValue);
}
return parts.join(' ').trim() || 'transparent';
}