UNPKG

@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
"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; }