Skip to content

Commit dcf6175

Browse files
committed
Add kmp xml deserializer
Throw exception in case of failure Fix format in XmlToImageVectorParser Remove shared test resources from xml module Fix names in XmlDeserializerTest Fix spotless checks simplify android color tests
1 parent f9922f5 commit dcf6175

11 files changed

Lines changed: 766 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
alias(libs.plugins.valkyrie.kmp)
3+
alias(libs.plugins.kover)
4+
alias(libs.plugins.kotlin.serialization)
5+
}
6+
7+
kotlin {
8+
sourceSets {
9+
commonMain.dependencies {
10+
implementation(projects.components.ir)
11+
implementation(projects.components.parser.common)
12+
implementation(libs.xmlutil)
13+
}
14+
commonTest {
15+
dependencies {
16+
implementation(libs.assertk)
17+
implementation(libs.kotlin.test)
18+
}
19+
}
20+
}
21+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package io.github.composegears.valkyrie.parser.kmp.xml
2+
3+
import io.github.composegears.valkyrie.ir.IrColor
4+
5+
private const val ANDROID_COLOR_PREFIX = "@android:color/"
6+
7+
internal object AndroidColorParser {
8+
9+
fun parse(value: String): IrColor? {
10+
return androidColorsMap[value.substringAfter(ANDROID_COLOR_PREFIX)]
11+
?.let { IrColor(it) }
12+
}
13+
14+
// NOTE: Taken from https://gist.github.com/sghael/2930380
15+
val androidColorsMap = mapOf(
16+
"white" to "#FFFFFF",
17+
"ivory" to "#FFFFF0",
18+
"light_yellow" to "#FFFFE0",
19+
"yellow" to "#FFFF00",
20+
"snow" to "#FFFAFA",
21+
"floral_white" to "#FFFAF0",
22+
"lemon_chiffon" to "#FFFACD",
23+
"cornsilk" to "#FFF8DC",
24+
"seashell" to "#FFF5EE",
25+
"lavender_blush" to "#FFF0F5",
26+
"papaya_whip" to "#FFEFD5",
27+
"blanched_almond" to "#FFEBCD",
28+
"misty_rose" to "#FFE4E1",
29+
"bisque" to "#FFE4C4",
30+
"moccasin" to "#FFE4B5",
31+
"navajo_white" to "#FFDEAD",
32+
"peach_puff" to "#FFDAB9",
33+
"cold" to "#FFD700",
34+
"pink" to "#FFC0CB",
35+
"light_pink" to "#FFB6C1",
36+
"orange" to "#FFA500",
37+
"light_salmon" to "#FFA07A",
38+
"dark_orange" to "#FF8C00",
39+
"coral" to "#FF7F50",
40+
"hot_pink" to "#FF69B4",
41+
"tomato" to "#FF6347",
42+
"orange_red" to "#FF4500",
43+
"deep_pink" to "#FF1493",
44+
"fuchsia" to "#FF00FF",
45+
"magenta" to "#FF00FF",
46+
"red" to "#FF0000",
47+
"old_lace" to "#FDF5E6",
48+
"light_goldenrod_yellow" to "#FAFAD2",
49+
"linen" to "#FAF0E6",
50+
"antique_white" to "#FAEBD7",
51+
"salmon" to "#FA8072",
52+
"ghost_white" to "#F8F8FF",
53+
"mint_cream" to "#F5FFFA",
54+
"white_smoke" to "#F5F5F5",
55+
"beige" to "#F5F5DC",
56+
"wheat" to "#F5DEB3",
57+
"sandy_brown" to "#F4A460",
58+
"azure" to "#F0FFFF",
59+
"honeydew" to "#F0FFF0",
60+
"alice_blue" to "#F0F8FF",
61+
"khaki" to "#F0E68C",
62+
"light_coral" to "#F08080",
63+
"pale_goldenrod" to "#EEE8AA",
64+
"violet" to "#EE82EE",
65+
"dark_salmon" to "#E9967A",
66+
"lavender" to "#E6E6FA",
67+
"light_cyan" to "#E0FFFF",
68+
"burlyWood" to "#DEB887",
69+
"plum" to "#DDA0DD",
70+
"gainsboro" to "#DCDCDC",
71+
"crimson" to "#DC143C",
72+
"pale_violet_red" to "#DB7093",
73+
"goldenrod" to "#DAA520",
74+
"orchid" to "#DA70D6",
75+
"thistle" to "#D8BFD8",
76+
"light_grey" to "#D3D3D3",
77+
"tan" to "#D2B48C",
78+
"chocolate" to "#D2691E",
79+
"peru" to "#CD853F",
80+
"indian_red" to "#CD5C5C",
81+
"medium_violet_red" to "#C71585",
82+
"silver" to "#C0C0C0",
83+
"dark_khaki" to "#BDB76B",
84+
"rosy_brown" to "#BC8F8F",
85+
"medium_orchid" to "#BA55D3",
86+
"dark_goldenrod" to "#B8860B",
87+
"fire_brick" to "#B22222",
88+
"powder_blue" to "#B0E0E6",
89+
"light_steel_blue" to "#B0C4DE",
90+
"pale_turquoise" to "#AFEEEE",
91+
"greenYellow" to "#ADFF2F",
92+
"light_blue" to "#ADD8E6",
93+
"dark_gray" to "#A9A9A9",
94+
"brown" to "#A52A2A",
95+
"sienna" to "#A0522D",
96+
"yellow_green" to "#9ACD32",
97+
"dark_orchid" to "#9932CC",
98+
"pale_green" to "#98FB98",
99+
"dark_violet" to "#9400D3",
100+
"mediumPurple" to "#9370DB",
101+
"_light_green" to "#90EE90",
102+
"dark_sea_green" to "#8FBC8F",
103+
"saddle_brown" to "#8B4513",
104+
"dark_magenta" to "#8B008B",
105+
"dark_red" to "#8B0000",
106+
"blue_violet" to "#8A2BE2",
107+
"light_sky_blue" to "#87CEFA",
108+
"sky_blue" to "#87CEEB",
109+
"gray" to "#808080",
110+
"olive" to "#808000",
111+
"purple" to "#800080",
112+
"maroon" to "#800000",
113+
"aquamarine" to "#7FFFD4",
114+
"chartreuse" to "#7FFF00",
115+
"lawn_green" to "#7CFC00",
116+
"medium_slate_blue" to "#7B68EE",
117+
"light_slate_gray" to "#778899",
118+
"slate_gray" to "#708090",
119+
"olive_drab" to "#6B8E23",
120+
"slate_blue" to "#6A5ACD",
121+
"dim_gray" to "#696969",
122+
"medium_aquamarine" to "#66CDAA",
123+
"cornflower_blue" to "#6495ED",
124+
"cadet_blue" to "#5F9EA0",
125+
"dark_olive_green" to "#556B2F",
126+
"indigo" to "#4B0082",
127+
"medium_turquoise" to "#48D1CC",
128+
"dark_slate_blue" to "#483D8B",
129+
"steel_blue" to "#4682B4",
130+
"royal_blue" to "#4169E1",
131+
"turquoise" to "#40E0D0",
132+
"medium_sea_green" to "#3CB371",
133+
"lime_green" to "#32CD32",
134+
"dark_slate_gray" to "#2F4F4F",
135+
"sea_green" to "#2E8B57",
136+
"forest_green" to "#228B22",
137+
"light_sea_green" to "#20B2AA",
138+
"dodger_blue" to "#1E90FF",
139+
"midnight_blue" to "#191970",
140+
"aqua" to "#00FFFF",
141+
"cyan" to "#00FFFF",
142+
"spring_green" to "#00FF7F",
143+
"lime" to "#00FF00",
144+
"medium_spring_green" to "#00FA9A",
145+
"dark_turquoise" to "#00CED1",
146+
"deep_sky_blue" to "#00BFFF",
147+
"dark_cyan" to "#008B8B",
148+
"teal" to "#008080",
149+
"green" to "#008000",
150+
"dark_green" to "#006400",
151+
"blue" to "#0000FF",
152+
"medium_blue" to "#0000CD",
153+
"dark_blue" to "#00008B",
154+
"navy" to "#000080",
155+
"black" to "#000000",
156+
)
157+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.github.composegears.valkyrie.parser.kmp.xml
2+
3+
import kotlinx.serialization.Polymorphic
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
import nl.adaptivity.xmlutil.serialization.XmlPolyChildren
7+
import nl.adaptivity.xmlutil.serialization.XmlSerialName
8+
9+
private const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"
10+
11+
@XmlSerialName("vector")
12+
@Serializable
13+
internal data class VectorDrawable(
14+
@XmlSerialName("name", ANDROID_NAMESPACE) val name: String? = null,
15+
@XmlSerialName("width", ANDROID_NAMESPACE) val widthInDp: String,
16+
@XmlSerialName("height", ANDROID_NAMESPACE) val heightInDp: String,
17+
@XmlSerialName("viewportWidth", ANDROID_NAMESPACE) val viewportWidth: Float,
18+
@XmlSerialName("viewportHeight", ANDROID_NAMESPACE) val viewportHeight: Float,
19+
@XmlSerialName("tint", ANDROID_NAMESPACE) val tint: String? = null,
20+
@XmlSerialName("autoMirrored", ANDROID_NAMESPACE) val autoMirrored: Boolean = false,
21+
22+
@XmlPolyChildren([GROUP, PATH])
23+
val children: List<@Polymorphic Child>,
24+
) {
25+
companion object {
26+
const val GROUP = "group"
27+
const val PATH = "path"
28+
}
29+
30+
sealed interface Child
31+
32+
@Serializable
33+
@SerialName(GROUP)
34+
@XmlSerialName(GROUP)
35+
data class Group(
36+
@XmlSerialName("name", ANDROID_NAMESPACE) val name: String? = null,
37+
@XmlSerialName("pivotX", ANDROID_NAMESPACE) val pivotX: Float? = null,
38+
@XmlSerialName("pivotY", ANDROID_NAMESPACE) val pivotY: Float? = null,
39+
@XmlSerialName("translateX", ANDROID_NAMESPACE) val translateX: Float? = null,
40+
@XmlSerialName("translateY", ANDROID_NAMESPACE) val translateY: Float? = null,
41+
@XmlSerialName("scaleX", ANDROID_NAMESPACE) val scaleX: Float? = null,
42+
@XmlSerialName("scaleY", ANDROID_NAMESPACE) val scaleY: Float? = null,
43+
@XmlSerialName("rotation", ANDROID_NAMESPACE) val rotation: Float? = null,
44+
45+
@XmlPolyChildren([GROUP, PATH])
46+
val children: List<@Polymorphic Child>,
47+
) : Child
48+
49+
@Serializable
50+
@SerialName(PATH)
51+
@XmlSerialName(PATH)
52+
data class Path(
53+
@XmlSerialName("name", ANDROID_NAMESPACE) val name: String? = null,
54+
@XmlSerialName("fillType", ANDROID_NAMESPACE) val fillType: String = "nonZero",
55+
@XmlSerialName("fillColor", ANDROID_NAMESPACE) val fillColor: String? = null,
56+
@XmlSerialName("pathData", ANDROID_NAMESPACE) val pathData: String,
57+
@XmlSerialName("alpha", ANDROID_NAMESPACE) val alpha: Float = 1.0f,
58+
@XmlSerialName("strokeWidth", ANDROID_NAMESPACE) val strokeWidth: String? = null,
59+
@XmlSerialName("strokeLineCap", ANDROID_NAMESPACE) val strokeLineCap: String? = null,
60+
@XmlSerialName("strokeLineJoin", ANDROID_NAMESPACE) val strokeLineJoin: String? = null,
61+
@XmlSerialName("strokeColor", ANDROID_NAMESPACE) val strokeColor: String? = null,
62+
@XmlSerialName("strokeAlpha", ANDROID_NAMESPACE) val strokeAlpha: String? = null,
63+
@XmlSerialName("strokeMiterLimit", ANDROID_NAMESPACE) val strokeMiterLimit: String? = null,
64+
) : Child
65+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.github.composegears.valkyrie.parser.kmp.xml
2+
3+
import kotlinx.serialization.decodeFromString
4+
import kotlinx.serialization.modules.SerializersModule
5+
import kotlinx.serialization.modules.polymorphic
6+
import kotlinx.serialization.modules.subclass
7+
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
8+
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
9+
import nl.adaptivity.xmlutil.serialization.XML
10+
11+
internal object XmlDeserializer {
12+
private val baseModule = SerializersModule {
13+
polymorphic(VectorDrawable.Child::class) {
14+
subclass(VectorDrawable.Group::class)
15+
subclass(VectorDrawable.Path::class)
16+
}
17+
}
18+
19+
@OptIn(ExperimentalXmlUtilApi::class)
20+
private val xmlConfig = XML(baseModule) {
21+
autoPolymorphic = true
22+
defaultPolicy {
23+
pedantic = false
24+
repairNamespaces = true
25+
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
26+
}
27+
}
28+
29+
fun deserialize(content: String): VectorDrawable = xmlConfig.decodeFromString(content)
30+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.github.composegears.valkyrie.parser.kmp.xml
2+
3+
import io.github.composegears.valkyrie.ir.IrColor
4+
import io.github.composegears.valkyrie.ir.IrFill
5+
import io.github.composegears.valkyrie.ir.IrImageVector
6+
import io.github.composegears.valkyrie.ir.IrPathFillType
7+
import io.github.composegears.valkyrie.ir.IrStroke
8+
import io.github.composegears.valkyrie.ir.IrStrokeLineCap
9+
import io.github.composegears.valkyrie.ir.IrStrokeLineJoin
10+
import io.github.composegears.valkyrie.ir.IrVectorNode
11+
import io.github.composegears.valkyrie.parser.common.PathParser
12+
13+
object XmlToImageVectorParser {
14+
fun parse(text: String): IrImageVector {
15+
return XmlDeserializer.deserialize(text).toIrImageVector()
16+
}
17+
18+
private fun VectorDrawable.toIrImageVector(): IrImageVector {
19+
return IrImageVector(
20+
name = name.orEmpty(),
21+
autoMirror = autoMirrored,
22+
defaultWidth = widthInDp.removeSuffix("dp").toFloat(),
23+
defaultHeight = heightInDp.removeSuffix("dp").toFloat(),
24+
viewportWidth = viewportWidth,
25+
viewportHeight = viewportHeight,
26+
nodes = children.map { it.toNode() },
27+
)
28+
}
29+
30+
private fun VectorDrawable.Child.toNode(): IrVectorNode {
31+
return when (this) {
32+
is VectorDrawable.Group -> this.toIrGroup()
33+
is VectorDrawable.Path -> this.toIrPath()
34+
}
35+
}
36+
37+
private fun VectorDrawable.Path.toIrPath(): IrVectorNode.IrPath {
38+
return IrVectorNode.IrPath(
39+
name = name.orEmpty(),
40+
fill = fillColor?.toIrColor()?.let { IrFill.Color(it) },
41+
fillAlpha = alpha,
42+
stroke = strokeColor?.toIrColor()?.let { IrStroke.Color(it) },
43+
strokeAlpha = strokeAlpha?.toFloatOrNull() ?: 1f,
44+
strokeLineWidth = strokeWidth?.toFloatOrNull() ?: 0f,
45+
strokeLineCap = strokeLineCap.asStrokeLineCap(),
46+
strokeLineJoin = strokeLineJoin.asStrokeLineJoin(),
47+
strokeLineMiter = strokeMiterLimit?.toFloatOrNull() ?: 4f,
48+
pathFillType = fillType.asFillType(),
49+
paths = PathParser.parsePathString(pathData),
50+
)
51+
}
52+
53+
private fun VectorDrawable.Group.toIrGroup(): IrVectorNode.IrGroup {
54+
return IrVectorNode.IrGroup(
55+
name = name.orEmpty(),
56+
rotate = rotation ?: 0f,
57+
pivotX = pivotX ?: 0f,
58+
pivotY = pivotY ?: 0f,
59+
scaleX = scaleX ?: 1f,
60+
scaleY = scaleY ?: 1f,
61+
translationX = translateX ?: 0f,
62+
translationY = translateY ?: 0f,
63+
clipPathData = mutableListOf(),
64+
// TODO: Add support for nested groups
65+
paths = children.filterIsInstance<VectorDrawable.Path>().map { it.toIrPath() }.toMutableList(),
66+
)
67+
}
68+
69+
private fun String?.asStrokeLineCap(): IrStrokeLineCap = IrStrokeLineCap.entries.find { it.name.equals(this, ignoreCase = true) } ?: IrStrokeLineCap.Butt
70+
71+
private fun String?.asStrokeLineJoin(): IrStrokeLineJoin = IrStrokeLineJoin.entries.find { it.name.equals(this, ignoreCase = true) } ?: IrStrokeLineJoin.Miter
72+
73+
private fun String.asFillType(): IrPathFillType = IrPathFillType.entries.find { it.name.equals(this, ignoreCase = true) } ?: IrPathFillType.NonZero
74+
75+
private fun String.toIrColor(): IrColor = AndroidColorParser.parse(this) ?: IrColor(this)
76+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.github.composegears.valkyrie.parser.kmp.xml
2+
3+
import assertk.assertThat
4+
import assertk.assertions.isEqualTo
5+
import io.github.composegears.valkyrie.ir.IrColor
6+
import kotlin.test.Test
7+
8+
class AndroidColorParserTest {
9+
10+
@Test
11+
fun `test parsing xml android colors`() {
12+
val colorMap = AndroidColorParser.androidColorsMap
13+
14+
colorMap.forEach { (colorName, hexValue) ->
15+
val androidColorName = "@android:color/$colorName"
16+
17+
assertThat(actual = AndroidColorParser.parse(androidColorName)).isEqualTo(IrColor(hexValue))
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)