@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
221 lines (220 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
Object.defineProperty(exports, "__esModule", { value: true });
exports.parse = parse;
exports.toCSSValue = toCSSValue;
const shared_utils_1 = require('../utils/shared-utils.cjs');
const length_1 = require('./length.cjs');
const percentage_1 = require('./percentage.cjs');
const color_1 = require('./color.cjs');
const css_variable_1 = require('./css-variable.cjs');
// Re-export for tests
const types_1 = require('../types.cjs');
// Type guards for text-decoration values
function isTextDecorationLine(value) {
return types_1.TEXT_DECORATION_LINE_KEYWORDS.includes(value.toLowerCase());
}
function isTextDecorationStyle(value) {
return types_1.TEXT_DECORATION_STYLE_KEYWORDS.includes(value.toLowerCase());
}
function isTextDecorationThicknessKeyword(value) {
return types_1.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
*/
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)) {
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 = (0, shared_utils_1.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 = (0, length_1.parse)(token);
if (lengthResult) {
textDecorationThickness = lengthResult;
remainingTokens.splice(i, 1);
i--; // Adjust index after splice
continue;
}
const percentageResult = (0, percentage_1.parse)(token);
if (percentageResult) {
textDecorationThickness = percentageResult;
remainingTokens.splice(i, 1);
i--; // Adjust index after splice
continue;
}
// Try color
const colorResult = (0, color_1.parse)(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
*/
function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables
if ('CSSvariable' in parsed) {
return (0, css_variable_1.toCSSValue)(parsed);
}
const parts = [];
// Add text-decoration-line
if (hasKeywordProperty(parsed.textDecorationLine)) {
if ((0, shared_utils_1.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 = (0, color_1.toCSSValue)(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 = (0, length_1.toCSSValue)(parsed.textDecorationThickness) ||
(0, percentage_1.toCSSValue)(parsed.textDecorationThickness);
if (thicknessValue) {
parts.push(thicknessValue);
}
}
return parts.length > 0 ? parts.join(' ') : 'none';
}
// Export centralized type for external use