From d65fdedd4372b4c7607365cf08ffbcb941db6684 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Mon, 8 Apr 2024 16:24:33 -0400 Subject: [PATCH] Improve error-handling in pre-processing --- .gitignore | 1 + .../kotlin/info/mechyrdia/lore/parser_html.kt | 19 +- .../info/mechyrdia/lore/parser_preprocess.kt | 229 +++++++++++------- .../lore/parser_preprocess_include.kt | 43 ++-- .../mechyrdia/lore/parser_preprocess_math.kt | 110 +++++++-- .../kotlin/info/mechyrdia/lore/parser_raw.kt | 51 ++-- src/jvmMain/resources/static/raw.css | 4 + 7 files changed, 307 insertions(+), 150 deletions(-) diff --git a/.gitignore b/.gitignore index fe9a04d..6e84251 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,6 @@ bin/ # Webapp specific logs/ test/ +test-*/ config.json font-src/ diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt index ae605bb..1a4a54e 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt @@ -266,7 +266,7 @@ class HtmlTagLexerTag( val tagMode: HtmlTagMode = HtmlTagMode.BLOCK, val tagCreator: TagCreator ) : HtmlLexerTag { - constructor(attributes: Map, tagMode: HtmlTagMode, tagCreator: TagCreator) : this({ attributes }, tagMode, tagCreator) + constructor(attributes: Map, tagMode: HtmlTagMode = HtmlTagMode.BLOCK, tagCreator: TagCreator) : this({ attributes }, tagMode, tagCreator) override fun processTag(env: LexerTagEnvironment, 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 { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt index 91ed9bb..b11ec61 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt @@ -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, 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 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): ParserTree { if (index.isEmpty()) return tree val tags = tree.filterIsInstance() - 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 +inline fun 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): ParserTree } -fun interface PreProcessorFunctionProvider : PreProcessorLexerTag { +interface PreProcessorFunctionProvider : PreProcessorLexerTag { + val tagName: String + 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)) + 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 override suspend fun provideFunction(param: String?) = param?.let { functions[it] } companion object { - operator fun invoke(library: Map) = object : PreProcessorFunctionLibrary() { + operator fun invoke(tagName: String, library: Map) = object : PreProcessorFunctionLibrary(tagName) { override val functions: Map = 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): ParserTree } -fun interface PreProcessorFilterProvider : PreProcessorLexerTag { +interface PreProcessorFilterProvider : PreProcessorLexerTag { + val tagName: String + suspend fun provideFilter(param: String?): PreProcessorFilter? override suspend fun processTag(env: AsyncLexerTagEnvironment, 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 override suspend fun provideFilter(param: String?) = param?.let { filters[it] } companion object { - operator fun invoke(library: Map) = object : PreProcessorFilterLibrary() { + operator fun invoke(tagName: String, library: Map) = object : PreProcessorFilterLibrary(tagName) { override val filters: Map = library } } @@ -210,60 +242,64 @@ suspend fun Map.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() 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() 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): 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))) + } + ), + )), ) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt index d4263e6..2c36691 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt @@ -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, env: AsyncLexerTagEnvironment): Any? { + suspend fun runScriptInternal(script: CompiledScript, bind: Map, env: AsyncLexerTagEnvironment): 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(continuation::resume) script.eval(bindings) } } - suspend fun runScriptSafe(scriptName: String, args: Map, env: AsyncLexerTagEnvironment, errorHandler: (Exception) -> ParserTree): ParserTree { + private suspend fun runScriptSafe(scriptName: String, bind: Map, env: AsyncLexerTagEnvironment, 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, env: AsyncLexerTagEnvironment, 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, 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 = emptyMap(), useResult: Consumer) { + fun runScript(scriptName: String, args: Map, useResult: Consumer) { 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) { + suspend { + val script = PreProcessorScriptLoader.loadFunction(scriptName)!! + PreProcessorScriptLoader.runScriptInternal(script, mutableMapOf(), env) + }.startCoroutine(Continuation(context) { result -> + result.onSuccess(useResult::accept) + result.onFailure(onError) + }) + } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt index 2b269e9..c7b8c73 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt @@ -1,5 +1,6 @@ package info.mechyrdia.lore +import java.time.Instant import kotlin.math.* fun 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 = 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): 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): 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): 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): 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): 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): 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 - companion object : PreProcessorFunctionLibrary() { + companion object : PreProcessorFunctionLibrary("logic") { override val functions: Map = 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): ParserTree { + return calculate(input.treeToText()) + } + + fun calculate(input: String): ParserTree + + companion object : PreProcessorFilterLibrary("format") { + override val filters: Map = 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 = 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() }, ) } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt index 9b04cc1..0356bca 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt @@ -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)}\"" diff --git a/src/jvmMain/resources/static/raw.css b/src/jvmMain/resources/static/raw.css index 4405fda..98422c0 100644 --- a/src/jvmMain/resources/static/raw.css +++ b/src/jvmMain/resources/static/raw.css @@ -47,6 +47,10 @@ th { font-style: italic; } +[data-format=error] { + color: #f00; +} + [data-align=left] { text-align: left; } -- 2.25.1