From 6149e424d91788ad706185e55b61d15ba552656d Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Sun, 28 Jul 2024 23:52:35 +0200 Subject: [PATCH] Implement `.let` function --- .../function/reflect/KFunctionAdapter.kt | 9 +++- .../reflect/NoAutoArgumentUnwrapping.kt | 10 +++++ .../quarkdown/function/value/DynamicValue.kt | 2 + .../kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt | 17 +++++++ .../iamgio/quarkdown/test/FullPipelineTest.kt | 44 +++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/NoAutoArgumentUnwrapping.kt diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/KFunctionAdapter.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/KFunctionAdapter.kt index 417c3b74..6a894541 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/KFunctionAdapter.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/KFunctionAdapter.kt @@ -46,7 +46,14 @@ class KFunctionAdapter>(private val function: KFunction) : // Corresponding KParameter. val param = function.parameters[parameter.index] - param to argument.value.unwrappedValue + // The argument is unwrapped unless the value class specifies not to. + // An example of a disabled unwrapping is DynamicValue, which is used to pass dynamically typed values as-is. + val arg = + argument.value.let { + if (it::class.hasAnnotation()) it else it.unwrappedValue + } + + param to arg } // Call the KFunction. diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/NoAutoArgumentUnwrapping.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/NoAutoArgumentUnwrapping.kt new file mode 100644 index 00000000..e09501ac --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/NoAutoArgumentUnwrapping.kt @@ -0,0 +1,10 @@ +package eu.iamgio.quarkdown.function.reflect + +import eu.iamgio.quarkdown.function.value.Value + +/** + * When invoking a function via [KFunctionAdapter], [Value] arguments are automatically unwrapped to their raw value, + * unless this annotation is present on the [Value] subclass. + */ +@Target(AnnotationTarget.CLASS) +annotation class NoAutoArgumentUnwrapping diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/DynamicValue.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/DynamicValue.kt index 51ece20c..4e3880be 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/DynamicValue.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/DynamicValue.kt @@ -1,6 +1,7 @@ package eu.iamgio.quarkdown.function.value import eu.iamgio.quarkdown.function.expression.visitor.ExpressionVisitor +import eu.iamgio.quarkdown.function.reflect.NoAutoArgumentUnwrapping import eu.iamgio.quarkdown.function.value.output.OutputValueVisitor /** @@ -13,6 +14,7 @@ import eu.iamgio.quarkdown.function.value.output.OutputValueVisitor * or simply an opaque wrapper for a generic value (e.g. a `Node`) * @see eu.iamgio.quarkdown.function.reflect.DynamicValueConverter */ +@NoAutoArgumentUnwrapping data class DynamicValue(override val unwrappedValue: Any?) : InputValue, OutputValue { override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) diff --git a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt index 9acac333..1678abd7 100644 --- a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt +++ b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt @@ -29,6 +29,7 @@ val Flow: Module = ::forEach, ::function, ::variable, + ::let, ::node, ) @@ -161,6 +162,22 @@ fun variable( ) } +/** + * Defines a temporary variable that can be used in the lambda body. + * Example: + * ``` + * .let {world} + * Hello, **.1**! + * ``` + * @param value value to use as a temporary variable + * @param body content to evaluate with the temporary variable. Accepts 1 parameter ([value] itself) + * @return the evaluation of [body] with [value] as a parameter + */ +fun let( + value: DynamicValue, + body: Lambda, +): OutputValue<*> = body.invokeDynamic(value) + /** * Creates a null invisible node that forces the expression it lies in to be evaluated as Markdown content. * This is a workaround that can be used at the beginning of lambda blocks (e.g. in a `.function`, `.if` or `.foreach` call) diff --git a/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt b/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt index 5de19dee..7b2502c6 100644 --- a/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt +++ b/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt @@ -249,6 +249,50 @@ class FullPipelineTest { ) { assertEquals("

Hello, world!

Hello, iamgio!

", it) } + + execute( + """ + .let {world} + Hello, **.1**! + """.trimIndent(), + ) { + assertEquals("

Hello, world!

", it) + } + + execute( + """ + .foreach {..3} + .let {.1} + x: + .sum {3} {.x} + """.trimIndent(), + ) { + assertEquals("456", it) + } + + execute( + """ + .foreach {..3} + .node + .let {.1} + x: + .sum {3} {.x} + """.trimIndent(), + ) { + assertEquals("

4

5

6

", it) + } + + execute( + """ + .let {code.txt} + file: + .let {.read {.file} {..2}} + .code + .1 + """.trimIndent(), + ) { + assertEquals("
Line 1\nLine 2
", it) + } } @Test