From 7dbd1d766bf10987375d156ba0368151f1ad2709 Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Wed, 3 Jul 2024 15:41:11 +0200 Subject: [PATCH] Implement ValueFactory.eval to use Markdown parsing when encountering a Node in expressions --- .../InvalidExpressionEvalException.kt | 14 ++++++++++ .../visitor/AppendExpressionVisitor.kt | 11 ++++++-- .../quarkdown/function/value/ValueFactory.kt | 28 +++++++++++++++++++ .../quarkdown/function/value/data/Lambda.kt | 11 ++------ .../kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt | 3 +- 5 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/function/error/internal/InvalidExpressionEvalException.kt diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/internal/InvalidExpressionEvalException.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/internal/InvalidExpressionEvalException.kt new file mode 100644 index 00000000..8ee5f7ef --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/internal/InvalidExpressionEvalException.kt @@ -0,0 +1,14 @@ +package eu.iamgio.quarkdown.function.error.internal + +import eu.iamgio.quarkdown.function.expression.ComposedExpression +import eu.iamgio.quarkdown.function.expression.Expression +import eu.iamgio.quarkdown.function.value.NodeValue +import eu.iamgio.quarkdown.function.value.ValueFactory + +/** + * An exception thrown when an [Expression] cannot be evaluated. + * Most commonly, this is thrown when a [NodeValue] appears in a [ComposedExpression], + * hence the content must be parsed as Markdown instead of expression. + * @see ValueFactory.eval + */ +class InvalidExpressionEvalException : Exception() diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/expression/visitor/AppendExpressionVisitor.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/expression/visitor/AppendExpressionVisitor.kt index a08af759..e597b25c 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/expression/visitor/AppendExpressionVisitor.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/expression/visitor/AppendExpressionVisitor.kt @@ -1,6 +1,7 @@ package eu.iamgio.quarkdown.function.expression.visitor import eu.iamgio.quarkdown.function.call.FunctionCall +import eu.iamgio.quarkdown.function.error.internal.InvalidExpressionEvalException import eu.iamgio.quarkdown.function.expression.ComposedExpression import eu.iamgio.quarkdown.function.expression.Expression import eu.iamgio.quarkdown.function.expression.append @@ -61,6 +62,7 @@ import eu.iamgio.quarkdown.function.value.VoidValue class AppendExpressionVisitor(private val other: Expression) : ExpressionVisitor { /** * @return string result of the concatenation between [this] and [other] + * @throws InvalidExpressionEvalException if either [this] or [other] is a [NodeValue] (see [eu.iamgio.quarkdown.function.value.ValueFactory.eval]) */ private fun Value<*>.concatenate(): InputValue<*> { val otherEval = other.eval() // Evaluate the next expression. @@ -69,9 +71,12 @@ class AppendExpressionVisitor(private val other: Expression) : ExpressionVisitor if (this is VoidValue) return otherEval as InputValue<*> if (otherEval is VoidValue) return this as InputValue<*> - // A NodeValue, which is only an OutputValue and not an InputValue, is appended to a lazy collection of values. - if (this is OutputValue<*> && (this is NodeValue || otherEval is NodeValue)) { - return visit(GeneralCollectionValue(listOf(this))) + // Whenever a NodeValue appears in a composed expression, it means the expected output is strictly meant to be + // a pure Markdown output node. Therefore, the thrown error is caught at eval-time and the expression + // is re-processed as Markdown content. + // See ValueFactory.eval for more information. + if (this is NodeValue || otherEval is NodeValue) { + throw InvalidExpressionEvalException() } // If the other value is a collection, add the current value to it as the first element. diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt index bc42b9f5..94ab5db2 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt @@ -10,6 +10,7 @@ import eu.iamgio.quarkdown.context.MutableContext import eu.iamgio.quarkdown.document.page.Size import eu.iamgio.quarkdown.document.page.SizeUnit import eu.iamgio.quarkdown.document.page.Sizes +import eu.iamgio.quarkdown.function.error.internal.InvalidExpressionEvalException import eu.iamgio.quarkdown.function.expression.ComposedExpression import eu.iamgio.quarkdown.function.expression.Expression import eu.iamgio.quarkdown.function.expression.eval @@ -342,4 +343,31 @@ object ValueFactory { }, ) } + + /** + * Evaluates an expression from a raw string input. + * @param raw string input that may contain both static values and function calls (e.g. `"2 + 2 is .sum {2} {2}"`) + * @param context context to retrieve the pipeline from + * @param fallback value to return if the expression is invalid or an error occurs during the evaluation. + * A common example of an invalid expression evaluation is when a [NodeValue] is present in a [ComposedExpression], hence the expected output is a pure Markdown output node. + * The fallback function defaults to returning a block-Markdown content node. + * @return the result of the evaluation of the expression (in the previous example: `ComposedExpression(DynamicValue("2 + 2 is "), FunctionCall(sum, 2, 2))`), + * or the result of the fallback function if the expression is invalid + */ + fun eval( + raw: String, + context: Context, + fallback: () -> OutputValue<*> = { blockMarkdown(raw, context).asNodeValue() }, + ): OutputValue<*> { + val expression = expression(raw, context) ?: return fallback() + + return try { + expression.eval().let { + it as? OutputValue<*> + ?: throw IllegalStateException("The result of the expression is not a suitable OutputValue: $it") + } + } catch (e: InvalidExpressionEvalException) { + fallback() + } + } } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/data/Lambda.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/data/Lambda.kt index 8c72ddcb..8beff5fd 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/data/Lambda.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/data/Lambda.kt @@ -3,7 +3,6 @@ package eu.iamgio.quarkdown.function.value.data import eu.iamgio.quarkdown.context.Context import eu.iamgio.quarkdown.function.SimpleFunction import eu.iamgio.quarkdown.function.error.InvalidLambdaArgumentCountException -import eu.iamgio.quarkdown.function.expression.eval import eu.iamgio.quarkdown.function.library.Library import eu.iamgio.quarkdown.function.reflect.DynamicValueConverter import eu.iamgio.quarkdown.function.reflect.FromDynamicType @@ -68,14 +67,8 @@ class Lambda( // Register the arguments in the context, which can be accessed as function calls. context.libraries += createLambdaParametersLibrary(*arguments) - val output = - ValueFactory.expression(action(arguments), context)?.eval() - ?: throw IllegalArgumentException("Cannot invoke dynamically-typed lambda: null result") - - return when (output) { - is OutputValue<*> -> output - else -> DynamicValue(output) - } + // The result of the lambda action is evaluated. + return ValueFactory.eval(action(arguments), context) } /** 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 8171dce3..0b009cf8 100644 --- a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt +++ b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt @@ -113,8 +113,7 @@ fun function( val args = this.links.values.map { it.value }.toTypedArray() // The final result is evaluated and returned as a dynamic, hence it can be used as any type. - val result = body.invokeDynamic(*args) - DynamicValue(result.unwrappedValue) + body.invokeDynamic(*args) } // The function is registered and ready to be called.