/** * Pure CSS utility functions for TemplateSampleForm. * Extracted here to keep the main form component under 300 lines. */ /** Normalize list-style CSS properties — preserves special values like disclosure-closed */ export function normalizeListStyleProps( obj: Record, ): Record { const normalized = { ...obj }; const specialListStyles = ["disclosure-closed", "disclosure-open", "none"]; if (normalized["list-style"]) { const listStyleValue = normalized["list-style"].split(" ")[0]; if (specialListStyles.includes(listStyleValue)) { delete normalized["list-style-type"]; return normalized; } if (!normalized["list-style-type"]) { normalized["list-style-type"] = listStyleValue; } } if (normalized["list-style-type"] && !normalized["list-style"]) { normalized["list-style"] = normalized["list-style-type"]; } return normalized; } /** Serialize a style object to a CSS declaration string */ export function styleObjectToCss(style: Record): string { return Object.entries(style) .filter(([, v]) => v !== null && v !== undefined && v !== "null" && v !== "") .map(([k, v]) => `${k}: ${String(v)};`) .join(" "); } /** * Parse CSS declarations from a string, correctly handling data URIs * (colons and semicolons inside url(...) are not treated as delimiters). */ export function parseCSSDeclarations(cssString: string): Array<[string, string]> { const results: Array<[string, string]> = []; let depth = 0; let current = ""; for (let i = 0; i < cssString.length; i++) { const ch = cssString[i]; if (ch === "(") depth++; else if (ch === ")") depth--; if (ch === ";" && depth === 0) { current = current.trim(); if (current) { const colonIdx = current.indexOf(":"); if (colonIdx > 0) { const prop = current.slice(0, colonIdx).trim(); const val = current.slice(colonIdx + 1).trim(); if (prop && val) results.push([prop, val]); } } current = ""; } else { current += ch; } } // Handle last declaration with no trailing semicolon current = current.trim(); if (current) { const colonIdx = current.indexOf(":"); if (colonIdx > 0) { const prop = current.slice(0, colonIdx).trim(); const val = current.slice(colonIdx + 1).trim(); if (prop && val) results.push([prop, val]); } } return results; } /** Convert a CSS string to a normalized style object (data-URI safe) */ export function cssStringToObject(cssString: string): Record { const obj: Record = {}; parseCSSDeclarations(cssString).forEach(([prop, val]) => { obj[prop] = val; }); return normalizeListStyleProps(obj); } /** Convert a simple CSS string to a style object (no data-URI support) */ export function parseCSSStringToObject(cssString: string): Record { const obj: Record = {}; cssString .split(";") .filter((d) => d.trim()) .forEach((declaration) => { const [property, value] = declaration.split(":").map((s) => s.trim()); if (property && value) { obj[property] = value; } }); return normalizeListStyleProps(obj); }