Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.jtransc.template.Minitemplate.kt Maven / Gradle / Ivy
package com.jtransc.template
import com.jtransc.ds.ListReader
import com.jtransc.error.invalidOp
import com.jtransc.imaging.ImagePropsDecoder
import com.jtransc.json.Json
import com.jtransc.lang.Dynamic
import com.jtransc.text.*
import java.io.File
class Minitemplate(val template: String, val config: Config = Config()) {
val templateTokens = Token.tokenize(template)
val node = BlockNode.parse(templateTokens, config)
class Config(
private val extraTags: List = listOf(),
private val extraFilters: List = listOf()
) {
val integratedFilters = listOf(
Filter("length") { subject, args -> Dynamic.length(subject) },
Filter("capitalize") { subject, args -> Dynamic.toString(subject).toLowerCase().capitalize() },
Filter("upper") { subject, args -> Dynamic.toString(subject).toUpperCase() },
Filter("lower") { subject, args -> Dynamic.toString(subject).toLowerCase() },
Filter("trim") { subject, args -> Dynamic.toString(subject).trim() },
Filter("quote") { subject, args -> Dynamic.toString(subject).quote() },
Filter("escape") { subject, args -> Dynamic.toString(subject).escape() },
Filter("json") { subject, args -> Json.encodeAny(subject) },
Filter("join") { subject, args -> Dynamic.toIterable(subject).map { Dynamic.toString(it) }.joinToString(Dynamic.toString(args[0])) },
Filter("file_exists") { subject, args -> File(Dynamic.toString(subject)).exists() },
Filter("image_info") { subject, args ->
if (subject is ByteArray) {
ImagePropsDecoder.tryDecodeHeader(subject)
} else {
val file = when (subject) {
is File -> subject
else -> File(Dynamic.toString(subject))
}
ImagePropsDecoder.tryDecodeHeader(file)
}
}
)
private val allTags = listOf(Tag.EMPTY, Tag.IF, Tag.FOR, Tag.SET, Tag.DEBUG) + 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
}
}
data class Filter(val name: String, val eval: (subject: Any?, args: List) -> Any?)
class Scope(val map: Any?, val parent: Scope? = null) {
operator fun get(key: Any?): Any? {
return Dynamic.accessAny(map, key) ?: parent?.get(key)
}
operator fun set(key: Any?, value: Any?) {
Dynamic.setAny(map, key, value)
}
}
operator fun invoke(args: Any?): String {
val str = StringBuilder()
val context = Context(Scope(args), config) { str.append(it) }
context.createScope { node.eval(context) }
return str.toString()
}
class Context(var scope: Scope, val config: Config, val write: (str: String) -> Unit) {
inline fun createScope(callback: () -> Unit) = this.apply {
val old = this.scope
this.scope = Scope(hashMapOf(), old)
callback()
this.scope = old
}
}
interface ExprNode {
fun eval(context: Context): Any?
data class VAR(val name: String) : ExprNode {
override fun eval(context: Context): Any? = context.scope[name]
}
data class LIT(val value: Any?) : ExprNode {
override fun eval(context: Context): Any? = value
}
data class ARRAY_LIT(val items: List) : ExprNode {
override fun eval(context: Context): Any? = items.map { it.eval(context) }
}
data class FILTER(val name: String, val expr: ExprNode, val params: List) : ExprNode {
override fun eval(context: Context): Any? {
val filter = context.config.filters[name] ?: invalidOp("Unknown filter '$name'")
return filter.eval(expr.eval(context), params.map { it.eval(context) })
}
}
data class ACCESS(val expr: ExprNode, val name: ExprNode) : ExprNode {
override fun eval(context: Context): Any? {
val obj = expr.eval(context)
val key = name.eval(context)
try {
return Dynamic.accessAny(obj, key)
} catch (t: Throwable) {
try {
return Dynamic.callAny(obj, key, listOf())
} catch (t: Throwable) {
return null
}
}
}
}
data class CALL(val method: ExprNode, val args: List) : ExprNode {
override fun eval(context: Context): Any? {
if (method !is ACCESS) {
return Dynamic.callAny(method.eval(context), args.map { it.eval(context) })
} else {
return Dynamic.callAny(method.expr.eval(context), method.name.eval(context), args.map { it.eval(context) })
}
}
}
data class BINOP(val l: ExprNode, val r: ExprNode, val op: String) : ExprNode {
override fun eval(context: Context): Any? = Dynamic.binop(l.eval(context), r.eval(context), op)
}
data class UNOP(val r: ExprNode, val op: String) : ExprNode {
override fun eval(context: Context): Any? = Dynamic.unop(r.eval(context), op)
}
companion object {
fun ListReader.expectPeek(vararg types: String): Token {
val token = this.peek()
if (token.text !in types) throw RuntimeException("Expected ${types.joinToString(", ")}")
return token
}
fun ListReader.expect(vararg types: String): Token {
val token = this.read()
if (token.text !in types) throw RuntimeException("Expected ${types.joinToString(", ")}")
return token
}
fun parse(str: String): ExprNode {
return parseFullExpr(Token.tokenize(str))
}
fun parseId(r: ListReader): String {
return r.read().text
}
fun expect(r: ListReader, vararg tokens: String) {
val token = r.read()
if (token.text !in tokens) invalidOp("Expected ${tokens.joinToString(", ")} but found $token")
}
fun parseFullExpr(r: ListReader): ExprNode {
val result = parseExpr(r)
if (r.hasMore && r.peek() !is Token.TEnd) {
invalidOp("Expected expression at " + r.peek() + " :: " + r.list.map { it.text }.joinToString(""))
}
return result
}
private val BINOPS = setOf(
"+", "-", "*", "/", "%",
"==", "!=", "<", ">", "<=", ">=", "<=>",
"&&", "||"
)
fun parseExpr(r: ListReader): ExprNode {
var result = parseFinal(r)
while (r.hasMore) {
if (r.peek() !is Token.TOperator || r.peek().text !in BINOPS) break
val operator = r.read().text
val right = parseFinal(r)
result = ExprNode.BINOP(result, right, operator)
}
// @TODO: Fix order!
return result
}
private fun parseFinal(r: ListReader): ExprNode {
var construct: ExprNode = when (r.peek().text) {
"!", "~", "-", "+" -> {
val op = r.read().text
ExprNode.UNOP(parseFinal(r), op)
}
"(" -> {
r.read()
val result = parseExpr(r)
if (r.read().text != ")") throw RuntimeException("Expected ')'")
result
}
// Array literal
"[" -> {
val items = arrayListOf()
r.read()
loop@ while (r.hasMore && r.peek().text != "]") {
items += parseExpr(r)
when (r.peek().text) {
"," -> r.read()
"]" -> continue@loop
else -> invalidOp("Expected , or ]")
}
}
r.expect("]")
ExprNode.ARRAY_LIT(items)
}
else -> {
if (r.peek() is Token.TNumber) {
ExprNode.LIT(r.read().text.toDouble())
} else if (r.peek() is Token.TString) {
ExprNode.LIT((r.read() as Token.TString).processedValue)
} else {
ExprNode.VAR(r.read().text)
}
}
}
loop@ while (r.hasMore) {
when (r.peek().text) {
"." -> {
r.read()
val id = r.read().text
construct = ExprNode.ACCESS(construct, ExprNode.LIT(id))
continue@loop
}
"[" -> {
r.read()
val expr = parseExpr(r)
construct = ExprNode.ACCESS(construct, expr)
val end = r.read()
if (end.text != "]") throw RuntimeException("Expected ']' but found $end")
}
"|" -> {
r.read()
val name = r.read().text
val args = arrayListOf()
if (r.peek().text == "(") {
r.read()
callargsloop@ while (r.hasMore && r.peek().text != ")") {
args += parseExpr(r)
when (r.expectPeek(",", ")").text) {
"," -> r.read()
")" -> break@callargsloop
}
}
r.expect(")")
}
construct = ExprNode.FILTER(name, construct, args)
}
"(" -> {
r.read()
val args = arrayListOf()
callargsloop@ while (r.hasMore && r.peek().text != ")") {
args += parseExpr(r)
when (r.expectPeek(",", ")").text) {
"," -> r.read()
")" -> break@callargsloop
}
}
r.expect(")")
construct = ExprNode.CALL(construct, args)
}
else -> break@loop
}
}
return construct
}
}
interface Token {
val text: String
data class TId(override val text: String) : Token
data class TNumber(override val text: String) : Token
data class TString(override val text: String, val processedValue: String) : Token
data class TOperator(override val text: String) : Token
data class TEnd(override val text: String = "") : Token
companion object {
private val OPERATORS = setOf(
"(", ")",
"[", "]",
"{", "}",
"&&", "||",
"&", "|", "^",
"==", "!=", "<", ">", "<=", ">=", "<=>",
"+", "-", "*", "/", "%", "**",
"!", "~",
".", ",", ";", ":",
"="
)
fun tokenize(str: String): ListReader {
val r = StrReader(str)
val out = arrayListOf()
fun emit(str: Token) {
out += str
}
while (r.hasMore) {
val start = r.offset
r.skipSpaces()
val id = r.readWhile { it.isLetterDigitOrUnderscore() }
if (id != null) {
if (id[0].isDigit()) emit(TNumber(id)) else emit(TId(id))
}
r.skipSpaces()
if (r.peek(3) in OPERATORS) emit(TOperator(r.read(3)))
if (r.peek(2) in OPERATORS) emit(TOperator(r.read(2)))
if (r.peek(1) in OPERATORS) emit(TOperator(r.read(1)))
if (r.peekch() == '\'' || r.peekch() == '"') {
val strStart = r.readch()
val strBody = r.readUntil { it == strStart } ?: ""
val strEnd = r.readch()
emit(TString(strStart + strBody + strEnd, strBody))
}
val end = r.offset
if (end == start) invalidOp("Don't know how to handle '${r.peekch()}'")
}
emit(TEnd())
return ListReader(out)
}
}
}
}
interface BlockNode {
fun eval(context: Context): Unit
data class GROUP(val children: List) : BlockNode {
override fun eval(context: Context) = Unit.apply { for (n in children) n.eval(context) }
}
data class TEXT(val content: String) : BlockNode {
override fun eval(context: Context) = Unit.apply { context.write(content) }
}
data class EXPR(val expr: ExprNode) : BlockNode {
override fun eval(context: Context) = Unit.apply { context.write(Dynamic.toString(expr.eval(context))) }
}
data class IF(val cond: ExprNode, val trueContent: BlockNode, val falseContent: BlockNode?) : BlockNode {
override fun eval(context: Context) = Unit.apply {
if (Dynamic.toBool(cond.eval(context))) {
trueContent.eval(context)
} else {
falseContent?.eval(context)
}
}
}
data class FOR(val varname: String, val expr: ExprNode, val loop: BlockNode) : BlockNode {
override fun eval(context: Context) = Unit.apply {
context.createScope {
for (v in Dynamic.toIterable(expr.eval(context))) {
context.scope[varname] = v
loop.eval(context)
}
}
}
}
data class SET(val varname: String, val expr: ExprNode) : BlockNode {
override fun eval(context: Context) = Unit.apply {
context.scope[varname] = expr.eval(context)
}
}
data class DEBUG(val expr: ExprNode) : BlockNode {
override fun eval(context: Context) = Unit.apply {
println(expr.eval(context))
}
}
companion object {
fun group(children: List): BlockNode = if (children.size == 1) children[0] else GROUP(children)
fun parse(tokens: List, config: Config): BlockNode {
val tr = ListReader(tokens)
fun handle(tag: Tag, token: Token.TTag): BlockNode {
val parts = arrayListOf()
var currentToken = token
val mutableChildren = arrayListOf()
fun emitPart() {
val clonedChildren = mutableChildren.toList()
parts += TagPart(currentToken, BlockNode.group(clonedChildren))
}
loop@ while (!tr.eof) {
val it = tr.read()
when (it) {
is Token.TLiteral -> mutableChildren += BlockNode.TEXT(it.content)
is Token.TExpr -> mutableChildren += BlockNode.EXPR(ExprNode.parse(it.content))
is Token.TTag -> {
when (it.name) {
tag.end -> break@loop
in tag.nextList -> {
emitPart()
currentToken = it
mutableChildren.clear()
}
else -> {
val newtag = config.tags[it.name] ?: invalidOp("Can't find tag ${it.name}")
if (newtag.end != null) {
mutableChildren += handle(newtag, it)
} else {
mutableChildren += newtag.buildNode(listOf(TagPart(it, BlockNode.TEXT(""))))
}
}
}
}
else -> break@loop
}
}
emitPart()
return tag.buildNode(parts)
}
return handle(Tag.EMPTY, Token.TTag("", ""))
}
}
}
data class TagPart(val token: Token.TTag, val body: BlockNode)
data class Tag(val name: String, val nextList: Set, val end: String?, val aliases: List = listOf(), val buildNode: (parts: List) -> BlockNode) {
companion object {
val EMPTY = Tag("", setOf(""), "") { parts ->
BlockNode.group(parts.map { it.body })
}
val IF = Tag("if", setOf("else"), "end") { parts ->
val main = parts[0]
val elseBlock = parts.getOrNull(1)
BlockNode.IF(ExprNode.parse(main.token.content), main.body, elseBlock?.body)
}
val FOR = Tag("for", setOf(), "end") { parts ->
val main = parts[0]
val tr = ExprNode.Token.tokenize(main.token.content)
val varname = ExprNode.parseId(tr)
ExprNode.expect(tr, "in")
val expr = ExprNode.parseExpr(tr)
BlockNode.FOR(varname, expr, main.body)
}
val DEBUG = Tag("debug", setOf(), null) { parts ->
BlockNode.DEBUG(ExprNode.parse(parts[0].token.content))
}
val SET = Tag("set", setOf(), null) { parts ->
val main = parts[0]
val tr = ExprNode.Token.tokenize(main.token.content)
val varname = ExprNode.parseId(tr)
ExprNode.expect(tr, "=")
val expr = ExprNode.parseExpr(tr)
BlockNode.SET(varname, expr)
}
}
}
interface Token {
data class TLiteral(val content: String) : Token
data class TExpr(val content: String) : Token
data class TTag(val name: String, val content: String) : Token
companion object {
fun tokenize(str: String): List {
val out = arrayListOf()
var lastPos = 0
fun emit(token: Token) {
if (token is TLiteral && token.content.isEmpty()) return
out += token
}
var pos = 0
while (pos < str.length) {
val c = str[pos++]
if (c == '{') {
if (pos >= str.length) break
val c2 = str[pos++]
if (c2 == '{' || c2 == '%') {
val startPos = pos - 2
val pos2 = if (c2 == '{') str.indexOf("}}", pos) else str.indexOf("%}", pos)
if (pos2 < 0) break
val content = str.substring(pos, pos2).trim()
if (lastPos != startPos) {
emit(TLiteral(str.substring(lastPos until startPos)))
}
if (c2 == '{') {
//println("expr: '$content'")
emit(TExpr(content))
} else {
val parts = content.split(' ', limit = 2)
//println("tag: '$content'")
emit(TTag(parts[0], parts.getOrElse(1) { "" }))
}
pos = pos2 + 2
lastPos = pos
}
}
}
emit(TLiteral(str.substring(lastPos, str.length)))
return out
}
}
}
}