UNPKG

@wix/css-property-parser

Version:

A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance

446 lines (445 loc) 13.8 kB
// CSS grid-template-columns property evaluator // Handles CSS grid-template-columns property values according to MDN specification // https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns import { GRID_TEMPLATE_KEYWORDS, GRID_TRACK_SIZING_KEYWORDS } from '../types.js'; import { isCssVariable, isGlobalKeyword } from '../utils/shared-utils.js'; 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'; /** * Parse a CSS grid-template-columns property string * @param value - CSS value string to parse * @returns Parsed grid template columns value 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 - parse them if valid if (isCssVariable(trimmed)) { return parseCSSVariable(trimmed); } // Handle global keywords (inherit, initial, unset, revert, revert-layer) if (isGlobalKeyword(trimmed)) { return { type: 'keyword', keyword: trimmed.toLowerCase() }; } // Handle grid template keywords (none, subgrid, masonry) const lowerValue = trimmed.toLowerCase(); if (GRID_TEMPLATE_KEYWORDS.includes(lowerValue)) { return { type: 'keyword', keyword: lowerValue }; } // Parse track list const tracks = parseTrackList(trimmed); if (tracks && tracks.length > 0) { return { type: 'grid-template', tracks }; } return null; } /** * Convert a parsed grid-template-columns value back to CSS string * @param parsed - Parsed grid template columns value * @returns CSS string or null if invalid */ export function toCSSValue(parsed) { if (!parsed) { return null; } // Handle CSS variables if ('CSSvariable' in parsed) { return cssVariableToCSSValue(parsed); } // Handle keywords if ('keyword' in parsed) { return parsed.keyword; } // Handle grid template with tracks if (parsed.type === 'grid-template' && 'tracks' in parsed) { const trackStrings = parsed.tracks.map(trackToCSSValue).filter(Boolean); return trackStrings.length > 0 ? trackStrings.join(' ') : null; } return null; } /** * Parse a track list string into an array of track items * @param value - CSS track list string * @returns Array of track list items or null if invalid */ function parseTrackList(value) { const tokens = tokenizeTrackList(value); if (!tokens || tokens.length === 0) { return null; } const tracks = []; let i = 0; while (i < tokens.length) { const token = tokens[i]; // Parse line names [name1 name2] if (token.startsWith('[') && token.endsWith(']')) { const lineName = parseLineName(token); if (lineName) { tracks.push(lineName); } else { return null; // Invalid line name should fail entire parse } i++; continue; } // Parse repeat() function if (token.startsWith('repeat(')) { const repeatResult = parseRepeatFunction(token); if (repeatResult) { tracks.push(repeatResult); } else { return null; // Invalid repeat should fail entire parse } i++; continue; } // Parse track size const trackSize = parseTrackSize(token); if (trackSize) { tracks.push(trackSize); } else { return null; // Invalid track size should fail entire parse } i++; } return tracks.length > 0 ? tracks : null; } /** * Tokenize track list while respecting function boundaries * @param value - CSS track list string * @returns Array of tokens */ function tokenizeTrackList(value) { const tokens = []; let current = ''; let depth = 0; let inBrackets = false; for (let i = 0; i < value.length; i++) { const char = value[i]; if (char === '[' && depth === 0) { // Start of line name at top level if (current.trim()) { tokens.push(current.trim()); current = ''; } inBrackets = true; current += char; } else if (char === ']' && inBrackets && depth === 0) { // End of line name at top level current += char; inBrackets = false; tokens.push(current.trim()); current = ''; } else if (char === '(' && !inBrackets) { // Start of function depth++; current += char; } else if (char === ')' && !inBrackets) { // End of function depth--; current += char; if (depth === 0) { tokens.push(current.trim()); current = ''; } } else if (char === ' ' && depth === 0 && !inBrackets) { // Space at top level (not inside function or brackets) if (current.trim()) { tokens.push(current.trim()); current = ''; } } else { // Regular character current += char; } } if (current.trim()) { tokens.push(current.trim()); } return tokens; } /** * Parse a line name token like [line-name1 line-name2] * @param token - Line name token * @returns GridLineName or null if invalid */ function parseLineName(token) { if (!token.startsWith('[') || !token.endsWith(']')) { return null; } const content = token.slice(1, -1).trim(); if (!content) { return null; } const names = content.split(/\s+/).filter(name => { // Validate line name (must be valid identifier, not 'span' or 'auto') return name && name !== 'span' && name !== 'auto' && /^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(name); }); return names.length > 0 ? { type: 'line-name', names } : null; } /** * Parse a track size value * @param token - Track size token * @returns GridTrackSize or null if invalid */ function parseTrackSize(token) { if (!token) { return null; } // Handle keywords if (GRID_TRACK_SIZING_KEYWORDS.includes(token.toLowerCase())) { return { type: 'keyword', keyword: token.toLowerCase() }; } // Handle minmax() function if (token.startsWith('minmax(') && token.endsWith(')')) { return parseMinMaxFunction(token); } // Handle fit-content() function if (token.startsWith('fit-content(') && token.endsWith(')')) { return parseFitContentFunction(token); } // Handle flex values (fr unit) if (token.endsWith('fr')) { const numStr = token.slice(0, -2); const num = parseFloat(numStr); if (!isNaN(num) && num >= 0) { return { type: 'flex', value: num, unit: 'fr' }; } } // Try to parse as percentage const percentageResult = parsePercentage(token); if (percentageResult) { return percentageResult; } // Try to parse as length const lengthResult = parseLength(token); if (lengthResult) { // Special handling for 0 value - ensure it has px unit if no unit specified if (lengthResult.type === 'length' && 'value' in lengthResult && lengthResult.value === 0 && !lengthResult.unit) { return { ...lengthResult, unit: 'px' }; } return lengthResult; } return null; } /** * Parse minmax() function * @param token - minmax() function token * @returns GridMinMaxFunction or null if invalid */ function parseMinMaxFunction(token) { if (!token.startsWith('minmax(') || !token.endsWith(')')) { return null; } const content = token.slice(7, -1); // Remove 'minmax(' and ')' const parts = content.split(',').map(p => p.trim()); if (parts.length !== 2) { return null; } const min = parseTrackSize(parts[0]); const max = parseTrackSize(parts[1]); if (min && max) { return { type: 'function', function: 'minmax', min, max }; } return null; } /** * Parse fit-content() function * @param token - fit-content() function token * @returns GridFitContentFunction or null if invalid */ function parseFitContentFunction(token) { if (!token.startsWith('fit-content(') || !token.endsWith(')')) { return null; } const content = token.slice(12, -1); // Remove 'fit-content(' and ')' // Try percentage first const percentageResult = parsePercentage(content); if (percentageResult) { return { type: 'function', function: 'fit-content', size: percentageResult }; } // Try length const lengthResult = parseLength(content); if (lengthResult) { return { type: 'function', function: 'fit-content', size: lengthResult }; } return null; } /** * Parse repeat() function * @param token - repeat() function token * @returns GridRepeatFunction or null if invalid */ function parseRepeatFunction(token) { if (!token.startsWith('repeat(') || !token.endsWith(')')) { return null; } const content = token.slice(7, -1); // Remove 'repeat(' and ')' const commaIndex = content.indexOf(','); if (commaIndex === -1) { return null; } const countStr = content.slice(0, commaIndex).trim(); const valuesStr = content.slice(commaIndex + 1).trim(); // Parse count let count; if (countStr === 'auto-fill') { count = 'auto-fill'; } else if (countStr === 'auto-fit') { count = 'auto-fit'; } else { const num = parseInt(countStr, 10); if (isNaN(num) || num < 1) { return null; } count = num; } // Parse values - use the same tokenizer but parse individually const valueTokens = tokenizeTrackList(valuesStr); if (!valueTokens || valueTokens.length === 0) { return null; } const values = []; for (const token of valueTokens) { if (token.startsWith('[') && token.endsWith(']')) { const lineName = parseLineName(token); if (lineName) { values.push(lineName); } else { return null; // Invalid line name } } else { const trackSize = parseTrackSize(token); if (trackSize) { values.push(trackSize); } else { return null; // Invalid track size } } } if (values.length === 0) { return null; } return { type: 'function', function: 'repeat', count, values }; } /** * Convert a track list item to CSS string * @param track - Track list item * @returns CSS string or null if invalid */ function trackToCSSValue(track) { if (!track) { return null; } // Handle line names if (track.type === 'line-name') { return `[${track.names.join(' ')}]`; } // Handle repeat function if (track.type === 'function' && track.function === 'repeat') { const repeatTrack = track; const valuesStr = repeatTrack.values.map(trackToCSSValue).filter(Boolean).join(' '); return `repeat(${repeatTrack.count}, ${valuesStr})`; } // Handle track sizes return trackSizeToCSSValue(track); } /** * Convert a track size to CSS string * @param trackSize - Track size value * @returns CSS string or null if invalid */ function trackSizeToCSSValue(trackSize) { if (!trackSize) { return null; } // Handle CSS variables if ('CSSvariable' in trackSize) { return cssVariableToCSSValue(trackSize); } // Handle keywords if ('keyword' in trackSize) { return trackSize.keyword; } // Handle flex values if (trackSize.type === 'flex') { const flexValue = trackSize; return `${flexValue.value}fr`; } // Handle functions if (trackSize.type === 'function') { if ('function' in trackSize) { if (trackSize.function === 'minmax') { const minmax = trackSize; const minStr = trackSizeToCSSValue(minmax.min); const maxStr = trackSizeToCSSValue(minmax.max); if (minStr && maxStr) { return `minmax(${minStr}, ${maxStr})`; } } else if (trackSize.function === 'fit-content') { const fitContent = trackSize; const size = fitContent.size; let sizeStr = null; if (size.type === 'percentage') { sizeStr = percentageToCSSValue(size); } else if (size.type === 'length') { sizeStr = lengthToCSSValue(size); } if (sizeStr) { return `fit-content(${sizeStr})`; } } } } // Handle length values if (trackSize.type === 'length' && 'unit' in trackSize) { return lengthToCSSValue(trackSize); } // Handle percentage values if (trackSize.type === 'percentage') { return percentageToCSSValue(trackSize); } return null; }