@wix/css-property-parser
Version:
A comprehensive TypeScript library for parsing and serializing CSS property values with full MDN specification compliance
255 lines (254 loc) • 8.11 kB
JavaScript
;
// String data type parser
// Handles parsing of CSS <string> values according to MDN specification
// https://developer.mozilla.org/en-US/docs/Web/CSS/string
// Supports single and double quoted strings with escape sequences
Object.defineProperty(exports, "__esModule", { value: true });
exports.parse = parse;
exports.toCSSValue = toCSSValue;
const shared_utils_1 = require('../utils/shared-utils.cjs');
const css_variable_1 = require('./css-variable.cjs');
/**
* Parses a CSS string value into structured components
* @param value - The CSS string value
* @returns Parsed string object or null if invalid
*/
function parse(value) {
if (!value || typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
if (trimmed === '')
return null;
// Check for CSS variables - use centralized resolver
if ((0, shared_utils_1.isCssVariable)(trimmed)) {
return (0, css_variable_1.parse)(trimmed);
}
// Must start and end with matching quotes
if (trimmed.length < 2)
return null;
const firstChar = trimmed[0];
const lastChar = trimmed[trimmed.length - 1];
// Check for single or double quotes
if (!((firstChar === '"' && lastChar === '"') ||
(firstChar === "'" && lastChar === "'"))) {
return null;
}
const quote = firstChar;
const rawContent = trimmed.slice(1, -1);
// Validate and unescape the content
if (!isValidStringContent(rawContent, quote)) {
return null;
}
const unescapedContent = unescapeString(rawContent);
return {
type: 'string',
value: unescapedContent,
quote
};
}
/**
* Converts a parsed string back to a CSS value string
* @param parsed - The parsed string object
* @returns CSS value string or null if invalid
*/
function toCSSValue(parsed) {
if (!parsed) {
return null;
}
// Handle CSS variables
if ('CSSvariable' in parsed) {
return (0, css_variable_1.toCSSValue)(parsed);
}
// Handle regular strings
if ('value' in parsed && 'quote' in parsed) {
// Validate the parsed string
if (typeof parsed.value !== 'string' ||
(parsed.quote !== '"' && parsed.quote !== "'")) {
return null;
}
const escaped = escapeString(parsed.value, parsed.quote);
return `${parsed.quote}${escaped}${parsed.quote}`;
}
return null;
}
// Internal helper functions (not exported)
/**
* Validates the content inside a quoted string
*/
function isValidStringContent(content, quote) {
let i = 0;
while (i < content.length) {
const char = content[i];
// Unescaped newlines are not allowed in CSS strings
if (char === '\n' || char === '\r') {
return false;
}
// Check for unescaped quotes
if (char === quote) {
return false;
}
// Handle escape sequences
if (char === '\\') {
if (i + 1 >= content.length) {
return false; // Backslash at end of string
}
const nextChar = content[i + 1];
// Valid escape sequences
if (isValidEscapeSequence(nextChar)) {
i += 2; // Skip the escape sequence
continue;
}
// Hex escape sequences (\123456)
if (isHexDigit(nextChar)) {
let hexLength = 1;
let j = i + 2;
// Consume up to 6 hex digits
while (j < content.length && hexLength < 6 && isHexDigit(content[j])) {
hexLength++;
j++;
}
// Optional whitespace after hex escape
if (j < content.length && isWhitespace(content[j])) {
j++;
}
i = j;
continue;
}
// Line continuation (backslash followed by newline)
if (nextChar === '\n') {
i += 2;
continue;
}
if (nextChar === '\r') {
i += 2;
if (i < content.length && content[i] === '\n') {
i++;
}
continue;
}
return false; // Invalid escape sequence
}
i++;
}
return true;
}
/**
* Unescapes a string content
*/
function unescapeString(content) {
let result = '';
let i = 0;
while (i < content.length) {
const char = content[i];
if (char === '\\' && i + 1 < content.length) {
const nextChar = content[i + 1];
// Standard escape sequences
switch (nextChar) {
case 'n':
result += '\n';
i += 2;
break;
case 'r':
result += '\r';
i += 2;
break;
case 't':
result += '\t';
i += 2;
break;
case '\\':
result += '\\';
i += 2;
break;
case '"':
result += '"';
i += 2;
break;
case "'":
result += "'";
i += 2;
break;
case 'f':
result += '\f';
i += 2;
break;
case 'v':
result += '\v';
i += 2;
break;
default:
// Hex escape sequences
if (isHexDigit(nextChar)) {
let hexString = nextChar;
let j = i + 2;
// Consume up to 6 hex digits total
while (j < content.length && hexString.length < 6 && isHexDigit(content[j])) {
hexString += content[j];
j++;
}
// Optional whitespace after hex escape
if (j < content.length && isWhitespace(content[j])) {
j++;
}
const codePoint = parseInt(hexString, 16);
result += String.fromCodePoint(codePoint);
i = j;
}
else if (nextChar === '\n') {
// Line continuation - skip both backslash and newline
i += 2;
}
else if (nextChar === '\r') {
// Line continuation with \r or \r\n
i += 2;
if (i < content.length && content[i] === '\n') {
i++;
}
}
else {
// Unknown escape - treat as literal
result += nextChar;
i += 2;
}
break;
}
}
else {
result += char;
i++;
}
}
return result;
}
/**
* Checks if a character is a valid escape sequence starter
*/
function isValidEscapeSequence(char) {
return ['n', 'r', 't', '\\', '"', "'", 'f', 'v'].includes(char);
}
/**
* Checks if a character is a hex digit
*/
function isHexDigit(char) {
return /[0-9a-fA-F]/.test(char);
}
/**
* Checks if a character is whitespace
*/
function isWhitespace(char) {
return /\s/.test(char);
}
/**
* Escapes a plain string for use in CSS
*/
function escapeString(str, quote = '"') {
return str
.replace(/\\/g, '\\\\')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\f/g, '\\f')
.replace(/\v/g, '\\v')
.replace(new RegExp(quote, 'g'), `\\${quote}`);
}