Skip to content

Commit 8451c79

Browse files
committed
Fix PSI parsing for material icon without materialIcon block, introduce BuilderExpression
1 parent 852d706 commit 8451c79

File tree

8 files changed

+251
-94
lines changed

8 files changed

+251
-94
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.github.composegears.valkyrie.psi.imagevector.common
2+
3+
import io.github.composegears.valkyrie.extensions.safeAs
4+
import io.github.composegears.valkyrie.psi.extension.childrenOfType
5+
import org.jetbrains.kotlin.psi.KtBlockExpression
6+
import org.jetbrains.kotlin.psi.KtCallExpression
7+
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
8+
9+
@JvmInline
10+
internal value class BuilderExpression(val callExpression: KtCallExpression)
11+
12+
internal fun KtBlockExpression.materialIconCall(): BuilderExpression? {
13+
val ktCallExpression = childrenOfType<KtCallExpression>().firstOrNull {
14+
it.calleeExpression?.text == "materialIcon"
15+
} ?: return null
16+
17+
return BuilderExpression(ktCallExpression)
18+
}
19+
20+
internal fun KtBlockExpression.builderExpression(): BuilderExpression? {
21+
val ktCallExpression = childrenOfType<KtCallExpression>().firstOrNull {
22+
it.calleeExpression?.text == "Builder"
23+
} ?: return null
24+
25+
return BuilderExpression(ktCallExpression)
26+
}
27+
28+
internal fun BuilderExpression.name(): String {
29+
val nameArgument = callExpression.valueArguments.find { arg ->
30+
arg?.getArgumentName()?.asName?.identifier == "name"
31+
}
32+
33+
return nameArgument?.getArgumentExpression().safeAs<KtStringTemplateExpression>()
34+
?.entries
35+
?.firstOrNull()
36+
?.text.orEmpty()
37+
}
38+
39+
internal fun BuilderExpression.defaultWidth(defaultValue: Float): Float {
40+
return extractFloat("defaultWidth", defaultValue)
41+
}
42+
43+
internal fun BuilderExpression.defaultHeight(defaultValue: Float): Float {
44+
return extractFloat("defaultHeight", defaultValue)
45+
}
46+
47+
internal fun BuilderExpression.viewportWidth(defaultValue: Float): Float {
48+
return extractFloat("viewportWidth", defaultValue)
49+
}
50+
51+
internal fun BuilderExpression.viewportHeight(defaultValue: Float): Float {
52+
return extractFloat("viewportHeight", defaultValue)
53+
}
54+
55+
internal fun BuilderExpression.autoMirror(defaultValue: Boolean = false): Boolean {
56+
return extractBoolean("autoMirror", defaultValue)
57+
}
58+
59+
private fun BuilderExpression.extractFloat(paramName: String, defaultValue: Float): Float {
60+
val argument = callExpression.valueArguments.find { arg ->
61+
arg?.getArgumentName()?.asName?.identifier == paramName
62+
}
63+
64+
val valueText = argument?.getArgumentExpression()?.text ?: return defaultValue
65+
66+
return valueText
67+
.removeSuffix(".dp")
68+
.removeSuffix("f")
69+
.removeSuffix("F")
70+
.toFloatOrNull() ?: defaultValue
71+
}
72+
73+
private fun BuilderExpression.extractBoolean(paramName: String, defaultValue: Boolean): Boolean {
74+
val argument = callExpression.valueArguments.find { arg ->
75+
arg?.getArgumentName()?.asName?.identifier == paramName
76+
}
77+
78+
val valueText = argument?.getArgumentExpression()?.text ?: return defaultValue
79+
return valueText.toBoolean()
80+
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
package io.github.composegears.valkyrie.psi.imagevector.parser
22

3-
import io.github.composegears.valkyrie.extensions.safeAs
43
import io.github.composegears.valkyrie.ir.IrColor
54
import io.github.composegears.valkyrie.ir.IrFill
65
import io.github.composegears.valkyrie.ir.IrImageVector
76
import io.github.composegears.valkyrie.ir.IrStrokeLineJoin
87
import io.github.composegears.valkyrie.ir.IrVectorNode
98
import io.github.composegears.valkyrie.ir.IrVectorNode.IrPath
109
import io.github.composegears.valkyrie.psi.extension.childrenOfType
10+
import io.github.composegears.valkyrie.psi.imagevector.common.autoMirror
11+
import io.github.composegears.valkyrie.psi.imagevector.common.builderExpression
12+
import io.github.composegears.valkyrie.psi.imagevector.common.defaultHeight
13+
import io.github.composegears.valkyrie.psi.imagevector.common.defaultWidth
1114
import io.github.composegears.valkyrie.psi.imagevector.common.extractPathFillType
15+
import io.github.composegears.valkyrie.psi.imagevector.common.materialIconCall
16+
import io.github.composegears.valkyrie.psi.imagevector.common.name
17+
import io.github.composegears.valkyrie.psi.imagevector.common.parseFloatArg
1218
import io.github.composegears.valkyrie.psi.imagevector.common.parsePath
19+
import io.github.composegears.valkyrie.psi.imagevector.common.viewportHeight
20+
import io.github.composegears.valkyrie.psi.imagevector.common.viewportWidth
1321
import org.jetbrains.kotlin.psi.KtBlockExpression
1422
import org.jetbrains.kotlin.psi.KtCallExpression
1523
import org.jetbrains.kotlin.psi.KtFile
1624
import org.jetbrains.kotlin.psi.KtProperty
17-
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
1825

1926
internal object MaterialImageVectorPsiParser {
2027

@@ -25,53 +32,44 @@ internal object MaterialImageVectorPsiParser {
2532

2633
val blockBody = property.getter?.bodyBlockExpression ?: return null
2734

28-
val materialIconCall = blockBody.childrenOfType<KtCallExpression>().firstOrNull {
29-
it.calleeExpression?.text == "materialIcon"
30-
} ?: return null
35+
val materialIconCall = blockBody.materialIconCall()
3136

32-
return IrImageVector(
33-
name = materialIconCall.extractIconName(),
34-
defaultWidth = 24f,
35-
defaultHeight = 24f,
36-
viewportWidth = 24f,
37-
viewportHeight = 24f,
38-
autoMirror = materialIconCall.extractAutoMirror(),
39-
nodes = blockBody.parseMaterialPath(),
40-
)
41-
}
42-
43-
private fun KtCallExpression.extractIconName(): String {
44-
val nameArgument = valueArguments.find { arg ->
45-
arg?.getArgumentName()?.asName?.identifier == "name"
37+
if (materialIconCall != null) {
38+
return IrImageVector(
39+
name = materialIconCall.name(),
40+
defaultWidth = 24f,
41+
defaultHeight = 24f,
42+
viewportWidth = 24f,
43+
viewportHeight = 24f,
44+
autoMirror = materialIconCall.autoMirror(),
45+
nodes = blockBody.parseMaterialPath(),
46+
)
4647
}
4748

48-
return nameArgument?.getArgumentExpression().safeAs<KtStringTemplateExpression>()
49-
?.entries
50-
?.firstOrNull()
51-
?.text.orEmpty()
52-
}
53-
54-
private fun KtCallExpression.extractAutoMirror(): Boolean {
55-
val autoMirrorArgument = valueArguments.find { arg ->
56-
arg?.getArgumentName()?.asName?.identifier == "autoMirror"
57-
}
49+
val builder = blockBody.builderExpression() ?: return null
5850

59-
return autoMirrorArgument?.getArgumentExpression()?.text?.toBoolean() ?: false
51+
return IrImageVector(
52+
name = builder.name().ifEmpty { property.name.orEmpty() },
53+
defaultWidth = builder.defaultWidth(0f),
54+
defaultHeight = builder.defaultHeight(0f),
55+
viewportWidth = builder.viewportWidth(0f),
56+
viewportHeight = builder.viewportHeight(0f),
57+
autoMirror = builder.autoMirror(),
58+
nodes = blockBody.parseMaterialPath(),
59+
)
6060
}
6161

6262
private fun KtBlockExpression.parseMaterialPath(): List<IrVectorNode> {
63-
val materialPathCall = childrenOfType<KtCallExpression>().firstOrNull {
64-
it.calleeExpression?.text == "materialPath"
65-
} ?: return emptyList()
63+
val materialPathCall = materialPathCall() ?: return emptyList()
6664

6765
val pathLambda = materialPathCall.lambdaArguments.firstOrNull()?.getLambdaExpression()
6866
val pathBody = pathLambda?.bodyExpression ?: return emptyList()
6967

7068
return listOf(
7169
IrPath(
7270
fill = IrFill.Color(IrColor("#FF000000")),
73-
fillAlpha = materialPathCall.extractFloat("fillAlpha", 1f),
74-
strokeAlpha = materialPathCall.extractFloat("strokeAlpha", 1f),
71+
fillAlpha = materialPathCall.parseFloatArg("fillAlpha") ?: 1f,
72+
strokeAlpha = materialPathCall.parseFloatArg("strokeAlpha") ?: 1f,
7573
strokeLineWidth = 1f,
7674
strokeLineJoin = IrStrokeLineJoin.Bevel,
7775
strokeLineMiter = 1f,
@@ -80,12 +78,12 @@ internal object MaterialImageVectorPsiParser {
8078
),
8179
)
8280
}
81+
}
8382

84-
private fun KtCallExpression.extractFloat(argName: String, defaultValue: Float): Float {
85-
val argument = valueArguments.find { arg ->
86-
arg?.getArgumentName()?.asName?.identifier == argName
87-
}
83+
private fun KtBlockExpression.materialPathCall(): KtCallExpression? {
84+
val ktCallExpression = childrenOfType<KtCallExpression>().firstOrNull {
85+
it.calleeExpression?.text == "materialPath"
86+
} ?: return null
8887

89-
return argument?.getArgumentExpression()?.text?.toFloatOrNull() ?: defaultValue
90-
}
88+
return ktCallExpression
9189
}

components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/RegularImageVectorPsiParser.kt

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@ package io.github.composegears.valkyrie.psi.imagevector.parser
33
import io.github.composegears.valkyrie.ir.IrImageVector
44
import io.github.composegears.valkyrie.ir.IrVectorNode
55
import io.github.composegears.valkyrie.psi.extension.childrenOfType
6+
import io.github.composegears.valkyrie.psi.imagevector.common.autoMirror
7+
import io.github.composegears.valkyrie.psi.imagevector.common.builderExpression
8+
import io.github.composegears.valkyrie.psi.imagevector.common.defaultHeight
9+
import io.github.composegears.valkyrie.psi.imagevector.common.defaultWidth
610
import io.github.composegears.valkyrie.psi.imagevector.common.extractPathFillType
711
import io.github.composegears.valkyrie.psi.imagevector.common.extractStrokeCap
812
import io.github.composegears.valkyrie.psi.imagevector.common.extractStrokeJoin
13+
import io.github.composegears.valkyrie.psi.imagevector.common.name
914
import io.github.composegears.valkyrie.psi.imagevector.common.parseClipPath
1015
import io.github.composegears.valkyrie.psi.imagevector.common.parseFill
1116
import io.github.composegears.valkyrie.psi.imagevector.common.parseFloatArg
1217
import io.github.composegears.valkyrie.psi.imagevector.common.parsePath
1318
import io.github.composegears.valkyrie.psi.imagevector.common.parseStringArg
1419
import io.github.composegears.valkyrie.psi.imagevector.common.parseStroke
20+
import io.github.composegears.valkyrie.psi.imagevector.common.viewportHeight
21+
import io.github.composegears.valkyrie.psi.imagevector.common.viewportWidth
1522
import org.jetbrains.kotlin.psi.KtBlockExpression
1623
import org.jetbrains.kotlin.psi.KtCallExpression
1724
import org.jetbrains.kotlin.psi.KtFile
@@ -28,63 +35,19 @@ internal object RegularImageVectorPsiParser {
2835
?: property.delegateExpression?.childrenOfType<KtBlockExpression>()?.firstOrNull()
2936
?: return null
3037

31-
val ktImageVector = blockBody.parseImageVectorParams() ?: return null
38+
val builder = blockBody.builderExpression() ?: return null
3239

3340
return IrImageVector(
34-
name = ktImageVector.name.ifEmpty { property.name.orEmpty() },
35-
defaultWidth = ktImageVector.defaultWidth,
36-
defaultHeight = ktImageVector.defaultHeight,
37-
viewportWidth = ktImageVector.viewportWidth,
38-
viewportHeight = ktImageVector.viewportHeight,
41+
name = builder.name().ifEmpty { property.name.orEmpty() },
42+
defaultWidth = builder.defaultWidth(0f),
43+
defaultHeight = builder.defaultHeight(0f),
44+
viewportWidth = builder.viewportWidth(0f),
45+
viewportHeight = builder.viewportHeight(0f),
46+
autoMirror = builder.autoMirror(),
3947
nodes = blockBody.parseApplyBlock(),
4048
)
4149
}
4250

43-
private fun KtBlockExpression.parseImageVectorParams(): IrImageVector? {
44-
val imageVectorBuilderCall = childrenOfType<KtCallExpression>().firstOrNull {
45-
it.calleeExpression?.text == "Builder"
46-
} ?: return null
47-
48-
var name = ""
49-
var defaultWidth = 0f
50-
var defaultHeight = 0f
51-
var viewportWidth = 0f
52-
var viewportHeight = 0f
53-
54-
imageVectorBuilderCall.valueArguments
55-
.forEach { arg ->
56-
val argName = arg.getArgumentName()?.asName?.identifier
57-
val argValue = arg.getArgumentExpression()?.text
58-
59-
when (argName) {
60-
"name" -> name = argValue?.removeSurrounding("\"").orEmpty()
61-
"defaultWidth" -> defaultWidth = parseValue(argValue)
62-
"defaultHeight" -> defaultHeight = parseValue(argValue)
63-
"viewportWidth" -> viewportWidth = parseValue(argValue)
64-
"viewportHeight" -> viewportHeight = parseValue(argValue)
65-
}
66-
}
67-
68-
return IrImageVector(
69-
name = name,
70-
defaultWidth = defaultWidth,
71-
defaultHeight = defaultHeight,
72-
viewportWidth = viewportWidth,
73-
viewportHeight = viewportHeight,
74-
nodes = emptyList(),
75-
)
76-
}
77-
78-
private fun parseValue(value: String?): Float {
79-
if (value == null) return 0f
80-
81-
return value
82-
.removeSuffix(".dp")
83-
.removeSuffix("f")
84-
.removeSuffix("F")
85-
.toFloatOrNull() ?: 0f
86-
}
87-
8851
private fun KtBlockExpression.parseApplyBlock(): List<IrVectorNode> {
8952
val vectorNodes = mutableListOf<IrVectorNode>()
9053

components/psi/imagevector/src/test/kotlin/io/github/composegears/valkyrie/psi/imagevector/KtFileToImageVectorParserTest.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedIconWith
2020
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedLinearGradient
2121
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedLinearGradientWithStroke
2222
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedMaterialIcon
23+
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedMaterialIconOnlyWithPath
2324
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedMaterialIconWithoutParam
2425
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedRadialGradient
2526
import io.github.composegears.valkyrie.psi.imagevector.expected.ExpectedSinglePath
@@ -81,20 +82,28 @@ class KtFileToImageVectorParserTest(
8182

8283
@Test
8384
fun `parse material icon`() = runInEdtAndGet {
84-
val ktFile = project.createKtFile(from = "backing/MaterialIcon.kt")
85+
val ktFile = project.createKtFile(from = "backing/MaterialIcon.all.kt")
8586
val imageVector = ImageVectorPsiParser.parseToIrImageVector(ktFile)?.toComposeImageVector()
8687

8788
assertThat(imageVector).isEqualTo(ExpectedMaterialIcon)
8889
}
8990

9091
@Test
9192
fun `parse material icon without param`() = runInEdtAndGet {
92-
val ktFile = project.createKtFile(from = "backing/MaterialIcon.without.param.kt")
93+
val ktFile = project.createKtFile(from = "backing/MaterialIcon.all.without.param.kt")
9394
val imageVector = ImageVectorPsiParser.parseToIrImageVector(ktFile)?.toComposeImageVector()
9495

9596
assertThat(imageVector).isEqualTo(ExpectedMaterialIconWithoutParam)
9697
}
9798

99+
@Test
100+
fun `parse material icon only with materialPath`() = runInEdtAndGet {
101+
val ktFile = project.createKtFile(from = "backing/MaterialIcon.material.path.kt")
102+
val imageVector = ImageVectorPsiParser.parseToIrImageVector(ktFile)?.toComposeImageVector()
103+
104+
assertThat(imageVector).isEqualTo(ExpectedMaterialIconOnlyWithPath)
105+
}
106+
98107
@Test
99108
fun `parse icon with import member`() = runInEdtAndGet {
100109
val ktFile = project.createKtFile(from = "backing/IconWithImportMember.kt")

components/psi/imagevector/src/test/kotlin/io/github/composegears/valkyrie/psi/imagevector/expected/MaterialIcon.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package io.github.composegears.valkyrie.psi.imagevector.expected
33
import androidx.compose.material.icons.materialIcon
44
import androidx.compose.material.icons.materialPath
55
import androidx.compose.ui.graphics.PathFillType
6+
import androidx.compose.ui.graphics.vector.ImageVector.Builder
7+
import androidx.compose.ui.unit.dp
68

79
val ExpectedMaterialIcon = materialIcon(name = "Filled.Settings", autoMirror = true) {
810
materialPath(
@@ -39,3 +41,46 @@ val ExpectedMaterialIconWithoutParam = materialIcon(name = "Bell") {
3941
moveTo(12.0f, 22.0f)
4042
}
4143
}
44+
45+
val ExpectedMaterialIconOnlyWithPath = Builder(
46+
name = "NotificationsAlert",
47+
defaultWidth = 24.0.dp,
48+
defaultHeight = 24.0.dp,
49+
viewportWidth = 24.0f,
50+
viewportHeight = 24.0f,
51+
).apply {
52+
materialPath {
53+
moveTo(18.75f, 16.5385f)
54+
verticalLineTo(11.1538f)
55+
curveTo(18.75f, 7.8477f, 16.905f, 5.08f, 13.6875f, 4.3477f)
56+
verticalLineTo(3.6154f)
57+
curveTo(13.6875f, 2.7215f, 12.9338f, 2.0f, 12.0f, 2.0f)
58+
curveTo(11.0662f, 2.0f, 10.3125f, 2.7215f, 10.3125f, 3.6154f)
59+
verticalLineTo(4.3477f)
60+
curveTo(7.0837f, 5.08f, 5.25f, 7.8369f, 5.25f, 11.1538f)
61+
verticalLineTo(16.5385f)
62+
lineTo(3.0f, 18.6923f)
63+
verticalLineTo(19.7692f)
64+
horizontalLineTo(21.0f)
65+
verticalLineTo(18.6923f)
66+
lineTo(18.75f, 16.5385f)
67+
close()
68+
moveTo(13.125f, 16.5385f)
69+
horizontalLineTo(10.875f)
70+
verticalLineTo(14.3846f)
71+
horizontalLineTo(13.125f)
72+
verticalLineTo(16.5385f)
73+
close()
74+
moveTo(13.125f, 12.2308f)
75+
horizontalLineTo(10.875f)
76+
verticalLineTo(7.9231f)
77+
horizontalLineTo(13.125f)
78+
verticalLineTo(12.2308f)
79+
close()
80+
moveTo(12.0f, 23.0f)
81+
curveTo(13.2375f, 23.0f, 14.25f, 22.0308f, 14.25f, 20.8462f)
82+
horizontalLineTo(9.75f)
83+
curveTo(9.75f, 22.0308f, 10.7512f, 23.0f, 12.0f, 23.0f)
84+
close()
85+
}
86+
}.build()

0 commit comments

Comments
 (0)