@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
210 lines (209 loc) • 7.69 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_COLUMN_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;
// Handle auto keyword
if (trimmed.toLowerCase() === 'auto') {
return { type: 'keyword', keyword: 'auto' };
}
// Tokenize to handle complex values
const tokens = tokenize(trimmed);
// Single token cases
if (tokens.length === 1) {
const token = tokens[0].toLowerCase();
// Auto keyword
if (token === 'auto') {
return { type: 'keyword', keyword: 'auto' };
}
// Integer value
const intValue = parseInt(token, 10);
if (!isNaN(intValue) && intValue !== 0 && token === intValue.toString()) {
return { type: 'integer', value: intValue };
}
// Custom identifier (line name)
if (/^[a-zA-Z_][\w-]*$/.test(token) && token !== 'span' && token !== 'auto') {
return { type: 'named-line', name: token };
}
return null;
}
// Two token cases
if (tokens.length === 2) {
const [first, second] = tokens.map(t => t.toLowerCase());
// span <integer>
if (first === 'span') {
const intValue = parseInt(second, 10);
if (!isNaN(intValue) && intValue > 0 && second === intValue.toString()) {
return { type: 'span', count: intValue };
}
// span <custom-ident>
if (/^[a-zA-Z_][\w-]*$/.test(second) && second !== 'span' && second !== 'auto') {
return { type: 'span', count: 1, name: second };
}
}
// <integer> span
if (second === 'span') {
const intValue = parseInt(first, 10);
if (!isNaN(intValue) && intValue !== 0 && first === intValue.toString()) {
return { type: 'span', count: Math.abs(intValue) };
}
}
// <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;
}
// Three token cases
if (tokens.length === 3) {
const [first, second, third] = tokens.map(t => t.toLowerCase());
// span <integer> <custom-ident>
if (first === 'span') {
const intValue = parseInt(second, 10);
if (!isNaN(intValue) && intValue > 0 && second === intValue.toString()) {
if (/^[a-zA-Z_][\w-]*$/.test(third) && third !== 'span' && third !== 'auto') {
return { type: 'span', count: intValue, name: third };
}
}
}
// span <custom-ident> <integer>
if (first === 'span') {
const intValue = parseInt(third, 10);
if (!isNaN(intValue) && intValue > 0 && third === intValue.toString()) {
if (/^[a-zA-Z_][\w-]*$/.test(second) && second !== 'span' && second !== 'auto') {
return { type: 'span', count: intValue, name: second };
}
}
}
// <integer> <custom-ident> span
if (third === 'span') {
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: 'span', count: Math.abs(intValue), name: second };
}
}
}
return null;
}
return null;
}
/**
* Converts a parsed grid line back to CSS string
*/
function gridLineToCSSValue(gridLine) {
if (!gridLine)
return 'auto';
switch (gridLine.type) {
case 'keyword':
return gridLine.keyword;
case 'integer':
return gridLine.value.toString();
case 'named-line':
if (gridLine.count !== undefined) {
return `${gridLine.count} ${gridLine.name}`;
}
return gridLine.name;
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 'auto';
}
}
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
if (isGlobalKeyword(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
// Handle grid-column keywords
const gridColumnKeyword = getValidKeyword(trimmed, GRID_COLUMN_KEYWORDS);
if (gridColumnKeyword) {
return { type: 'keyword', keyword: gridColumnKeyword };
}
// Check for slash separator (shorthand with start/end)
if (trimmed.includes(' / ')) {
const parts = trimmed.split(' / ').map(part => part.trim());
if (parts.length === 2) {
const start = parseGridLine(parts[0]);
const end = parseGridLine(parts[1]);
if (start && end) {
return {
type: 'grid-placement',
start: start,
end: end
};
}
}
return null;
}
// Single grid line value (applies to grid-column-start only)
const gridLine = parseGridLine(trimmed);
if (gridLine) {
return {
type: 'grid-placement',
start: gridLine,
end: { type: 'keyword', keyword: 'auto' }
};
}
return null;
}
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 values
if (parsed.type === 'grid-placement') {
const start = gridLineToCSSValue(parsed.start || null);
const end = gridLineToCSSValue(parsed.end || null);
// If end is auto, we can omit it
if (end === 'auto') {
return start;
}
return `${start} / ${end}`;
}
return null;
}