UNPKG

@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
"use strict"; // 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}`); }