From d7d186c40e84a217e08cf86d305bcfcdfff9d667 Mon Sep 17 00:00:00 2001 From: soywiz Date: Thu, 25 Jul 2024 08:16:44 +0200 Subject: [PATCH] Initial commit --- README.md | 2 +- korlibs-simple/src/korlibs/simple/Simple.kt | 4 - .../.gitignore | 0 .../module.yaml | 8 +- .../src/korlibs/template/Korte.kt | 679 ++++++++++++++++++ .../src/korlibs/template/KorteDefaults.kt | 520 ++++++++++++++ .../src/korlibs/template/KorteExprNode.kt | 491 +++++++++++++ .../template/_Template.dynamic.common.kt | 391 ++++++++++ .../korlibs/template/_Template.internal.kt | 265 +++++++ .../src/korlibs/template/_Template.util.kt | 82 +++ .../korlibs/template/_Template.dynamic.jvm.kt | 114 +++ .../korlibs/template/_Template.dynamic.js.kt | 54 ++ .../korlibs/template/_Template.dynamic.jvm.kt | 132 ++++ .../template/_Template.dynamic.native.kt | 14 + .../template/_Template.dynamic.wasm.kt | 14 + .../test/korlibs/template/BaseTest.kt | 27 + .../template/TemplateInheritanceTest.kt | 199 +++++ .../test/korlibs/template/TemplateTest.kt | 550 ++++++++++++++ .../test/korlibs/template/suspendTest.kt | 66 ++ .../korlibs/template/TemplateJvmTest.kt | 102 +++ .../korlibs/template/MultiThreadingTests.kt | 22 + 21 files changed, 3728 insertions(+), 8 deletions(-) delete mode 100644 korlibs-simple/src/korlibs/simple/Simple.kt rename {korlibs-simple => korlibs-template}/.gitignore (100%) rename {korlibs-simple => korlibs-template}/module.yaml (57%) create mode 100644 korlibs-template/src/korlibs/template/Korte.kt create mode 100644 korlibs-template/src/korlibs/template/KorteDefaults.kt create mode 100644 korlibs-template/src/korlibs/template/KorteExprNode.kt create mode 100644 korlibs-template/src/korlibs/template/_Template.dynamic.common.kt create mode 100644 korlibs-template/src/korlibs/template/_Template.internal.kt create mode 100644 korlibs-template/src/korlibs/template/_Template.util.kt create mode 100644 korlibs-template/src@android/korlibs/template/_Template.dynamic.jvm.kt create mode 100644 korlibs-template/src@js/korlibs/template/_Template.dynamic.js.kt create mode 100644 korlibs-template/src@jvm/korlibs/template/_Template.dynamic.jvm.kt create mode 100644 korlibs-template/src@native/korlibs/template/_Template.dynamic.native.kt create mode 100644 korlibs-template/src@wasm/korlibs/template/_Template.dynamic.wasm.kt create mode 100644 korlibs-template/test/korlibs/template/BaseTest.kt create mode 100644 korlibs-template/test/korlibs/template/TemplateInheritanceTest.kt create mode 100644 korlibs-template/test/korlibs/template/TemplateTest.kt create mode 100644 korlibs-template/test/korlibs/template/suspendTest.kt create mode 100644 korlibs-template/test@jvm/korlibs/template/TemplateJvmTest.kt create mode 100644 korlibs-template/test@native/korlibs/template/MultiThreadingTests.kt diff --git a/README.md b/README.md index bca9b5c..03ed700 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# korlibs-library-template \ No newline at end of file +# korlibs-template \ No newline at end of file diff --git a/korlibs-simple/src/korlibs/simple/Simple.kt b/korlibs-simple/src/korlibs/simple/Simple.kt deleted file mode 100644 index 8349a68..0000000 --- a/korlibs-simple/src/korlibs/simple/Simple.kt +++ /dev/null @@ -1,4 +0,0 @@ -package korlibs.simple - -class Simple { -} \ No newline at end of file diff --git a/korlibs-simple/.gitignore b/korlibs-template/.gitignore similarity index 100% rename from korlibs-simple/.gitignore rename to korlibs-template/.gitignore diff --git a/korlibs-simple/module.yaml b/korlibs-template/module.yaml similarity index 57% rename from korlibs-simple/module.yaml rename to korlibs-template/module.yaml index 399d46a..beacc55 100644 --- a/korlibs-simple/module.yaml +++ b/korlibs-template/module.yaml @@ -4,10 +4,12 @@ product: apply: [ ../common.module-template.yaml ] -aliases: - - jvmAndAndroid: [jvm, android] - dependencies: + - com.soywiz:korlibs-serialization:6.0.0: exported + - com.soywiz:korlibs-platform:6.0.0: exported + - com.soywiz:korlibs-string:6.0.0 + - org.jetbrains.kotlinx:atomicfu:0.24.0: exported test-dependencies: + - org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC diff --git a/korlibs-template/src/korlibs/template/Korte.kt b/korlibs-template/src/korlibs/template/Korte.kt new file mode 100644 index 0000000..37bf4d4 --- /dev/null +++ b/korlibs-template/src/korlibs/template/Korte.kt @@ -0,0 +1,679 @@ +package korlibs.template + +import korlibs.io.serialization.yaml.* +import korlibs.template.dynamic.* +import korlibs.template.internal.* +import korlibs.template.util.* +import kotlin.collections.set + +open class KorteTemplates( + var root: KorteNewTemplateProvider, + var includes: KorteNewTemplateProvider = root, + var layouts: KorteNewTemplateProvider = root, + val config: KorteTemplateConfig = KorteTemplateConfig(), + var cache: Boolean = true +) { + @PublishedApi + internal val tcache = KorteAsyncCache() + + fun invalidateCache() { + tcache.invalidateAll() + } + + @PublishedApi + internal suspend fun cache(name: String, callback: suspend () -> KorteTemplate): KorteTemplate = when { + cache -> tcache.call(name) { callback() } + else -> callback() + } + + open suspend fun getInclude(name: String): KorteTemplate = cache("include/$name") { + KorteTemplate(name, this@KorteTemplates, includes.newGetSure(name), config).init() + } + + open suspend fun getLayout(name: String): KorteTemplate = cache("layout/$name") { + KorteTemplate(name, this@KorteTemplates, layouts.newGetSure(name), config).init() + } + + open suspend fun get(name: String): KorteTemplate = cache("base/$name") { + KorteTemplate(name, this@KorteTemplates, root.newGetSure(name), config).init() + } + + suspend fun render(name: String, vararg args: Pair): String = get(name).invoke(*args) + suspend fun render(name: String, args: Any?): String { + val template = get(name) + val renderered = template(args) + return renderered + } + suspend fun prender(name: String, vararg args: Pair): KorteAsyncTextWriterContainer = + get(name).prender(*args) + + suspend fun prender(name: String, args: Any?): KorteAsyncTextWriterContainer = get(name).prender(args) +} + +open class KorteTemplateContent( + val text: String, + val contentType: String? = null, + val chunkProcessor: ((String) -> String) = { it } +) + +class KorteTemplate internal constructor( + val name: String, + val templates: KorteTemplates, + val templateContent: KorteTemplateContent, + val config: KorteTemplateConfig = KorteTemplateConfig() +) { + val template get() = templateContent.text + // @TODO: Move to parse plugin + extra + var frontMatter: Map? = null + + val blocks = hashMapOf() + val parseContext = ParseContext(this, config) + val templateTokens = KorteToken.tokenize(template, KorteFilePosContext(KorteFileContext(name, template), 0)) + lateinit var rootNode: KorteBlock; private set + + suspend fun init(): KorteTemplate { + rootNode = KorteBlock.parse(templateTokens, parseContext) + // @TODO: Move to parse plugin + extra + if (frontMatter != null) { + val layout = frontMatter?.get("layout") + if (layout != null) { + rootNode = DefaultBlocks.BlockGroup( + listOf( + DefaultBlocks.BlockCapture("content", rootNode, templateContent.contentType), + DefaultBlocks.BlockExtends(KorteExprNode.LIT(layout)) + ) + ) + } + } + return this + } + + class ParseContext(val template: KorteTemplate, val config: KorteTemplateConfig) { + val templates: KorteTemplates get() = template.templates + } + + class Scope(val map: Any?, val mapper: KorteObjectMapper2, val parent: KorteTemplate.Scope? = null) : KorteDynamicContext { + // operator + suspend fun get(key: Any?): Any? = KorteDynamic2.accessAny(map, key, mapper) ?: parent?.get(key) + + // operator + suspend fun set(key: Any?, value: Any?) { + KorteDynamic2.setAny(map, key, value, mapper) + } + } + + data class ExecResult(val context: KorteTemplate.EvalContext, val str: String) + + interface DynamicInvokable { + suspend fun invoke(ctx: KorteTemplate.EvalContext, args: List): Any? + } + + class Macro(val name: String, val argNames: List, val code: KorteBlock) : DynamicInvokable { + override suspend fun invoke(ctx: KorteTemplate.EvalContext, args: List): Any? { + return ctx.createScope { + for ((key, value) in this.argNames.zip(args)) { + ctx.scope.set(key, value) + } + KorteRawString(ctx.capture { + code.eval(ctx) + }) + } + } + } + + data class BlockInTemplateEval(val name: String, val block: KorteBlock, val template: TemplateEvalContext) { + val parent: BlockInTemplateEval? + get() { + return template.parent?.getBlockOrNull(name) + } + + suspend fun eval(ctx: EvalContext) = ctx.setTempTemplate(template) { + val oldBlock = ctx.currentBlock + try { + ctx.currentBlock = this + return@setTempTemplate block.eval(ctx) + } finally { + ctx.currentBlock = oldBlock + } + } + } + + class TemplateEvalContext(val template: KorteTemplate) { + val name: String = template.name + val templates: KorteTemplates get() = template.templates + + var parent: TemplateEvalContext? = null + val root: TemplateEvalContext get() = parent?.root ?: this + + fun getBlockOrNull(name: String): BlockInTemplateEval? = + template.blocks[name]?.let { BlockInTemplateEval(name, it, this@TemplateEvalContext) } + ?: parent?.getBlockOrNull(name) + + fun getBlock(name: String): BlockInTemplateEval = + getBlockOrNull(name) ?: BlockInTemplateEval(name, DefaultBlocks.BlockText(""), this) + + class WithArgs(val context: TemplateEvalContext, val args: Any?, val mapper: KorteObjectMapper2, val parentScope: KorteTemplate.Scope? = null) : + KorteAsyncTextWriterContainer { + override suspend fun write(writer: suspend (String) -> Unit) { + context.exec2(args, mapper, parentScope, writer) + } + } + + fun withArgs(args: Any?, mapper: KorteObjectMapper2 = KorteMapper2, parentScope: KorteTemplate.Scope? = null) = WithArgs(this, args, mapper, parentScope) + + suspend fun exec2(args: Any?, mapper: KorteObjectMapper2, parentScope: KorteTemplate.Scope? = null, writer: suspend (String) -> Unit): KorteTemplate.EvalContext { + val scope = Scope(args, mapper, parentScope) + if (template.frontMatter != null) for ((k, v) in template.frontMatter!!) scope.set(k, v) + val context = KorteTemplate.EvalContext(this, scope, template.config, mapper = mapper, write = writer) + eval(context) + return context + } + + suspend fun exec(args: Any?, mapper: KorteObjectMapper2 = KorteMapper2, parentScope: KorteTemplate.Scope? = null): ExecResult { + val str = StringBuilder() + val scope = Scope(args, mapper, parentScope) + if (template.frontMatter != null) for ((k, v) in template.frontMatter!!) scope.set(k, v) + val context = KorteTemplate.EvalContext(this, scope, template.config, mapper, write = { str.append(it) }) + eval(context) + return ExecResult(context, str.toString()) + } + + suspend fun exec(vararg args: Pair, mapper: KorteObjectMapper2 = KorteMapper2, parentScope: KorteTemplate.Scope? = null): ExecResult = + exec(hashMapOf(*args), mapper, parentScope) + + operator suspend fun invoke(args: Any?, mapper: KorteObjectMapper2 = KorteMapper2, parentScope: KorteTemplate.Scope? = null): String = exec(args, mapper, parentScope).str + operator suspend fun invoke(vararg args: Pair, mapper: KorteObjectMapper2 = KorteMapper2, parentScope: KorteTemplate.Scope? = null): String = + exec(hashMapOf(*args), mapper, parentScope).str + + suspend fun eval(context: KorteTemplate.EvalContext) { + try { + context.setTempTemplate(this) { + context.createScope { template.rootNode.eval(context) } + } + } catch (e: StopEvaluatingException) { + } + } + } + + class StopEvaluatingException : Exception() + + class EvalContext( + var currentTemplate: TemplateEvalContext, + var scope: KorteTemplate.Scope, + val config: KorteTemplateConfig, + val mapper: KorteObjectMapper2, + var write: suspend (str: String) -> Unit + ) : KorteDynamicContext { + val leafTemplate: TemplateEvalContext = currentTemplate + val templates = currentTemplate.templates + val macros = hashMapOf() + var currentBlock: BlockInTemplateEval? = null + + internal val filterCtxPool = Pool { KorteFilter.Ctx() } + + inline fun setTempTemplate(template: TemplateEvalContext, callback: () -> T): T { + val oldTemplate = this.currentTemplate + try { + this.currentTemplate = template + return callback() + } finally { + this.currentTemplate = oldTemplate + } + } + + inline fun capture(callback: () -> Unit): String = this.run { + var out = "" + val old = write + try { + write = { out += it } + callback() + } finally { + write = old + } + out + } + + inline fun captureRaw(callback: () -> Unit): KorteRawString = KorteRawString(capture(callback)) + + inline fun createScope(content: MutableMap<*, *> = LinkedHashMap(), callback: () -> T): T { + val old = this.scope + try { + this.scope = KorteTemplate.Scope(content, mapper, old) + return callback() + } finally { + this.scope = old + } + } + } + + fun addBlock(name: String, body: KorteBlock) { + blocks[name] = body + } + + //suspend operator fun invoke(hashMap: Any?, mapper: ObjectMapper2 = Mapper2): String = Template.TemplateEvalContext(this).invoke(hashMap, mapper = mapper) + //suspend operator fun invoke(vararg args: Pair, mapper: ObjectMapper2 = Mapper2): String = Template.TemplateEvalContext(this).invoke(*args, mapper = mapper) + + suspend fun createEvalContext() = KorteTemplate.TemplateEvalContext(this) + suspend operator fun invoke(hashMap: Any?, mapper: KorteObjectMapper2 = KorteMapper2): String = + createEvalContext().invoke(hashMap, mapper = mapper) + + suspend operator fun invoke(vararg args: Pair, mapper: KorteObjectMapper2 = KorteMapper2): String = + createEvalContext().invoke(*args, mapper = mapper) + + suspend fun prender(vararg args: Pair, mapper: KorteObjectMapper2 = KorteMapper2): KorteAsyncTextWriterContainer { + return createEvalContext().withArgs(HashMap(args.toMap()), mapper) + } + + suspend fun prender(args: Any?, mapper: KorteObjectMapper2 = KorteMapper2): KorteAsyncTextWriterContainer { + return createEvalContext().withArgs(args, mapper) + } +} + +suspend fun KorteTemplate( + template: String, + templates: KorteTemplates, + includes: KorteNewTemplateProvider = templates.includes, + layouts: KorteNewTemplateProvider = templates.layouts, + config: KorteTemplateConfig = templates.config, + cache: Boolean = templates.cache, +): KorteTemplate { + val root = KorteTemplateProvider(mapOf("template" to template)) + return KorteTemplates( + root = root, + includes = includes, + layouts = layouts, + config = config, + cache = cache, + ).get("template") +} + +suspend fun KorteTemplate(template: String, config: KorteTemplateConfig = KorteTemplateConfig()): KorteTemplate = KorteTemplates( + KorteTemplateProvider(mapOf("template" to template)), + config = config +).get("template") + +open class KorteTemplateConfig( + extraTags: List = listOf(), + extraFilters: List = listOf(), + extraFunctions: List = listOf(), + var unknownFilter: KorteFilter = KorteFilter("unknown") { tok.exception("Unknown filter '$name'") }, + val autoEscapeMode: KorteAutoEscapeMode = KorteAutoEscapeMode.HTML, + // Here we can convert markdown into html if required. This is available at the template level + content + named blocks + val contentTypeProcessor: (content: String, contentType: String?) -> String = { content, _ -> content }, + val frontMatterParser: (String) -> Any? = { Yaml.decode(it) }, + @Suppress("UNUSED_PARAMETER") dummy: Unit = Unit, // To avoid tailing lambda +) { + val extra = LinkedHashMap() + + val integratedFunctions = KorteDefaultFunctions.ALL + val integratedFilters = KorteDefaultFilters.ALL + val integratedTags = KorteDefaultTags.ALL + + private val allFunctions = integratedFunctions + extraFunctions + private val allTags = integratedTags + extraTags + private val allFilters = integratedFilters + extraFilters + + val tags = hashMapOf().apply { + for (tag in allTags) { + this[tag.name] = tag + for (alias in tag.aliases) this[alias] = tag + } + } + + val filters = hashMapOf().apply { + for (filter in allFilters) this[filter.name] = filter + } + + val functions = hashMapOf().apply { + for (func in allFunctions) this[func.name] = func + } + + fun register(vararg its: KorteTag) = this.apply { for (it in its) tags[it.name] = it } + fun register(vararg its: KorteFilter) = this.apply { for (it in its) filters[it.name] = it } + fun register(vararg its: KorteFunction) = this.apply { for (it in its) functions[it.name] = it } + + var variableProcessor: KorteVariableProcessor = { name -> + scope.get(name) + } + + fun replaceVariablePocessor(func: suspend KorteTemplate.EvalContext.(name: String, previous: KorteVariableProcessor) -> Any?) { + val previous = variableProcessor + variableProcessor = { eval -> + this.func(eval, previous) + } + } + + var writeBlockExpressionResult: KorteWriteBlockExpressionResultFunction = { value -> + this.write(when (value) { + is KorteRawString -> contentTypeProcessor(value.str, value.contentType) + else -> autoEscapeMode.transform(contentTypeProcessor(KorteDynamic2.toString(value), null)) + }) + } + + fun replaceWriteBlockExpressionResult(func: suspend KorteTemplate.EvalContext.(value: Any?, previous: KorteWriteBlockExpressionResultFunction) -> Unit) { + val previous = writeBlockExpressionResult + writeBlockExpressionResult = { eval -> + this.func(eval, previous) + } + } +} + +typealias KorteWriteBlockExpressionResultFunction = suspend KorteTemplate.EvalContext.(value: Any?) -> Unit +typealias KorteVariableProcessor = suspend KorteTemplate.EvalContext.(name: String) -> Any? + +open class KorteTemplateConfigWithTemplates( + extraTags: List = listOf(), + extraFilters: List = listOf(), + extraFunctions: List = listOf() +) : KorteTemplateConfig(extraTags, extraFilters, extraFunctions) { + var templates = KorteTemplates(KorteTemplateProvider(mapOf()), config = this) + fun cache(value: Boolean) = this.apply { templates.cache = value } + fun root(root: KorteNewTemplateProvider, includes: KorteNewTemplateProvider = root, layouts: KorteNewTemplateProvider = root) = + this.apply { + templates.root = root + templates.includes = includes + templates.layouts = layouts + } +} + +interface KorteNewTemplateProvider { + suspend fun newGet(template: String): KorteTemplateContent? +} + +interface KorteTemplateProvider : KorteNewTemplateProvider { + class NotFoundException(val template: String) : RuntimeException("Can't find template '$template'") + + override suspend fun newGet(template: String): KorteTemplateContent? = get(template)?.let { KorteTemplateContent(it) } + suspend fun get(template: String): String? +} + +suspend fun KorteNewTemplateProvider.newGetSure(template: String) = newGet(template) + ?: throw KorteTemplateProvider.NotFoundException(template) + +suspend fun KorteTemplateProvider.getSure(template: String) = get(template) + ?: throw KorteTemplateProvider.NotFoundException(template) + +fun KorteTemplateProvider(map: Map): KorteTemplateProvider = object : KorteTemplateProvider { + override suspend fun get(template: String): String? = map[template] +} + +fun KorteTemplateProvider(vararg map: Pair): KorteTemplateProvider = KorteTemplateProvider(map.toMap()) + +fun KorteNewTemplateProvider(map: Map): KorteNewTemplateProvider = object : KorteNewTemplateProvider { + override suspend fun newGet(template: String): KorteTemplateContent? = map[template] +} +fun KorteNewTemplateProvider(vararg map: Pair): KorteNewTemplateProvider = KorteNewTemplateProvider(map.toMap()) + +data class KorteTag(val name: String, val nextList: Set, val end: Set?, val aliases: List = listOf(), val buildNode: suspend BuildContext.() -> KorteBlock) : KorteDynamicContext { + data class Part(val tag: KorteToken.TTag, val body: KorteBlock) + data class BuildContext(val context: KorteTemplate.ParseContext, val chunks: List) +} + +class KorteRawString(val str: String, val contentType: String? = null) { + override fun toString(): String = str +} + +data class KorteFilter(val name: String, val eval: suspend Ctx.() -> Any?) { + class Ctx : KorteDynamicContext { + lateinit var context: KorteTemplate.EvalContext + lateinit var tok: KorteExprNode.Token + lateinit var name: String + val mapper get() = context.mapper + var subject: Any? = null + var args: List = listOf() + } +} + +data class KorteFunction(val name: String, val eval: suspend KorteTemplate.EvalContext.(args: List) -> Any?) { + suspend fun eval(args: List, context: KorteTemplate.EvalContext) = eval.invoke(context, args) +} + +open class KorteException(val msg: String, val context: KorteFilePosContext) : RuntimeException() { + + override val message: String get() = "$msg at $context" + + //override fun toString(): String = message +} + +fun korteException(msg: String, context: KorteFilePosContext): Nothing = throw KorteException(msg, context) + +interface KorteBlock : KorteDynamicContext { + suspend fun eval(context: KorteTemplate.EvalContext) + + companion object { + fun group(children: List): KorteBlock = + if (children.size == 1) children[0] else DefaultBlocks.BlockGroup(children) + + private val LINES_REGEX = Regex("(\\r\\n|\\n)") + + class Parse(val tokens: List, val parseContext: KorteTemplate.ParseContext) { + val tr = KorteListReader(tokens, tokens.lastOrNull()) + + suspend fun handle(tag: KorteTag, token: KorteToken.TTag): KorteBlock { + val parts = arrayListOf() + var currentToken = token + val children = arrayListOf() + + fun emitPart() { + parts += KorteTag.Part(currentToken, group(children.toList())) + } + + loop@ while (!tr.eof) { + val it = tr.read() + when (it) { + is KorteToken.TLiteral -> { + var text = it.content + // it.content.startsWith("---") + if (children.isEmpty() && it.content.startsWith("---")) { + val lines = it.content.split(LINES_REGEX) + if (lines[0] == "---") { + val slines = lines.drop(1) + val index = slines.indexOf("---") + if (index >= 0) { + val yamlLines = slines.slice(0 until index) + val outside = slines.slice(index + 1 until slines.size) + val yamlText = yamlLines.joinToString("\n") + val yaml = parseContext.config.frontMatterParser(yamlText) + if (yaml is Map<*, *>) { + parseContext.template.frontMatter = yaml as Map + } + text = outside.joinToString("\n") + } + } + } + children += DefaultBlocks.BlockText(parseContext.template.templateContent.chunkProcessor(text)) + } + is KorteToken.TExpr -> { + children += DefaultBlocks.BlockExpr(KorteExprNode.parse(it.content, it.posContext)) + } + is KorteToken.TTag -> { + when (it.name) { + in (tag.end ?: setOf()) -> break@loop + in tag.nextList -> { + emitPart() + currentToken = it + children.clear() + } + else -> { + val newtag = parseContext.config.tags[it.name] + ?: it.exception("Can't find tag ${it.name} with content ${it.content}") + children += when { + newtag.end != null -> handle(newtag, it) + else -> newtag.buildNode( + KorteTag.BuildContext( + parseContext, + listOf(KorteTag.Part(it, DefaultBlocks.BlockText(""))) + ) + ) + } + } + } + } + else -> break@loop + } + } + + emitPart() + + return tag.buildNode(KorteTag.BuildContext(parseContext, parts)) + } + } + + suspend fun parse(tokens: List, parseContext: KorteTemplate.ParseContext): KorteBlock { + return Parse(tokens, parseContext).handle(KorteDefaultTags.Empty, KorteToken.TTag("", "")) + } + } +} + +class KorteAutoEscapeMode(val transform: (String) -> String) { + companion object { + val HTML = KorteAutoEscapeMode { it.htmlspecialchars() } + val RAW = KorteAutoEscapeMode { it } + } +} + +data class KorteFileContext(val fileName: String, val fileContent: String) { + val lines by lazy { fileContent.split("\n") } + val lineOffsets by lazy { + ArrayList().apply { + var offset = 0 + for (line in lines) { + add(offset) + offset += line.length + } + add(fileContent.length) + } + } + fun findRow0At(pos: Int): Int { + for (n in 0 until lineOffsets.size - 1) { + val start = lineOffsets[n] + val end = lineOffsets[n + 1] + if (pos in start until end) return n + } + return -1 + } + + companion object { + val DUMMY = KorteFileContext("unknown", "") + } +} + +data class KorteFilePosContext(val file: KorteFileContext, val pos: Int) { + val fileName get() = file.fileName + val fileContent get() = file.fileContent + val row0: Int by lazy { file.findRow0At(pos) } + val row get() = row0 + 1 + val column0 get() = pos - file.lineOffsets[row0] + val column get() = column0 + 1 + + fun withPosAdd(add: Int) = this.copy(pos = pos + add) + + fun exception(msg: String): Nothing = korteException(msg, this) + + override fun toString(): String = "$fileName:$row:$column" +} + +interface KorteTokenContext { + var file: KorteFileContext + var pos: Int + val posContext: KorteFilePosContext get() = KorteFilePosContext(file, pos) + + fun exception(msg: String): Nothing = posContext.exception(msg) + + class Mixin : KorteTokenContext { + override var file: KorteFileContext = KorteFileContext.DUMMY + override var pos: Int = -1 + } +} + +sealed class KorteToken : KorteTokenContext { + var trimLeft = false + var trimRight = false + + data class TLiteral(val content: String) : KorteToken(), KorteTokenContext by KorteTokenContext.Mixin() + data class TExpr(val content: String) : KorteToken(), KorteTokenContext by KorteTokenContext.Mixin() + data class TTag(val name: String, val content: String) : KorteToken(), KorteTokenContext by KorteTokenContext.Mixin() { + val tokens by lazy { KorteExprNode.Token.tokenize(content, posContext) } + val expr by lazy { KorteExprNode.parse(this) } + } + + companion object { + // @TODO: Use StrReader + fun tokenize(str: String, context: KorteFilePosContext): List { + val out = arrayListOf() + var lastPos = 0 + + fun emit(token: KorteToken, pos: Int) { + if (token is TLiteral && token.content.isEmpty()) return + out += token + token.file = context.file + token.pos = context.pos + pos + } + + var pos = 0 + loop@ while (pos < str.length) { + val c = str[pos++] + // {# {% {{ }} %} #} + if (c == '{') { + if (pos >= str.length) break + val c2 = str[pos++] + when (c2) { + // Comment + '#' -> { + val startPos = pos - 2 + if (lastPos != startPos) { + emit(TLiteral(str.substring(lastPos until startPos)), startPos) + } + val endCommentP1 = str.indexOf("#}", startIndex = pos) + val endComment = if (endCommentP1 >= 0) endCommentP1 + 2 else str.length + lastPos = endComment + pos = endComment + } + '{', '%' -> { + val startPos = pos - 2 + val pos2 = if (c2 == '{') str.indexOf("}}", pos) else str.indexOf("%}", pos) + if (pos2 < 0) break@loop + val trimLeft = str[pos] == '-' + val trimRight = str[pos2 - 1] == '-' + + val p1 = if (trimLeft) pos + 1 else pos + val p2 = if (trimRight) pos2 - 1 else pos2 + + val content = str.substring(p1, p2).trim() + if (lastPos != startPos) emit(TLiteral(str.substring(lastPos until startPos)), startPos) + + val token = when (c2) { + '{' -> TExpr(content) + else -> { + val parts = content.split(' ', limit = 2) + TTag(parts[0], parts.getOrElse(1) { "" }) + } + } + token.trimLeft = trimLeft + token.trimRight = trimRight + emit(token, p1) + pos = pos2 + 2 + lastPos = pos + } + } + } + } + emit(TLiteral(str.substring(lastPos, str.length)), lastPos) + + for ((n, cur) in out.withIndex()) { + if (cur is KorteToken.TLiteral) { + val trimStart = out.getOrNull(n - 1)?.trimRight ?: false + val trimEnd = out.getOrNull(n + 1)?.trimLeft ?: false + out[n] = when { + trimStart && trimEnd -> TLiteral(cur.content.trim()) + trimStart -> TLiteral(cur.content.trimStart()) + trimEnd -> TLiteral(cur.content.trimEnd()) + else -> cur + } + } + } + + return out + } + } +} diff --git a/korlibs-template/src/korlibs/template/KorteDefaults.kt b/korlibs-template/src/korlibs/template/KorteDefaults.kt new file mode 100644 index 0000000..4f1bc0a --- /dev/null +++ b/korlibs-template/src/korlibs/template/KorteDefaults.kt @@ -0,0 +1,520 @@ +package korlibs.template + +import korlibs.template.dynamic.* +import korlibs.template.internal.* +import korlibs.util.* +import kotlin.coroutines.cancellation.* +import kotlin.math.* + +//@Suppress("unused") +object KorteDefaultFilters { + val Capitalize = KorteFilter("capitalize") { subject.toDynamicString().lowercase().capitalize() } + val Join = KorteFilter("join") { + subject.toDynamicList().joinToString(args[0].toDynamicString()) { it.toDynamicString() } + } + val First = KorteFilter("first") { subject.toDynamicList().firstOrNull() } + val Last = KorteFilter("last") { subject.toDynamicList().lastOrNull() } + val Split = KorteFilter("split") { subject.toDynamicString().split(args[0].toDynamicString()) } + val Concat = KorteFilter("concat") { subject.toDynamicString() + args[0].toDynamicString() } + val Length = KorteFilter("length") { subject.dynamicLength() } + val Quote = KorteFilter("quote") { subject.toDynamicString().quote() } + val Raw = KorteFilter("raw") { KorteRawString(subject.toDynamicString()) } + val Replace = KorteFilter("replace") { subject.toDynamicString().replace(args[0].toDynamicString(), args[1].toDynamicString()) } + val Reverse = + KorteFilter("reverse") { (subject as? String)?.reversed() ?: subject.toDynamicList().reversed() } + + val Slice = KorteFilter("slice") { + val lengthArg = args.getOrNull(1) + val start = args.getOrNull(0).toDynamicInt() + val length = lengthArg?.toDynamicInt() ?: subject.dynamicLength() + if (subject is String) { + val str = subject.toDynamicString() + str.slice(start.coerceIn(0, str.length) until (start + length).coerceIn(0, str.length)) + } else { + val list = subject.toDynamicList() + list.slice(start.coerceIn(0, list.size) until (start + length).coerceIn(0, list.size)) + } + } + + val Sort = KorteFilter("sort") { + if (args.isEmpty()) { + subject.toDynamicList().sortedBy { it.toDynamicString() } + } else { + subject.toDynamicList() + .map { it to KorteDynamic2.accessAny(it, args[0], mapper).toDynamicString() } + .sortedBy { it.second } + .map { it.first } + } + } + val Trim = KorteFilter("trim") { subject.toDynamicString().trim() } + + val Lower = KorteFilter("lower") { subject.toDynamicString().lowercase() } + val Upper = KorteFilter("upper") { subject.toDynamicString().uppercase() } + val Downcase = KorteFilter("downcase") { subject.toDynamicString().lowercase() } + val Upcase = KorteFilter("upcase") { subject.toDynamicString().uppercase() } + + val Merge = KorteFilter("merge") { + val arg = args.getOrNull(0) + subject.toDynamicList() + arg.toDynamicList() + } + val JsonEncode = KorteFilter("json_encode") { + Json_stringify(subject) + } + val Format = KorteFilter("format") { + subject.toDynamicString().format(*(args.toTypedArray() as Array)) + } + // EXTRA from Kotlin + val Chunked = KorteFilter("chunked") { + subject.toDynamicList().chunked(args[0].toDynamicInt()) + } + val WhereExp = KorteFilter("where_exp") { + val ctx = this.context + val list = this.subject.toDynamicList() + val itemName = if (args.size >= 2) args[0].toDynamicString() else "it" + val itemExprStr = args.last().toDynamicString() + val itemExpr = KorteExprNode.parse(itemExprStr, KorteFilePosContext(KorteFileContext("", itemExprStr), 0)) + + ctx.createScope { + list.filter { + ctx.scope.set(itemName, it) + itemExpr.eval(ctx).toDynamicBool() + } + } + } + val Where = KorteFilter("where") { + val itemName = args[0] + val itemValue = args[1] + subject.toDynamicList().filter { KorteDynamic2.contains(KorteDynamic2.accessAny(it, itemName, mapper), itemValue) } + + } + val Map = KorteFilter("map") { + val key = this.args[0].toDynamicString() + this.subject.toDynamicList().map { KorteDynamic2.accessAny(it, key, mapper) } + } + val Size = KorteFilter("size") { subject.dynamicLength() } + val Uniq = KorteFilter("uniq") { + this.toDynamicList().distinct() + } + + val Abs = KorteFilter("abs") { + val subject = subject + when (subject) { + is Int -> subject.absoluteValue + is Double -> subject.absoluteValue + is Long -> subject.absoluteValue + else -> subject.toDynamicDouble().absoluteValue + } + } + + val AtMost = KorteFilter("at_most") { + val l = subject.toDynamicNumber() + val r = args[0].toDynamicNumber() + if (l >= r) r else l + } + + val AtLeast = KorteFilter("at_least") { + val l = subject.toDynamicNumber() + val r = args[0].toDynamicNumber() + if (l <= r) r else l + } + + val Ceil = KorteFilter("ceil") { + ceil(subject.toDynamicNumber().toDouble()).toDynamicCastToType(subject) + } + val Floor = KorteFilter("floor") { + floor(subject.toDynamicNumber().toDouble()).toDynamicCastToType(subject) + } + val Round = KorteFilter("round") { + round(subject.toDynamicNumber().toDouble()).toDynamicCastToType(subject) + } + val Times = KorteFilter("times") { + (subject.toDynamicDouble() * args[0].toDynamicDouble()).toDynamicCastToType(combineTypes(subject, args[0])) + } + val Modulo = KorteFilter("modulo") { + (subject.toDynamicDouble() % args[0].toDynamicDouble()).toDynamicCastToType(combineTypes(subject, args[0])) + } + val DividedBy = KorteFilter("divided_by") { + (subject.toDynamicDouble() / args[0].toDynamicDouble()).toDynamicCastToType(combineTypes(subject, args[0])) + } + val Minus = KorteFilter("minus") { + (subject.toDynamicDouble() - args[0].toDynamicDouble()).toDynamicCastToType(combineTypes(subject, args[0])) + } + val Plus = KorteFilter("plus") { + (subject.toDynamicDouble() + args[0].toDynamicDouble()).toDynamicCastToType(combineTypes(subject, args[0])) + } + val Default = KorteFilter("default") { + if (subject == null || subject == false || subject == "") args[0] else subject + } + + val ALL = listOf( + // String + Capitalize, Lower, Upper, Downcase, Upcase, Quote, Raw, Replace, Trim, + // Array + Join, Split, Concat, WhereExp, Where, First, Last, Map, Size, Uniq, Length, Chunked, Sort, Merge, + // Array/String + Reverse, Slice, + // Math + Abs, AtMost, AtLeast, Ceil, Floor, Round, Times, Modulo, DividedBy, Minus, Plus, + // Any + JsonEncode, Format + ) +} + +@Suppress("unused") +object KorteDefaultFunctions { + val Cycle = KorteFunction("cycle") { args -> + val list = args.getOrNull(0).toDynamicList() + val index = args.getOrNull(1).toDynamicInt() + list[index umod list.size] + } + + val Range = KorteFunction("range") { args -> + val left = args.getOrNull(0) + val right = args.getOrNull(1) + val step = (args.getOrNull(2) ?: 1).toDynamicInt() + if (left is Number || right is Number) { + val l = left.toDynamicInt() + val r = right.toDynamicInt() + ((l..r) step step).toList() + } else { + TODO("Unsupported '$left'/'$right' for ranges") + } + } + + + val Parent = KorteFunction("parent") { + //ctx.tempDropTemplate { + val blockName = currentBlock?.name + + if (blockName != null) { + captureRaw { + currentBlock?.parent?.eval(this) + } + } else { + "" + } + } + + val ALL = listOf(Cycle, Range, Parent) +} + +@Suppress("unused") +object KorteDefaultTags { + val BlockTag = KorteTag("block", setOf(), setOf("end", "endblock")) { + val part = chunks.first() + val tr = part.tag.tokens + val name = KorteExprNode.parseId(tr) + if (name.isEmpty()) throw IllegalArgumentException("block without name") + val contentType = if (tr.hasMore) KorteExprNode.parseId(tr) else null + tr.expectEnd() + context.template.addBlock(name, part.body) + DefaultBlocks.BlockBlock(name, contentType ?: this.context.template.templateContent.contentType) + } + + val Capture = KorteTag("capture", setOf(), setOf("end", "endcapture")) { + val main = chunks[0] + val tr = main.tag.tokens + val varname = KorteExprNode.parseId(tr) + val contentType = if (tr.hasMore) KorteExprNode.parseId(tr) else null + tr.expectEnd() + DefaultBlocks.BlockCapture(varname, main.body, contentType) + } + + val Debug = KorteTag("debug", setOf(), null) { + DefaultBlocks.BlockDebug(chunks[0].tag.expr) + } + + val Empty = KorteTag("", setOf(""), null) { + KorteBlock.group(chunks.map { it.body }) + } + + val Extends = KorteTag("extends", setOf(), null) { + val part = chunks.first() + val parent = KorteExprNode.parseExpr(part.tag.tokens) + DefaultBlocks.BlockExtends(parent) + } + + val For = KorteTag("for", setOf("else"), setOf("end", "endfor")) { + val main = chunks[0] + val elseTag = chunks.getOrNull(1)?.body + val tr = main.tag.tokens + val varnames = arrayListOf() + do { + varnames += KorteExprNode.parseId(tr) + } while (tr.tryRead(",") != null) + KorteExprNode.expect(tr, "in") + val expr = KorteExprNode.parseExpr(tr) + tr.expectEnd() + DefaultBlocks.BlockFor(varnames, expr, main.body, elseTag) + } + + fun KorteTag.BuildContext.BuildIf(isIf: Boolean): KorteBlock { + class Branch(val part: KorteTag.Part) { + val expr get() = part.tag.expr + val body get() = part.body + val realExpr get() = if (part.tag.name.contains("unless")) { + KorteExprNode.UNOP(expr, "!") + } else { + expr + } + } + + val branches = arrayListOf() + var elseBranch: KorteBlock? = null + + for (part in chunks) { + when (part.tag.name) { + "if", "elseif", "elsif", "unless", "elseunless" -> branches += Branch(part) + "else" -> elseBranch = part.body + } + } + + + val branchesRev = branches.reversed() + val firstBranch = branchesRev.first() + + var node: KorteBlock = DefaultBlocks.BlockIf(firstBranch.realExpr, firstBranch.body, elseBranch) + for (branch in branchesRev.takeLast(branchesRev.size - 1)) { + node = DefaultBlocks.BlockIf(branch.realExpr, branch.body, node) + } + + return node + } + + val If = KorteTag("if", setOf("else", "elseif", "elsif", "elseunless"), setOf("end", "endif")) { BuildIf(isIf = true) } + val Unless = KorteTag("unless", setOf("else", "elseif", "elsif", "elseunless"), setOf("end", "endunless")) { BuildIf(isIf = true) } + + val Import = KorteTag("import", setOf(), null) { + val part = chunks.first() + val s = part.tag.tokens + val file = s.parseExpr() + s.expect("as") + val name = s.read().text + s.expectEnd() + DefaultBlocks.BlockImport(file, name) + } + + val Include = KorteTag("include", setOf(), null) { + val main = chunks.first() + val tr = main.tag.tokens + val expr = KorteExprNode.parseExpr(tr) + val params = linkedMapOf() + while (tr.hasMore) { + val id = KorteExprNode.parseId(tr) + tr.expect("=") + val expr = KorteExprNode.parseExpr(tr) + params[id] = expr + } + tr.expectEnd() + DefaultBlocks.BlockInclude(expr, params, main.tag.posContext, main.tag.content) + } + + val Macro = KorteTag("macro", setOf(), setOf("end", "endmacro")) { + val part = chunks[0] + val s = part.tag.tokens + val funcname = s.parseId() + s.expect("(") + val params = s.parseIdList() + s.expect(")") + s.expectEnd() + DefaultBlocks.BlockMacro(funcname, params, part.body) + } + + val Set = KorteTag("set", setOf(), null) { + val main = chunks[0] + val tr = main.tag.tokens + val varname = KorteExprNode.parseId(tr) + KorteExprNode.expect(tr, "=") + val expr = KorteExprNode.parseExpr(tr) + tr.expectEnd() + DefaultBlocks.BlockSet(varname, expr) + } + + val Assign = KorteTag("assign", setOf(), null) { + Set.buildNode(this) + } + + val Switch = KorteTag("switch", setOf("case", "default"), setOf("end", "endswitch")) { + var subject: KorteExprNode? = null + val cases = arrayListOf>() + var defaultCase: KorteBlock? = null + + for (part in this.chunks) { + val body = part.body + when (part.tag.name) { + "switch" -> subject = part.tag.expr + "case" -> cases += part.tag.expr to body + "default" -> defaultCase = body + } + } + if (subject == null) error("No subject set in switch") + //println(this.chunks) + object : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + val subjectValue = subject.eval(context) + for ((case, block) in cases) { + if (subjectValue == case.eval(context)) { + block.eval(context) + return + } + } + defaultCase?.eval(context) + return + } + } + } + + val ALL = listOf( + BlockTag, + Capture, Debug, + Empty, Extends, For, If, Unless, Switch, Import, Include, Macro, Set, + // Liquid + Assign + ) +} + +var KorteTemplateConfig.debugPrintln by korteExtraProperty({ extra }) { { v: Any? -> println(v) } } + +object DefaultBlocks { + data class BlockBlock(val name: String, val contentType: String?) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + //val oldBlock = context.currentBlock + //try { + // val block = context.leafTemplate.getBlock(name) + // context.currentBlock = block + // block.block.eval(context) + //} finally { + // context.currentBlock = oldBlock + //} + context.leafTemplate.getBlock(name).eval(context) + } + } + + data class BlockCapture(val varname: String, val content: KorteBlock, val contentType: String? = null) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + val result = context.capture { + content.eval(context) + } + context.scope.set(varname, KorteRawString(result, contentType)) + } + } + + data class BlockDebug(val expr: KorteExprNode) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + context.config.debugPrintln(expr.eval(context)) + } + } + + data class BlockExpr(val expr: KorteExprNode) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + context.config.writeBlockExpressionResult(context, expr.eval(context)) + } + } + + data class BlockExtends(val expr: KorteExprNode) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + val result = expr.eval(context) + val parentTemplate = KorteTemplate.TemplateEvalContext(context.templates.getLayout(result.toDynamicString())) + context.currentTemplate.parent = parentTemplate + parentTemplate.eval(context) + throw KorteTemplate.StopEvaluatingException() + //context.template.parent + } + } + + data class BlockFor(val varnames: List, val expr: KorteExprNode, val loop: KorteBlock, val elseNode: KorteBlock?) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + context.createScope { + var index = 0 + val items = expr.eval(context).toDynamicList() + val loopValue = hashMapOf() + context.scope.set("loop", loopValue) + loopValue["length"] = items.size + for (v in items) { + if (v is Pair<*, *> && varnames.size >= 2) { + context.scope.set(varnames[0], v.first) + context.scope.set(varnames[1], v.second) + } else { + context.scope.set(varnames[0], v) + } + loopValue["index"] = index + 1 + loopValue["index0"] = index + loopValue["revindex"] = items.size - index - 1 + loopValue["revindex0"] = items.size - index + loopValue["first"] = (index == 0) + loopValue["last"] = (index == items.size - 1) + loop.eval(context) + index++ + } + if (index == 0) { + elseNode?.eval(context) + } + } + } + } + + data class BlockGroup(val children: List) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + for (n in children) n.eval(context) + } + } + + data class BlockIf(val cond: KorteExprNode, val trueContent: KorteBlock, val falseContent: KorteBlock?) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + if (cond.eval(context).toDynamicBool()) { + trueContent.eval(context) + } else { + falseContent?.eval(context) + } + } + } + + data class BlockImport(val fileExpr: KorteExprNode, val exportName: String) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + val ctx = + KorteTemplate.TemplateEvalContext(context.templates.getInclude(fileExpr.eval(context).toString())).exec() + .context + context.scope.set(exportName, ctx.macros) + } + } + + data class BlockInclude( + val fileNameExpr: KorteExprNode, + val params: LinkedHashMap, + val filePos: KorteFilePosContext, + val tagContent: String + ) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + val fileName = fileNameExpr.eval(context).toDynamicString() + val evalParams = params.mapValues { it.value.eval(context) }.toMutableMap() + context.createScope { + context.scope.set("include", evalParams) + val includeTemplate = try { + context.templates.getInclude(fileName) + } catch (e: Throwable) { + if (e is CancellationException) throw e + korteException("Can't include template ($tagContent): ${e.message}", filePos) + } + KorteTemplate.TemplateEvalContext(includeTemplate).eval(context) + } + } + } + + data class BlockMacro(val funcname: String, val args: List, val body: KorteBlock) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + context.macros[funcname] = KorteTemplate.Macro(funcname, args, body) + } + } + + data class BlockSet(val varname: String, val expr: KorteExprNode) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + context.scope.set(varname, expr.eval(context)) + } + } + + data class BlockText(val content: String) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) { + context.write(content) + } + } +} diff --git a/korlibs-template/src/korlibs/template/KorteExprNode.kt b/korlibs-template/src/korlibs/template/KorteExprNode.kt new file mode 100644 index 0000000..36f4e3c --- /dev/null +++ b/korlibs-template/src/korlibs/template/KorteExprNode.kt @@ -0,0 +1,491 @@ +package korlibs.template + +import korlibs.template.dynamic.* +import korlibs.template.internal.* +import korlibs.template.util.* +import kotlin.coroutines.cancellation.* + +interface KorteExprNode : KorteDynamicContext { + suspend fun eval(context: KorteTemplate.EvalContext): Any? + + data class VAR(val name: String) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + return context.config.variableProcessor(context, name) + } + } + + data class LIT(val value: Any?) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? = value + } + + data class ARRAY_LIT(val items: List) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + return items.map { it.eval(context) } + } + } + + data class OBJECT_LIT(val items: List>) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + return items.map { it.first.eval(context) to it.second.eval(context) }.toMap() + } + } + + data class FILTER(val name: String, val expr: KorteExprNode, val params: List, val tok: KorteExprNode.Token) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + val filter = context.config.filters[name] ?: context.config.filters["unknown"] ?: context.config.unknownFilter + return context.filterCtxPool.alloc { + it.tok = tok + it.name = name + it.context = context + it.subject = expr.eval(context) + it.args = params.map { it.eval(context) } + filter.eval(it) + } + } + } + + data class ACCESS(val expr: KorteExprNode, val name: KorteExprNode) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + val obj = expr.eval(context) + val key = name.eval(context) + return try { + return KorteDynamic2.accessAny(obj, key, context.mapper) + } catch (e: Throwable) { + if (e is CancellationException) throw e + try { + KorteDynamic2.callAny(obj, "invoke", listOf(key), mapper = context.mapper) + } catch (e: Throwable) { + if (e is CancellationException) throw e + null + } + } + } + } + + data class CALL(val method: KorteExprNode, val args: List) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + val processedArgs = args.map { it.eval(context) } + when (method) { + is KorteExprNode.ACCESS -> { + val obj = method.expr.eval(context) + val methodName = method.name.eval(context) + //println("" + obj + ":" + methodName) + if (obj is Map<*, *>) { + val k = obj[methodName] + if (k is KorteTemplate.DynamicInvokable) { + return k.invoke(context, processedArgs) + } + } + //return obj.dynamicCallMethod(methodName, *processedArgs.toTypedArray(), mapper = context.mapper) + return KorteDynamic2.callAny(obj, methodName, processedArgs.toList(), mapper = context.mapper) + } + is KorteExprNode.VAR -> { + val func = context.config.functions[method.name] + if (func != null) { + return func.eval(processedArgs, context) + } + } + } + //return method.eval(context).dynamicCall(processedArgs.toTypedArray(), mapper = context.mapper) + return KorteDynamic2.callAny(method.eval(context), processedArgs.toList(), mapper = context.mapper) + } + } + + data class BINOP(val l: KorteExprNode, val r: KorteExprNode, val op: String) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + val lr = l.eval(context) + val rr = r.eval(context) + return when (op) { + "~" -> lr.toDynamicString() + rr.toDynamicString() + ".." -> KorteDefaultFunctions.Range.eval(listOf(lr, rr), context) + else -> KorteDynamic2.binop(lr, rr, op) + } + } + } + + data class TERNARY(val cond: KorteExprNode, val etrue: KorteExprNode, val efalse: KorteExprNode) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + return if (cond.eval(context).toDynamicBool()) { + etrue.eval(context) + } else { + efalse.eval(context) + } + } + } + + data class UNOP(val r: KorteExprNode, val op: String) : KorteExprNode { + override suspend fun eval(context: KorteTemplate.EvalContext): Any? { + return when (op) { + "", "+" -> r.eval(context) + else -> KorteDynamic2.unop(r.eval(context), op) + } + } + } + + companion object { + fun parse(tag: korlibs.template.KorteToken.TTag): KorteExprNode = parse(tag.content, tag.posContext) + + fun parse(str: String, context: KorteFilePosContext): KorteExprNode { + val tokens = KorteExprNode.Token.tokenize(str, context) + if (tokens.list.isEmpty()) context.exception("No expression") + return KorteExprNode.parseFullExpr(tokens).also { + tokens.expectEnd() + } + } + + fun parse(str: String, fileName: String = "expression"): KorteExprNode { + return KorteExprNode.parse(str, KorteFilePosContext(KorteFileContext(fileName, str), 1)) + } + + fun parseId(r: KorteListReader): String { + return r.tryRead()?.text ?: (r.tryPrev() ?: r.ctx)?.exception("Expected id") ?: TODO() + } + + fun expect(r: KorteListReader, vararg tokens: String) { + val token = r.tryRead() ?: r.prevOrContext().exception("Expected ${tokens.joinToString(", ")} but found end") + if (token.text !in tokens) token.exception("Expected ${tokens.joinToString(", ")} but found $token") + } + + fun parseFullExpr(r: KorteListReader): KorteExprNode { + try { + val result = KorteExprNode.parseExpr(r) + if (r.hasMore) { + r.peek() + .exception("Expected expression at " + r.peek() + " :: " + r.list.map { it.text }.joinToString("")) + } + return result + } catch (e: KorteListReader.OutOfBoundsException) { + r.list.last().exception("Incomplete expression") + } + } + + private val BINOPS_PRIORITIES_LIST = listOf( + listOf("*", "/", "%"), + listOf("+", "-", "~"), + listOf("==", "===", "!=", "!==", "<", ">", "<=", ">=", "<=>"), + listOf("&&"), + listOf("||"), + listOf("and"), + listOf("or"), + listOf("in"), + listOf("contains"), + listOf(".."), + listOf("?:") + ) + + private val BINOPS = BINOPS_PRIORITIES_LIST.withIndex() + .flatMap { (index, ops) -> ops.map { it to index } } + .toMap() + + fun binopPr(str: String) = BINOPS[str] ?: 0 + + fun parseBinExpr(r: KorteListReader): KorteExprNode { + var result = parseFinal(r) + while (r.hasMore) { + //if (r.peek() !is ExprNode.Token.TOperator || r.peek().text !in ExprNode.BINOPS) break + if (r.peek().text !in KorteExprNode.BINOPS) break + val operator = r.read().text + val right = parseFinal(r) + if (result is BINOP) { + val a = result.l + val lop = result.op + val b = result.r + val rop = operator + val c = right + val lopPr = binopPr(lop) + val ropPr = binopPr(rop) + if (lopPr > ropPr) { + result = BINOP(a, BINOP(b, c, rop), lop) + continue + } + } + result = BINOP(result, right, operator) + } + return result + } + + fun parseTernaryExpr(r: KorteListReader): KorteExprNode { + var left = this.parseBinExpr(r) + if (r.hasMore) { + if (r.peek().text == "?") { + r.skip() + val middle = parseExpr(r) + r.expect(":") + val right = parseExpr(r) + left = TERNARY(left, middle, right) + } + } + return left + } + + fun parseExpr(r: KorteListReader): KorteExprNode { + return parseTernaryExpr(r) + } + + private fun parseFinal(r: KorteListReader): KorteExprNode { + if (!r.hasMore) r.prevOrContext().exception("Expected expression") + val tok = r.peek().text.uppercase() + var construct: KorteExprNode = when (tok) { + "!", "~", "-", "+", "NOT" -> { + val op = tok + r.skip() + UNOP( + parseFinal(r), when (op) { + "NOT" -> "!" + else -> op + } + ) + } + + "(" -> { + r.read() + val result = KorteExprNode.parseExpr(r) + if (r.read().text != ")") throw RuntimeException("Expected ')'") + UNOP(result, "") + } + // Array literal + "[" -> { + r.read() + val items = arrayListOf() + loop@ while (r.hasMore && r.peek().text != "]") { + items += KorteExprNode.parseExpr(r) + when (r.peek().text) { + "," -> r.read() + "]" -> continue@loop + else -> r.peek().exception("Expected , or ]") + } + } + r.expect("]") + ARRAY_LIT(items) + } + // Object literal + "{" -> { + r.read() + val items = arrayListOf>() + loop@ while (r.hasMore && r.peek().text != "}") { + val k = KorteExprNode.parseFinal(r) + r.expect(":") + val v = KorteExprNode.parseExpr(r) + items += k to v + when (r.peek().text) { + "," -> r.read() + "}" -> continue@loop + else -> r.peek().exception("Expected , or }") + } + } + r.expect("}") + OBJECT_LIT(items) + } + + else -> { + // Number + if (r.peek() is KorteExprNode.Token.TNumber) { + val ntext = r.read().text + when (ntext.toDouble()) { + ntext.toIntOrNull()?.toDouble() -> LIT(ntext.toIntOrNull() ?: 0) + ntext.toLongOrNull()?.toDouble() -> LIT(ntext.toLongOrNull() ?: 0L) + else -> LIT(ntext.toDoubleOrNull() ?: 0.0) + } + } + // String + else if (r.peek() is KorteExprNode.Token.TString) { + LIT((r.read() as Token.TString).processedValue) + } + // ID + else { + val str = r.read().text + when (str) { + "true" -> LIT(true) + "false" -> LIT(false) + "null", "nil" -> LIT(null) + else -> VAR(str) + } + } + } + } + + loop@ while (r.hasMore) { + when (r.peek().text) { + "." -> { + r.read() + val id = r.read().text + construct = ACCESS(construct, LIT(id)) + continue@loop + } + + "[" -> { + r.read() + val expr = KorteExprNode.parseExpr(r) + construct = ACCESS(construct, expr) + val end = r.read() + if (end.text != "]") end.exception("Expected ']' but found $end") + } + + "|" -> { + val tok = r.read() + val name = r.tryRead()?.text ?: "" + val args = arrayListOf() + if (name.isEmpty()) tok.exception("Missing filter name") + if (r.hasMore) { + when (r.peek().text) { + // jekyll/liquid syntax + ":" -> { + r.read() + callargsloop@ while (r.hasMore) { + args += KorteExprNode.parseExpr(r) + if (r.hasMore && r.peek().text == ",") r.read() + } + } + // twig syntax + "(" -> { + r.read() + callargsloop@ while (r.hasMore && r.peek().text != ")") { + args += KorteExprNode.parseExpr(r) + when (r.expectPeek(",", ")").text) { + "," -> r.read() + ")" -> break@callargsloop + } + } + r.expect(")") + } + } + } + construct = FILTER(name, construct, args, tok) + } + + "(" -> { + r.read() + val args = arrayListOf() + callargsloop@ while (r.hasMore && r.peek().text != ")") { + args += KorteExprNode.parseExpr(r) + when (r.expectPeek(",", ")").text) { + "," -> r.read() + ")" -> break@callargsloop + } + } + r.expect(")") + construct = CALL(construct, args) + } + + else -> break@loop + } + } + return construct + } + } + + interface Token : KorteTokenContext { + val text: String + + data class TId(override val text: String) : KorteExprNode.Token, KorteTokenContext by KorteTokenContext.Mixin() + data class TNumber(override val text: String) : KorteExprNode.Token, KorteTokenContext by KorteTokenContext.Mixin() + data class TString(override val text: String, val processedValue: String) : KorteExprNode.Token, KorteTokenContext by KorteTokenContext.Mixin() + data class TOperator(override val text: String) : KorteExprNode.Token, KorteTokenContext by KorteTokenContext.Mixin() + data class TEnd(override val text: String = "") : KorteExprNode.Token, KorteTokenContext by KorteTokenContext.Mixin() + + companion object { + private val OPERATORS = setOf( + "(", ")", + "[", "]", + "{", "}", + "&&", "||", + "&", "|", "^", + "==", "===", "!=", "!==", "<", ">", "<=", ">=", "<=>", + "?:", + "..", + "+", "-", "*", "/", "%", "**", + "!", "~", + ".", ",", ";", ":", "?", + "=" + ) + + fun KorteExprNode.Token.annotate(context: KorteFilePosContext, tpos: Int) = this.apply { + pos = context.pos + tpos + file = context.file + } + + fun tokenize(str: String, context: KorteFilePosContext): KorteListReader { + val r = KorteStrReader(str) + val out = arrayListOf() + fun emit(str: KorteExprNode.Token, tpos: Int) { + str.annotate(context, tpos) + out += str + } + while (r.hasMore) { + val start = r.pos + r.skipSpaces() + val dstart = r.pos + val id = r.readWhile(Char::isLetterDigitOrUnderscore) + if (id.isNotEmpty()) { + if (id[0].isDigit()) { + if (r.peekChar() == '.' && r.peek(2)[1].isDigit()) { + r.skip() + val decimalPart = r.readWhile(Char::isLetterDigitOrUnderscore) + emit(KorteExprNode.Token.TNumber("$id.$decimalPart"), dstart) + } else { + emit(KorteExprNode.Token.TNumber(id), dstart) + } + } else { + emit(KorteExprNode.Token.TId(id), dstart) + } + } + r.skipSpaces() + val dstart2 = r.pos + if (r.peek(3) in OPERATORS) emit(TOperator(r.read(3)), dstart2) + if (r.peek(2) in OPERATORS) emit(TOperator(r.read(2)), dstart2) + if (r.peek(1) in OPERATORS) emit(TOperator(r.read(1)), dstart2) + if (r.peekChar() == '\'' || r.peekChar() == '"') { + val dstart3 = r.pos + val strStart = r.read() + val strBody = r.readUntil(strStart) ?: context.withPosAdd(dstart3).exception("String literal not closed") + val strEnd = r.read() + emit(KorteExprNode.Token.TString(strStart + strBody + strEnd, strBody.unescape()), dstart3) + } + val end = r.pos + if (end == start) { + context.withPosAdd(end).exception("Don't know how to handle '${r.peekChar()}'") + } + } + val dstart = r.pos + //emit(ExprNode.Token.TEnd(), dstart) + return KorteListReader(out, TEnd().annotate(context, dstart)) + } + } + } +} + +fun KorteListReader.expectEnd() { + if (hasMore) peek().exception("Unexpected token '${peek().text}'") +} + +fun KorteListReader.tryRead(vararg types: String): KorteExprNode.Token? { + val token = this.peek() + if (token.text in types) { + this.read() + return token + } else { + return null + } +} + +fun KorteListReader.expectPeek(vararg types: String): KorteExprNode.Token { + val token = this.peek() + if (token.text !in types) throw RuntimeException("Expected ${types.joinToString(", ")} but found '${token.text}'") + return token +} + +fun KorteListReader.expect(vararg types: String): KorteExprNode.Token { + val token = this.read() + if (token.text !in types) throw RuntimeException("Expected ${types.joinToString(", ")}") + return token +} + +fun KorteListReader.parseExpr() = KorteExprNode.parseExpr(this) +fun KorteListReader.parseId() = KorteExprNode.parseId(this) +fun KorteListReader.parseIdList(): List { + val ids = arrayListOf() + do { + ids += parseId() + } while (tryRead(",") != null) + return ids +} diff --git a/korlibs-template/src/korlibs/template/_Template.dynamic.common.kt b/korlibs-template/src/korlibs/template/_Template.dynamic.common.kt new file mode 100644 index 0000000..1a5bae6 --- /dev/null +++ b/korlibs-template/src/korlibs/template/_Template.dynamic.common.kt @@ -0,0 +1,391 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.dynamic + +import korlibs.template.internal.* +import kotlin.math.* +import kotlin.reflect.* + +//expect class DynamicBase { +// //fun getFields(obj: Any?): List +// //fun getMethods(obj: Any?): List +// //fun invoke(obj: Any?, name: String, args: List): Any? +// //fun getFunctionArity(obj: Any?, name: String): Int +// fun dynamicGet(obj: Any?, name: String): Any? +// fun dynamicSet(obj: Any?, name: String, value: Any?): Unit +//} + +object KorteDynamic2 { + fun binop(l: Any?, r: Any?, op: String): Any? = when (op) { + "+" -> { + when (l) { + is String -> l.toString() + toString(r) + is Iterable<*> -> toIterable(l) + toIterable(r) + else -> toDouble(l) + toDouble(r) + } + } + "-" -> toDouble(l) - toDouble(r) + "*" -> toDouble(l) * toDouble(r) + "/" -> toDouble(l) / toDouble(r) + "%" -> toDouble(l) % toDouble(r) + "**" -> toDouble(l).pow(toDouble(r)) + "&" -> toInt(l) and toInt(r) + "|" -> toInt(l) or toInt(r) + "^" -> toInt(l) xor toInt(r) + "&&" -> toBool(l) && toBool(r) + "||" -> toBool(l) || toBool(r) + "and" -> toBool(l) && toBool(r) + "or" -> toBool(l) || toBool(r) + "==" -> when { + l is Number && r is Number -> l.toDouble() == r.toDouble() + l is String || r is String -> l.toString() == r.toString() + else -> l == r + } + "!=" -> when { + l is Number && r is Number -> l.toDouble() != r.toDouble() + l is String || r is String -> l.toString() != r.toString() + else -> l != r + } + "===" -> l === r + "!==" -> l !== r + "<" -> compare(l, r) < 0 + "<=" -> compare(l, r) <= 0 + ">" -> compare(l, r) > 0 + ">=" -> compare(l, r) >= 0 + "in" -> contains(r, l) + "contains" -> contains(l, r) + "?:" -> if (toBool(l)) l else r + else -> error("Not implemented binary operator '$op'") + } + + fun unop(r: Any?, op: String): Any? = when (op) { + "+" -> r + "-" -> -toDouble(r) + "~" -> toInt(r).inv() + "!" -> !toBool(r) + else -> error("Not implemented unary operator $op") + } + + fun contains(collection: Any?, element: Any?): Boolean { + if (collection == element) return true + return when (collection) { + is String -> collection.contains(element.toString()) + is Set<*> -> element in collection + else -> element in toList(collection) + } + } + + fun compare(l: Any?, r: Any?): Int { + if (l is Number && r is Number) { + return l.toDouble().compareTo(r.toDouble()) + } + val lc = toComparable(l) + val rc = toComparable(r) + if (lc::class.isInstance(rc)) { + return lc.compareTo(rc) + } else { + return -1 + } + } + + @Suppress("UNCHECKED_CAST") + fun toComparable(it: Any?): Comparable = when (it) { + null -> 0 as Comparable + is Comparable<*> -> it as Comparable + else -> it.toString() as Comparable + } + + fun toBool(it: Any?): Boolean = when (it) { + null -> false + else -> toBoolOrNull(it) ?: true + } + + fun toBoolOrNull(it: Any?): Boolean? = when (it) { + null -> null + is Boolean -> it + is Number -> it.toDouble() != 0.0 + is String -> it.isNotEmpty() && it != "0" && it != "false" + else -> null + } + + fun toNumber(it: Any?): Number = when (it) { + null -> 0.0 + is Number -> it + else -> it.toString().toNumber() + } + + fun String.toNumber(): Number = (this.toIntOrNull() as? Number?) ?: this.toDoubleOrNull() ?: Double.NaN + + fun toInt(it: Any?): Int = toNumber(it).toInt() + fun toLong(it: Any?): Long = toNumber(it).toLong() + fun toDouble(it: Any?): Double = toNumber(it).toDouble() + + fun toString(value: Any?): String = when (value) { + null -> "" + is String -> value + is Double -> { + if (value == value.toInt().toDouble()) { + value.toInt().toString() + } else { + value.toString() + } + } + is Iterable<*> -> "[" + value.map { toString(it) }.joinToString(", ") + "]" + is Map<*, *> -> "{" + value.map { toString(it.key).quote() + ": " + toString(it.value) }.joinToString(", ") + "}" + else -> value.toString() + } + + fun length(subject: Any?): Int = when (subject) { + null -> 0 + is Array<*> -> subject.size + is List<*> -> subject.size + is Map<*, *> -> subject.size + is Iterable<*> -> subject.count() + else -> subject.toString().length + } + + fun toList(it: Any?): List<*> = toIterable(it).toList() + + fun toIterable(it: Any?): Iterable<*> = when (it) { + null -> listOf() + //is Dynamic2Iterable -> it.dynamic2Iterate() + is Iterable<*> -> it + is CharSequence -> it.toList() + is Map<*, *> -> it.toList() + else -> listOf() + } + + suspend fun accessAny(instance: Any?, key: Any?, mapper: KorteObjectMapper2): Any? = mapper.accessAny(instance, key) + + suspend fun setAny(instance: Any?, key: Any?, value: Any?, mapper: KorteObjectMapper2): Unit { + when (instance) { + null -> Unit + is KorteDynamic2Settable -> { + instance.dynamic2Set(key, value) + Unit + } + is MutableMap<*, *> -> { + (instance as MutableMap).set(key, value) + Unit + } + is MutableList<*> -> { + (instance as MutableList)[toInt(key)] = value + Unit + } + else -> { + KorteDynamicContext { + when { + mapper.hasProperty(instance, key.toDynamicString()) -> { + mapper.set(instance, key, value) + Unit + } + mapper.hasMethod(instance, key.toDynamicString()) -> { + mapper.invokeAsync( + instance::class as KClass, + instance as Any?, + key.toDynamicString(), + listOf(value) + ) + Unit + } + else -> Unit + } + } + Unit + } + } + } + + suspend fun callAny(any: Any?, args: List, mapper: KorteObjectMapper2): Any? = + callAny(any, "invoke", args, mapper = mapper) + + suspend fun callAny(any: Any?, methodName: Any?, args: List, mapper: KorteObjectMapper2): Any? = when (any) { + null -> null + (any is KorteDynamic2Callable) -> (any as KorteDynamic2Callable).dynamic2Call(methodName, args) + else -> mapper.invokeAsync(any::class as KClass, any, KorteDynamicContext { methodName.toDynamicString() }, args) + } + + //fun dynamicCast(any: Any?, target: KClass<*>): Any? = TODO() +} + +interface KorteDynamicContext { + companion object { + @PublishedApi internal val Instance = object : KorteDynamicContext { } + + inline operator fun invoke(callback: KorteDynamicContext.() -> T): T = callback(Instance) + } + + operator fun Number.compareTo(other: Number): Int = this.toDouble().compareTo(other.toDouble()) + + fun combineTypes(a: Any?, b: Any?): Any? { + if (a == null || b == null) return null + if (a is Number && b is Number) { + if (a is Double || b is Double) return 0.0 + if (a is Float || b is Float) return 0f + if (a is Long || b is Long) return 0L + if (a is Int || b is Int) return 0 + return 0.0 + } + return a + } + + fun Any?.toDynamicCastToType(other: Any?) = when (other) { + is Boolean -> this.toDynamicBool() + is Int -> this.toDynamicInt() + is Long -> this.toDynamicLong() + is Float -> this.toDynamicDouble().toFloat() + is Double -> this.toDynamicDouble() + is String -> this.toDynamicString() + else -> this + } + fun Any?.toDynamicString() = KorteDynamic2.toString(this) + fun Any?.toDynamicBool() = KorteDynamic2.toBool(this) + fun Any?.toDynamicInt() = KorteDynamic2.toInt(this) + fun Any?.toDynamicLong() = KorteDynamic2.toLong(this) + fun Any?.toDynamicDouble() = KorteDynamic2.toDouble(this) + fun Any?.toDynamicNumber() = KorteDynamic2.toNumber(this) + fun Any?.toDynamicList() = KorteDynamic2.toList(this) + fun Any?.dynamicLength() = KorteDynamic2.length(this) + // @TODO: Bug JVM IR 1.5.0-RC: https://youtrack.jetbrains.com/issue/KT-46223 + suspend fun Any?.dynamicGet(key: Any?, mapper: KorteObjectMapper2): Any? = KorteDynamic2.accessAny(this, key, mapper) + + // @TODO: Bug JVM IR 1.5.0-RC: https://youtrack.jetbrains.com/issue/KT-46223 + suspend fun Any?.dynamicSet(key: Any?, value: Any?, mapper: KorteObjectMapper2) = + KorteDynamic2.setAny(this, key, value, mapper) + + // @TODO: Bug JVM IR 1.5.0-RC: https://youtrack.jetbrains.com/issue/KT-46223 + suspend fun Any?.dynamicCall(vararg args: Any?, mapper: KorteObjectMapper2) = + KorteDynamic2.callAny(this, args.toList(), mapper = mapper) + + // @TODO: Bug JVM IR 1.5.0-RC: https://youtrack.jetbrains.com/issue/KT-46223 + suspend fun Any?.dynamicCallMethod(methodName: Any?, vararg args: Any?, mapper: KorteObjectMapper2) = + KorteDynamic2.callAny(this, methodName, args.toList(), mapper = mapper) +//suspend internal fun Any?.dynamicCastTo(target: KClass<*>) = Dynamic2.dynamicCast(this, target) + +} + +interface KorteDynamic2Gettable { + suspend fun dynamic2Get(key: Any?): Any? +} + +interface KorteDynamic2Settable { + suspend fun dynamic2Set(key: Any?, value: Any?) +} + +interface KorteDynamic2Callable { + suspend fun dynamic2Call(methodName: Any?, params: List): Any? +} + +//interface Dynamic2Iterable { +// suspend fun dynamic2Iterate(): Iterable +//} + +interface KorteDynamicShapeRegister { + fun register(prop: KProperty<*>): KorteDynamicShapeRegister + fun register(callable: KCallable<*>): KorteDynamicShapeRegister + fun register(name: String, callback: suspend T.(args: List) -> Any?): KorteDynamicShapeRegister + fun register(vararg items: KProperty<*>) = this.apply { for (item in items) register(item) } + fun register(vararg items: KCallable<*>, dummy: Unit = Unit) = this.apply { for (item in items) register(item) } +} + +class KorteDynamicShape : KorteDynamicShapeRegister { + private val propertiesByName = LinkedHashMap>() + private val methodsByName = LinkedHashMap>() + private val smethodsByName = LinkedHashMap) -> Any?>() + + override fun register(prop: KProperty<*>) = this.apply { propertiesByName[prop.name] = prop } + override fun register(name: String, callback: suspend T.(args: List) -> Any?): KorteDynamicShapeRegister = this.apply { smethodsByName[name] = callback } + override fun register(callable: KCallable<*>) = this.apply { methodsByName[callable.name] = callable } + + fun hasProp(key: String): Boolean = key in propertiesByName + fun hasMethod(key: String): Boolean = key in methodsByName || key in smethodsByName + fun getProp(instance: T, key: Any?): Any? = (propertiesByName[key] as? KProperty1?)?.get(instance) + fun setProp(instance: T, key: Any?, value: Any?) { (propertiesByName[key] as? KMutableProperty1?)?.set(instance, value) } + + @Suppress("RedundantSuspendModifier") + suspend fun callMethod(instance: T, key: Any?, args: List): Any? { + val smethod = smethodsByName[key] + if (smethod != null) { + return smethod(instance, args) + } + + val method = methodsByName[key] + if (method != null) { + //println("METHOD: ${method.name} : $method : ${method::class}") + return when (method) { + is KFunction0<*> -> method.invoke() + is KFunction1<*, *> -> (method as KFunction1).invoke(instance) + is KFunction2<*, *, *> -> (method as KFunction2).invoke(instance, args[0]) + is KFunction3<*, *, *, *> -> (method as KFunction3).invoke(instance, args[0], args[1]) + is KFunction4<*, *, *, *, *> -> (method as KFunction4).invoke(instance, args[0], args[1], args[2]) + else -> error("TYPE not a KFunction") + } + } + + //println("Can't find method: $key in $instance :: smethods=$smethodsByName, methods=$methodsByName") + return null + } +} + +object KorteDynamicTypeScope + +fun KorteDynamicType(callback: KorteDynamicShapeRegister.() -> Unit): KorteDynamicType = object : KorteDynamicType { + val shape = KorteDynamicShape().apply(callback) + override val KorteDynamicTypeScope.__dynamicShape: KorteDynamicShape get() = shape +} + +interface KorteDynamicType { + val KorteDynamicTypeScope.__dynamicShape: KorteDynamicShape +} + +@Suppress("UNCHECKED_CAST") +interface KorteObjectMapper2 { + val KorteDynamicType<*>.dynamicShape: KorteDynamicShape get() = this.run { KorteDynamicTypeScope.run { this.__dynamicShape as KorteDynamicShape } } + + fun hasProperty(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasProp(key) + return false + } + fun hasMethod(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasMethod(key) + return false + } + suspend fun invokeAsync(type: KClass, instance: Any?, key: String, args: List): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.callMethod(instance, key, args) + return null + } + suspend fun set(instance: Any, key: Any?, value: Any?) { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.setProp(instance, key, value) + } + suspend fun get(instance: Any, key: Any?): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.getProp(instance, key) + return null + } + suspend fun accessAny(instance: Any?, key: Any?): Any? = when (instance) { + null -> null + is KorteDynamic2Gettable -> instance.dynamic2Get(key) + is Map<*, *> -> instance[key] + is Iterable<*> -> instance.toList()[KorteDynamic2.toInt(key)] + else -> accessAnyObject(instance, key) + } + suspend fun accessAnyObject(instance: Any?, key: Any?): Any? { + if (instance == null) return null + val keyStr = KorteDynamicContext { key.toDynamicString() } + return when { + hasProperty(instance, keyStr) -> { + //println("Access dynamic property : $keyStr") + get(instance, key) + } + hasMethod(instance, keyStr) -> { + //println("Access dynamic method : $keyStr") + invokeAsync(instance::class as KClass, instance as Any?, keyStr, listOf()) + } + else -> { + //println("Access dynamic null : '$keyStr'") + null + } + } + } +} + +expect val KorteMapper2: KorteObjectMapper2 diff --git a/korlibs-template/src/korlibs/template/_Template.internal.kt b/korlibs-template/src/korlibs/template/_Template.internal.kt new file mode 100644 index 0000000..60339b8 --- /dev/null +++ b/korlibs-template/src/korlibs/template/_Template.internal.kt @@ -0,0 +1,265 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.internal + +import korlibs.template.util.KorteDeferred +import kotlinx.atomicfu.locks.* +import kotlin.coroutines.coroutineContext +import kotlin.reflect.* + +internal typealias Lock = SynchronizedObject +internal inline operator fun Lock.invoke(block: () -> T): T = synchronized(this@invoke) { block() } + +internal class KorteAsyncCache { + private val lock = Lock() + @PublishedApi + internal val deferreds = LinkedHashMap>() + + fun invalidateAll() { + lock { deferreds.clear() } + } + + @Suppress("UNCHECKED_CAST") + suspend operator fun invoke(key: String, gen: suspend () -> T): T { + val ctx = coroutineContext + val deferred = + lock { (deferreds.getOrPut(key) { KorteDeferred.asyncImmediately(ctx) { gen() } } as KorteDeferred) } + return deferred.await() + } + + suspend fun call(key: String, gen: suspend () -> T): T { + return invoke(key, gen) + } +} + +internal class korteExtraProperty(val getExtraMap: R.() -> MutableMap, val name: String? = null, val default: () -> T) { + inline operator fun getValue(thisRef: R, property: KProperty<*>): T = + getExtraMap(thisRef)[name ?: property.name] as T? ?: default() + + inline operator fun setValue(thisRef: R, property: KProperty<*>, value: T) { + getExtraMap(thisRef)[name ?: property.name] = value + } +} + +internal class KorteStrReader(val str: String, var pos: Int = 0) { + val length get() = str.length + val hasMore get() = pos < length + + inline fun skipWhile(f: (Char) -> Boolean) { while (hasMore && f(peek())) skip() } + fun skipUntil(f: (Char) -> Boolean): Unit = skipWhile { !f(it) } + + // @TODO: https://youtrack.jetbrains.com/issue/KT-29577 + private fun posSkip(count: Int): Int { + val out = this.pos + this.pos += count + return out + } + + fun skip() = skip(1) + fun peekChar(): Char = if (hasMore) this.str[this.pos] else '\u0000' + fun peek(): Char = if (hasMore) this.str[this.pos] else '\u0000' + fun read(): Char = if (hasMore) this.str[posSkip(1)] else '\u0000' + fun unread() = skip(-1) + + fun substr(start: Int, len: Int = length - pos): String { + val start = (start).coerceIn(0, length) + val end = (start + len).coerceIn(0, length) + return this.str.substring(start, end) + } + + fun skip(count: Int) = this.apply { this.pos += count } + fun peek(count: Int): String = this.substr(this.pos, count) + fun read(count: Int): String = this.peek(count).also { skip(count) } + + fun readUntil(v: Char): String? { + val start = pos + skipUntil { it == v } + val end = pos + return if (hasMore) this.str.substring(start, end) else null + } + + private inline fun readBlock(callback: () -> Unit): String { + val start = pos + callback() + val end = pos + return substr(start, end - start) + } + + fun skipSpaces() = skipWhile { it.isWhitespaceFast() } + fun readWhile(f: (Char) -> Boolean): String = readBlock { skipWhile(f) } + fun readUntil(f: (Char) -> Boolean): String = readBlock { skipUntil(f) } +} + +internal fun KorteStrReader.readStringLit(reportErrors: Boolean = true): String { + val out = StringBuilder() + val quotec = read() + when (quotec) { + '"', '\'' -> Unit + else -> throw RuntimeException("Invalid string literal") + } + var closed = false + while (hasMore) { + val c = read() + if (c == '\\') { + val cc = read() + out.append( + when (cc) { + '\\' -> '\\'; '/' -> '/'; '\'' -> '\''; '"' -> '"' + 'b' -> '\b'; 'f' -> '\u000c'; 'n' -> '\n'; 'r' -> '\r'; 't' -> '\t' + 'u' -> read(4).toInt(0x10).toChar() + else -> throw RuntimeException("Invalid char '$cc'") + } + ) + } else if (c == quotec) { + closed = true + break + } else { + out.append(c) + } + } + if (!closed && reportErrors) { + throw RuntimeException("String literal not closed! '${this.str}'") + } + return out.toString() +} + +internal fun Char.isWhitespaceFast(): Boolean = this == ' ' || this == '\t' || this == '\r' || this == '\n' + +internal infix fun Int.umod(other: Int): Int { + val rm = this % other + val remainder = if (rm == -0) 0 else rm + return when { + remainder < 0 -> remainder + other + else -> remainder + } +} + +internal fun Char.isLetterDigitOrUnderscore(): Boolean = this.isLetterOrDigit() || this == '_' || this == '$' +internal fun Char.isPrintable(): Boolean = this in '\u0020'..'\u007e' || this in '\u00a1'..'\u00ff' + +internal const val HEX_DIGITS_LOWER = "0123456789abcdef" +internal fun String.isQuoted(): Boolean = this.startsWith('"') && this.endsWith('"') +internal fun String?.quote(): String = if (this != null) "\"${this.escape()}\"" else "null" +internal fun String.unquote(): String = if (isQuoted()) this.substring(1, this.length - 1).unescape() else this +internal fun String._escape(unicode: Boolean): String { + val out = StringBuilder(this.length + 16) + for (c in this) { + when (c) { + '\\' -> out.append("\\\\") + '"' -> out.append("\\\"") + '\n' -> out.append("\\n") + '\r' -> out.append("\\r") + '\t' -> out.append("\\t") + else -> when { + !unicode && c in '\u0000'..'\u001f' -> { + out.append("\\x") + out.append(HEX_DIGITS_LOWER[(c.code ushr 4) and 0xF]) + out.append(HEX_DIGITS_LOWER[(c.code ushr 0) and 0xF]) + } + unicode && !c.isPrintable() -> { + out.append("\\u") + out.append(HEX_DIGITS_LOWER[(c.code ushr 12) and 0xF]) + out.append(HEX_DIGITS_LOWER[(c.code ushr 8) and 0xF]) + out.append(HEX_DIGITS_LOWER[(c.code ushr 4) and 0xF]) + out.append(HEX_DIGITS_LOWER[(c.code ushr 0) and 0xF]) + } + else -> out.append(c) + } + } + } + return out.toString() +} +internal fun String.escape(): String = _escape(unicode = false) +internal fun String.escapeUnicode(): String = _escape(unicode = true) +internal fun String.unescape(): String { + val out = StringBuilder(this.length) + var n = 0 + while (n < this.length) { + val c = this[n++] + when (c) { + '\\' -> { + val c2 = this[n++] + when (c2) { + '\\' -> out.append('\\') + '"' -> out.append('\"') + 'n' -> out.append('\n') + 'r' -> out.append('\r') + 't' -> out.append('\t') + 'x', 'u' -> { + val N = if (c2 == 'u') 4 else 2 + val chars = this.substring(n, n + N) + n += N + out.append(chars.toInt(16).toChar()) + } + else -> { + out.append("\\$c2") + } + } + } + else -> out.append(c) + } + } + return out.toString() +} + +internal fun String.htmlspecialchars(): String = buildString(this@htmlspecialchars.length + 16) { + for (it in this@htmlspecialchars) { + when (it) { + '"' -> append(""") + '\'' -> append("'") + '<' -> append("<") + '>' -> append(">") + '&' -> append("&") + else -> append(it) + } + } +} +internal fun Json_stringify(value: Any?): String = buildString(128) { this.jsonStringify(value) } +internal fun StringBuilder.jsonStringify(value: Any?) { + when (value) { + null -> append("null") + is Boolean -> append(value == true) + is Number -> append(value) + is String -> append('"').append(value.escapeUnicode()).append('"') + is Iterable<*> -> { + append('[') + var first = true + for (v in value) { + if (!first) append(',') + jsonStringify(v) + first = false + } + append(']') + } + is Map<*, *> -> { + append('{') + var first = true + for ((k, v) in value) { + if (!first) append(',') + jsonStringify(k.toString()) + append(':') + jsonStringify(v) + first = false + } + append('}') + } + else -> TODO() + } +} + +internal class Pool(val gen: () -> T) { + private val allocated = arrayListOf() + fun alloc(): T = if (allocated.isNotEmpty()) allocated.removeLast() else gen() + fun free(value: T): Unit = run { allocated.add(value) } + inline fun alloc(block: (T) -> R): R { + val v = alloc() + try { + return block(v) + } finally { + free(v) + } + } +} + +internal val invalidOp: Nothing get() = throw RuntimeException() +internal fun invalidOp(msg: String): Nothing = throw RuntimeException(msg) diff --git a/korlibs-template/src/korlibs/template/_Template.util.kt b/korlibs-template/src/korlibs/template/_Template.util.kt new file mode 100644 index 0000000..e76cb60 --- /dev/null +++ b/korlibs-template/src/korlibs/template/_Template.util.kt @@ -0,0 +1,82 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.util + +import korlibs.template.internal.* +import kotlin.coroutines.* + +interface KorteAsyncTextWriterContainer { + suspend fun write(writer: suspend (String) -> Unit) +} + +class KorteDeferred { + private val lock = Lock() + private var result: Result? = null + private val continuations = arrayListOf>() + + fun completeWith(result: Result) { + //println("completeWith: $result") + lock { + this.result = result + } + resolveIfRequired() + } + + fun completeExceptionally(t: Throwable) = completeWith(Result.failure(t)) + fun complete(value: T) = completeWith(Result.success(value)) + + // @TODO: Cancellable? + suspend fun await(): T = suspendCoroutine { c -> + lock { + continuations += c + } + //println("await:$c") + resolveIfRequired() + } + + private fun resolveIfRequired() { + val result = lock { result } + if (result != null) { + for (v in lock { + if (continuations.isEmpty()) emptyList() else continuations.toList().also { continuations.clear() } + }) { + //println("resume:$v") + v.resumeWith(result) + } + } + } + + fun toContinuation(coroutineContext: CoroutineContext) = object : Continuation { + override val context: CoroutineContext = coroutineContext + override fun resumeWith(result: Result) = completeWith(result) + } + + companion object { + fun asyncImmediately(coroutineContext: CoroutineContext, callback: suspend () -> T): KorteDeferred = + KorteDeferred().also { deferred -> + callback.startCoroutine(object : Continuation { + override val context: CoroutineContext = coroutineContext + override fun resumeWith(result: Result) = deferred.completeWith(result) + }) + } + } +} + +class KorteListReader constructor(val list: List, val ctx: T? = null) { + class OutOfBoundsException(val list: KorteListReader<*>, val pos: Int) : RuntimeException() + + var position = 0 + val size: Int get() = list.size + val eof: Boolean get() = position >= list.size + val hasMore: Boolean get() = position < list.size + fun peekOrNull(): T? = list.getOrNull(position) + fun peek(): T = list.getOrNull(position) ?: throw OutOfBoundsException(this, position) + fun tryPeek(ahead: Int): T? = list.getOrNull(position + ahead) + fun skip(count: Int = 1) = this.apply { this.position += count } + fun read(): T = peek().apply { skip(1) } + fun tryPrev(): T? = list.getOrNull(position - 1) + fun prev(): T = tryPrev() ?: throw OutOfBoundsException(this, position - 1) + fun tryRead(): T? = if (hasMore) read() else null + fun prevOrContext(): T = tryPrev() ?: ctx ?: throw TODO("Context not defined") + override fun toString(): String = "ListReader($list)" +} diff --git a/korlibs-template/src@android/korlibs/template/_Template.dynamic.jvm.kt b/korlibs-template/src@android/korlibs/template/_Template.dynamic.jvm.kt new file mode 100644 index 0000000..3c9c4bb --- /dev/null +++ b/korlibs-template/src@android/korlibs/template/_Template.dynamic.jvm.kt @@ -0,0 +1,114 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.dynamic + +import korlibs.template.util.KorteDeferred +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +open class JvmObjectMapper2 : KorteObjectMapper2 { + class ClassReflectCache(val clazz: KClass) { + data class MyProperty( + val name: String, + val getter: Method? = null, + val setter: Method? = null, + val field: Field? = null + ) + + val jclass = clazz.java + val methodsByName = jclass.allDeclaredMethods.associateBy { it.name } + val fieldsByName = jclass.allDeclaredFields.associateBy { it.name } + val potentialPropertyNamesFields = jclass.allDeclaredFields.map { it.name } + val potentialPropertyNamesGetters = + jclass.allDeclaredMethods.filter { it.name.startsWith("get") }.map { it.name.substring(3).decapitalize() } + val potentialPropertyNames = (potentialPropertyNamesFields + potentialPropertyNamesGetters).toSet() + val propByName = potentialPropertyNames.map { propName -> + MyProperty( + propName, + methodsByName["get${propName.capitalize()}"], + methodsByName["set${propName.capitalize()}"], + fieldsByName[propName] + ) + }.associateBy { it.name } + } + + val KClass<*>.classInfo by WeakPropertyThis, ClassReflectCache<*>> { ClassReflectCache(this) } + + override fun hasProperty(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasProp(key) + return key in instance::class.classInfo.propByName + } + + override fun hasMethod(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasMethod(key) + return instance::class.classInfo.methodsByName[key] != null + } + + override suspend fun invokeAsync(type: KClass, instance: Any?, key: String, args: List): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.callMethod(instance, key, args) + val method = type.classInfo.methodsByName[key] ?: return null + return method.invokeSuspend(instance, args) + } + + override suspend fun set(instance: Any, key: Any?, value: Any?) { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.setProp(instance, key, value) + val prop = instance::class.classInfo.propByName[key] ?: return + when { + prop.setter != null -> prop.setter.invoke(instance, value) + prop.field != null -> prop.field.set(instance, value) + else -> Unit + } + } + + override suspend fun get(instance: Any, key: Any?): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.getProp(instance, key) + val prop = instance::class.classInfo.propByName[key] ?: return null + return when { + prop.getter != null -> prop.getter.invoke(instance) + prop.field != null -> prop.field.get(instance) + else -> null + } + } +} + +private class WeakPropertyThis(val gen: T.() -> V) { + val map = WeakHashMap() + + operator fun getValue(obj: T, property: KProperty<*>): V = map.getOrPut(obj) { gen(obj) } + operator fun setValue(obj: T, property: KProperty<*>, value: V) { map[obj] = value } +} + +private val Class<*>.allDeclaredFields: List + get() = this.declaredFields.toList() + (this.superclass?.allDeclaredFields?.toList() ?: listOf()) + +private fun Class<*>.isSubtypeOf(that: Class<*>) = that.isAssignableFrom(this) + +private val Class<*>.allDeclaredMethods: List + get() = this.declaredMethods.toList() + (this.superclass?.allDeclaredMethods?.toList() ?: listOf()) + +suspend fun Method.invokeSuspend(obj: Any?, args: List): Any? { + val method = this@invokeSuspend + val cc = coroutineContext + + val lastParam = method.parameterTypes.lastOrNull() + val margs = java.util.ArrayList(args) + var deferred: KorteDeferred? = null + + if (lastParam != null && lastParam.isAssignableFrom(Continuation::class.java)) { + deferred = KorteDeferred() + margs += deferred.toContinuation(cc) + } + val result = method.invoke(obj, *margs.toTypedArray()) + return when (result) { + COROUTINE_SUSPENDED -> deferred?.await() + else -> result + } +} + +actual val KorteMapper2: KorteObjectMapper2 = JvmObjectMapper2() diff --git a/korlibs-template/src@js/korlibs/template/_Template.dynamic.js.kt b/korlibs-template/src@js/korlibs/template/_Template.dynamic.js.kt new file mode 100644 index 0000000..57f21ce --- /dev/null +++ b/korlibs-template/src@js/korlibs/template/_Template.dynamic.js.kt @@ -0,0 +1,54 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.dynamic + +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.reflect.KClass + +open class JsObjectMapper2 : KorteObjectMapper2 { + override fun hasProperty(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasProp(key) + val tof = jsTypeOf(instance.asDynamic()[key]) + return tof !== "undefined" && tof !== "function" + } + + override fun hasMethod(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasMethod(key) + return jsTypeOf(instance.asDynamic()[key]) !== "undefined" + } + + override suspend fun invokeAsync(type: KClass, instance: Any?, key: String, args: List): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.callMethod(instance, key, args) + val function = instance.asDynamic()[key] ?: return super.invokeAsync(type, instance, key, args) + //val function = instance.asDynamic()[key] ?: return null + return suspendCoroutine { c -> + val arity: Int = function.length.unsafeCast() + val rargs = when { + args.size != arity -> args + listOf(c) + else -> args + } + try { + val result = function.apply(instance, rargs.toTypedArray()) + if (result != kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) { + c.resume(result) + } + } catch (e: Throwable) { + c.resumeWithException(e) + } + } + } + + override suspend fun set(instance: Any, key: Any?, value: Any?) { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.setProp(instance, key, value) + instance.asDynamic()[key] = value + } + + override suspend fun get(instance: Any, key: Any?): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.getProp(instance, key) + return instance.asDynamic()[key] + } +} + +actual val KorteMapper2: KorteObjectMapper2 = JsObjectMapper2() diff --git a/korlibs-template/src@jvm/korlibs/template/_Template.dynamic.jvm.kt b/korlibs-template/src@jvm/korlibs/template/_Template.dynamic.jvm.kt new file mode 100644 index 0000000..55bb1b0 --- /dev/null +++ b/korlibs-template/src@jvm/korlibs/template/_Template.dynamic.jvm.kt @@ -0,0 +1,132 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.dynamic + +import korlibs.template.util.KorteDeferred +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +open class JvmObjectMapper2 : KorteObjectMapper2 { + class ClassReflectCache(val clazz: KClass) { + data class MyProperty( + val name: String, + val getter: Method? = null, + val setter: Method? = null, + val field: Field? = null + ) + + val jclass = clazz.java + val methodsByName = jclass.allDeclaredMethods.associateBy { it.name } + val fieldsByName = jclass.allDeclaredFields.associateBy { it.name } + val potentialPropertyNamesFields = jclass.allDeclaredFields.map { it.name } + val potentialPropertyNamesGetters = + jclass.allDeclaredMethods.filter { it.name.startsWith("get") }.map { it.name.substring(3).decapitalize() } + val potentialPropertyNames = (potentialPropertyNamesFields + potentialPropertyNamesGetters).toSet() + + //Matches Strings that start with "is" followed by a capital letter + private val startsWithIs = Regex("^is[A-Z]") + + val propByName = potentialPropertyNames.map { propName -> + + + //In case propertyName matches startsWithIs, the underlying getter/setter methods are called differently + //e.g. if the variable is called `isX`, the getter is called `isX` (and not `getIsX`), and the setter is called `setX` (and not `setIsX`) + if (startsWithIs in propName) { + MyProperty( + propName, + methodsByName[propName], + methodsByName["set${propName.removePrefix("is")}"], + fieldsByName[propName] + ) + } else { + MyProperty( + propName, + methodsByName["get${propName.capitalize()}"], + methodsByName["set${propName.capitalize()}"], + fieldsByName[propName] + ) + } + + }.associateBy { it.name } + } + + val KClass<*>.classInfo by WeakPropertyThis, ClassReflectCache<*>> { ClassReflectCache(this) } + + override fun hasProperty(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasProp(key) + return key in instance::class.classInfo.propByName + } + + override fun hasMethod(instance: Any, key: String): Boolean { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.hasMethod(key) + return instance::class.classInfo.methodsByName[key] != null + } + + override suspend fun invokeAsync(type: KClass, instance: Any?, key: String, args: List): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.callMethod(instance, key, args) + val method = type.classInfo.methodsByName[key] ?: return null + return method.invokeSuspend(instance, args) + } + + override suspend fun set(instance: Any, key: Any?, value: Any?) { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.setProp(instance, key, value) + val prop = instance::class.classInfo.propByName[key] ?: return + when { + prop.setter != null -> prop.setter.invoke(instance, value) + prop.field != null -> prop.field.set(instance, value) + else -> Unit + } + } + + override suspend fun get(instance: Any, key: Any?): Any? { + if (instance is KorteDynamicType<*>) return instance.dynamicShape.getProp(instance, key) + val prop = instance::class.classInfo.propByName[key] ?: return null + return when { + prop.getter != null -> prop.getter.invoke(instance) + prop.field != null -> prop.field.get(instance) + else -> null + } + } +} + +private class WeakPropertyThis(val gen: T.() -> V) { + val map = WeakHashMap() + + operator fun getValue(obj: T, property: KProperty<*>): V = map.getOrPut(obj) { gen(obj) } + operator fun setValue(obj: T, property: KProperty<*>, value: V) { map[obj] = value } +} + +private val Class<*>.allDeclaredFields: List + get() = this.declaredFields.toList() + (this.superclass?.allDeclaredFields?.toList() ?: listOf()) + +private fun Class<*>.isSubtypeOf(that: Class<*>) = that.isAssignableFrom(this) + +private val Class<*>.allDeclaredMethods: List + get() = this.declaredMethods.toList() + (this.superclass?.allDeclaredMethods?.toList() ?: listOf()) + +suspend fun Method.invokeSuspend(obj: Any?, args: List): Any? { + val method = this@invokeSuspend + val cc = coroutineContext + + val lastParam = method.parameterTypes.lastOrNull() + val margs = java.util.ArrayList(args) + var deferred: KorteDeferred? = null + + if (lastParam != null && lastParam.isAssignableFrom(Continuation::class.java)) { + deferred = KorteDeferred() + margs += deferred.toContinuation(cc) + } + val result = method.invoke(obj, *margs.toTypedArray()) + return when (result) { + COROUTINE_SUSPENDED -> deferred?.await() + else -> result + } +} + +actual val KorteMapper2: KorteObjectMapper2 = JvmObjectMapper2() diff --git a/korlibs-template/src@native/korlibs/template/_Template.dynamic.native.kt b/korlibs-template/src@native/korlibs/template/_Template.dynamic.native.kt new file mode 100644 index 0000000..5e44631 --- /dev/null +++ b/korlibs-template/src@native/korlibs/template/_Template.dynamic.native.kt @@ -0,0 +1,14 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.dynamic + +// @TODO: Hopefully someday: https://github.com/Kotlin/kotlinx.serialization/tree/dev +open class NativeObjectMapper2 : KorteObjectMapper2 { + //override fun hasProperty(instance: Any, key: String): Boolean = TODO("Not supported in native yet") + //override fun hasMethod(instance: Any, key: String): Boolean = TODO("Not supported in native yet") + //override suspend fun invokeAsync(type: KClass, instance: Any?, key: String, args: List) = TODO("Not supported in native yet") + //override suspend fun set(instance: Any, key: Any?, value: Any?) = TODO("Not supported in native yet") + //override suspend fun get(instance: Any, key: Any?): Any? = TODO("Not supported in native yet") +} + +actual val KorteMapper2: KorteObjectMapper2 = NativeObjectMapper2() diff --git a/korlibs-template/src@wasm/korlibs/template/_Template.dynamic.wasm.kt b/korlibs-template/src@wasm/korlibs/template/_Template.dynamic.wasm.kt new file mode 100644 index 0000000..5e44631 --- /dev/null +++ b/korlibs-template/src@wasm/korlibs/template/_Template.dynamic.wasm.kt @@ -0,0 +1,14 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.template.dynamic + +// @TODO: Hopefully someday: https://github.com/Kotlin/kotlinx.serialization/tree/dev +open class NativeObjectMapper2 : KorteObjectMapper2 { + //override fun hasProperty(instance: Any, key: String): Boolean = TODO("Not supported in native yet") + //override fun hasMethod(instance: Any, key: String): Boolean = TODO("Not supported in native yet") + //override suspend fun invokeAsync(type: KClass, instance: Any?, key: String, args: List) = TODO("Not supported in native yet") + //override suspend fun set(instance: Any, key: Any?, value: Any?) = TODO("Not supported in native yet") + //override suspend fun get(instance: Any, key: Any?): Any? = TODO("Not supported in native yet") +} + +actual val KorteMapper2: KorteObjectMapper2 = NativeObjectMapper2() diff --git a/korlibs-template/test/korlibs/template/BaseTest.kt b/korlibs-template/test/korlibs/template/BaseTest.kt new file mode 100644 index 0000000..3b63a01 --- /dev/null +++ b/korlibs-template/test/korlibs/template/BaseTest.kt @@ -0,0 +1,27 @@ +package korlibs.template + +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +open class BaseTest { + +} + +inline fun expectException(message: String, callback: () -> Unit) { + var exception: Throwable? = null + try { + callback() + } catch (e: Throwable) { + exception = e + } + val e = exception + if (e != null) { + if (e is T) { + assertEquals(message, e.message) + } else { + throw e + } + } else { + assertTrue(false, "Expected ${T::class} with message '$message'") + } +} diff --git a/korlibs-template/test/korlibs/template/TemplateInheritanceTest.kt b/korlibs-template/test/korlibs/template/TemplateInheritanceTest.kt new file mode 100644 index 0000000..d3b7781 --- /dev/null +++ b/korlibs-template/test/korlibs/template/TemplateInheritanceTest.kt @@ -0,0 +1,199 @@ +package korlibs.template + +import kotlin.test.Test +import kotlin.test.assertEquals + +class TemplateInheritanceTest { + @Test + fun simple() = suspendTest { + assertEquals( + "hello", + KorteTemplates( + KorteTemplateProvider( + "a" to "hello" + ) + ).get("a").invoke() + ) + } + + @Test + fun block() = suspendTest { + assertEquals( + "hello", + KorteTemplates( + KorteTemplateProvider( + "a" to "{% block test %}hello{% end %}" + ) + ).get("a")() + ) + } + + @Test + fun extends() = suspendTest { + val template = KorteTemplates( + KorteTemplateProvider( + "a" to """{% block test %}a{% end %}""", + "b" to """{% extends "a" %}{% block test %}b{% end %}""" + ) + ).get("b") + assertEquals( + "b", + template() + ) + } + + @Test + fun doubleExtends() = suspendTest { + assertEquals( + "c", + KorteTemplates( + KorteTemplateProvider( + "a" to """{% block test %}a{% end %}""", + "b" to """{% extends "a" %}{% block test %}b{% end %}""", + "c" to """{% extends "b" %}{% block test %}c{% end %}""" + ) + ).get("c")() + ) + } + + @Test + fun blockParent() = suspendTest { + assertEquals( + "TEXT", + KorteTemplates( + KorteTemplateProvider( + "a" to """{% block test %}TEXT{% end %}""", + "b" to """{% extends "a" %}{% block test %}{{ parent() }}{% end %}""" + ) + ).get("b")() + ) + } + + @Test + fun blockDoubleParent() = suspendTest { + assertEquals( + "TEXT", + KorteTemplates( + KorteTemplateProvider( + "a" to """{% block test %}TEXT{% end %}""", + "b" to """{% extends "a" %}{% block test %}{{ parent() }}{% end %}""", + "c" to """{% extends "b" %}{% block test %}{{ parent() }}{% end %}""" + ) + ).get("c")() + ) + } + + @Test + fun nestedBlocks() = suspendTest { + assertEquals( + "left:LEFTright:RIGHT", + KorteTemplates( + KorteTemplateProvider( + "root" to """{% block main %}test{% end %}""", + "2column" to """{% extends "root" %} {% block main %}{% block left %}left{% end %}{% block right %}right{% end %}{% end %} """, + "mypage" to """{% extends "2column" %} {% block right %}{{ parent() }}:RIGHT{% end %} {% block left %}{{ parent() }}:LEFT{% end %} """ + ) + ).get("mypage")() + ) + } + + @Test + fun doubleExtends2() = suspendTest { + assertEquals( + "abcc", + KorteTemplates( + KorteTemplateProvider( + "a" to """{% block b1 %}a{% end %}{% block b2 %}a{% end %}{% block b3 %}a{% end %}{% block b4 %}a{% end %}""", + "b" to """{% extends "a" %}{% block b2 %}b{% end %}{% block b4 %}b{% end %}""", + "c" to """{% extends "b" %}{% block b3 %}c{% end %}{% block b4 %}c{% end %}""" + ) + ).get("c")() + ) + } + + @Test + fun include() = suspendTest { + assertEquals( + "Hello World, Carlos.", + KorteTemplates( + KorteTemplateProvider( + "include" to """World""", + "username" to """Carlos""", + "main" to """Hello {% include "include" %}, {% include "username" %}.""" + ) + ).get("main")() + ) + } + + @Test + fun includeWithParams() = suspendTest { + assertEquals( + "Hello World.", + KorteTemplates( + KorteTemplateProvider( + "include" to """{{ include.name }}""", + "main" to """Hello {% include "include" name="World" %}.""" + ) + ).get("main")() + ) + } + + @Test + fun jekyllLayout() = suspendTest { + assertEquals( + "Hello Carlos.", + KorteTemplates( + KorteTemplateProvider( + "mylayout" to """Hello {{ content }}.""", + "main" to """ + --- + layout: mylayout + name: Carlos + --- + {{ name }} + """.trimIndent() + ) + ).get("main")() + ) + } + + @Test + fun jekyllLayoutEx() = suspendTest { + assertEquals( + "
side

Content

", + KorteTemplates( + KorteNewTemplateProvider( + "root" to KorteTemplateContent(""" + {{ content }} + """.trimIndent()), + "twocolumns" to KorteTemplateContent(""" + --- + layout: root + --- +
side
{{ content }}
+ """.trimIndent()), + "main" to KorteTemplateContent(""" + --- + layout: twocolumns + mycontent: Content + --- +

{{ mycontent }}

+ """.trimIndent()) + ) + ).get("main")() + ) + } + + // @TODO: + //@Test + //fun operatorPrecedence() = sync { + // Assert.assertEquals("${2 + 3 * 5}", Template("{{ 1 + 2 * 3 }}")()) + // Assert.assertEquals("${2 * 3 + 5}", Template("{{ 2 * 3 + 5 }}")()) + //} + + @Test + fun operatorPrecedence() = suspendTest { + assertEquals("true", KorteTemplate("{{ 1 in [1, 2] }}")()) + assertEquals("false", KorteTemplate("{{ 3 in [1, 2] }}")()) + } +} diff --git a/korlibs-template/test/korlibs/template/TemplateTest.kt b/korlibs-template/test/korlibs/template/TemplateTest.kt new file mode 100644 index 0000000..1fc6a13 --- /dev/null +++ b/korlibs-template/test/korlibs/template/TemplateTest.kt @@ -0,0 +1,550 @@ +package korlibs.template + +import korlibs.template.dynamic.KorteDynamicContext +import korlibs.template.dynamic.KorteDynamicType +import korlibs.template.dynamic.KorteMapper2 +import korlibs.template.util.KorteDeferred +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals + +class TemplateTest : BaseTest() { + //@Reflect + data class Person(@JsName("name") val name: String, @JsName("surname") val surname: String) : + KorteDynamicType by KorteDynamicType({ register(Person::name, Person::surname) }) + + @Test + fun testDummy() = suspendTest { + assertEquals("hello", (KorteTemplate("hello"))(null)) + } + + @Test + fun testDoubleLiteral() = suspendTest { + assertEquals("1.1", (KorteTemplate("{{ 1.1 }}"))(null)) + } + + @Test + fun testChunked() = suspendTest { + assertEquals("[[1, 2], [3, 4], [5]]", (KorteTemplate("{{ [1, 2, 3, 4, 5]|chunked(2) }}"))(null)) + } + + @Test + fun testSwitch() = suspendTest { + val template = KorteTemplate( + """ + {% switch value %} + {% case "a" %}1 + {% case "b" %}2 + {% default %}3 + {% endswitch %} + """.trimIndent() + ) + assertEquals("1", template(mapOf("value" to "a")).trim()) + assertEquals("2", template(mapOf("value" to "b")).trim()) + assertEquals("3", template(mapOf("value" to "c")).trim()) + assertEquals("3", template(mapOf("value" to "d")).trim()) + } + + @Test + fun testSimple() = suspendTest { + assertEquals("hello soywiz", KorteTemplate("hello {{ name }}")("name" to "soywiz")) + assertEquals("soywizsoywiz", KorteTemplate("{{name}}{{ name }}")("name" to "soywiz")) + } + + @Test + fun testAnd() = suspendTest { + assertEquals("true", KorteTemplate("{{ 1 and 2 }}")()) + assertEquals("false", KorteTemplate("{{ 0 and 0 }}")()) + assertEquals("false", KorteTemplate("{{ 0 and 1 }}")()) + assertEquals("false", KorteTemplate("{{ 1 and 0 }}")()) + } + + @Test + fun testOr() = suspendTest { + assertEquals("true", KorteTemplate("{{ 1 or 2 }}")()) + assertEquals("false", KorteTemplate("{{ 0 or 0 }}")()) + assertEquals("true", KorteTemplate("{{ 0 or 1 }}")()) + assertEquals("true", KorteTemplate("{{ 1 or 0 }}")()) + } + + @Test + fun testIn() = suspendTest { + assertEquals("true", KorteTemplate("{{ 'soy' in name }}")("name" to "soywiz")) + assertEquals("false", KorteTemplate("{{ 'demo' in name }}")("name" to "soywiz")) + } + + @Test + fun testContains() = suspendTest { + assertEquals("true", KorteTemplate("{{ name contains 'soy' }}")("name" to "soywiz")) + assertEquals("false", KorteTemplate("{{ name contains 'demo' }}")("name" to "soywiz")) + } + + @Test + fun testFor() = suspendTest { + val tpl = KorteTemplate("{% for n in numbers %}{{ n }}{% end %}") + assertEquals("", tpl("numbers" to listOf())) + assertEquals("123", tpl("numbers" to listOf(1, 2, 3))) + } + + @Test + fun testForAdv() = suspendTest { + val tpl = + KorteTemplate("{% for n in numbers %}{{ n }}:{{ loop.index0 }}:{{ loop.index }}:{{ loop.revindex }}:{{ loop.revindex0 }}:{{ loop.first }}:{{ loop.last }}:{{ loop.length }}{{ '\\n' }}{% end %}") + assertEquals( + """ + a:0:1:2:3:true:false:3 + b:1:2:1:2:false:false:3 + c:2:3:0:1:false:true:3 + """.trimIndent().trim(), + tpl("numbers" to listOf("a", "b", "c")).trim() + ) + } + + @Test + fun testForMap() = suspendTest { + val tpl = KorteTemplate("{% for k, v in map %}{{ k }}:{{v}}{% end %}") + assertEquals("a:10b:c", tpl("map" to mapOf("a" to 10, "b" to "c"))) + } + + @Test + fun testForElse() = suspendTest { + val tpl = KorteTemplate("{% for n in numbers %}{{ n }}{% else %}none{% end %}") + assertEquals("123", tpl("numbers" to listOf(1, 2, 3))) + assertEquals("none", tpl("numbers" to listOf())) + } + + @Test + fun testForElse2() = suspendTest { + val tpl = KorteTemplate("{% for n in numbers %}[{{ n }}]{% else %}none{% end %}") + assertEquals("[1][2][3]", tpl("numbers" to listOf(1, 2, 3))) + assertEquals("none", tpl("numbers" to listOf())) + } + + @Test + fun testDebug() = suspendTest { + var result: String? = null + var stdout = "" + val tpl = KorteTemplate("a {% debug 'hello ' + name %} b", KorteTemplateConfig().apply { + debugPrintln = { stdout += "$it" } + }) + result = tpl("name" to "world") + assertEquals("hello world", stdout.trim()) + assertEquals("a b", result) + } + + @Test + fun testCapture() = suspendTest { + assertEquals("REPEAT and REPEAT", KorteTemplate("{% capture variable %}REPEAT{% endcapture %}{{ variable }} and {{ variable }}")()) + } + + @Test + fun testSimpleIf() = suspendTest { + assertEquals("true", KorteTemplate("{% if cond %}true{% else %}false{% end %}")("cond" to 1)) + assertEquals("false", KorteTemplate("{% if cond %}true{% else %}false{% end %}")("cond" to 0)) + assertEquals("true", KorteTemplate("{% if cond %}true{% end %}")("cond" to 1)) + assertEquals("", KorteTemplate("{% if cond %}true{% end %}")("cond" to 0)) + } + + @Test + fun testSimpleUnless() = suspendTest { + assertEquals("1false", KorteTemplate("{% unless cond %}1true{% else %}1false{% end %}")("cond" to 1)) + assertEquals("2true", KorteTemplate("{% unless cond %}2true{% else %}2false{% end %}")("cond" to 0)) + assertEquals("3true", KorteTemplate("{% unless !cond %}3true{% end %}")("cond" to 1)) + assertEquals("4true", KorteTemplate("{% unless cond %}4true{% end %}")("cond" to 0)) + assertEquals("", KorteTemplate("{% unless !cond %}5true{% end %}")("cond" to 0)) + } + + @Test + fun testNot() = suspendTest { + assertEquals("true", KorteTemplate("{% if not cond %}true{% end %}")("cond" to 0)) + } + + @Test + fun testSimpleElseIf() = suspendTest { + val tpl = + KorteTemplate("{% if v == 1 %}one{% elseif v == 2 %}two{% elsif v < 5 %}less than five{% elseif v > 8 %}greater than eight{% else %}other{% end %}") + assertEquals("one", tpl("v" to 1)) + assertEquals("two", tpl("v" to 2)) + assertEquals("less than five", tpl("v" to 3)) + assertEquals("less than five", tpl("v" to 4)) + assertEquals("other", tpl("v" to 5)) + assertEquals("other", tpl("v" to 6)) + assertEquals("greater than eight", tpl("v" to 9)) + } + + @Test + fun testEval() = suspendTest { + assertEquals("-5", KorteTemplate("{{ -(1 + 4) }}")(null)) + assertEquals("false", KorteTemplate("{{ 1 == 2 }}")(null)) + assertEquals("true", KorteTemplate("{{ 1 < 2 }}")(null)) + assertEquals("true", KorteTemplate("{{ 1 <= 1 }}")(null)) + } + + @Test + fun testExists() = suspendTest { + assertEquals("false", KorteTemplate("{% if prop %}true{% else %}false{% end %}")(null)) + assertEquals("true", KorteTemplate("{% if prop %}true{% else %}false{% end %}")("prop" to "any")) + assertEquals("false", KorteTemplate("{% if prop %}true{% else %}false{% end %}")("prop" to "")) + } + + @Test + fun testIfBooleanLiterals() = suspendTest { + assertEquals("true", KorteTemplate("{% if true %}true{% end %}")(null)) + assertEquals("false", KorteTemplate("{% if !false %}false{% end %}")(null)) + } + + @Test + fun testOverwriteFilter() = suspendTest { + assertEquals("HELLO", KorteTemplate("{{ 'hello' | upper }}")(null)) + assertEquals("[hello]", KorteTemplate("{{ 'hello' | upper }}", KorteTemplateConfig(extraFilters = listOf(KorteFilter("upper") { "[" + subject.toDynamicString() + "]" })))(null)) + } + + @Test + fun testCustomUnknownFilter() = suspendTest { + assertEquals("-ERROR-", KorteTemplate("{{ 'hello' | asdasdasdasdas }}", KorteTemplateConfig(extraFilters = listOf(KorteFilter("unknown") { "-ERROR-" })))(null)) + } + + @Test + fun testForAccess() = suspendTest { + assertEquals( + ":Zard:Ballesteros", + KorteTemplate("{% for n in persons %}:{{ n.surname }}{% end %}")( + "persons" to listOf( + Person("Soywiz", "Zard"), + Person("Carlos", "Ballesteros") + ) + ) + ) + assertEquals( + "ZardBallesteros", + KorteTemplate("{% for n in persons %}{{ n['sur'+'name'] }}{% end %}")( + "persons" to listOf( + Person( + "Soywiz", + "Zard" + ), Person("Carlos", "Ballesteros") + ) + ) + ) + assertEquals( + "ZardBallesteros", + KorteTemplate("{% for nin in persons %}{{ nin['sur'+'name'] }}{% end %}")( + "persons" to listOf( + Person( + "Soywiz", + "Zard" + ), Person("Carlos", "Ballesteros") + ) + ) + ) + } + + @Test + fun testStrictEquality() = suspendTest { + assertEquals("false", KorteTemplate("{{ '1' === 1 }}")()) + assertEquals("true", KorteTemplate("{{ '1' !== 1 }}")()) + } + + @Test + fun testEquality() = suspendTest { + assertEquals("true", KorteTemplate("{{ '1' == 1 }}")()) + assertEquals("false", KorteTemplate("{{ '1' == 0 }}")()) + } + + @Test + fun testFilters() = suspendTest { + assertEquals("CARLOS", KorteTemplate("{{ name|upper }}")("name" to "caRLos")) + assertEquals("carlos", KorteTemplate("{{ name|lower }}")("name" to "caRLos")) + assertEquals("Carlos", KorteTemplate("{{ name|capitalize }}")("name" to "caRLos")) + assertEquals("Carlos", KorteTemplate("{{ (name)|capitalize }}")("name" to "caRLos")) + assertEquals("Carlos", KorteTemplate("{{ 'caRLos'|capitalize }}")(null)) + assertEquals("hello KorTE", KorteTemplate("{{'hello world' | replace('world', 'KorTE')}}")(null)) + } + + @Test + fun testFilterArgument() = suspendTest { + assertEquals("[car, los]", KorteTemplate("{{ name | split: '|' }}")("name" to "car|los")) + } + + @Test + fun testArrayLiterals() = suspendTest { + assertEquals("1234", KorteTemplate("{% for n in [1, 2, 3, 4] %}{{ n }}{% end %}")(null)) + assertEquals("", KorteTemplate("{% for n in [] %}{{ n }}{% end %}")(null)) + assertEquals("1, 2, 3, 4", KorteTemplate("{{ [1, 2, 3, 4]|join(', ') }}")(null)) + } + + @Test + fun testElvis() = suspendTest { + assertEquals("1", KorteTemplate("{{ 1 ?: 2 }}")(null)) + assertEquals("2", KorteTemplate("{{ 0 ?: 2 }}")(null)) + } + + @Test + fun testMerge() = suspendTest { + assertEquals("[1, 2, 3, 4]", KorteTemplate("{{ [1, 2]|merge([3, 4]) }}")(null)) + } + + @Test + fun testJsonEncode() = suspendTest { + assertEquals("{\"a\":2}", KorteTemplate("{{ {'a': 2}|json_encode()|raw }}")(null)) + } + + @Test + fun testComment() = suspendTest { + assertEquals("a", KorteTemplate("{# {{ 1 }} #}a{# #}")(null)) + } + + @Test + fun testFormat() = suspendTest { + assertEquals("hello test of 3", KorteTemplate("{{ 'hello %s of %d'|format('test', 3) }}")(null)) + } + + @Test + fun testTernary() = suspendTest { + assertEquals("2", KorteTemplate("{{ 1 ? 2 : 3 }}")(null)) + assertEquals("3", KorteTemplate("{{ 0 ? 2 : 3 }}")(null)) + } + + @Test + fun testSet() = suspendTest { + assertEquals("1,2,3", KorteTemplate("{% set a = [1,2,3] %}{{ a|join(',') }}")(null)) + } + + @Test + fun testAccessGetter() = suspendTest { + val success = "success!" + + class Test1 : KorteDynamicType by KorteDynamicType({ register(Test1::a) }) { + @JsName("a") + val a: String get() = success + } + + assertEquals(success, KorteTemplate("{{ test.a }}")("test" to Test1())) + } + + @Test + fun testCustomTag() = suspendTest { + class CustomNode(val text: String) : KorteBlock { + override suspend fun eval(context: KorteTemplate.EvalContext) = context.write("CUSTOM($text)") + } + + val CustomTag = KorteTag("custom", setOf(), null) { + CustomNode(chunks.first().tag.content) + } + + assertEquals( + "CUSTOM(test)CUSTOM(demo)", + KorteTemplate("{% custom test %}{% custom demo %}", KorteTemplateConfig(extraTags = listOf(CustomTag))) + .invoke(null) + ) + } + + @Test + fun testSlice() = suspendTest { + val map = linkedMapOf("v" to listOf(1, 2, 3, 4)) + assertEquals("[1, 2, 3, 4]", KorteTemplate("{{ v }}")(map)) + assertEquals("[2, 3, 4]", KorteTemplate("{{ v|slice(1) }}")(map)) + assertEquals("[2, 3]", KorteTemplate("{{ v|slice(1, 2) }}")(map)) + assertEquals("ello", KorteTemplate("{{ v|slice(1) }}")(mapOf("v" to "hello"))) + assertEquals("el", KorteTemplate("{{ v|slice(1, 2) }}")(mapOf("v" to "hello"))) + } + + @Test + fun testReverse() = suspendTest { + val map = linkedMapOf("v" to listOf(1, 2, 3, 4)) + assertEquals("[4, 3, 2, 1]", KorteTemplate("{{ v|reverse }}")(map)) + assertEquals("olleh", KorteTemplate("{{ v|reverse }}")(mapOf("v" to "hello"))) + assertEquals("le", KorteTemplate("{{ v|slice(1, 2)|reverse }}")(mapOf("v" to "hello"))) + } + + @Test + fun testObject() = suspendTest { + assertEquals("""{"foo": 1, "bar": 2}""", KorteTemplate("{{ { 'foo': 1, 'bar': 2 } }}")()) + } + + @Test + fun testFuncCycle() = suspendTest { + assertEquals("a", KorteTemplate("{{ cycle(['a', 'b'], 2) }}")()) + assertEquals("b", KorteTemplate("{{ cycle(['a', 'b'], -1) }}")()) + } + + @Test + fun testRange() = suspendTest { + assertEquals("[0, 1, 2, 3]", KorteTemplate("{{ 0..3 }}")()) + assertEquals("[0, 1, 2, 3]", KorteTemplate("{{ range(0,3) }}")()) + assertEquals("[0, 2]", KorteTemplate("{{ range(0,3,2) }}")()) + } + + @Test + fun testEscape() = suspendTest { + assertEquals("<a>", KorteTemplate("{{ a }}")("a" to "")) + assertEquals("", KorteTemplate("{{ a|raw }}")("a" to "")) + assertEquals("<A>", KorteTemplate("{{ a|raw|upper }}")("a" to "")) + assertEquals("", KorteTemplate("{{ a|upper|raw }}")("a" to "")) + } + + @Test + fun testTrim() = suspendTest { + assertEquals("""a 1 b""", KorteTemplate("a {{ 1 }} b")()) + assertEquals("""a1 b""", KorteTemplate("a {{- 1 }} b")()) + assertEquals("""a 1b""", KorteTemplate("a {{ 1 -}} b")()) + assertEquals("""a1b""", KorteTemplate("a {{- 1 -}} b")()) + + assertEquals("""a b""", KorteTemplate("a {% set a=1 %} b")()) + assertEquals("""a b""", KorteTemplate("a {%- set a=1 %} b")()) + assertEquals("""a b""", KorteTemplate("a {% set a=1 -%} b")()) + assertEquals("""ab""", KorteTemplate("a {%- set a=1 -%} b")()) + } + + @Test + fun testOperatorPrecedence() = suspendTest { + assertEquals("${4 + 5 * 7}", KorteTemplate("{{ 4+5*7 }}")()) + assertEquals("${4 * 5 + 7}", KorteTemplate("{{ 4*5+7 }}")()) + } + + @Test + fun testOperatorPrecedence2() = suspendTest { + assertEquals("${(4 + 5) * 7}", KorteTemplate("{{ (4+5)*7 }}")()) + assertEquals("${(4 * 5) + 7}", KorteTemplate("{{ (4*5)+7 }}")()) + assertEquals("${4 + (5 * 7)}", KorteTemplate("{{ 4+(5*7) }}")()) + assertEquals("${4 * (5 + 7)}", KorteTemplate("{{ 4*(5+7) }}")()) + } + + @Test + fun testOperatorPrecedence3() = suspendTest { + assertEquals("${-(4 + 5)}", KorteTemplate("{{ -(4+5) }}")()) + assertEquals("${+(4 + 5)}", KorteTemplate("{{ +(4+5) }}")()) + } + + @Test + fun testFrontMatter() = suspendTest { + assertEquals( + """hello""", + KorteTemplate( + """ + --- + title: hello + --- + {{ title }} + """.trimIndent() + )() + ) + } + + @Test + fun testFrontMatterWindows() = suspendTest { + assertEquals( + "hello", + KorteTemplate("---\r\ntitle: hello\r\n---\r\n{{ title }}")() + ) + } + + class TestMethods : KorteDynamicType by KorteDynamicType({ + register("mytest123") { mytest123() } + register("sum") { sum(it[0].toDynamicInt(), it[1].toDynamicInt()) } + }), KorteDynamicContext { + var field = 1 + + suspend fun mytest123(): Int { + val deferred = KorteDeferred() + deferred.complete(field) + val r = deferred.await() + return r + 7 + } + + @JsName("sum") + suspend fun sum(a: Int, b: Int): Int { + return a + b + } + } + + @Test + fun testSuspendClass1() = suspendTest { + assertEquals("""8""", KorteTemplate("{{ v.mytest123 }}")("v" to TestMethods(), mapper = KorteMapper2)) + } + + @Test + fun testSuspendClass2() = suspendTest { + assertEquals("""8""", KorteTemplate("{{ v.mytest123() }}")("v" to TestMethods(), mapper = KorteMapper2)) + } + + @Test + fun testSuspendClass3() = suspendTest { + assertEquals("""8""", KorteTemplate("{{ v.sum(3, 5) }}")("v" to TestMethods(), mapper = KorteMapper2)) + } + + //@Test fun testStringInterpolation() = sync { + // assertEquals("a2b", Template("{{ \"a#{7 - 5}b\" }}")()) + //} + + @Test + fun testConcatOperator() = suspendTest { + assertEquals("12", KorteTemplate("{{ 1 ~ 2 }}")()) + } + + @Test + fun testUnknownFilter() = suspendTest { + expectException("Unknown filter 'unknownFilter' at template:1:6") { KorteTemplate("{{ 'a'|unknownFilter }}")() } + } + + @Test + fun testMissingFilterName() = suspendTest { + expectException("Missing filter name at template:1:6") { KorteTemplate("{{ 'a'| }}")() } + } + + @Test + fun testCustomBlockWriter() = suspendTest { + val config = KorteTemplateConfig().also { + it.replaceWriteBlockExpressionResult { value, previous -> + if (value == null) throw NullPointerException("null") + previous(value) + } + } + assertEquals("a", KorteTemplate("{{ 'a' }}", config)()) + expectException("null") { KorteTemplate("{{ null }}", config)() } + } + + @Test + fun testCustomVariablePreprocessor() = suspendTest { + val config = KorteTemplateConfig().also { + it.replaceVariablePocessor { name, previous -> + previous(name) ?: throw NullPointerException("Variable: $name cannot be null.") + } + } + assertEquals("a", KorteTemplate("{{ var1 }}", config)(mapOf("var1" to "a"))) + expectException("Variable: var2 cannot be null.") { KorteTemplate("{{ var2 }}", config)() } + } + + @Test fun testInvalid1() = suspendTest { expectException("String literal not closed at template:1:3") { KorteTemplate("{{ ' }}")() } } + @Test fun testInvalid2() = suspendTest { expectException("No expression at template:1:3") { KorteTemplate("{{ }}")() } } + @Test fun testInvalid3() = suspendTest { expectException("Expected expression at template:1:5") { KorteTemplate("{{ 1 + }}")() } } + @Test fun testInvalid4() = suspendTest { expectException("Unexpected token 'world' at template:1:13") { KorteTemplate("{% set a = hello world %}")() } } + @Test fun testInvalid5() = suspendTest { expectException("Expected id at template:1:3") { KorteTemplate("{% set %}")() } } + @Test fun testInvalid6() = suspendTest { expectException("Expected = but found end at template:1:3") { KorteTemplate("{% set a %}")() } } + @Test fun testInvalid7() = suspendTest { expectException("Expected expression at template:1:5") { KorteTemplate("{% set a = %}")() } } + + @Test + fun testImportMacros() = suspendTest { + val templates = KorteTemplates( + KorteTemplateProvider( + "root.html" to "{% import '_macros.html' as macros %}{{ macros.putUserLink('hello') }}", + "_macros.html" to "{% macro putUserLink(user) %}{{ user }}{% endmacro %}" + ) + ) + assertEquals("hello", templates.get("root.html").invoke(hashMapOf())) + } + + @Test + fun testCustomEscapeMode() = suspendTest { + val template = "{{ test }}" + assertEquals( + """ + <WORLD> + + """.trimIndent(), + listOf(KorteAutoEscapeMode.HTML, KorteAutoEscapeMode.RAW) + .map { KorteTemplate(template, KorteTemplateConfig(autoEscapeMode = it))("test" to "") } + .joinToString("\n") + ) + } +} diff --git a/korlibs-template/test/korlibs/template/suspendTest.kt b/korlibs-template/test/korlibs/template/suspendTest.kt new file mode 100644 index 0000000..026c063 --- /dev/null +++ b/korlibs-template/test/korlibs/template/suspendTest.kt @@ -0,0 +1,66 @@ +package korlibs.template + +import kotlinx.coroutines.test.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +fun suspendTest(callback: suspend TestScope.() -> Unit): TestResult = runTest { callback() } + +fun runBlockingNoSuspensions(callback: suspend () -> T): T { + var completed = false + lateinit var rresult: T + var resultEx: Throwable? = null + var suspendCount = 0 + + callback.startCoroutineUndispatched(object : Continuation { + override val context: CoroutineContext = object : + AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { + override val key: CoroutineContext.Key<*> = ContinuationInterceptor.Key + override fun interceptContinuation(continuation: Continuation): Continuation = + continuation.also { suspendCount++ } + } + + private val unitInstance get() = Unit + + override fun resumeWith(result: Result) { + val exception = result.exceptionOrNull() + if (exception != null) { + resultEx = exception + completed = true + println(exception) + } else { + //val value = + val rvalue = result.getOrThrow() ?: (unitInstance as T) + rresult = rvalue + completed = true + } + } + }) + if (!completed) throw RuntimeException("runBlockingNoSuspensions was not completed synchronously! suspendCount=$suspendCount") + if (resultEx != null) throw resultEx!! + return rresult +} + +private fun (suspend () -> T).startCoroutineUndispatched(completion: Continuation) { + startDirect(completion) { + withCoroutineContext(completion.context, null) { + startCoroutineUninterceptedOrReturn(completion) + } + } +} + +private inline fun startDirect(completion: Continuation, block: () -> Any?) { + val value = try { + block() + } catch (e: Throwable) { + completion.resumeWithException(e) + return + } + if (value !== COROUTINE_SUSPENDED) { + @Suppress("UNCHECKED_CAST") + completion.resume(value as T) + } +} + +private inline fun withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = + block() diff --git a/korlibs-template/test@jvm/korlibs/template/TemplateJvmTest.kt b/korlibs-template/test@jvm/korlibs/template/TemplateJvmTest.kt new file mode 100644 index 0000000..7df3b1e --- /dev/null +++ b/korlibs-template/test@jvm/korlibs/template/TemplateJvmTest.kt @@ -0,0 +1,102 @@ +package korlibs.template + +import korlibs.template.dynamic.* +import java.time.LocalDate +import java.time.Month +import kotlin.test.Test +import kotlin.test.assertEquals + +class TemplateJvmTest { + class GeoLocation { + @JvmField + var latitude: Double = 0.0 + @JvmField + var longitude: Double = 0.0 + } + + @Test + // https://github.com/korlibs/korte/issues/18 + fun testJvmFieldNotBeingAccepted() = suspendTest { + val location = GeoLocation() + location.latitude = 1.0 + location.longitude = 2.0 + assertEquals( + "1,2", + KorteTemplate("{{ location.latitude }},{{ location.longitude }}")("location" to location) + ) + } + + @Test + // https://github.com/korlibs/korte/issues/20 + fun testJvmLocalDate() = suspendTest { + val startDate = LocalDate.of(2021, Month.NOVEMBER, 7) + val days = 3 + + data class Entry(val title: String, val date: LocalDate) + val rows = (1..10).map { Entry(title = "test$it", date = LocalDate.of(2021, Month.NOVEMBER, 5 + it)) } + + assertEquals( + "test2,test3,test4,", + KorteTemplate("{% for row in rows %}{% if row.date >= startDate && row.date < startDate.plusDays(days) %}{{ row.title }},{% endif %}{% endfor %}")("startDate" to startDate, "days" to days, "rows" to rows) + ) + } + + @Suppress("unused") + class Getter { + @JvmField var a: Int? = 10 + @JvmField var b: Int? = 10 + var c: Int? = 10 + fun getA(): Int? = null + fun getB(): Int? = 20 + } + + @Suppress("unused") + class GetterWithMap : LinkedHashMap() { + @JvmField var a: Int? = 10 + @JvmField var b: Int? = 10 + var c: Int? = 10 + fun getA(): Int? = null + fun getB(): Int? = 20 + } + + @Test + fun testGetter() = suspendTest { + assertEquals( + ",20,10", + KorteTemplate("{{ data.a }},{{ data.b }},{{ data.c }}")("data" to Getter()) + ) + } + + @Test + fun testGetterCustomMapper() = suspendTest { + val getter = GetterWithMap().also { it["a"] = "A"; it["b"] = "B" } + assertEquals( + "A,B,", + KorteTemplate("{{ data.a }},{{ data.b }},{{ data.c }}")("data" to getter) + ) + assertEquals( + "A,20,10", + KorteTemplate("{{ data.a }},{{ data.b }},{{ data.c }}")("data" to getter, mapper = object : KorteObjectMapper2 by KorteMapper2 { + override suspend fun accessAny(instance: Any?, key: Any?): Any? { + return super.accessAnyObject(instance, key) ?: super.accessAny(instance, key) + } + }) + ) + } + + @Test + fun testIs() = suspendTest { + data class Data(var isHighlight: String, var ishighlight: String) + + val x = Data( + isHighlight = "Text1", + ishighlight = "Text2" + ) + + val tpl1 = KorteTemplate("{{ numbers.isHighlight }}") + val tpl2 = KorteTemplate("{{ numbers.ishighlight }}") + + assertEquals("Text1", tpl1("numbers" to x)) + assertEquals("Text2", tpl2("numbers" to x)) + } +} diff --git a/korlibs-template/test@native/korlibs/template/MultiThreadingTests.kt b/korlibs-template/test@native/korlibs/template/MultiThreadingTests.kt new file mode 100644 index 0000000..5e0a46a --- /dev/null +++ b/korlibs-template/test@native/korlibs/template/MultiThreadingTests.kt @@ -0,0 +1,22 @@ +package korlibs.template + +import korlibs.template.dynamic.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlin.test.Test +import kotlin.test.assertEquals + +class MultiThreadingTests { + data class Model(val x: Int) : KorteDynamicType by KorteDynamicType({ + register(Model::x) + }) + @Test + fun testTemplateEvaluationOnBackgroundThread() = runBlocking { + withContext(Dispatchers.Default) { + val template = KorteTemplate("{{x+1}}") + val rendered = template(Model(x = 2)) + assertEquals("3", rendered) + } + } +}