From dff58464fba41c724ae48145222f549cbbd1e000 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Sun, 7 Apr 2024 18:05:14 -0400 Subject: [PATCH] Begin moving away from Pebble templates --- .../kotlin/info/mechyrdia/lore/http_utils.kt | 3 +- .../kotlin/info/mechyrdia/lore/parser_html.kt | 8 +- .../info/mechyrdia/lore/parser_lexer_async.kt | 68 +++++ .../info/mechyrdia/lore/parser_preprocess.kt | 269 ++++++++++++++++++ .../lore/parser_preprocess_include.kt | 5 + .../mechyrdia/lore/parser_preprocess_json.kt | 61 ++++ .../mechyrdia/lore/parser_preprocess_math.kt | 157 ++++++++++ .../kotlin/info/mechyrdia/lore/parser_tree.kt | 6 + .../info/mechyrdia/lore/preparser_config.kt | 8 +- 9 files changed, 574 insertions(+), 11 deletions(-) create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt index 263a715..566dba7 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt @@ -7,5 +7,4 @@ data class HttpRedirectException(val url: String, val permanent: Boolean) : Runt fun redirect(url: String, permanent: Boolean = false): Nothing = throw HttpRedirectException(url, permanent) -context(ApplicationCall) -inline fun redirectHref(resource: T, permanent: Boolean = false, hash: String? = null): Nothing = redirect(href(resource, hash), permanent) +inline fun ApplicationCall.redirectHref(resource: T, permanent: Boolean = false, hash: String? = null): Nothing = redirect(href(resource, hash), permanent) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt index 7383dfa..478612f 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt @@ -79,11 +79,11 @@ operator fun > Entities.unaryPlus() = onTagContentEntity(t fun > C.unsafe(block: Unsafe.() -> Unit) = onTagContentUnsafe(block) fun ParserTree.shouldSplitSections(): Boolean = firstOrNull()?.let { - it is ParserTreeNode.Tag && it.tag.lowercase() == "h1" + it is ParserTreeNode.Tag && it isTag "h1" } == true fun ParserTree.splitSections(): List = splitBefore { - it is ParserTreeNode.Tag && it.tag.lowercase() == "h2" + it is ParserTreeNode.Tag && it isTag "h2" } fun ParserTreeNode.isWhitespace() = when (this) { @@ -95,7 +95,7 @@ fun ParserTreeNode.isWhitespace() = when (this) { fun ParserTreeNode.isParagraph(inlineTags: Set): Boolean = when (this) { is ParserTreeNode.Text -> true ParserTreeNode.LineBreak -> false - is ParserTreeNode.Tag -> tag.lowercase() in inlineTags && subNodes.isParagraph(inlineTags) + is ParserTreeNode.Tag -> this isTag inlineTags && subNodes.isParagraph(inlineTags) } fun ParserTree.isParagraph(inlineTags: Set): Boolean = any { it.isParagraph(inlineTags) } @@ -217,7 +217,7 @@ fun ParserTree.treeToText(): String = joinToString(separator = "") { ParserTreeNode.LineBreak -> " " is ParserTreeNode.Tag -> it.subNodes.treeToText() } -} +}.trim() fun interface HtmlTextBodyLexerTag : HtmlLexerTag { override fun processTag(env: LexerTagEnvironment, param: String?, subNodes: ParserTree): HtmlBuilderSubject { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt new file mode 100644 index 0000000..6889efc --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt @@ -0,0 +1,68 @@ +package info.mechyrdia.lore + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +data class AsyncLexerTagEnvironment( + val context: TContext, + private val processTags: AsyncLexerTags, + private val processText: AsyncLexerTextProcessor, + private val processBreak: AsyncLexerLineBreakProcessor, + private val processInvalidTag: AsyncLexerTagFallback, + private val combiner: AsyncLexerCombiner +) { + suspend fun processTree(parserTree: ParserTree): TSubject { + return combiner.processAndCombine(this, parserTree) + } + + suspend fun processNode(parserTreeNode: ParserTreeNode): TSubject { + return when (parserTreeNode) { + is ParserTreeNode.Text -> processText.processText(this, parserTreeNode.text) + ParserTreeNode.LineBreak -> processBreak.processLineBreak(this) + is ParserTreeNode.Tag -> processTags[parserTreeNode.tag]?.processTag(this, parserTreeNode.param, parserTreeNode.subNodes) + ?: processInvalidTag.processInvalidTag(this, parserTreeNode.tag, parserTreeNode.param, parserTreeNode.subNodes) + } + } +} + +@JvmInline +value class AsyncLexerTags private constructor(private val tags: Map>) { + operator fun get(name: String) = tags[name.lowercase()] + + operator fun plus(other: AsyncLexerTags) = AsyncLexerTags(tags + other.tags) + + companion object { + fun empty() = AsyncLexerTags(emptyMap()) + + operator fun invoke(tags: Map>) = AsyncLexerTags(tags.mapKeys { (name, _) -> name.lowercase() }) + } +} + +fun interface AsyncLexerTagProcessor { + suspend fun processTag(env: AsyncLexerTagEnvironment, param: String?, subNodes: ParserTree): TSubject +} + +fun interface AsyncLexerTagFallback { + suspend fun processInvalidTag(env: AsyncLexerTagEnvironment, tag: String, param: String?, subNodes: ParserTree): TSubject +} + +fun interface AsyncLexerTextProcessor { + suspend fun processText(env: AsyncLexerTagEnvironment, text: String): TSubject +} + +fun interface AsyncLexerLineBreakProcessor { + suspend fun processLineBreak(env: AsyncLexerTagEnvironment): TSubject +} + +fun interface AsyncLexerCombiner { + suspend fun processAndCombine(env: AsyncLexerTagEnvironment, nodes: ParserTree): TSubject { + return combine(env, coroutineScope { + nodes.map { + async { env.processNode(it) } + }.awaitAll() + }) + } + + suspend fun combine(env: AsyncLexerTagEnvironment, subjects: List): TSubject +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt new file mode 100644 index 0000000..598aedc --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt @@ -0,0 +1,269 @@ +package info.mechyrdia.lore + +import info.mechyrdia.JsonStorageCodec +import io.ktor.server.application.* +import io.ktor.server.request.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import java.time.Instant + +class PreProcessingContext private constructor( + val variables: MutableMap, + val parent: PreProcessingContext? = null, +) { + constructor(name: String, value: String, parent: PreProcessingContext? = null) : this(mutableMapOf(name to value.textToTree()), parent) + constructor(parent: PreProcessingContext? = null, vararg variables: Pair) : this(mutableMapOf(*variables), parent) + + operator fun get(name: String): ParserTree = variables[name] ?: parent?.get(name) ?: "null".textToTree() + + operator fun set(name: String, value: ParserTree) { + variables[name] = value + } + + operator fun set(name: String, value: String) { + variables[name] = value.textToTree() + } + + operator fun contains(name: String): Boolean = name in variables || (parent?.contains(name) == true) + + operator fun plus(other: Map) = PreProcessingContext(other.toMutableMap(), this) + + fun toMap(): Map = parent?.toMap().orEmpty() + variables + + companion object { + operator fun invoke(variables: Map, parent: PreProcessingContext? = null) = PreProcessingContext(variables.toMutableMap(), parent) + + const val PAGE_PATH_KEY = "PAGE_PATH" + const val INSTANT_NOW_KEY = "INSTANT_NOW" + + context(ApplicationCall) + fun defaults() = PreProcessingContext( + null, + PAGE_PATH_KEY to request.path().removePrefix("/").removePrefix("lore").textToTree(), + INSTANT_NOW_KEY to Instant.now().toEpochMilli().toString().textToTree(), + ) + } +} + +typealias PreProcessingSubject = ParserTree + +object PreProcessorUtils : AsyncLexerTagFallback, AsyncLexerTextProcessor, AsyncLexerLineBreakProcessor, AsyncLexerCombiner { + override suspend fun processInvalidTag(env: AsyncLexerTagEnvironment, tag: String, param: String?, subNodes: ParserTree): PreProcessingSubject { + return listOf( + ParserTreeNode.Tag( + tag = tag, + param = param, + subNodes = env.processTree(subNodes) + ) + ) + } + + override suspend fun processText(env: AsyncLexerTagEnvironment, text: String): PreProcessingSubject { + return text.textToTree() + } + + override suspend fun processLineBreak(env: AsyncLexerTagEnvironment): PreProcessingSubject { + return listOf(ParserTreeNode.LineBreak) + } + + override suspend fun combine(env: AsyncLexerTagEnvironment, subjects: List): PreProcessingSubject { + return subjects.flatten() + } + + fun withContext(env: AsyncLexerTagEnvironment, newContext: PreProcessingContext): AsyncLexerTagEnvironment { + return env.copy(context = newContext) + } + + suspend fun processWithContext(env: AsyncLexerTagEnvironment, newContext: PreProcessingContext, input: ParserTree): ParserTree { + return withContext(env, newContext).processTree(input) + } + + fun indexTree(tree: ParserTree, index: List): ParserTree { + if (index.isEmpty()) return tree + val tags = tree.filterIsInstance() + if (tags.isEmpty()) return emptyList() + + val head = index.first() + val tail = index.drop(1) + + val firstTag = tags.first() + return if (firstTag isTag "item" && firstTag.param == null) { + head.toIntOrNull()?.let { listIndex -> + tree.asPreProcessorList().getOrNull(listIndex) + }?.let { indexTree(it, tail) }.orEmpty() + } else if (firstTag isTag "arg" && firstTag.param != null) { + tree.asPreProcessorMap()[head]?.let { indexTree(it, tail) }.orEmpty() + } else emptyList() + } +} + +fun interface PreProcessorLexerTag : AsyncLexerTagProcessor + +fun String.textToTree(): ParserTree = listOf(ParserTreeNode.Text(this)) + +fun interface PreProcessorFunction { + suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree +} + +fun interface PreProcessorFunctionProvider : PreProcessorLexerTag { + suspend fun provideFunction(param: String?): PreProcessorFunction? + + override suspend fun processTag(env: AsyncLexerTagEnvironment, param: String?, subNodes: ParserTree): PreProcessingSubject { + val args = subNodes.asPreProcessorMap().mapValuesSuspend { _, value -> env.processTree(value) } + val ctx = PreProcessingContext(args, env.context) + + val func = provideFunction(param) ?: return emptyList() + return func.execute(PreProcessorUtils.withContext(env, ctx)) + } +} + +abstract class PreProcessorFunctionLibrary : PreProcessorFunctionProvider { + abstract val functions: Map + + override suspend fun provideFunction(param: String?) = param?.let { functions[it] } + + companion object { + operator fun invoke(library: Map) = object : PreProcessorFunctionLibrary() { + override val functions: Map = library + } + } +} + +@JvmInline +value class PreProcessorVariableFunction(private val variable: String) : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + return env.processTree(env.context[variable]) + } +} + +object PreProcessorVariableInvoker : PreProcessorFunctionProvider { + override suspend fun provideFunction(param: String?): PreProcessorFunction? { + return param?.let { PreProcessorVariableFunction(it) } + } +} + +fun ParserTree.asPreProcessorList(): List = mapNotNull { + if (it !is ParserTreeNode.Tag || it isNotTag "item" || it.param != null) + null + else + it.subNodes +} + +fun ParserTree.asPreProcessorMap(): Map = mapNotNull { + if (it !is ParserTreeNode.Tag || it isNotTag "arg" || it.param == null) + null + else + it.param to it.subNodes +}.toMap() + +suspend fun List.mapSuspend(processor: suspend (T) -> R) = coroutineScope { + map { + async { + processor(it) + } + }.awaitAll() +} + +suspend fun Map.mapValuesSuspend(processor: suspend (K, V) -> R) = coroutineScope { + map { (k, v) -> + async { + k to processor(k, v) + } + }.awaitAll() +}.toMap() + +enum class PreProcessorTags(val type: PreProcessorLexerTag) { + EVAL(PreProcessorLexerTag { env, param, subNodes -> + param?.toIntOrNull()?.let { times -> + var tree = subNodes + repeat(times) { + tree = env.processTree(tree) + } + tree + } ?: env.processTree(subNodes) + }), + LAZY(PreProcessorLexerTag { _, _, subNodes -> + subNodes + }), + VAL(PreProcessorLexerTag { env, _, subNodes -> + env.processTree(subNodes).treeToText().textToTree() + }), + VAR(PreProcessorLexerTag { env, _, subNodes -> + val varName = env.processTree(subNodes).treeToText() + env.context[varName] + }), + ENV(PreProcessorVariableInvoker), + SET(PreProcessorLexerTag { env, param, subNodes -> + param?.let { varName -> + env.context[varName] = env.processTree(subNodes) + } + + emptyList() + }), + INDEX(PreProcessorLexerTag { env, param, subNodes -> + val inputList = env.processTree(subNodes).asPreProcessorList() + + (param?.toIntOrNull() ?: param?.let { + env.processTree(env.context[param]).treeToNumberOrNull(String::toIntOrNull) + })?.let { index -> + inputList.getOrNull(index)?.let { env.processTree(it) } + }.orEmpty() + }), + MEMBER(PreProcessorLexerTag { env, param, subNodes -> + PreProcessorUtils.indexTree(env.processTree(subNodes), param?.split('.').orEmpty()) + }), + FOREACH(PreProcessorLexerTag { env, param, subNodes -> + param?.let { itemVar -> + val subTags = subNodes.filterIsInstance() + val list = subTags.singleOrNull { it isTag "in" }?.subNodes + ?.let { env.processTree(it) } + ?.asPreProcessorList() + + val body = subTags.singleOrNull { it isTag "do" }?.subNodes + if (list != null && body != null) + list.mapSuspend { item -> + PreProcessorUtils.processWithContext(env, env.context + mapOf(itemVar to item), body) + }.flatten() + else null + }.orEmpty() + }), + MAP(PreProcessorLexerTag { env, param, subNodes -> + param?.let { itemVar -> + val subTags = subNodes.filterIsInstance() + val list = subTags.singleOrNull { it isTag "in" }?.subNodes + ?.let { env.processTree(it) } + ?.asPreProcessorList() + + val body = subTags.singleOrNull { it isTag "do" }?.subNodes + if (list != null && body != null) + list.mapSuspend { item -> + ParserTreeNode.Tag("item", null, PreProcessorUtils.processWithContext(env, env.context + mapOf(itemVar to item), body)) + } + else null + }.orEmpty() + }), + IF(PreProcessorLexerTag { env, param, subNodes -> + param?.let { boolVar -> + if (env.context[boolVar].treeToBooleanOrNull() == true) + env.processTree(subNodes) + else null + }.orEmpty() + }), + UNLESS(PreProcessorLexerTag { env, param, subNodes -> + param?.let { boolVar -> + if (env.context[boolVar].treeToBooleanOrNull() == false) + env.processTree(subNodes) + else null + }.orEmpty() + }), + MATH(PreProcessorMathOperators), + MATH_TEST(PreProcessorMathPredicate), + LOGIC(PreProcessorLogicOperator), + JSON_PARSE(PreProcessorLexerTag { _, _, subNodes -> + JsonStorageCodec.parseToJsonElement(subNodes.treeToText()).toPreProcessTree() + }), + JSON_STRINGIFY(PreProcessorLexerTag { env, _, subNodes -> + env.processTree(subNodes).toPreProcessJson().toString().textToTree() + }), +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt new file mode 100644 index 0000000..cee5a70 --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt @@ -0,0 +1,5 @@ +package info.mechyrdia.lore + +object PreProcessorScriptLoader { + +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt new file mode 100644 index 0000000..106c187 --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt @@ -0,0 +1,61 @@ +package info.mechyrdia.lore + +import kotlinx.serialization.json.* + +fun JsonElement.toPreProcessTree(): ParserTree = when (this) { + JsonNull -> emptyList() + + is JsonPrimitive -> if (isString) + ParserState.parseText(content) + else listOf(ParserTreeNode.Text(content)) + + is JsonArray -> map { + ParserTreeNode.Tag("item", null, it.toPreProcessTree()) + } + + is JsonObject -> map { + ParserTreeNode.Tag("arg", it.key, it.value.toPreProcessTree()) + } +} + +fun ParserTreeNode.unparse(): String = when (this) { + is ParserTreeNode.Text -> text + ParserTreeNode.LineBreak -> "\n\n" + is ParserTreeNode.Tag -> buildString { + append("[") + append(tag) + param?.let { + append("=") + append(it) + } + append("]") + + append(subNodes.unparse()) + + append("[/") + append(tag) + append("]") + } +} + +fun ParserTree.unparse() = joinToString(separator = "") { it.unparse() } + +fun ParserTree.toPreProcessJson(): JsonElement { + val noBlanks = filterNot { it.isWhitespace() } + return if (noBlanks.all { it is ParserTreeNode.Tag && it isTag "item" && it.param == null }) + JsonArray(asPreProcessorList().map { it.toPreProcessJson() }) + else if (noBlanks.all { it is ParserTreeNode.Tag && it isTag "arg" && it.param != null }) + JsonObject(asPreProcessorMap().mapValues { (_, it) -> it.toPreProcessJson() }) + else if (noBlanks.size == 1) + when (val node = noBlanks.single()) { + is ParserTreeNode.Text -> JsonPrimitive(node.text) + ParserTreeNode.LineBreak -> JsonPrimitive("\n\n") + is ParserTreeNode.Tag -> if (node isTag "val" && node.param == null) { + val value = node.subNodes.treeToText() + value.toBooleanStrictOrNull()?.let { JsonPrimitive(it) } + ?: value.toDoubleOrNull()?.let { JsonPrimitive(it) } + ?: JsonPrimitive(value) + } else JsonPrimitive(node.unparse()) + } + else JsonPrimitive(unparse()) +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt new file mode 100644 index 0000000..4aeb0c6 --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt @@ -0,0 +1,157 @@ +package info.mechyrdia.lore + +import kotlin.math.* + +fun ParserTree.treeToNumberOrNull(convert: String.() -> T?) = treeToText().convert() + +fun ParserTree.treeToBooleanOrNull() = when (treeToText().lowercase()) { + "true" -> true + "false" -> false + else -> null +} + +fun Number.numberToTree(): ParserTree = listOf(ParserTreeNode.Tag("val", null, "%f".format(toDouble()).textToTree())) + +fun Boolean.booleanToTree(): ParserTree = listOf(ParserTreeNode.Tag("val", null, toString().textToTree())) + +object PreProcessorMathOperators : PreProcessorFunctionLibrary() { + override val functions: Map = mapOf( + "neg" to PreProcessorMathUnaryOperator(Double::unaryMinus), + "sin" to PreProcessorMathUnaryOperator(::sin), + "cos" to PreProcessorMathUnaryOperator(::cos), + "tan" to PreProcessorMathUnaryOperator(::tan), + "asin" to PreProcessorMathUnaryOperator(::asin), + "acos" to PreProcessorMathUnaryOperator(::acos), + "atan" to PreProcessorMathUnaryOperator(::atan), + "sqrt" to PreProcessorMathUnaryOperator(::sqrt), + "cbrt" to PreProcessorMathUnaryOperator(::cbrt), + "ceil" to PreProcessorMathUnaryOperator(::ceil), + "floor" to PreProcessorMathUnaryOperator(::floor), + "trunc" to PreProcessorMathUnaryOperator(::truncate), + "round" to PreProcessorMathUnaryOperator(::round), + + "add" to PreProcessorMathBinaryOperator(Double::plus), + "sub" to PreProcessorMathBinaryOperator(Double::minus), + "mul" to PreProcessorMathBinaryOperator(Double::times), + "div" to PreProcessorMathBinaryOperator(Double::div), + "mod" to PreProcessorMathBinaryOperator(Double::mod), + "pow" to PreProcessorMathBinaryOperator(Double::pow), + "log" to PreProcessorMathBinaryOperator(::log), + "min" to PreProcessorMathBinaryOperator(::min), + "max" to PreProcessorMathBinaryOperator(::max), + "hypot" to PreProcessorMathBinaryOperator(::hypot), + "atan2" to PreProcessorMathBinaryOperator(::atan2), + + "min" to PreProcessorMathVariadicOperator(List::min), + "max" to PreProcessorMathVariadicOperator(List::max), + "sum" to PreProcessorMathVariadicOperator(List::sum), + "prod" to PreProcessorMathVariadicOperator { it.fold(1.0, Double::times) }, + "mean" to PreProcessorMathVariadicOperator { it.sum() / it.size.coerceAtLeast(1) }, + ) +} + +fun interface PreProcessorMathUnaryOperator : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + val input = env.processTree(env.context["in"]).treeToNumberOrNull(String::toDoubleOrNull) ?: 0.0 + + return calculate(input).numberToTree() + } + + fun calculate(input: Double): Double +} + +fun interface PreProcessorMathBinaryOperator : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + val left = env.processTree(env.context["left"]).treeToNumberOrNull(String::toDoubleOrNull) ?: 0.0 + val right = env.processTree(env.context["right"]).treeToNumberOrNull(String::toDoubleOrNull) ?: 0.0 + + return calculate(left, right).numberToTree() + } + + fun calculate(left: Double, right: Double): Double +} + +fun interface PreProcessorMathVariadicOperator : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + val args = env.processTree(env.context["in"]).asPreProcessorList().mapNotNull { it.treeToNumberOrNull(String::toDoubleOrNull) } + + return calculate(args).numberToTree() + } + + fun calculate(args: List): Double +} + +fun interface PreProcessorMathPredicate : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + val left = env.processTree(env.context["left"]).treeToNumberOrNull(String::toDoubleOrNull) ?: 0.0 + val right = env.processTree(env.context["right"]).treeToNumberOrNull(String::toDoubleOrNull) ?: 0.0 + + return calculate(left, right).booleanToTree() + } + + fun calculate(left: Double, right: Double): Boolean + + companion object : PreProcessorFunctionLibrary() { + override val functions: Map = mapOf( + "eq" to PreProcessorMathPredicate { left, right -> left == right }, + "lt" to PreProcessorMathPredicate { left, right -> left < right }, + "gt" to PreProcessorMathPredicate { left, right -> left > right }, + "ne" to PreProcessorMathPredicate { left, right -> left != right }, + "le" to PreProcessorMathPredicate { left, right -> left <= right }, + "ge" to PreProcessorMathPredicate { left, right -> left >= right }, + ) + } +} + +fun interface PreProcessorLogicBinaryOperator : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + val left = env.processTree(env.context["left"]).treeToBooleanOrNull() == true + val right = env.processTree(env.context["right"]).treeToBooleanOrNull() == true + + return calculate(left, right).booleanToTree() + } + + fun calculate(left: Boolean, right: Boolean): Boolean +} + +fun interface PreProcessorLogicOperator : PreProcessorFunction { + override suspend fun execute(env: AsyncLexerTagEnvironment): ParserTree { + val input = env.processTree(env.context["in"]).asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() } + + return calculate(input).booleanToTree() + } + + fun calculate(inputs: List): Boolean + + companion object : PreProcessorFunctionLibrary() { + override val functions: Map = mapOf( + "not" to PreProcessorFunction { env -> + env.processTree(env.context["in"]) + .treeToBooleanOrNull() + ?.let { "${!it}".textToTree() } + ?: emptyList() + }, + + "and" to PreProcessorLogicBinaryOperator { left, right -> left && right }, + "or" to PreProcessorLogicBinaryOperator { left, right -> left || right }, + "xor" to PreProcessorLogicBinaryOperator { left, right -> left xor right }, + "nand" to PreProcessorLogicBinaryOperator { left, right -> !(left && right) }, + "nor" to PreProcessorLogicBinaryOperator { left, right -> !(left || right) }, + "xnor" to PreProcessorLogicBinaryOperator { left, right -> !(left xor right) }, + "implies" to PreProcessorLogicBinaryOperator { left, right -> left || !right }, + + "all" to PreProcessorLogicOperator { inputs -> inputs.all { it } }, + "any" to PreProcessorLogicOperator { inputs -> inputs.any { it } }, + "notAll" to PreProcessorLogicOperator { inputs -> inputs.any { !it } }, + "none" to PreProcessorLogicOperator { inputs -> inputs.none { it } }, + "count" to PreProcessorFunction { env -> + env.processTree(env.context["in"]) + .asPreProcessorList() + .mapNotNull { it.treeToBooleanOrNull() } + .count { it } + .toString() + .textToTree() + }, + ) + } +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt index f868933..a65f575 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt @@ -12,6 +12,12 @@ sealed class ParserTreeNode { data class Tag(val tag: String, val param: String?, val subNodes: ParserTree) : ParserTreeNode() } +infix fun ParserTreeNode.Tag.isTag(test: String) = tag.equals(test, ignoreCase = true) +infix fun ParserTreeNode.Tag.isTag(test: Collection) = test.any { tag.equals(it, ignoreCase = true) } + +infix fun ParserTreeNode.Tag.isNotTag(test: String) = !tag.equals(test, ignoreCase = true) +infix fun ParserTreeNode.Tag.isNotTag(test: Collection) = test.none { tag.equals(it, ignoreCase = true) } + typealias ParserTree = List sealed class ParserTreeBuilderState { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt index 07860ed..e3d210e 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt @@ -296,11 +296,9 @@ object PebbleScriptLoader { val script = scriptFile.readText() val digest = hasher.get().digest(script.toByteArray()).joinToString(separator = "") { it.toUByte().toString(16) } - cache[digest]?.let { return it } - - val compiledScript = (scriptEngine.get() as Compilable).compile(script) - cache[digest] = compiledScript - return compiledScript + return cache.computeIfAbsent(digest) { + (scriptEngine.get() as Compilable).compile(script) + } } private fun runScript(scriptName: String, script: CompiledScript, input: Any?, args: MutableMap, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? { -- 2.25.1