Begin moving away from Pebble templates
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 7 Apr 2024 22:05:14 +0000 (18:05 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 7 Apr 2024 22:05:14 +0000 (18:05 -0400)
src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt [new file with mode: 0644]
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt [new file with mode: 0644]
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt [new file with mode: 0644]
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt [new file with mode: 0644]
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt [new file with mode: 0644]
src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt
src/jvmMain/kotlin/info/mechyrdia/lore/preparser_config.kt

index 263a71583aa5ffe3bc7ff797bd13214debbcaf2a..566dba7d92551393abf67c097be14929004fcde4 100644 (file)
@@ -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 <reified T : Any> redirectHref(resource: T, permanent: Boolean = false, hash: String? = null): Nothing = redirect(href(resource, hash), permanent)
+inline fun <reified T : Any> ApplicationCall.redirectHref(resource: T, permanent: Boolean = false, hash: String? = null): Nothing = redirect(href(resource, hash), permanent)
index 7383dfab28c09b47885a93be91804bdb5160ed0a..478612feb7b205e793be9e8a9eff218db641f025 100644 (file)
@@ -79,11 +79,11 @@ operator fun <T, C : TagConsumer<T>> Entities.unaryPlus() = onTagContentEntity(t
 fun <T, C : TagConsumer<T>> 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<ParserTree> = 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<String>): 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<String>): 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<HtmlBuilderContext, HtmlBuilderSubject>, 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 (file)
index 0000000..6889efc
--- /dev/null
@@ -0,0 +1,68 @@
+package info.mechyrdia.lore
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+data class AsyncLexerTagEnvironment<TContext, TSubject>(
+       val context: TContext,
+       private val processTags: AsyncLexerTags<TContext, TSubject>,
+       private val processText: AsyncLexerTextProcessor<TContext, TSubject>,
+       private val processBreak: AsyncLexerLineBreakProcessor<TContext, TSubject>,
+       private val processInvalidTag: AsyncLexerTagFallback<TContext, TSubject>,
+       private val combiner: AsyncLexerCombiner<TContext, TSubject>
+) {
+       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<TContext, TSubject> private constructor(private val tags: Map<String, AsyncLexerTagProcessor<TContext, TSubject>>) {
+       operator fun get(name: String) = tags[name.lowercase()]
+       
+       operator fun plus(other: AsyncLexerTags<TContext, TSubject>) = AsyncLexerTags(tags + other.tags)
+       
+       companion object {
+               fun <TContext, TSubject> empty() = AsyncLexerTags<TContext, TSubject>(emptyMap())
+               
+               operator fun <TContext, TSubject> invoke(tags: Map<String, AsyncLexerTagProcessor<TContext, TSubject>>) = AsyncLexerTags(tags.mapKeys { (name, _) -> name.lowercase() })
+       }
+}
+
+fun interface AsyncLexerTagProcessor<TContext, TSubject> {
+       suspend fun processTag(env: AsyncLexerTagEnvironment<TContext, TSubject>, param: String?, subNodes: ParserTree): TSubject
+}
+
+fun interface AsyncLexerTagFallback<TContext, TSubject> {
+       suspend fun processInvalidTag(env: AsyncLexerTagEnvironment<TContext, TSubject>, tag: String, param: String?, subNodes: ParserTree): TSubject
+}
+
+fun interface AsyncLexerTextProcessor<TContext, TSubject> {
+       suspend fun processText(env: AsyncLexerTagEnvironment<TContext, TSubject>, text: String): TSubject
+}
+
+fun interface AsyncLexerLineBreakProcessor<TContext, TSubject> {
+       suspend fun processLineBreak(env: AsyncLexerTagEnvironment<TContext, TSubject>): TSubject
+}
+
+fun interface AsyncLexerCombiner<TContext, TSubject> {
+       suspend fun processAndCombine(env: AsyncLexerTagEnvironment<TContext, TSubject>, nodes: ParserTree): TSubject {
+               return combine(env, coroutineScope {
+                       nodes.map {
+                               async { env.processNode(it) }
+                       }.awaitAll()
+               })
+       }
+       
+       suspend fun combine(env: AsyncLexerTagEnvironment<TContext, TSubject>, subjects: List<TSubject>): 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 (file)
index 0000000..598aedc
--- /dev/null
@@ -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<String, ParserTree>,
+       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<String, ParserTree>) : 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<String, ParserTree>) = PreProcessingContext(other.toMutableMap(), this)
+       
+       fun toMap(): Map<String, ParserTree> = parent?.toMap().orEmpty() + variables
+       
+       companion object {
+               operator fun invoke(variables: Map<String, ParserTree>, 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<PreProcessingContext, PreProcessingSubject>, AsyncLexerTextProcessor<PreProcessingContext, PreProcessingSubject>, AsyncLexerLineBreakProcessor<PreProcessingContext, PreProcessingSubject>, AsyncLexerCombiner<PreProcessingContext, PreProcessingSubject> {
+       override suspend fun processInvalidTag(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, 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<PreProcessingContext, PreProcessingSubject>, text: String): PreProcessingSubject {
+               return text.textToTree()
+       }
+       
+       override suspend fun processLineBreak(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): PreProcessingSubject {
+               return listOf(ParserTreeNode.LineBreak)
+       }
+       
+       override suspend fun combine(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, subjects: List<PreProcessingSubject>): PreProcessingSubject {
+               return subjects.flatten()
+       }
+       
+       fun withContext(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, newContext: PreProcessingContext): AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject> {
+               return env.copy(context = newContext)
+       }
+       
+       suspend fun processWithContext(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, newContext: PreProcessingContext, input: ParserTree): ParserTree {
+               return withContext(env, newContext).processTree(input)
+       }
+       
+       fun indexTree(tree: ParserTree, index: List<String>): ParserTree {
+               if (index.isEmpty()) return tree
+               val tags = tree.filterIsInstance<ParserTreeNode.Tag>()
+               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<PreProcessingContext, PreProcessingSubject>
+
+fun String.textToTree(): ParserTree = listOf(ParserTreeNode.Text(this))
+
+fun interface PreProcessorFunction {
+       suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): ParserTree
+}
+
+fun interface PreProcessorFunctionProvider : PreProcessorLexerTag {
+       suspend fun provideFunction(param: String?): PreProcessorFunction?
+       
+       override suspend fun processTag(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, 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<String, PreProcessorFunction>
+       
+       override suspend fun provideFunction(param: String?) = param?.let { functions[it] }
+       
+       companion object {
+               operator fun invoke(library: Map<String, PreProcessorFunction>) = object : PreProcessorFunctionLibrary() {
+                       override val functions: Map<String, PreProcessorFunction> = library
+               }
+       }
+}
+
+@JvmInline
+value class PreProcessorVariableFunction(private val variable: String) : PreProcessorFunction {
+       override suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): 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<ParserTree> = mapNotNull {
+       if (it !is ParserTreeNode.Tag || it isNotTag "item" || it.param != null)
+               null
+       else
+               it.subNodes
+}
+
+fun ParserTree.asPreProcessorMap(): Map<String, ParserTree> = mapNotNull {
+       if (it !is ParserTreeNode.Tag || it isNotTag "arg" || it.param == null)
+               null
+       else
+               it.param to it.subNodes
+}.toMap()
+
+suspend fun <T, R> List<T>.mapSuspend(processor: suspend (T) -> R) = coroutineScope {
+       map {
+               async {
+                       processor(it)
+               }
+       }.awaitAll()
+}
+
+suspend fun <K, V, R> Map<K, V>.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<ParserTreeNode.Tag>()
+                       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<ParserTreeNode.Tag>()
+                       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 (file)
index 0000000..cee5a70
--- /dev/null
@@ -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 (file)
index 0000000..106c187
--- /dev/null
@@ -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 (file)
index 0000000..4aeb0c6
--- /dev/null
@@ -0,0 +1,157 @@
+package info.mechyrdia.lore
+
+import kotlin.math.*
+
+fun <T : Number> 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<String, PreProcessorFunction> = 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<Double>::min),
+               "max" to PreProcessorMathVariadicOperator(List<Double>::max),
+               "sum" to PreProcessorMathVariadicOperator(List<Double>::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<PreProcessingContext, PreProcessingSubject>): 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<PreProcessingContext, PreProcessingSubject>): 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<PreProcessingContext, PreProcessingSubject>): ParserTree {
+               val args = env.processTree(env.context["in"]).asPreProcessorList().mapNotNull { it.treeToNumberOrNull(String::toDoubleOrNull) }
+               
+               return calculate(args).numberToTree()
+       }
+       
+       fun calculate(args: List<Double>): Double
+}
+
+fun interface PreProcessorMathPredicate : PreProcessorFunction {
+       override suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): 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<String, PreProcessorFunction> = 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<PreProcessingContext, PreProcessingSubject>): 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<PreProcessingContext, PreProcessingSubject>): ParserTree {
+               val input = env.processTree(env.context["in"]).asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
+               
+               return calculate(input).booleanToTree()
+       }
+       
+       fun calculate(inputs: List<Boolean>): Boolean
+       
+       companion object : PreProcessorFunctionLibrary() {
+               override val functions: Map<String, PreProcessorFunction> = 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()
+                       },
+               )
+       }
+}
index f8689337bae348fb9fd516a1122847c4cca10838..a65f57598d6bf9b67cd599440fa432e423cbe55b 100644 (file)
@@ -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<String>) = 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<String>) = test.none { tag.equals(it, ignoreCase = true) }
+
 typealias ParserTree = List<ParserTreeNode>
 
 sealed class ParserTreeBuilderState {
index 07860ed452a51b77f59e9fc370f99d51b790c035..e3d210e9b9212558ef07f6ad8a9240287b220d8b 100644 (file)
@@ -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<String, Any?>, self: PebbleTemplate, context: EvaluationContext, lineNumber: Int): Any? {