UNPKG

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