@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
JavaScript
// 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