@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
120 lines (119 loc) • 4.38 kB
JavaScript
// Font Family property parser - Modern API Implementation
// Handles parsing of CSS font-family property according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-family
import { isCssVariable, isGlobalKeyword } from '../utils/shared-utils.js';
import { parse as parseCSSVariable, toCSSValue as cssVariableToCSSValue } from './css-variable.js';
// Generic font families as per CSS spec
const GENERIC_FAMILIES = [
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace',
'ui-rounded', 'math', 'emoji', 'fangsong'
];
/**
* Parse font family value - reusing logic from font.ts but modernized
*/
function parseFontFamily(value) {
if (!value || value.trim() === '') {
return null;
}
const families = [];
const parts = value.split(',');
for (const part of parts) {
const trimmed = part.trim();
if (trimmed === '')
continue;
// Check for invalid tokens that suggest this isn't a font family
// Reject if it contains CSS units (px, em, rem, %, etc.)
if (/\d+(px|em|rem|pt|pc|in|cm|mm|ex|ch|vh|vw|vmin|vmax|%)(\s|$)/i.test(trimmed)) {
return null;
}
// Handle quotes - preserve them but validate the content
const family = trimmed;
let unquotedFamily = family;
if ((family.startsWith('"') && family.endsWith('"')) ||
(family.startsWith("'") && family.endsWith("'"))) {
unquotedFamily = family.slice(1, -1);
// Reject empty quoted strings
if (unquotedFamily === '') {
return null;
}
// Process Unicode escape sequences in quoted strings
try {
unquotedFamily = unquotedFamily.replace(/\\u([0-9a-fA-F]{4})/g, (match, hex) => {
return String.fromCharCode(parseInt(hex, 16));
});
}
catch {
// Invalid Unicode escape sequence
return null;
}
// Reject quoted strings with invalid characters (newlines, tabs, etc.)
if (/[\n\r\t]/.test(unquotedFamily)) {
return null;
}
}
// Validate font family names for invalid characters
// Generic families are always valid
const lowerFamily = unquotedFamily.toLowerCase();
if (!GENERIC_FAMILIES.includes(lowerFamily)) {
// For non-generic families, specifically reject characters that are clearly invalid in font names
// Reject if contains numbers followed by units or other clearly non-font-name patterns
if (/^\d/.test(unquotedFamily) || /[<>{}[\]()@#$%^&*+=|\\:;"?/]/.test(unquotedFamily)) {
return null;
}
}
families.push(family);
}
if (families.length === 0) {
return null;
}
return { type: 'font-list', families };
}
/**
* Parses a CSS font-family property value
* @param value - The CSS font-family property string
* @returns Parsed font family object or null if invalid
*/
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 (modern pattern)
if (isCssVariable(trimmed)) {
return parseCSSVariable(trimmed);
}
// Global keywords - not supported in current type system for font-family
// TODO: Add FontFamilyKeywordValue type to support global keywords
if (isGlobalKeyword(trimmed)) {
return null;
}
// Parse font family list
const result = parseFontFamily(trimmed);
if (result) {
return result;
}
return null;
}
/**
* Converts a parsed font family back to a CSS value string
* @param parsed - The parsed font family object
* @returns CSS value string or null if invalid
*/
export function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables
if ('CSSvariable' in parsed) {
return cssVariableToCSSValue(parsed);
}
// Handle font family list
if ('families' in parsed && parsed.type === 'font-list') {
return parsed.families.join(', ');
}
return null;
}