@@ -18,6 +18,7 @@ import org.jetbrains.kotlin.resolve.BindingContext
18
18
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
19
19
import java.awt.Color
20
20
import kotlin.math.abs
21
+ import kotlin.math.roundToInt
21
22
22
23
/* *
23
24
* This constant prevents the color tracer from following references ridiculously deep into the codebase.
@@ -57,6 +58,10 @@ class KobwebColorProvider : ElementColorProvider {
57
58
override fun setColorTo (element : PsiElement , color : Color ) = Unit
58
59
}
59
60
61
+ // navigationElement returns the element where a feature like "Go to declaration" would point:
62
+ // The source declaration, if found, and not a compiled one, which would make further analyzing impossible.
63
+ private fun KtSimpleNameExpression.findDeclaration (): PsiElement ? = this .mainReference.resolve()?.navigationElement
64
+
60
65
/* *
61
66
* Tries resolving references as deep as possible and checks if a Kobweb color is being referred to.
62
67
*
@@ -82,13 +87,10 @@ private fun traceColor(element: PsiElement, currentDepth: Int = 0): Color? {
82
87
83
88
is KtPropertyAccessor -> element.bodyExpression
84
89
85
- is KtCallExpression -> null .also {
86
- val calleeExpression = element.calleeExpression as ? KtNameReferenceExpression ? : return @also
87
- val callee = calleeExpression.findDeclaration() as ? KtNamedFunction ? : return @also
88
-
89
- tryParseKobwebColorFunctionCall(callee, element.valueArguments)?.let { parsedColor ->
90
- return parsedColor
91
- }
90
+ is KtCallExpression -> {
91
+ val color = element.tryParseKobwebColorFunctionColor()
92
+ if (color != null ) return color
93
+ null
92
94
}
93
95
94
96
else -> null
@@ -99,105 +101,144 @@ private fun traceColor(element: PsiElement, currentDepth: Int = 0): Color? {
99
101
} else null
100
102
}
101
103
104
+ private fun Float.toColorInt (): Int {
105
+ return (this * 255f ).roundToInt().coerceIn(0 , 255 )
106
+ }
107
+
102
108
/* *
103
- * Checks if a called function is a Kobweb color function and if it is, tries extracting the color from the call.
104
- *
105
- * @param callee The function being called, that might be a Kobweb color function
106
- * @param valueArguments The arguments the [callee] is called with
109
+ * Checks if a call expression represents a Kobweb color function call and if so, try extracting the color from it.
107
110
*
108
111
* @return The specified color, if it could be parsed and the callee is a Kobweb color function, otherwise null
109
112
*/
110
- private fun tryParseKobwebColorFunctionCall (
111
- callee : KtNamedFunction ,
112
- valueArguments : Collection <KtValueArgument >
113
- ): Color ? = with (valueArguments) {
114
- when {
115
- callee.isKobwebColorFunction(" rgb(r: Int, g: Int, b: Int)" ) ->
116
- evaluateArguments<Int >(3 )?.let { args ->
117
- tryCreateRgbColor(args[0 ], args[1 ], args[2 ])
118
- }
119
-
120
- callee.isKobwebColorFunction(" rgb(value: Int)" ) ->
121
- evaluateArguments<Int >(1 )?.let { args ->
122
- tryCreateRgbColor(args[0 ])
123
- }
124
-
125
- callee.isKobwebColorFunction(" rgba(value: Int)" , " rgba(value: Long)" ) ->
126
- (evaluateArguments<Int >(1 ) ? : evaluateArguments<Long , Int >(1 ) { it.toInt() })?.let { args ->
127
- tryCreateRgbColor(args[0 ] shr Byte .SIZE_BITS )
128
- }
129
-
130
- callee.isKobwebColorFunction(" argb(value: Int)" , " argb(value: Long)" ) ->
131
- (evaluateArguments<Int >(1 ) ? : evaluateArguments<Long , Int >(1 ) { it.toInt() })?.let { args ->
132
- tryCreateRgbColor(args[0 ] and 0x00_FF_FF_FF )
133
- }
134
-
135
- callee.isKobwebColorFunction(" hsl(h: Float, s: Float, l: Float)" ) ->
136
- evaluateArguments<Float >(3 )?.let { args ->
137
- tryCreateHslColor(args[0 ], args[1 ], args[2 ])
138
- }
139
-
140
- callee.isKobwebColorFunction(" hsla(h: Float, s: Float, l: Float, a: Float)" ) ->
141
- evaluateArguments<Float >(4 )?.let { args ->
142
- tryCreateHslColor(args[0 ], args[1 ], args[2 ])
143
- }
113
+ private fun KtCallExpression.tryParseKobwebColorFunctionColor (): Color ? {
114
+ " $KOBWEB_COLOR_COMPANION_FQ_NAME .rgb" .let { rgbFqn ->
115
+ this .extractConstantArguments1<Int >(rgbFqn)?.let { (rgb) ->
116
+ return tryCreateRgbColor(rgb)
117
+ }
144
118
145
- else -> null
119
+ this .extractConstantArguments1<Long >(rgbFqn)?.let { (rgb) ->
120
+ return tryCreateRgbColor(rgb.toInt())
121
+ }
122
+
123
+ this .extractConstantArguments3<Int , Int , Int >(rgbFqn)?.let { (r, g, b) ->
124
+ return tryCreateRgbColor(r, g, b)
125
+ }
126
+
127
+ this .extractConstantArguments3<Float , Float , Float >(rgbFqn)?.let { (r, g, b) ->
128
+ return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
129
+ }
146
130
}
147
- }
148
131
132
+ " $KOBWEB_COLOR_COMPANION_FQ_NAME .rgba" .let { rgbaFqn ->
133
+ this .extractConstantArguments1<Int >(rgbaFqn)?.let { (rgb) ->
134
+ return tryCreateRgbColor(rgb shr 8 )
135
+ }
149
136
150
- // navigationElement returns the element where a feature like "Go to declaration" would point:
151
- // The source declaration, if found, and not a compiled one, which would make further analyzing impossible.
152
- private fun KtSimpleNameExpression. findDeclaration (): PsiElement ? = this .mainReference.resolve()?.navigationElement
137
+ this .extractConstantArguments1< Long >(rgbaFqn)?. let { (rgb) ->
138
+ return tryCreateRgbColor(rgb.toInt() shr 8 )
139
+ }
153
140
154
- /* *
155
- * Evaluates a collection of value arguments to the specified type.
156
- *
157
- * For example, if we have a collection of decimal, hex, and binary arguments,
158
- * this method can parse them into regular integer values, so 123, 0x7B and 0b0111_1011
159
- * would all evaluate to 123.
160
- *
161
- * @param argCount The size the original and evaluated collections must have. If this value disagrees with the size of
162
- * the passed in collection, it will throw an exception; it's essentially treated like an assertion at that point.
163
- * Otherwise, it's used to avoid returning a final, evaluated array of unexpected size.
164
- * @param evaluatedValueMapper Convenience parameter to avoid having to type `.map { ... }.toTypedArray()`
165
- *
166
- * @return the evaluated arguments of length [argCount] if evaluation of **all** arguments succeeded,
167
- * and [argCount] elements were passed for evaluation, otherwise null
168
- */
169
- private inline fun <reified Evaluated , reified Mapped > Collection<KtValueArgument>.evaluateArguments (
170
- argCount : Int ,
171
- evaluatedValueMapper : (Evaluated ) -> Mapped
172
- ): Array <Mapped >? {
141
+ this .extractConstantArguments4<Int , Int , Int , Int >(rgbaFqn)?.let { (r, g, b) ->
142
+ return tryCreateRgbColor(r, g, b)
143
+ }
144
+
145
+ this .extractConstantArguments4<Int , Int , Int , Float >(rgbaFqn)?.let { (r, g, b) ->
146
+ return tryCreateRgbColor(r, g, b)
147
+ }
148
+
149
+ this .extractConstantArguments4<Float , Float , Float , Float >(rgbaFqn)?.let { (r, g, b) ->
150
+ return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
151
+ }
152
+ }
153
+
154
+ " $KOBWEB_COLOR_COMPANION_FQ_NAME .argb" .let { argbFqn ->
155
+ this .extractConstantArguments1<Int >(argbFqn)?.let { (rgb) ->
156
+ return tryCreateRgbColor(rgb and 0x00_FF_FF_FF )
157
+ }
158
+
159
+ this .extractConstantArguments1<Long >(argbFqn)?.let { (rgb) ->
160
+ return tryCreateRgbColor(rgb.toInt() and 0x00_FF_FF_FF )
161
+ }
173
162
174
- check(this .size == argCount) { " evaluateArguments called on a collection expecting $argCount arguments, but it only had ${this .size} " }
163
+ this .extractConstantArguments4<Int , Int , Int , Int >(argbFqn)?.let { (_, r, g, b) ->
164
+ return tryCreateRgbColor(r, g, b)
165
+ }
175
166
176
- val constantExpressions = this .mapNotNull { it.getArgumentExpression() as ? KtConstantExpression }
167
+ this .extractConstantArguments4<Float , Int , Int , Int >(argbFqn)?.let { (_, r, g, b) ->
168
+ return tryCreateRgbColor(r, g, b)
169
+ }
177
170
178
- val evaluatedArguments = constantExpressions.mapNotNull { expr ->
179
- val bindingContext = expr.analyze(BodyResolveMode .PARTIAL )
180
- val constant = bindingContext.get(BindingContext .COMPILE_TIME_VALUE , expr) ? : return @mapNotNull null
181
- val type = bindingContext.getType(expr) ? : return @mapNotNull null
182
- constant.getValue(type) as ? Evaluated
171
+ this .extractConstantArguments4<Float , Float , Float , Float >(argbFqn)?.let { (_, r, g, b) ->
172
+ return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
173
+ }
183
174
}
184
175
185
- return if (evaluatedArguments.size != argCount) null
186
- else evaluatedArguments.map(evaluatedValueMapper).toTypedArray()
187
- }
176
+ " $KOBWEB_COLOR_COMPANION_FQ_NAME .hsl" .let { hslFqn ->
177
+ this .extractConstantArguments3<Int , Float , Float >(hslFqn)?.let { (h, s, l) ->
178
+ return tryCreateHslColor(h, s, l)
179
+ }
188
180
189
- private inline fun <reified Evaluated > Collection<KtValueArgument>.evaluateArguments (argCount : Int ) =
190
- evaluateArguments<Evaluated , Evaluated >(argCount) { it }
181
+ this .extractConstantArguments3<Float , Float , Float >(hslFqn)?.let { (h, s, l) ->
182
+ return tryCreateHslColor(h.roundToInt(), s, l)
183
+ }
184
+ }
191
185
192
- private fun KtNamedFunction. isKobwebColorFunction ( vararg functionSignatures : String ): Boolean {
193
- val actualFqName = this .kotlinFqName?.asString() ? : return false
194
- val actualParameters = this .valueParameterList?.text ? : return false
195
- val actual = actualFqName + actualParameters
186
+ " $KOBWEB_COLOR_COMPANION_FQ_NAME .hsla " . let { hslaFqn ->
187
+ this .extractConstantArguments4< Int , Float , Float , Float >(hslaFqn)?. let { (h, s, l) ->
188
+ return tryCreateHslColor(h, s, l)
189
+ }
196
190
197
- return functionSignatures.any { functionSignature ->
198
- val expected = " $KOBWEB_COLOR_COMPANION_FQ_NAME . $functionSignature "
199
- expected == actual
191
+ this .extractConstantArguments4< Float , Float , Float , Float >(hslaFqn)?. let { (h, s, l) ->
192
+ return tryCreateHslColor(h.roundToInt(), s, l)
193
+ }
200
194
}
195
+
196
+ return null
197
+ }
198
+
199
+ private data class Values <T1 , T2 , T3 , T4 >(
200
+ val v1 : T1 ,
201
+ val v2 : T2 ,
202
+ val v3 : T3 ,
203
+ val v4 : T4
204
+ )
205
+
206
+ private inline fun <reified T > KtValueArgument.extractConstantValue (): T ? {
207
+ val constantExpression = getArgumentExpression() as ? KtConstantExpression ? : return null
208
+ val bindingContext = constantExpression.analyze(BodyResolveMode .PARTIAL )
209
+ val constant = bindingContext.get(BindingContext .COMPILE_TIME_VALUE , constantExpression) ? : return null
210
+ val type = bindingContext.getType(constantExpression) ? : return null
211
+ return constant.getValue(type) as ? T
212
+ }
213
+
214
+ private fun KtCallExpression.valueArgumentsIf (fqn : String , requiredSize : Int ): List <KtValueArgument >? {
215
+ val calleeExpression = calleeExpression as ? KtNameReferenceExpression ? : return null
216
+ val callee = calleeExpression.findDeclaration() as ? KtNamedFunction ? : return null
217
+ if (callee.kotlinFqName?.asString() != fqn) return null
218
+ return valueArguments.takeIf { it.size == requiredSize }
219
+ }
220
+
221
+ private inline fun <reified I > KtCallExpression.extractConstantArguments1 (fqn : String ): Values <I , Unit , Unit , Unit >? {
222
+ val valueArguments = valueArgumentsIf(fqn, 1 ) ? : return null
223
+ val v1: I ? = valueArguments[0 ].extractConstantValue()
224
+ return if (v1 != null ) Values (v1, Unit , Unit , Unit ) else null
225
+ }
226
+
227
+ private inline fun <reified I1 , reified I2 , reified I3 > KtCallExpression.extractConstantArguments3 (fqn : String ): Values <I1 , I2 , I3 , Unit >? {
228
+ val valueArguments = valueArgumentsIf(fqn, 3 ) ? : return null
229
+ val v1: I1 ? = valueArguments[0 ].extractConstantValue()
230
+ val v2: I2 ? = valueArguments[1 ].extractConstantValue()
231
+ val v3: I3 ? = valueArguments[2 ].extractConstantValue()
232
+ return if (v1 != null && v2 != null && v3 != null ) Values (v1, v2, v3, Unit ) else null
233
+ }
234
+
235
+ private inline fun <reified I1 , reified I2 , reified I3 , reified I4 > KtCallExpression.extractConstantArguments4 (fqn : String ): Values <I1 , I2 , I3 , I4 >? {
236
+ val valueArguments = valueArgumentsIf(fqn, 4 ) ? : return null
237
+ val v1: I1 ? = valueArguments[0 ].extractConstantValue()
238
+ val v2: I2 ? = valueArguments[1 ].extractConstantValue()
239
+ val v3: I3 ? = valueArguments[2 ].extractConstantValue()
240
+ val v4: I4 ? = valueArguments[3 ].extractConstantValue()
241
+ return if (v1 != null && v2 != null && v3 != null && v4 != null ) Values (v1, v2, v3, v4) else null
201
242
}
202
243
203
244
private fun tryCreateRgbColor (r : Int , g : Int , b : Int ) =
@@ -206,11 +247,15 @@ private fun tryCreateRgbColor(r: Int, g: Int, b: Int) =
206
247
private fun tryCreateRgbColor (rgb : Int ) =
207
248
runCatching { Color (rgb) }.getOrNull()
208
249
209
- private fun tryCreateHslColor (hue : Float , saturation : Float , lightness : Float ): Color ? {
250
+ // Expected values:
251
+ // hue: 0-360
252
+ // saturation: 0-1
253
+ // lightness: 0-1
254
+ private fun tryCreateHslColor (hue : Int , saturation : Float , lightness : Float ): Color ? {
210
255
// https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
211
256
val chroma = (1 - abs(2 * lightness - 1 )) * saturation
212
257
val intermediateValue = chroma * (1 - abs(((hue / 60 ) % 2 ) - 1 ))
213
- val hueSection = (hue.toInt() % 360 ) / 60
258
+ val hueSection = (hue % 360 ) / 60
214
259
val r: Float
215
260
val g: Float
216
261
val b: Float
0 commit comments