@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
258 lines (257 loc) • 10.4 kB
JavaScript
"use strict";
// CSS grid-area property evaluator
// Handles CSS grid-area shorthand property values according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-area
Object.defineProperty(exports, "__esModule", { value: true });
exports.parse = parse;
exports.toCSSValue = toCSSValue;
const shared_utils_1 = require('../utils/shared-utils.cjs');
const css_variable_1 = require('./css-variable.cjs');
/**
* 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' };
}
// Handle span syntax: "span" followed by integer and/or custom-ident
if (trimmed.toLowerCase().includes('span')) {
const tokens = trimmed.split(/\s+/);
// Find span token
const spanIndex = tokens.findIndex(token => token.toLowerCase() === 'span');
if (spanIndex === -1)
return null;
// Get remaining tokens (excluding 'span')
const remainingTokens = [...tokens.slice(0, spanIndex), ...tokens.slice(spanIndex + 1)];
if (remainingTokens.length === 0) {
// Just "span" - defaults to span 1
return { type: 'span', count: 1 };
}
if (remainingTokens.length === 1) {
const token = remainingTokens[0];
// Check if it's an integer
const intValue = parseInt(token, 10);
if (!isNaN(intValue) && intValue > 0 && token === intValue.toString()) {
return { type: 'span', count: intValue };
}
// Check if it's a custom identifier
if (/^[a-zA-Z_][\w-]*$/.test(token) && token !== 'auto') {
return { type: 'span', count: 1, name: token };
}
return null;
}
if (remainingTokens.length === 2) {
const [first, second] = remainingTokens;
// Try parsing as integer + custom-ident
const intValue1 = parseInt(first, 10);
if (!isNaN(intValue1) && intValue1 > 0 && first === intValue1.toString()) {
if (/^[a-zA-Z_][\w-]*$/.test(second) && second !== 'auto') {
return { type: 'span', count: intValue1, name: second };
}
}
// Try parsing as custom-ident + integer
const intValue2 = parseInt(second, 10);
if (!isNaN(intValue2) && intValue2 > 0 && second === intValue2.toString()) {
if (/^[a-zA-Z_][\w-]*$/.test(first) && first !== 'auto') {
return { type: 'span', count: intValue2, name: first };
}
}
return null;
}
return null;
}
// Handle multiple tokens (integer + custom-ident without span)
const tokens = trimmed.split(/\s+/);
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 GridLineValue back to CSS string representation
*/
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': {
let result = 'span';
if (gridLine.count && gridLine.count > 1) {
result += ` ${gridLine.count}`;
}
if (gridLine.name) {
result += ` ${gridLine.name}`;
}
// If count is 1 and no name, just return "span"
if (gridLine.count === 1 && !gridLine.name) {
return 'span';
}
return result;
}
default:
return 'auto';
}
}
/**
* Parses a CSS grid-area property string into structured components
* Syntax: <grid-line> [ / <grid-line> [ / <grid-line> [ / <grid-line> ]]]
* Values represent: grid-row-start / grid-column-start / grid-row-end / grid-column-end
*/
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 ((0, shared_utils_1.isCssVariable)(trimmed)) {
return (0, css_variable_1.parse)(trimmed);
}
// Handle global keywords
if ((0, shared_utils_1.isGlobalKeyword)(trimmed)) {
return { type: 'keyword', keyword: trimmed.toLowerCase() };
}
// Parse grid area syntax: up to 4 values separated by " / "
const parts = trimmed.split(' / ').map(part => part.trim());
if (parts.length > 4) {
return null; // Too many values
}
// Parse each part as a grid line
const gridLines = parts.map(part => parseGridLine(part));
// Check if any part failed to parse
if (gridLines.some(line => line === null)) {
return null;
}
// Apply MDN specification rules for missing values
const [rowStart, columnStart, rowEnd, columnEnd] = gridLines;
// Build result according to MDN specification defaults
const result = { type: 'grid-area' };
// Always set grid-row-start
if (rowStart) {
result.rowStart = rowStart;
}
// Set grid-column-start (defaults to auto if not provided)
if (parts.length >= 2 && columnStart) {
result.columnStart = columnStart;
}
else if (parts.length === 1) {
// If only one value provided, it can be a custom-ident that applies to all 4 properties
// OR it's just the row-start value with others defaulting to auto
result.columnStart = { type: 'keyword', keyword: 'auto' };
}
// Set grid-row-end (defaults based on grid-row-start)
if (parts.length >= 3 && rowEnd) {
result.rowEnd = rowEnd;
}
else if (rowStart && rowStart.type === 'named-line' && !rowStart.count) {
// If grid-row-start is a custom-ident, grid-row-end defaults to that same custom-ident
result.rowEnd = { ...rowStart };
}
else {
result.rowEnd = { type: 'keyword', keyword: 'auto' };
}
// Set grid-column-end (defaults based on grid-column-start)
if (parts.length === 4 && columnEnd) {
result.columnEnd = columnEnd;
}
else if (result.columnStart && result.columnStart.type === 'named-line' && !result.columnStart.count) {
// If grid-column-start is a custom-ident, grid-column-end defaults to that same custom-ident
result.columnEnd = { ...result.columnStart };
}
else {
result.columnEnd = { type: 'keyword', keyword: 'auto' };
}
return result;
}
/**
* Converts a parsed GridAreaValue back to CSS string representation
*/
function toCSSValue(parsed) {
if (!parsed)
return null;
if (parsed.type === 'keyword') {
return parsed.keyword;
}
if (parsed.type === 'variable') {
return (0, css_variable_1.toCSSValue)(parsed);
}
if (parsed.type === 'grid-area') {
const parts = [];
// Helper function to check if a grid line is auto
const isAuto = (gridLine) => gridLine?.type === 'keyword' && gridLine.keyword === 'auto';
// Helper function to check if two grid lines are equal
const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
// Always include row-start
parts.push(gridLineToCSSValue(parsed.rowStart));
// Check what we need to include
const needColumnStart = !isAuto(parsed.columnStart);
const needRowEnd = !isAuto(parsed.rowEnd) && !isEqual(parsed.rowStart, parsed.rowEnd);
const needColumnEnd = !isAuto(parsed.columnEnd) && !isEqual(parsed.columnStart, parsed.columnEnd);
// Special case: if it's a named area (same name for start/end), only include one value
if (parsed.rowStart && parsed.rowStart.type === 'named-line' &&
isEqual(parsed.rowStart, parsed.rowEnd) &&
isAuto(parsed.columnStart) &&
isAuto(parsed.columnEnd)) {
return parts[0];
}
// Include column-start if needed
if (needColumnStart || needRowEnd || needColumnEnd) {
parts.push(gridLineToCSSValue(parsed.columnStart));
}
// Include row-end if needed
if (needRowEnd || needColumnEnd) {
parts.push(gridLineToCSSValue(parsed.rowEnd));
}
// Include column-end if needed
if (needColumnEnd) {
parts.push(gridLineToCSSValue(parsed.columnEnd));
}
return parts.join(' / ');
}
return null;
}