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 inline CSS class styles in SVGs, plus a showcase toggle and new sunny icon, with comprehensive tests.

  • UI Painter (experimental):
    • Introduces EmbeddedToInlineCssStyleSvgPatchHint implementing PainterSvgPatchHint to convert embedded CSS <style> rules (class selectors) into inline style attributes and remove class/style blocks.
    • Supports multiple selectors/classes, inline precedence, comments, CDATA, minified CSS, and URL refs; ignores non-class selectors.
    • API surface updated in platform/jewel/ui/api-dump-experimental.txt.
  • Tests:
    • Adds EmbeddedToInlineCssStyleSvgPatchHintTest with extensive cases covering parsing, cascade, multiple blocks, and no-op scenarios.
  • Showcase:
    • Adds ShowcaseIcons.sunny and SVG asset platform/jewel/samples/showcase/src/main/resources/icons/sunny.svg.
    • Updates components/Icons.kt to include a checkbox to enable the hint and render the sunny icon with EmbeddedToInlineCssStyleSvgPatchHint.
    • API dump updated to include getSunny().

Written by Cursor Bugbot for commit fcba5c1. 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

@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
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

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) {

Copy link
Collaborator Author

@nebojsa-vuksic nebojsa-vuksic Dec 9, 2025

Choose a reason for hiding this comment

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

Unfortunately, it doesn't :(

public interface NodeList {
    /**
     * Returns the <code>index</code>th item in the collection. If
     * <code>index</code> is greater than or equal to the number of nodes in
     * the list, this returns <code>null</code>.
     * @param index Index into the collection.
     * @return The node at the <code>index</code>th position in the
     *   <code>NodeList</code>, or <code>null</code> if that is not a valid
     *   index.
     */
    public Node item(int index);

    /**
     * The number of nodes in the list. The range of valid child node indices
     * is 0 to <code>length-1</code> inclusive.
     */
    public int getLength();

}

But I can add an extension property for it if you think it's necessary.

@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)

Copy link
Collaborator Author

@nebojsa-vuksic nebojsa-vuksic Dec 9, 2025

Choose a reason for hiding this comment

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

@rock3r @faogustavo The current implementation has non-intuitive behavior that I'd like to clarify and improve.

Current Behavior:

When processing this example:

<style>.blue-rect { fill: blue; stroke: black; stroke-width: 2; }</style>
<rect class="blue-rect" style="fill: orange" />

The implementation:

  1. Collects class properties into a map:
   "fill" -> "blue"
   "stroke" -> "black"
   "stroke-width" -> "2"
  1. Collects inline style properties:
   "fill" -> "orange"
  1. Merges them by replacing class property values with inline values when keys match

This produces:

style="fill: orange; stroke: black; stroke-width: 2"

because value of a fill attribute has been overwritten. If there was no occurences of the fill attribute in the class definition, the inline style property would just be appended to the end.

While this achieves the correct CSS cascade behavior (inline overrides class), the map-based replacement approach is not intuitive. It also ensures only one occurrence of each property, but the mechanism isn't obvious from reading the code.

I'll refactor this to use a simpler, more conventional approach: prepend class properties, then append inline properties. This makes the CSS cascade explicit:

style="fill: blue; stroke: black; stroke-width: 2; fill: orange"

Copy link
Collaborator Author

@nebojsa-vuksic nebojsa-vuksic Dec 9, 2025

Choose a reason for hiding this comment

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

I've updated implementation to do following thing:

  • resolve all class references and merge them together. This will result in a list of unique properties where the value of the property is equal to the one from the last occurence of the property definition.
  • In don style attribute, prepend class style properties before already present style attributes

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: maybe some test on the scenario that Seb mentioned could be added?
smth like this for example

    @Test
    fun `parse duplicate properties in class selector`() {
        doTestEmbeddedCssInlining(
            input =
                """
                <svg xmlns="http://www.w3.org/2000/svg">
                    <style type="text/css">
                      .blue-rect { fill: blue; stroke: black; fill: red; stroke-width: 2; }
                    </style>
                    <rect class="blue-rect" x="120" y="120" width="60" height="60"/>
                </svg>
                """,
            expectedOutput =
                """
                <svg xmlns="http://www.w3.org/2000/svg">
                    <rect style="fill:red;stroke:black;stroke-width:2" x="120" y="120" width="60" height="60"/>
                </svg>
                """,
        )
    }

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

Signed-off-by: Nebojsa.Vuksic <[email protected]>
@nebojsa-vuksic nebojsa-vuksic force-pushed the nebojsa.vuksic/JEWEL-1072-svg-css-class-support- branch from 032e284 to fcba5c1 Compare December 9, 2025 12:38
@nebojsa-vuksic nebojsa-vuksic requested a review from rock3r December 9, 2025 13:36
@nebojsa-vuksic
Copy link
Collaborator Author

Ready to merge

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