Skip to content

Commit

Permalink
Implement ValueFactory.eval to use Markdown parsing when encountering…
Browse files Browse the repository at this point in the history
… a Node in expressions
  • Loading branch information
iamgio committed Jul 3, 2024
1 parent b865ea5 commit 7dbd1d7
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -61,6 +62,7 @@ import eu.iamgio.quarkdown.function.value.VoidValue
class AppendExpressionVisitor(private val other: Expression) : ExpressionVisitor<Expression> {
/**
* @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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

/**
Expand Down
3 changes: 1 addition & 2 deletions stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Flow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 7dbd1d7

Please sign in to comment.