Improve error-handling in pre-processing
authorLanius Trolling <lanius@laniustrolling.dev>
Mon, 8 Apr 2024 20:24:33 +0000 (16:24 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Mon, 8 Apr 2024 20:24:33 +0000 (16:24 -0400)
.gitignore
src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt
src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt
src/jvmMain/resources/static/raw.css

index fe9a04dcf2d4bf83a0f773970e1c9e0b4d55f1cb..6e84251219a51841f99dc0b505d2c339081d6353 100644 (file)
@@ -44,5 +44,6 @@ bin/
 # Webapp specific
 logs/
 test/
+test-*/
 config.json
 font-src/
index ae605bbf415cb85532e24b20a7e58cee9ba66419..1a4a54e64d97f99ca0db5c9b0b44ed65d333b3f5 100644 (file)
@@ -266,7 +266,7 @@ class HtmlTagLexerTag(
        val tagMode: HtmlTagMode = HtmlTagMode.BLOCK,
        val tagCreator: TagCreator
 ) : HtmlLexerTag {
-       constructor(attributes: Map<String, String>, tagMode: HtmlTagMode, tagCreator: TagCreator) : this({ attributes }, tagMode, tagCreator)
+       constructor(attributes: Map<String, String>, tagMode: HtmlTagMode = HtmlTagMode.BLOCK, tagCreator: TagCreator) : this({ attributes }, tagMode, tagCreator)
        
        override fun processTag(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>, param: String?, subNodes: ParserTree): HtmlBuilderSubject {
                val body = tagMode.combine(env, subNodes)
@@ -406,6 +406,8 @@ enum class FactbookFormattingTag(val type: HtmlLexerTag) {
        })),
        BLOCKQUOTE(HtmlTagLexerTag(tagCreator = TagConsumer<*>::blockQuote.toTagCreator())),
        
+       ERROR(HtmlTagLexerTag(attributes = mapOf("style" to "color: #f00"), tagCreator = TagConsumer<*>::div.toTagCreator())),
+       
        H1(HtmlHeaderLexerTag(tagCreator = TagConsumer<*>::h1.toTagCreator()) { null }),
        H2(HtmlHeaderLexerTag(tagCreator = TagConsumer<*>::h2.toTagCreator(), ParserTree::treeToAnchorText)),
        H3(HtmlHeaderLexerTag(tagCreator = TagConsumer<*>::h3.toTagCreator(), ParserTree::treeToAnchorText)),
@@ -476,6 +478,18 @@ enum class FactbookFormattingTag(val type: HtmlLexerTag) {
        TD(HtmlTagLexerTag(attributes = ::processTableCell, tagMode = HtmlTagMode.ITEM, tagCreator = TagConsumer<*>::td.toTagCreator())),
        TH(HtmlTagLexerTag(attributes = ::processTableCell, tagMode = HtmlTagMode.ITEM, tagCreator = TagConsumer<*>::th.toTagCreator())),
        
+       MOMENT(HtmlTextBodyLexerTag { _, _, content ->
+               val epochMilli = content.toLongOrNull()
+               if (epochMilli == null)
+                       ({ +content })
+               else
+                       ({
+                               span(classes = "moment") {
+                                       style = "display:none"
+                                       +"$epochMilli"
+                               }
+                       })
+       }),
        LINK(HtmlTagLexerTag(attributes = ::processInternalLink, tagMode = HtmlTagMode.INLINE, tagCreator = TagConsumer<*>::a.toTagCreator())),
        EXTLINK(HtmlTagLexerTag(attributes = ::processExternalLink, tagMode = HtmlTagMode.INLINE, tagCreator = TagConsumer<*>::a.toTagCreator())),
        ANCHOR(HtmlTextBodyLexerTag { _, _, content ->
@@ -655,6 +669,8 @@ enum class CommentFormattingTag(val type: HtmlLexerTag) {
        CODE(FactbookFormattingTag.CODE.type),
        CODE_BLOCK(FactbookFormattingTag.CODE_BLOCK.type),
        
+       ERROR(HtmlTagLexerTag(attributes = mapOf("style" to "color: #f00"), tagCreator = TagConsumer<*>::div.toTagCreator())),
+       
        ALIGN(FactbookFormattingTag.ALIGN.type),
        ASIDE(FactbookFormattingTag.ASIDE.type),
        
@@ -696,6 +712,7 @@ enum class CommentFormattingTag(val type: HtmlLexerTag) {
        }),
        
        QUOTE(FactbookFormattingTag.BLOCKQUOTE.type),
+       EPOCH(FactbookFormattingTag.MOMENT.type),
        ;
        
        companion object {
index 91ed9bbb149dc702e779be89e6c2b091728d835b..b11ec6120cfc19b2e259d9c9ed69fce9da466d9a 100644 (file)
@@ -7,15 +7,15 @@ import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
 import java.time.Instant
+import kotlin.math.roundToInt
 
 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 get(name: String): ParserTree = variables[name] ?: parent?.get(name) ?: formatErrorToParserTree("Unable to resolve variable $name")
        
        operator fun set(name: String, value: ParserTree) {
                if (parent != null && name in parent)
@@ -92,49 +92,75 @@ object PreProcessorUtils : AsyncLexerTagFallback<PreProcessingContext, PreProces
        fun indexTree(tree: ParserTree, index: List<String>): ParserTree {
                if (index.isEmpty()) return tree
                val tags = tree.filterIsInstance<ParserTreeNode.Tag>()
-               if (tags.isEmpty()) return emptyList()
+               if (tags.isEmpty()) return formatErrorToParserTree("Cannot index into empty input value")
                
                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 ->
+                       head.toDoubleOrNull()?.roundToInt()?.let { listIndex ->
                                tree.asPreProcessorList().getOrNull(listIndex)
-                       }?.let { indexTree(it, tail) }.orEmpty()
+                       }?.let { indexTree(it, tail) }.formatError("Index $head is not present in input value")
                } else if (firstTag isTag "arg" && firstTag.param != null) {
-                       tree.asPreProcessorMap()[head]?.let { indexTree(it, tail) }.orEmpty()
-               } else emptyList()
+                       tree.asPreProcessorMap()[head]?.let { indexTree(it, tail) }.formatError("Index $head is not present in input value")
+               } else formatErrorToParserTree("Cannot index into non-collection input value")
        }
 }
 
 fun interface PreProcessorLexerTag : AsyncLexerTagProcessor<PreProcessingContext, PreProcessingSubject>
 
+inline fun <T : Any> T?.requireParam(tag: String, block: (T) -> ParserTree): ParserTree {
+       return if (this == null)
+               formatErrorToParserTree("Parameter is required for tag $tag")
+       else block(this)
+}
+
+inline fun String?.forbidParam(tag: String, block: () -> ParserTree): ParserTree {
+       return if (this != null)
+               formatErrorToParserTree("Parameter is forbidden for tag $tag")
+       else block()
+}
+
+fun formatErrorToParserTree(error: String): ParserTree {
+       return listOf(ParserTreeNode.Tag("error", null, listOf(ParserTreeNode.Text(error))))
+}
+
+fun ParserTree?.formatError(error: String): ParserTree {
+       return this ?: formatErrorToParserTree(error)
+}
+
+fun ParserTree.isNull() = all { it.isWhitespace() || (it is ParserTreeNode.Tag && it isTag "error") }
+
 fun String.textToTree(): ParserTree = listOf(ParserTreeNode.Text(this))
 
 fun interface PreProcessorFunction {
        suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): ParserTree
 }
 
-fun interface PreProcessorFunctionProvider : PreProcessorLexerTag {
+interface PreProcessorFunctionProvider : PreProcessorLexerTag {
+       val tagName: String
+       
        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))
+               return param?.let { provideFunction(it) }.requireParam(tagName) {
+                       val args = subNodes.asPreProcessorMap().mapValuesSuspend { _, value -> env.processTree(value) }
+                       val ctx = PreProcessingContext(args, env.context)
+                       
+                       val func = provideFunction(param) ?: return emptyList()
+                       func.execute(PreProcessorUtils.withContext(env, ctx))
+               }
        }
 }
 
-abstract class PreProcessorFunctionLibrary : PreProcessorFunctionProvider {
+abstract class PreProcessorFunctionLibrary(override val tagName: String) : 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() {
+               operator fun invoke(tagName: String, library: Map<String, PreProcessorFunction>) = object : PreProcessorFunctionLibrary(tagName) {
                        override val functions: Map<String, PreProcessorFunction> = library
                }
        }
@@ -148,6 +174,8 @@ value class PreProcessorVariableFunction(private val variable: String) : PreProc
 }
 
 object PreProcessorVariableInvoker : PreProcessorFunctionProvider {
+       override val tagName: String = "env"
+       
        override suspend fun provideFunction(param: String?): PreProcessorFunction? {
                return param?.let { PreProcessorVariableFunction(it) }
        }
@@ -157,22 +185,26 @@ fun interface PreProcessorFilter {
        suspend fun execute(input: ParserTree, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): ParserTree
 }
 
-fun interface PreProcessorFilterProvider : PreProcessorLexerTag {
+interface PreProcessorFilterProvider : PreProcessorLexerTag {
+       val tagName: String
+       
        suspend fun provideFilter(param: String?): PreProcessorFilter?
        
        override suspend fun processTag(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, param: String?, subNodes: ParserTree): PreProcessingSubject {
-               val filter = provideFilter(param) ?: return emptyList()
-               return filter.execute(subNodes, env)
+               return param?.let { provideFilter(it) }.requireParam(tagName) {
+                       val filter = provideFilter(param) ?: return emptyList()
+                       filter.execute(subNodes, env)
+               }
        }
 }
 
-abstract class PreProcessorFilterLibrary : PreProcessorFilterProvider {
+abstract class PreProcessorFilterLibrary(override val tagName: String) : PreProcessorFilterProvider {
        abstract val filters: Map<String, PreProcessorFilter>
        
        override suspend fun provideFilter(param: String?) = param?.let { filters[it] }
        
        companion object {
-               operator fun invoke(library: Map<String, PreProcessorFilter>) = object : PreProcessorFilterLibrary() {
+               operator fun invoke(tagName: String, library: Map<String, PreProcessorFilter>) = object : PreProcessorFilterLibrary(tagName) {
                        override val filters: Map<String, PreProcessorFilter> = library
                }
        }
@@ -210,60 +242,64 @@ suspend fun <K, V, R> Map<K, V>.mapValuesSuspend(processor: suspend (K, V) -> R)
 
 enum class PreProcessorTags(val type: PreProcessorLexerTag) {
        EVAL(PreProcessorLexerTag { env, param, subNodes ->
-               param?.toIntOrNull()?.let { times ->
+               param?.toDoubleOrNull()?.roundToInt().requireParam("eval") { times ->
                        var tree = subNodes
                        repeat(times) {
                                tree = env.processTree(tree)
                        }
                        tree
-               } ?: env.processTree(subNodes)
+               }
        }),
-       LAZY(PreProcessorLexerTag { _, _, subNodes ->
-               subNodes
+       LAZY(PreProcessorLexerTag { _, param, subNodes ->
+               param.forbidParam("lazy") { subNodes }
        }),
-       VAL(PreProcessorLexerTag { env, _, subNodes ->
-               env.processTree(subNodes).treeToText().textToTree()
+       VAL(PreProcessorLexerTag { env, param, subNodes ->
+               param.forbidParam("val") {
+                       env.processTree(subNodes).treeToText().textToTree()
+               }
        }),
-       VAR(PreProcessorLexerTag { env, _, subNodes ->
-               val varName = env.processTree(subNodes).treeToText()
-               env.context[varName]
+       VAR(PreProcessorLexerTag { env, param, subNodes ->
+               param.forbidParam("var") {
+                       env.context[env.processTree(subNodes).treeToText()]
+               }
        }),
        ENV(PreProcessorVariableInvoker),
        SET(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { varName ->
+               param.requireParam("set") { varName ->
                        env.context[varName] = env.processTree(subNodes)
+                       emptyList()
                }
-               
-               emptyList()
        }),
        SET_GLOBAL(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { varName ->
+               param.requireParam("set_global") { varName ->
                        env.context.setGlobal(varName, env.processTree(subNodes))
+                       emptyList()
                }
-               
-               emptyList()
        }),
        SET_LOCAL(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { varName ->
+               param.requireParam("set_local") { varName ->
                        env.context.setLocal(varName, env.processTree(subNodes))
+                       emptyList()
                }
-               
-               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()
+               (param?.toDoubleOrNull() ?: param?.let {
+                       env.processTree(env.context[param]).treeToNumberOrNull(String::toDoubleOrNull)
+               })?.roundToInt().requireParam("index") { index ->
+                       inputList.getOrNull(index)
+                               ?.let { env.processTree(it) }
+                               .formatError("Index $index is not present in input list")
+               }
        }),
        MEMBER(PreProcessorLexerTag { env, param, subNodes ->
-               PreProcessorUtils.indexTree(env.processTree(subNodes), param?.split('.').orEmpty())
+               param?.split('.').requireParam("member") { index ->
+                       PreProcessorUtils.indexTree(env.processTree(subNodes), index)
+               }
        }),
        FOREACH(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { itemVar ->
+               param.requireParam("foreach") { itemVar ->
                        val subTags = subNodes.filterIsInstance<ParserTreeNode.Tag>()
                        val list = subTags.singleOrNull { it isTag "in" }?.subNodes
                                ?.let { env.processTree(it) }
@@ -274,11 +310,11 @@ enum class PreProcessorTags(val type: PreProcessorLexerTag) {
                                list.mapSuspend { item ->
                                        PreProcessorUtils.processWithContext(env, env.context + mapOf(itemVar to item), body)
                                }.flatten()
-                       else null
-               }.orEmpty()
+                       else formatErrorToParserTree("Expected child tag [in] to take list input and child tag [do] to take loop body")
+               }
        }),
        MAP(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { itemVar ->
+               param.requireParam("map") { itemVar ->
                        val subTags = subNodes.filterIsInstance<ParserTreeNode.Tag>()
                        val list = subTags.singleOrNull { it isTag "in" }?.subNodes
                                ?.let { env.processTree(it) }
@@ -289,55 +325,74 @@ enum class PreProcessorTags(val type: PreProcessorLexerTag) {
                                list.mapSuspend { item ->
                                        ParserTreeNode.Tag("item", null, PreProcessorUtils.processWithContext(env, env.context + mapOf(itemVar to item), body))
                                }
-                       else null
-               }.orEmpty()
+                       else formatErrorToParserTree("Expected child tag [in] to take list input and child tag [do] to take loop body")
+               }
        }),
        IF(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { boolVar ->
-                       if (env.context[boolVar].treeToBooleanOrNull() == true)
-                               env.processTree(subNodes)
-                       else null
-               }.orEmpty()
+               param.requireParam("if") { boolVar ->
+                       env.context[boolVar].treeToBooleanOrNull()?.let {
+                               if (it) env.processTree(subNodes) else emptyList()
+                       }.formatError("Expected variable $boolVar to contain boolean value")
+               }
        }),
        UNLESS(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { boolVar ->
-                       if (env.context[boolVar].treeToBooleanOrNull() == false)
-                               env.processTree(subNodes)
-                       else null
-               }.orEmpty()
+               param.requireParam("unless") { boolVar ->
+                       env.context[boolVar].treeToBooleanOrNull()?.let {
+                               if (it) emptyList() else env.processTree(subNodes)
+                       }.formatError("Expected variable $boolVar to contain boolean value")
+               }
        }),
        MATH(PreProcessorMathOperators),
        LOGIC(PreProcessorLogicOperator),
-       TEST(PreProcessorLogicOperator),
-       JSON_PARSE(PreProcessorLexerTag { _, _, subNodes ->
-               JsonStorageCodec.parseToJsonElement(subNodes.treeToText()).toPreProcessTree()
+       TEST(PreProcessorInputTest),
+       JSON_PARSE(PreProcessorLexerTag { _, param, subNodes ->
+               param.forbidParam("json_parse") {
+                       JsonStorageCodec.parseToJsonElement(subNodes.treeToText()).toPreProcessTree()
+               }
        }),
-       JSON_STRINGIFY(PreProcessorLexerTag { env, _, subNodes ->
-               env.processTree(subNodes).toPreProcessJson().toString().textToTree()
+       JSON_STRINGIFY(PreProcessorLexerTag { env, param, subNodes ->
+               param.forbidParam("json_stringify") {
+                       env.processTree(subNodes).toPreProcessJson().toString().textToTree()
+               }
        }),
-       SCRIPT(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { scriptName ->
+       FUNCTION(PreProcessorLexerTag { env, param, subNodes ->
+               param.requireParam("function") { scriptName ->
                        PreProcessorScriptLoader.runScriptSafe(scriptName, subNodes.asPreProcessorMap(), env) {
                                it.renderInBBCode()
                        }
-               }.orEmpty()
+               }
+       }),
+       FILTER(PreProcessorLexerTag { env, param, subNodes ->
+               param.requireParam("filter") { scriptName ->
+                       PreProcessorScriptLoader.runScriptSafe(scriptName, subNodes, env) {
+                               it.renderInBBCode()
+                       }
+               }
        }),
        WITH_DATA_FILE(PreProcessorLexerTag { env, param, subNodes ->
-               param?.let { dataFileName ->
-                       val args = FactbookLoader.loadFactbookContext(dataFileName.split('/'))
-                       env.copy(context = env.context + args).processTree(subNodes)
-               }.orEmpty()
+               param.requireParam("with_data_file") { dataFileName ->
+                       try {
+                               val args = FactbookLoader.loadFactbookContext(dataFileName.split('/'))
+                               env.copy(context = env.context + args).processTree(subNodes)
+                       } catch (ex: Exception) {
+                               ex.renderInBBCode()
+                       }
+               }
        }),
        IMPORT(PreProcessorLexerTag { _, param, subNodes ->
-               param?.let { templateName ->
+               param.requireParam("import") { templateName ->
                        PreProcessorTemplateLoader.runTemplateWith(templateName, subNodes.asPreProcessorMap())
-               }.orEmpty()
+               }
        }),
-       INCLUDE(PreProcessorLexerTag { env, _, subNodes ->
-               PreProcessorTemplateLoader.runTemplateHere(env.processTree(subNodes).treeToText(), env)
+       INCLUDE(PreProcessorLexerTag { env, param, subNodes ->
+               param.forbidParam("include") {
+                       PreProcessorTemplateLoader.runTemplateHere(env.processTree(subNodes).treeToText(), env)
+               }
        }),
-       TEMPLATE(PreProcessorLexerTag { env, _, subNodes ->
-               PreProcessorTemplateLoader.loadTemplate(env.processTree(subNodes).treeToText())
+       TEMPLATE(PreProcessorLexerTag { env, param, subNodes ->
+               param.forbidParam("include") {
+                       PreProcessorTemplateLoader.loadTemplate(env.processTree(subNodes).treeToText())
+               }
        }),
        ;
        
@@ -358,13 +413,13 @@ suspend fun ParserTree.preProcess(context: Map<String, ParserTree>): ParserTree
 }
 
 fun Exception.renderInBBCode(): ParserTree = listOf(
-       ParserTreeNode.LineBreak,
-       ParserTreeNode.Tag("b", null, listOf(ParserTreeNode.Text("${this::class.qualifiedName}: $message"))),
-       ParserTreeNode.LineBreak,
-       ParserTreeNode.Tag("ul", null,
-               stackTraceToString().split(System.lineSeparator()).map {
-                       ParserTreeNode.Tag("li", null, listOf(ParserTreeNode.Text(it)))
-               }
-       ),
-       ParserTreeNode.LineBreak,
+       ParserTreeNode.Tag("error", null, listOf(
+               ParserTreeNode.Tag("b", null, listOf(ParserTreeNode.Text("${this::class.qualifiedName}: $message"))),
+               ParserTreeNode.LineBreak,
+               ParserTreeNode.Tag("ul", null,
+                       stackTraceToString().split(System.lineSeparator()).map {
+                               ParserTreeNode.Tag("li", null, listOf(ParserTreeNode.Text(it)))
+                       }
+               ),
+       )),
 )
index d4263e64ebc735563ceea122e17f4b8990284be6..2c366919ccabd490384e6263c7b064b80a869ff9 100644 (file)
@@ -3,8 +3,6 @@ package info.mechyrdia.lore
 import info.mechyrdia.JsonStorageCodec
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import info.mechyrdia.lore.PebbleJsonLoader.convertJson
-import info.mechyrdia.lore.PebbleJsonLoader.deconvertJson
 import io.github.reactivecircus.cache4k.Cache
 import io.ktor.util.*
 import kotlinx.coroutines.Dispatchers
@@ -63,8 +61,8 @@ object PreProcessorScriptLoader {
                else
                        json.booleanOrNull ?: json.intOrNull ?: json.double
                
-               is JsonObject -> json.mapValues { (_, it) -> convertJson(it) }
-               is JsonArray -> json.map { convertJson(it) }
+               is JsonObject -> json.mapValues { (_, it) -> jsonToGroovy(it) }
+               is JsonArray -> json.map { jsonToGroovy(it) }
        }
        
        fun groovyToJson(data: Any?): JsonElement = when (data) {
@@ -72,29 +70,28 @@ object PreProcessorScriptLoader {
                is String -> JsonPrimitive(data)
                is Number -> JsonPrimitive(data)
                is Boolean -> JsonPrimitive(data)
-               is List<*> -> JsonArray(data.map { deconvertJson(it) })
-               is Set<*> -> JsonArray(data.map { deconvertJson(it) })
-               is Map<*, *> -> JsonObject(data.map { (k, v) -> k.toString() to deconvertJson(v) }.toMap())
+               is List<*> -> JsonArray(data.map { groovyToJson(it) })
+               is Set<*> -> JsonArray(data.map { groovyToJson(it) })
+               is Map<*, *> -> JsonObject(data.map { (k, v) -> k.toString() to groovyToJson(v) }.toMap())
                else -> throw ClassCastException("Expected null, String, Number, Boolean, List, Set, or Map for converted data, got $data of type ${data::class.qualifiedName}")
        }
        
-       suspend fun runScriptInternal(script: CompiledScript, args: MutableMap<String, Any?>, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): Any? {
+       suspend fun runScriptInternal(script: CompiledScript, bind: Map<String, Any?>, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): Any? {
                return suspendCancellableCoroutine { continuation ->
                        val bindings = SimpleBindings()
+                       bindings.putAll(bind)
                        bindings["stdlib"] = PreProcessorScriptStdlib(env, continuation.context, continuation::resumeWithException)
                        bindings["ctx"] = PreProcessorScriptVarContext { jsonToGroovy(env.context[it].toPreProcessJson()) }
-                       bindings["args"] = args
                        bindings["finish"] = Consumer<Any?>(continuation::resume)
                        
                        script.eval(bindings)
                }
        }
        
-       suspend fun runScriptSafe(scriptName: String, args: Map<String, ParserTree>, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
+       private suspend fun runScriptSafe(scriptName: String, bind: Map<String, Any?>, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
                return try {
                        val script = loadFunction(scriptName)!!
-                       val internalArgs = args.mapValuesTo(mutableMapOf()) { (_, it) -> jsonToGroovy(it.toPreProcessJson()) }
-                       val result = runScriptInternal(script, internalArgs, env)
+                       val result = runScriptInternal(script, bind, env)
                        return if (result is String)
                                ParserState.parseText(result)
                        else
@@ -103,6 +100,15 @@ object PreProcessorScriptLoader {
                        errorHandler(ex)
                }
        }
+       
+       suspend fun runScriptSafe(scriptName: String, args: Map<String, ParserTree>, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
+               val groovyArgs = args.mapValuesTo(mutableMapOf()) { (_, it) -> jsonToGroovy(it.toPreProcessJson()) }
+               return runScriptSafe(scriptName, mapOf("args" to groovyArgs), env, errorHandler)
+       }
+       
+       suspend fun runScriptSafe(scriptName: String, input: ParserTree, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
+               return runScriptSafe(scriptName, mapOf("text" to input.unparse()), env, errorHandler)
+       }
 }
 
 fun interface PreProcessorScriptVarContext {
@@ -118,8 +124,7 @@ class PreProcessorScriptStdlib(private val env: AsyncLexerTagEnvironment<PreProc
                return PreProcessorScriptLoader.jsonToGroovy(JsonStorageCodec.parseToJsonElement(json))
        }
        
-       @JvmOverloads
-       fun loadScript(scriptName: String, args: Map<String, Any?> = emptyMap(), useResult: Consumer<Any?>) {
+       fun runScript(scriptName: String, args: Map<String, Any?>, useResult: Consumer<Any?>) {
                suspend {
                        val script = PreProcessorScriptLoader.loadFunction(scriptName)!!
                        val argsMutable = if (args is MutableMap) args else args.toMutableMap()
@@ -129,4 +134,14 @@ class PreProcessorScriptStdlib(private val env: AsyncLexerTagEnvironment<PreProc
                        result.onFailure(onError)
                })
        }
+       
+       fun runScript(scriptName: String, useResult: Consumer<Any?>) {
+               suspend {
+                       val script = PreProcessorScriptLoader.loadFunction(scriptName)!!
+                       PreProcessorScriptLoader.runScriptInternal(script, mutableMapOf(), env)
+               }.startCoroutine(Continuation(context) { result ->
+                       result.onSuccess(useResult::accept)
+                       result.onFailure(onError)
+               })
+       }
 }
index 2b269e9d772b0441a929867ab6ce6993fab4ac0c..c7b8c73b9dae807dbdb766249306f348d584a673 100644 (file)
@@ -1,5 +1,6 @@
 package info.mechyrdia.lore
 
+import java.time.Instant
 import kotlin.math.*
 
 fun <T : Number> ParserTree.treeToNumberOrNull(convert: String.() -> T?) = treeToText().convert()
@@ -14,7 +15,7 @@ fun Number.numberToTree(): ParserTree = listOf(ParserTreeNode.Tag("val", null, "
 
 fun Boolean.booleanToTree(): ParserTree = listOf(ParserTreeNode.Tag("val", null, toString().textToTree()))
 
-object PreProcessorMathOperators : PreProcessorFunctionLibrary() {
+object PreProcessorMathOperators : PreProcessorFunctionLibrary("math") {
        override val functions: Map<String, PreProcessorFunction> = mapOf(
                "neg" to PreProcessorMathUnaryOperator(Double::unaryMinus),
                "sin" to PreProcessorMathUnaryOperator(::sin),
@@ -59,9 +60,12 @@ object PreProcessorMathOperators : PreProcessorFunctionLibrary() {
 
 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
+               val input = env.processTree(env.context["in"])
                
-               return calculate(input).numberToTree()
+               return input.treeToNumberOrNull(String::toDoubleOrNull)
+                       ?.let { calculate(it) }
+                       ?.numberToTree()
+                       .formatError("Math operations require numerical inputs, got ${input.unparse()}")
        }
        
        fun calculate(input: Double): Double
@@ -69,8 +73,14 @@ fun interface PreProcessorMathUnaryOperator : PreProcessorFunction {
 
 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
+               val leftValue = env.processTree(env.context["left"])
+               val rightValue = env.processTree(env.context["right"])
+               
+               val left = leftValue.treeToNumberOrNull(String::toDoubleOrNull)
+               val right = rightValue.treeToNumberOrNull(String::toDoubleOrNull)
+               
+               if (left == null || right == null)
+                       return formatErrorToParserTree("Math operations require numerical inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
                
                return calculate(left, right).numberToTree()
        }
@@ -80,7 +90,11 @@ fun interface PreProcessorMathBinaryOperator : PreProcessorFunction {
 
 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) }
+               val argsList = env.processTree(env.context["in"])
+               val args = argsList.asPreProcessorList().mapNotNull { it.treeToNumberOrNull(String::toDoubleOrNull) }
+               
+               if (args.isEmpty() && argsList.isNotEmpty())
+                       return formatErrorToParserTree("Math operations require numerical inputs, got ${argsList.unparse()}")
                
                return calculate(args).numberToTree()
        }
@@ -90,8 +104,14 @@ fun interface PreProcessorMathVariadicOperator : PreProcessorFunction {
 
 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
+               val leftValue = env.processTree(env.context["left"])
+               val rightValue = env.processTree(env.context["right"])
+               
+               val left = leftValue.treeToNumberOrNull(String::toDoubleOrNull)
+               val right = rightValue.treeToNumberOrNull(String::toDoubleOrNull)
+               
+               if (left == null || right == null)
+                       return formatErrorToParserTree("Math operations require numerical inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
                
                return calculate(left, right).booleanToTree()
        }
@@ -101,8 +121,14 @@ fun interface PreProcessorMathPredicate : PreProcessorFunction {
 
 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
+               val leftValue = env.processTree(env.context["left"])
+               val rightValue = env.processTree(env.context["right"])
+               
+               val left = leftValue.treeToBooleanOrNull()
+               val right = rightValue.treeToBooleanOrNull()
+               
+               if (left == null || right == null)
+                       return formatErrorToParserTree("Logical operations require boolean inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
                
                return calculate(left, right).booleanToTree()
        }
@@ -112,20 +138,26 @@ fun interface PreProcessorLogicBinaryOperator : PreProcessorFunction {
 
 fun interface PreProcessorLogicOperator : PreProcessorFunction {
        override suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): ParserTree {
-               val input = env.processTree(env.context["in"]).asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
+               val argsList = env.processTree(env.context["in"])
+               val args = argsList.asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
                
-               return calculate(input).booleanToTree()
+               if (args.isEmpty() && argsList.isNotEmpty())
+                       return formatErrorToParserTree("Logical operations require boolean inputs, got ${argsList.unparse()}")
+               
+               return calculate(args).booleanToTree()
        }
        
        fun calculate(inputs: List<Boolean>): Boolean
        
-       companion object : PreProcessorFunctionLibrary() {
+       companion object : PreProcessorFunctionLibrary("logic") {
                override val functions: Map<String, PreProcessorFunction> = mapOf(
                        "not" to PreProcessorFunction { env ->
-                               env.processTree(env.context["in"])
+                               val input = env.processTree(env.context["in"])
+                               
+                               input
                                        .treeToBooleanOrNull()
                                        ?.let { "${!it}".textToTree() }
-                                       .orEmpty()
+                                       .formatError("Logical operations require boolean inputs, got ${input.unparse()}")
                        },
                        
                        "and" to PreProcessorLogicBinaryOperator { left, right -> left && right },
@@ -138,15 +170,41 @@ fun interface PreProcessorLogicOperator : PreProcessorFunction {
                        
                        "all" to PreProcessorLogicOperator { inputs -> inputs.all { it } },
                        "any" to PreProcessorLogicOperator { inputs -> inputs.any { it } },
-                       "notAll" to PreProcessorLogicOperator { inputs -> inputs.any { !it } },
+                       "not_all" 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()
+                               val argsList = env.processTree(env.context["in"])
+                               val args = argsList.asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
+                               
+                               if (args.isEmpty() && argsList.isNotEmpty())
+                                       formatErrorToParserTree("Logical operations require boolean inputs, got ${argsList.unparse()}")
+                               else
+                                       args.count { it }.numberToTree()
+                       },
+               )
+       }
+}
+
+fun interface PreProcessorFormatter : PreProcessorFilter {
+       override suspend fun execute(input: ParserTree, env: AsyncLexerTagEnvironment<PreProcessingContext, PreProcessingSubject>): ParserTree {
+               return calculate(input.treeToText())
+       }
+       
+       fun calculate(input: String): ParserTree
+       
+       companion object : PreProcessorFilterLibrary("format") {
+               override val filters: Map<String, PreProcessorFilter> = mapOf(
+                       "iso_instant" to PreProcessorFormatter {
+                               it.toLongOrNull()
+                                       ?.let { long ->
+                                               Instant.ofEpochMilli(long).toString().textToTree()
+                                       }.formatError("ISO Instant values must be formatted as base-10 long values, got $it")
+                       },
+                       "local_instant" to PreProcessorFormatter {
+                               it.toLongOrNull()
+                                       ?.let { long ->
+                                               listOf(ParserTreeNode.Tag("moment", null, listOf(ParserTreeNode.Text(long.toString()))))
+                                       }.formatError("ISO Instant values must be formatted as base-10 long values, got $it")
                        },
                )
        }
@@ -159,12 +217,14 @@ fun interface PreProcessorInputTest : PreProcessorFilter {
        
        fun calculate(input: ParserTree): Boolean
        
-       companion object : PreProcessorFilterLibrary() {
+       companion object : PreProcessorFilterLibrary("test") {
                override val filters: Map<String, PreProcessorFilter> = mapOf(
+                       "null" to PreProcessorInputTest { it.isNull() },
                        "empty" to PreProcessorInputTest { it.isEmpty() },
                        "blank" to PreProcessorInputTest { it.isWhitespace() },
-                       "notempty" to PreProcessorInputTest { it.isNotEmpty() },
-                       "notblank" to PreProcessorInputTest { !it.isWhitespace() },
+                       "not_null" to PreProcessorInputTest { !it.isNull() },
+                       "not_empty" to PreProcessorInputTest { it.isNotEmpty() },
+                       "not_blank" to PreProcessorInputTest { !it.isWhitespace() },
                )
        }
 }
index 9b04cc14c45c115b2f2867940a6c37803b692dc5..0356bca2479639f74c0689aaee41e6456a9ad891 100644 (file)
@@ -1,9 +1,8 @@
 package info.mechyrdia.lore
 
-import info.mechyrdia.Configuration
 import io.ktor.util.*
 import kotlinx.html.*
-import java.io.File
+import java.time.Instant
 
 fun String.toRawLink() = substringBeforeLast('#').sanitizeLink().toInternalUrl() + "?format=raw"
 
@@ -21,7 +20,7 @@ private class HtmlDataFormatTag(val dataFormat: String) : HtmlLexerTag {
                return {
                        span {
                                attributes["data-format"] = dataFormat
-                               content()
+                               +content
                        }
                }
        }
@@ -45,7 +44,16 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                attributes["data-format"] = "code"
-                               pre { content() }
+                               pre { +content }
+                       }
+               })
+       }),
+       ERROR(HtmlLexerTag { env, _, subNodes ->
+               val content = HtmlLexerProcessor.combineInline(env, subNodes)
+               ({
+                       div {
+                               attributes["data-format"] = "error"
+                               +content
                        }
                })
        }),
@@ -63,7 +71,7 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                alignment?.let { attributes["data-align"] = it }
-                               content()
+                               +content
                        }
                })
        }),
@@ -75,7 +83,7 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                alignment?.let { attributes["data-aside"] = it }
-                               content()
+                               +content
                        }
                })
        }),
@@ -84,25 +92,23 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                val (width, height) = getSizeParam(param)
                val styleValue = getRawImageSizeStyleValue(width, height)
                
-               if (url.endsWith(".svg")) {
-                       ({
-                               iframe {
-                                       src = "/assets/images/$url"
-                                       style = styleValue
-                               }
-                       })
-               } else {
-                       ({
-                               img(src = "/assets/images/$url") {
-                                       width?.let { attributes["data-width"] = "$it" }
-                                       height?.let { attributes["data-height"] = "$it" }
-                                       style = styleValue
-                               }
-                       })
-               }
+               ({
+                       img(src = "/assets/images/$url") {
+                               width?.let { attributes["data-width"] = "$it" }
+                               height?.let { attributes["data-height"] = "$it" }
+                               style = styleValue
+                       }
+               })
        }),
        MODEL(HtmlNotSupportedInRawViewTag("Unfortunately, raw view does not support interactive 3D model views")),
        QUIZ(HtmlNotSupportedInRawViewTag("Unfortunately, raw view does not support interactive quizzes")),
+       MOMENT(HtmlTextBodyLexerTag { _, _, content ->
+               val epochMilli = content.toLongOrNull()
+               if (epochMilli == null)
+                       ({ +content })
+               else
+                       ({ +Instant.ofEpochMilli(epochMilli).toString() })
+       }),
        LINK(HtmlTagLexerTag(attributes = ::processRawInternalLink, tagMode = HtmlTagMode.INLINE, tagCreator = TagConsumer<*>::a.toTagCreator())),
        REDIRECT(HtmlTextBodyLexerTag { _, _, content ->
                val url = content.toRawLink()
@@ -134,4 +140,3 @@ fun ParserTree.toRawHtml(): TagConsumer<*>.() -> Any? {
 }
 
 fun getRawImageSizeStyleValue(width: Int?, height: Int?) = width?.let { "width:${it * 0.25}px;" }.orEmpty() + height?.let { "height:${it * 0.25}px;" }.orEmpty()
-fun getRawImageSizeAttributes(width: Int?, height: Int?) = width?.let { " data-width=\"$it\"" }.orEmpty() + height?.let { " data-height=\"$it\"" }.orEmpty() + " style=\"${getRawImageSizeStyleValue(width, height)}\""
index 4405fdada8ab833e4ea1d6762de4b7ec02414e57..98422c03158a3fd317ba101465ca3281d9e048e7 100644 (file)
@@ -47,6 +47,10 @@ th {
        font-style: italic;
 }
 
+[data-format=error] {
+       color: #f00;
+}
+
 [data-align=left] {
        text-align: left;
 }