From 4aa41fbf970aa32390f877c33399011c7a54dcdb Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 6 Jun 2026 19:32:18 +0300 Subject: [PATCH] fix(dom-css): parse inline styles with CSSOM --- .../docs/development/dom-css-parser-audit.md | 4 ++-- packages/dom-css/src/style-attribute.ts | 22 +++++++++++++------ packages/dom-css/tests/runtime.test.ts | 15 +++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/docs/development/dom-css-parser-audit.md b/packages/docs/development/dom-css-parser-audit.md index 86f846879..df7ee01aa 100644 --- a/packages/docs/development/dom-css-parser-audit.md +++ b/packages/docs/development/dom-css-parser-audit.md @@ -5,7 +5,7 @@ OpenPencil's DOM/CSS compatibility layer should not grow hand-rolled CSS parsing ## Current dependency-backed pieces - HTML parsing: `parse5` in the headless runtime, native `DOMParser` in the browser runtime. -- Stylesheet parsing: `@acemir/cssom` in the headless runtime, native CSSOM in the browser runtime. +- Stylesheet parsing and headless inline style declaration parsing: `@acemir/cssom` in the headless runtime, native CSSOM in the browser runtime. - Tailwind generation: Tailwind v4 `compile()` / `build()`. - Color parsing: `@open-pencil/core/color` (`culori`-backed). @@ -18,7 +18,7 @@ These are intentionally limited and should not be expanded without replacing the | Selector matching/specificity | `packages/dom-css/src/headless-css.ts` | Supports simple tag/id/class selectors plus descendant and child combinators. Rejects pseudo/classes and attributes. | Replace with a selector engine over DesignDOM or run browser/runtime oracle for complex CSS. | | Shorthand expansion | `packages/dom-css/src/headless-css.ts` | Expands simple margin/padding boxes, border color/width, and background color. | Use parsed declarations from CSSOM/native computed style; avoid adding new shorthand parsers. | | `calc()` / custom properties | `packages/dom-css/src/headless-css.ts` | Resolves variables by direct lookup and only handles `calc( * )`. | Browser runtime for real computed values; do not add arithmetic or fallback parsing manually. | -| Inline style strings | `packages/dom-css/src/style-attribute.ts`, `packages/dom-css/src/jsx/runtime.ts` | Splits simple `style="a: b;"` text into declarations. | Use native `CSSStyleDeclaration` in browser paths; keep only for simple authored test data or replace with a CSS declaration parser. | +| JSX object style serialization | `packages/dom-css/src/jsx/runtime.ts` | Serializes object style props to inline CSS strings with simple camelCase to kebab-case conversion. | Keep as JSX authoring serialization only; use CSSOM/native parsing after HTML parsing. | | Shadow values | `packages/dom-css/src/css-values.ts` | Extracts one color and numeric offsets for simple shadows. | Replace with a CSS value parser before supporting multiple shadows, inset, color functions, or spread edge cases. | | Numeric lengths | `packages/dom-css/src/css-values.ts` | Parses px/rem-ish numbers with `Number.parseFloat`. | Consume browser-computed pixel values where available; keep headless numeric parsing narrow. | diff --git a/packages/dom-css/src/style-attribute.ts b/packages/dom-css/src/style-attribute.ts index 4ea37a83d..7073955ca 100644 --- a/packages/dom-css/src/style-attribute.ts +++ b/packages/dom-css/src/style-attribute.ts @@ -1,16 +1,24 @@ +import { parse } from '@acemir/cssom' +import type { CSSStyleRuleLike } from '@acemir/cssom' + import type { DesignStyleDeclaration } from './types' +function firstStyleRule(cssText: string): CSSStyleRuleLike | null { + const [rule] = parse(cssText).cssRules + if (!rule || typeof rule !== 'object' || !('style' in rule)) return null + return rule as CSSStyleRuleLike +} + export function parseStyleAttribute(value: string | undefined): DesignStyleDeclaration | undefined { if (!value) return undefined - const style: DesignStyleDeclaration = {} - for (const declaration of value.split(';')) { - const separatorIndex = declaration.indexOf(':') - if (separatorIndex <= 0) continue + const rule = firstStyleRule(`*{${value}}`) + if (!rule) return undefined - const property = declaration.slice(0, separatorIndex).trim().toLowerCase() - const propertyValue = declaration.slice(separatorIndex + 1).trim() - if (property.length > 0 && propertyValue.length > 0) style[property] = propertyValue + const style: DesignStyleDeclaration = {} + for (const property of Array.from(rule.style)) { + const propertyValue = rule.style.getPropertyValue(property) + if (propertyValue) style[property] = propertyValue } return Object.keys(style).length > 0 ? style : undefined diff --git a/packages/dom-css/tests/runtime.test.ts b/packages/dom-css/tests/runtime.test.ts index 671243031..b52372715 100644 --- a/packages/dom-css/tests/runtime.test.ts +++ b/packages/dom-css/tests/runtime.test.ts @@ -32,6 +32,21 @@ describe('@open-pencil/dom-css runtime', () => { expect(section.children[0]).toEqual({ type: 'text', text: 'OpenPencil' }) }) + it('parses inline style values with embedded semicolons', () => { + const runtime = createHeadlessCSSRuntime() + const document = runtime.parseHTML( + '
OpenPencil
' + ) + const section = document.children[0] + + expect(section?.type).toBe('element') + if (section?.type !== 'element') return + expect(section.inlineStyle?.['background-image']).toBe( + "url('data:image/svg+xml;utf8,')" + ) + expect(section.inlineStyle?.width).toBe('320px') + }) + it('computes selector specificity, inheritance, and shorthands', async () => { const runtime = createHeadlessCSSRuntime() const parsed = runtime.parseHTML(`