UNPKG

@wix/css-property-parser

Version:

A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance

217 lines (216 loc) 8.58 kB
// CSS text-decoration property parser // Handles parsing of CSS text-decoration property according to MDN specification // https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration import { isCssVariable, isGlobalKeyword, tokenize } from '../utils/shared-utils.js'; import { parse as parseLength, toCSSValue as lengthToCSSValue } from './length.js'; import { parse as parsePercentage, toCSSValue as percentageToCSSValue } from './percentage.js'; import { parse as parseColor, toCSSValue as colorToCSSValue } from './color.js'; import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js'; // Re-export for tests import { TEXT_DECORATION_LINE_KEYWORDS, TEXT_DECORATION_STYLE_KEYWORDS, TEXT_DECORATION_THICKNESS_KEYWORDS } from '../types.js'; // Type guards for text-decoration values function isTextDecorationLine(value) { return TEXT_DECORATION_LINE_KEYWORDS.includes(value.toLowerCase()); } function isTextDecorationStyle(value) { return TEXT_DECORATION_STYLE_KEYWORDS.includes(value.toLowerCase()); } function isTextDecorationThicknessKeyword(value) { return TEXT_DECORATION_THICKNESS_KEYWORDS.includes(value.toLowerCase()); } function hasKeywordProperty(value) { return value !== null && typeof value === 'object' && 'keyword' in value; } function hasLinesProperty(value) { return value !== null && typeof value === 'object' && 'lines' in value; } /** * Parses a CSS text-decoration property string into structured components * Follows MDN specification: https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration */ 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 if (isGlobalKeyword(trimmed)) { const globalKeyword = trimmed.toLowerCase(); // Create properly typed global keyword objects for each property const lineGlobalKeyword = { type: 'keyword', keyword: globalKeyword }; const styleGlobalKeyword = { type: 'keyword', keyword: globalKeyword }; const colorGlobalKeyword = { type: 'keyword', keyword: globalKeyword }; const thicknessGlobalKeyword = { type: 'keyword', keyword: globalKeyword }; return { textDecorationLine: lineGlobalKeyword, textDecorationStyle: styleGlobalKeyword, textDecorationColor: colorGlobalKeyword, textDecorationThickness: thicknessGlobalKeyword }; } // Parse shorthand components using shared tokenizer const tokens = tokenize(trimmed); // Initialize constituent properties with defaults let textDecorationLine = { type: 'keyword', lines: ['none'] }; let textDecorationStyle = { type: 'keyword', keyword: 'solid' }; let textDecorationColor = { type: 'color', format: 'named', values: { name: 'currentcolor' } }; let textDecorationThickness = { type: 'keyword', keyword: 'auto' }; // Track which tokens have been consumed const remainingTokens = [...tokens]; const lineValues = []; let foundLine = false; // Parse each token and assign to appropriate constituent property for (let i = 0; i < remainingTokens.length; i++) { const token = remainingTokens[i]; // Try text-decoration-line if (isTextDecorationLine(token)) { const lineValue = token.toLowerCase(); if (lineValue === 'none') { // 'none' cannot be combined with other lines if (lineValues.length > 0) { return null; } lineValues.push(lineValue); } else { // Check if 'none' was already set if (lineValues.includes('none')) { return null; } // Don't add duplicates if (!lineValues.includes(lineValue)) { lineValues.push(lineValue); } } foundLine = true; remainingTokens.splice(i, 1); i--; // Adjust index after splice continue; } // Try text-decoration-style if (isTextDecorationStyle(token)) { textDecorationStyle = { type: 'keyword', keyword: token.toLowerCase() }; remainingTokens.splice(i, 1); i--; // Adjust index after splice continue; } // Try text-decoration-thickness keywords if (isTextDecorationThicknessKeyword(token)) { textDecorationThickness = { type: 'keyword', keyword: token.toLowerCase() }; remainingTokens.splice(i, 1); i--; // Adjust index after splice continue; } // Try length/percentage for thickness const lengthResult = parseLength(token); if (lengthResult) { textDecorationThickness = lengthResult; remainingTokens.splice(i, 1); i--; // Adjust index after splice continue; } const percentageResult = parsePercentage(token); if (percentageResult) { textDecorationThickness = percentageResult; remainingTokens.splice(i, 1); i--; // Adjust index after splice continue; } // Try color const colorResult = parseColor(token); if (colorResult) { // Handle CSS variables from color parser if ('CSSvariable' in colorResult) { // CSS variables shouldn't be parsed at the token level in shorthand // They should be handled at the top level continue; } textDecorationColor = colorResult; remainingTokens.splice(i, 1); i--; // Adjust index after splice continue; } } // If there are remaining unrecognized tokens, the input is invalid if (remainingTokens.length > 0) { return null; } // Must have at least one line decoration specified if (!foundLine) { return null; } // Set the line decoration with all collected lines textDecorationLine = { type: 'keyword', lines: lineValues.length > 0 ? lineValues : ['none'] }; return { textDecorationLine, textDecorationStyle, textDecorationColor, textDecorationThickness }; } /** * Converts a parsed text-decoration back to a CSS value string */ export function toCSSValue(parsed) { if (!parsed) { return null; } // Handle CSS variables if ('CSSvariable' in parsed) { return cssVariableToCSSValue(parsed); } const parts = []; // Add text-decoration-line if (hasKeywordProperty(parsed.textDecorationLine)) { if (isGlobalKeyword(parsed.textDecorationLine.keyword)) { return parsed.textDecorationLine.keyword; } } else if (hasLinesProperty(parsed.textDecorationLine)) { const lines = parsed.textDecorationLine.lines; if (lines.length > 0 && !lines.includes('none')) { parts.push(...lines); } } // Add text-decoration-style if (hasKeywordProperty(parsed.textDecorationStyle)) { if (parsed.textDecorationStyle.keyword !== 'solid') { parts.push(parsed.textDecorationStyle.keyword); } } // Add text-decoration-color let colorValue = null; if ('keyword' in parsed.textDecorationColor) { // Handle TextDecorationColorKeyword colorValue = parsed.textDecorationColor.keyword; } else { // Handle CSSColorValue colorValue = colorToCSSValue(parsed.textDecorationColor); } if (colorValue && colorValue !== 'currentcolor') { parts.push(colorValue); } // Add text-decoration-thickness if (hasKeywordProperty(parsed.textDecorationThickness)) { if (parsed.textDecorationThickness.keyword !== 'auto') { parts.push(parsed.textDecorationThickness.keyword); } } else { const thicknessValue = lengthToCSSValue(parsed.textDecorationThickness) || percentageToCSSValue(parsed.textDecorationThickness); if (thicknessValue) { parts.push(thicknessValue); } } return parts.length > 0 ? parts.join(' ') : 'none'; } // Export centralized type for external use