@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
212 lines (211 loc) • 7.22 kB
JavaScript
import { isCssVariable, isGlobalKeyword, tokenize, getValidKeyword } from '../utils/shared-utils.js';
import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js';
import { GRID_ROW_KEYWORDS } from '../types.js';
/**
* Parses a single grid line value (<grid-line>)
* Syntax: auto | <custom-ident> | [ [ <integer> ] && <custom-ident>? ] | [ span && [ <integer> || <custom-ident> ] ]
*/
function parseGridLine(value) {
if (!value || typeof value !== 'string')
return null;
const trimmed = value.trim();
if (trimmed === '')
return null;
// CSS variables
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Handle global keywords first
if (isGlobalKeyword(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
// Handle auto keyword
if (trimmed.toLowerCase() === 'auto') {
return { type: 'keyword', keyword: 'auto' };
}
// Handle span syntax
if (trimmed.toLowerCase().includes('span')) {
const tokens = tokenize(trimmed.toLowerCase());
// Find span keyword
const spanIndex = tokens.findIndex(token => token === 'span');
if (spanIndex === -1)
return null;
let count;
let name;
// Process tokens around span
const otherTokens = tokens.filter((_, index) => index !== spanIndex);
for (const token of otherTokens) {
const intValue = parseInt(token, 10);
if (!isNaN(intValue) && intValue > 0) {
if (count !== undefined)
return null; // Multiple numbers not allowed
count = intValue;
}
else if (/^[a-zA-Z_][\w-]*$/.test(token) && token !== 'auto') {
if (name !== undefined)
return null; // Multiple names not allowed
name = token;
}
else {
return null; // Invalid token
}
}
// Span requires at least count OR name
if (count === undefined && name === undefined) {
count = 1; // Default span count
}
return { type: 'span', count: count || 1, name };
}
// Handle multi-token values (number + identifier combinations)
const tokens = tokenize(trimmed);
if (tokens.length === 2) {
const [first, second] = tokens;
// <integer> <custom-ident>
const intValue = parseInt(first, 10);
if (!isNaN(intValue) && intValue !== 0 && first === intValue.toString()) {
if (/^[a-zA-Z_][\w-]*$/.test(second) && second !== 'span' && second !== 'auto') {
return { type: 'named-line', name: second, count: intValue };
}
}
// <custom-ident> <integer>
const intValue2 = parseInt(second, 10);
if (!isNaN(intValue2) && intValue2 !== 0 && second === intValue2.toString()) {
if (/^[a-zA-Z_][\w-]*$/.test(first) && first !== 'span' && first !== 'auto') {
return { type: 'named-line', name: first, count: intValue2 };
}
}
return null;
}
// Handle single token values
if (tokens.length === 1) {
const token = tokens[0];
// Integer line number
const intValue = parseInt(token, 10);
if (!isNaN(intValue) && intValue !== 0 && token === intValue.toString()) {
return { type: 'integer', value: intValue };
}
// Custom identifier (grid area name or line name)
if (/^[a-zA-Z_][\w-]*$/.test(token) && token !== 'span' && token !== 'auto') {
return { type: 'named-line', name: token };
}
return null;
}
return null;
}
/**
* Converts a grid line value back to CSS string
*/
function gridLineToCSSValue(gridLine) {
if (!gridLine)
return null;
switch (gridLine.type) {
case 'keyword':
return gridLine.keyword;
case 'integer':
return gridLine.value.toString();
case 'named-line': {
if (!gridLine.name)
return null;
const parts = [];
if (gridLine.count !== undefined && gridLine.count !== 1) {
parts.push(gridLine.count.toString());
}
parts.push(gridLine.name);
return parts.join(' ');
}
case 'span': {
const parts = ['span'];
if (gridLine.count !== undefined && gridLine.count !== 1) {
parts.push(gridLine.count.toString());
}
if (gridLine.name) {
parts.push(gridLine.name);
}
return parts.join(' ');
}
case 'variable':
return cssVariableToCSSValue(gridLine) || 'auto';
default:
return null;
}
}
/**
* Parses a CSS grid-row property string into structured components
* Syntax: <grid-line> [ / <grid-line> ]?
*/
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);
}
// Handle global keywords first
if (isGlobalKeyword(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
// Handle grid-row keywords
const gridRowKeyword = getValidKeyword(trimmed, GRID_ROW_KEYWORDS);
if (gridRowKeyword) {
return { type: 'keyword', keyword: gridRowKeyword };
}
// Handle shorthand syntax with slash
if (trimmed.includes(' / ')) {
const parts = trimmed.split(' / ');
if (parts.length === 2) {
const start = parseGridLine(parts[0].trim());
const end = parseGridLine(parts[1].trim());
if (start && end) {
return {
type: 'grid-placement',
start,
end
};
}
}
return null;
}
// Handle single grid line value
const gridLine = parseGridLine(trimmed);
if (gridLine) {
return {
type: 'grid-placement',
start: gridLine
};
}
return null;
}
/**
* Converts a parsed grid-row back to a CSS value string
*/
export function toCSSValue(parsed) {
if (!parsed)
return null;
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle keywords
if (parsed.type === 'keyword') {
return parsed.keyword;
}
// Handle grid placement
if (parsed.type === 'grid-placement') {
const start = gridLineToCSSValue(parsed.start || null);
const end = gridLineToCSSValue(parsed.end || null);
if (start && end) {
// Don't output "auto" for end value if it's auto
if (end === 'auto') {
return start;
}
return `${start} / ${end}`;
}
else if (start) {
return start;
}
}
return null;
}