# Webapp specific
logs/
test/
+test-*/
config.json
font-src/
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)
})),
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)),
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 ->
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),
}),
QUOTE(FactbookFormattingTag.BLOCKQUOTE.type),
+ EPOCH(FactbookFormattingTag.MOMENT.type),
;
companion object {
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)
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
}
}
}
object PreProcessorVariableInvoker : PreProcessorFunctionProvider {
+ override val tagName: String = "env"
+
override suspend fun provideFunction(param: String?): PreProcessorFunction? {
return param?.let { PreProcessorVariableFunction(it) }
}
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
}
}
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) }
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) }
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())
+ }
}),
;
}
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)))
+ }
+ ),
+ )),
)
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
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) {
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
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 {
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()
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)
+ })
+ }
}
package info.mechyrdia.lore
+import java.time.Instant
import kotlin.math.*
fun <T : Number> ParserTree.treeToNumberOrNull(convert: String.() -> T?) = treeToText().convert()
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),
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
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()
}
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()
}
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()
}
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()
}
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 },
"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")
},
)
}
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() },
)
}
}
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"
return {
span {
attributes["data-format"] = dataFormat
- content()
+ +content
}
}
}
({
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
}
})
}),
({
div {
alignment?.let { attributes["data-align"] = it }
- content()
+ +content
}
})
}),
({
div {
alignment?.let { attributes["data-aside"] = it }
- content()
+ +content
}
})
}),
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()
}
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)}\""
font-style: italic;
}
+[data-format=error] {
+ color: #f00;
+}
+
[data-align=left] {
text-align: left;
}