@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
JavaScript
// CSS grid-template-rows property evaluator
// Handles CSS grid-template-rows property values according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-rows
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-rows property string
* @param value - CSS value string to parse
* @returns Parsed grid template rows 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-rows value back to CSS string
* @param parsed - Parsed grid template rows 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;
}