Skip to content

Conversation

@nebojsa-vuksic
Copy link
Collaborator

@nebojsa-vuksic nebojsa-vuksic commented Dec 4, 2025

Summary

This PR adds EmbeddedToInlineCssStyleSvgPatchHint - a new painter hint that enables rendering of SVG files exported from vector graphics editors that use embedded CSS <style> blocks with class selector references instead of inline styles.

Problem

Design tools like Adobe Illustrator, Inkscape, and Figma commonly export SVGs using CSS class references:

<style>.st0 { fill: #FF0000; }</style>
<circle class="st0" cx="10" cy="10" r="5"/>

Many design tools export SVGs with CSS class references like .st0, .st1. Since Skiko does not support CSS class selector references, these icons render incorrectly or not at all.

Solution

This hint transforms class-based styles into inline style attributes during SVG loading. The hint acts as a preprocessing step:

  1. Parses all <style type="text/css"> blocks
  2. Extracts class selector rules (.className { ... })
  3. Applies styles to elements with matching class attributes
  4. Respects CSS cascade rules (multiple classes, inline precedence)
  5. Removes <style> blocks and class attributes
  6. Returns processed SVG ready for rendering

Before & After

Before change ('sun' icon was rendered without color)

Screenshot 2025-12-04 at 22 46 30

After change

Screenshot 2025-12-04 at 22 46 18

Transformation Example

Input SVG (from design tool):

<svg xmlns="http://www.w3.org/2000/svg">
  <style type="text/css">
    .st0 { fill: red; opacity: 0.5; }
  </style>
  <circle class="st0" cx="10" cy="10" r="5"/>
</svg>

Output (after hint processing):

<svg xmlns="http://www.w3.org/2000/svg">
  <circle style="fill:red;opacity:0.5" cx="10" cy="10" r="5"/>
</svg>

Implementation Details

What Changed

  • New class: EmbeddedToInlineCssStyleSvgPatchHint implementing PainterSvgPatchHint
  • CSS Parser: Parses <style type="text/css"> blocks, supporting minified CSS, comments, and CDATA
  • CSS Inliner: Applies class styles to elements with proper cascade support
  • Processing: Removes <style> blocks and class attributes after inlining

CSS Feature Support

✅ Supported Features

Feature Description Example
Class Selectors Basic .className selectors .st0 { fill: red; }
Multiple Selectors Comma-separated selectors in one rule .st0, .st1 { fill: red; }
Multiple Classes Elements with multiple classes (cascade supported) <circle class="base override">
Inline Style Precedence Inline styles override class styles <circle class="st0" style="fill: blue">
CSS Cascade Later classes override earlier properties Class order: base override
Minified CSS CSS without whitespace/newlines .st0{fill:red;opacity:0.5;}
CSS Comments Both block /* */ and inline comments /* comment */ .st0 { fill: red; }
CDATA Sections XML CDATA wrapping <![CDATA[ .st0 { ... } ]]>
URL References url() for gradients, patterns, filters fill: url(#gradient1);
Multiple Style Blocks Multiple <style> elements in one SVG Two or more <style> blocks
All CSS Properties Any valid CSS property fill, stroke, opacity, clip-rule, etc.

❌ Not Supported (Intentionally)

Feature Behavior
ID Selectors (#id) Ignored - not processed
Element Selectors (circle, rect) Ignored - not processed
Attribute Selectors ([attr="value"]) Ignored - not processed
Pseudo-classes (:hover, :focus) Ignored - not processed
Combinators (.parent .child, .parent > .child) Ignored - not processed
At-rules (@media, @keyframes) Ignored - not processed

The hint focuses on static rendering scenarios with class selectors only, which covers the vast majority of SVG exports from design tools.

CSS Cascade & Conflict Resolution

When the same CSS property is defined in multiple places, the hint resolves conflicts according to standard CSS cascade rules:

Priority Source Behavior
Highest Inline style attribute Overrides all class-based properties
Medium Later classes (rightmost) Override earlier classes for the same property
Lowest Earlier classes (leftmost) Used only when property not defined elsewhere

How It Works

Given: <circle class="A B C" style="x: 1">

For any CSS property:

  1. Check inline style → if defined, use it
  2. Check class C → if defined and no inline, use it
  3. Check class B → if defined and not in C or inline, use it
  4. Check class A → if defined and not in B, C, or inline, use it

Example

  .base { fill: green; opacity: 0.5; stroke: purple; }
  .override { opacity: 0.9; }

  <circle class="base override" style="stroke: black">

Property resolution:

  • fill: ✅ .base (no conflict, only source)
  • opacity: ✅ .override (conflicts with .base, later class wins)
  • stroke: ✅ inline style (conflicts with .base, inline wins)

Result: style="fill:green;opacity:0.9;stroke:black"

Usage

// Apply hint when loading SVG with embedded CSS
Icon(
    key = iconKey,
    contentDescription = "Icon with embedded CSS",
    hints = arrayOf(EmbeddedToInlineCssStyleSvgPatchHint),
    modifier = Modifier.size(96.dp),
)

// Or conditionally
Icon(
    key = iconKey,
    contentDescription = "Icon",
    hints = if (needsCssInlining) {
        arrayOf(EmbeddedToInlineCssStyleSvgPatchHint)
    } else {
        emptyArray()
    },
)

Showcase Demo

  • Added interactive demo in Icons showcase with checkbox toggle
  • Uses ShowcaseIcons.sunny SVG with embedded CSS styles
  • Demonstrates the hint's effect on rendering

Release notes

New features

  • Added EmbeddedToInlineCssStyleSvgPatchHint painter hint to support rendering SVG files with embedded CSS class selectors exported from vector graphics editors
  • Converts CSS <style> blocks with .className selectors to inline style attributes during SVG loading
  • Supports CSS cascade (multiple classes, inline style precedence), minified CSS, comments, CDATA sections, and URL references for gradients/patterns
  • Interactive showcase demo added to Icons panel demonstrating the feature

Note

Adds EmbeddedToInlineCssStyleSvgPatchHint to convert embedded CSS class rules to inline styles in SVGs, plus a showcase demo and asset.

  • UI/Painter:
    • EmbeddedToInlineCssStyleSvgPatchHint: new PainterSvgPatchHint that parses <style> blocks, inlines class-based CSS to style attributes, removes style/class.
  • Tests:
    • Comprehensive unit tests covering multiple selectors, minified CSS, comments, CDATA, URL refs, multiple classes, cascade, and edge cases.
  • Showcase:
    • Add ShowcaseIcons.sunny and icons/sunny.svg.
    • Icons screen: checkbox toggle to enable the hint and example rendering via Image(ShowcaseIcons.sunny, hints = arrayOf(EmbeddedToInlineCssStyleSvgPatchHint)).
  • API:
    • Experimental API surface updated to expose the new hint.

Written by Cursor Bugbot for commit 032e284. This will update automatically on new commits. Configure here.

@nebojsa-vuksic nebojsa-vuksic changed the title JEWEL-1072 Support CSS class references in SVG rendering [JEWEL-1072] Support CSS class references in SVG rendering Dec 4, 2025
@nebojsa-vuksic nebojsa-vuksic self-assigned this Dec 4, 2025
@nebojsa-vuksic nebojsa-vuksic force-pushed the nebojsa.vuksic/JEWEL-1072-svg-css-class-support- branch from 4253870 to 6aedad3 Compare December 4, 2025 22:22
selectors.forEach { selector ->
// Only process simple class selectors
if (selector.matches(CLASS_SELECTOR_PATTERN)) {
rules[selector] = CssRule(selector = selector, properties = properties)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate CSS selectors overwrite instead of merge properties

When the same CSS class selector appears multiple times (e.g., .st0 { fill: red; } .st0 { stroke: blue; }), the code completely replaces the rule instead of merging properties. At line 286 in parseCssBlock and line 413 in addStyleElement, the assignment rules[selector] = ... and cache[className] = rule overwrites existing entries. Per CSS cascade rules and the documented behavior ("Non-conflicting properties are merged"), properties from duplicate selectors should be merged, with later values overriding earlier ones for the same property. This could cause incorrect SVG rendering when design tools export duplicate class definitions.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point, but I'm ok with a follow-up issue/PR for this in 0.34

Add EmbeddedToInlineCssStyleSvgPatchHint to convert embedded CSS class
selectors in SVG files to inline style attributes.

Many vector graphics editors (Adobe Illustrator, Inkscape, Figma) export SVGs
with embedded <style> blocks containing CSS rules instead of inline styles:

  <style type="text/css">
    .st0 { fill: red; opacity: 0.5; }
  </style>
  <circle class="st0" cx="10" cy="10" r="5"/>

This hint transforms class-based styles into inline attributes, enabling
proper rendering in Jewel painters:

  <circle style="fill:red;opacity:0.5" cx="10" cy="10" r="5"/>

Implementation features:
- CSS parser supporting minified CSS, comments, and CDATA sections
- Multiple selector handling (.st0, .st1 { ... })
- Full CSS cascade: multiple classes with later override, inline precedence
- URL reference preservation for gradients and patterns
- Processes only class selectors; ignores ID, element, and attribute selectors
- Removes processed <style> blocks and class attributes after inlining

Includes comprehensive test suite (31 tests) covering:
- CSS parsing edge cases (minification, comments, CDATA, multiple blocks)
- Cascade behavior (multiple classes, inline style precedence)
- Real-world SVG exports from design tools
- Edge cases (empty styles, missing classes, whitespace handling)

Signed-off-by: Nebojsa.Vuksic <[email protected]>
@nebojsa-vuksic nebojsa-vuksic force-pushed the nebojsa.vuksic/JEWEL-1072-svg-css-class-support- branch from 6aedad3 to 032e284 Compare December 5, 2025 00:17
@rock3r rock3r added the Jewel label Dec 5, 2025
Comment on lines +20 to +28
* ## Problem
* Skiko SVG rendering enginee doesn't support CSS class references:
* ```xml
* <style>.st0 { fill: red; }</style>
* <circle class="st0"/> <!-- Won't render correctly -->
* ```
*
* ## Solution
* This hint converts class-based styles to inline styles:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* ## Problem
* Skiko SVG rendering enginee doesn't support CSS class references:
* ```xml
* <style>.st0 { fill: red; }</style>
* <circle class="st0"/> <!-- Won't render correctly -->
* ```
*
* ## Solution
* This hint converts class-based styles to inline styles:

I would remove this part, KDocs shouldn't look like issue descriptions — they only illustrate what an API does.

selectors.forEach { selector ->
// Only process simple class selectors
if (selector.matches(CLASS_SELECTOR_PATTERN)) {
rules[selector] = CssRule(selector = selector, properties = properties)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point, but I'm ok with a follow-up issue/PR for this in 0.34

}

/** Uses XPath to efficiently query elements with a specific attribute. */
private fun Element.getElementsWithAttributeXPath(attributeName: String): List<Element> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private fun Element.getElementsWithAttributeXPath(attributeName: String): List<Element> {
private fun Element.getElementsWithAttribute(attributeName: String): List<Element> {

The "XPath" part of the function name seems unnecessary and a bit confusing, since it makes it sound like the parameter should be an XPath expression, but the usage of XPaths is just an implementation detail

private fun Element.removeAllStyleElements() {
val styleElements = getElementsByTagName("style")
// Remove in reverse to avoid index shifting issues
for (i in styleElements.length - 1 downTo 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: assuming it exists for this type of collection

Suggested change
for (i in styleElements.length - 1 downTo 0) {
for (i in styleElements.lastIndex downTo 0) {

* "0.5"}
*/
fun parseInlineStyle(style: String): Map<String, String> {
return style
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: expression syntax

@rock3r
Copy link
Collaborator

rock3r commented Dec 5, 2025

Great job, just a couple small things to adjust.

Copy link
Collaborator

@faogustavo faogustavo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with a minor comment

expectedOutput =
"""
<svg xmlns="http://www.w3.org/2000/svg">
<rect style="fill:orange;stroke:black;stroke-width:2" x="120" y="120" width="60" height="60"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this expected? In theory styles defined in the components should take precedence, so they should to be in the end. (at least it's how it works on HTML, not sure if the same happens on SVG - And that's what is happening in the next test)

Image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, if you define color multiple times in the class, the last one should be the one that's applied, yeah. But maybe I'm misunderstanding what you're asking, as that's a separate potential issue.

In this case:

  • The class defined fill: blue
  • The style defined fill: orange

So fill: orange is the correct expected output, no?


@nebojsa-vuksic we need to cover this scenario too:

<style type="text/css">
  .blue-rect { fill: blue; stroke: black; fill: red; stroke-width: 2; }
</style>

I expect a fill: red output. Same in this case:

<rect style="fill: orange; fill: red;" x="120" y="120" width="60" height="60"/>

(and permutations)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants