UNPKG

@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
// 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'; }