ekt/src/main/kotlin/net/shadowfacts/ekt/EKT.kt

179 lines
5.3 KiB
Kotlin
Raw Normal View History

2017-08-04 19:38:25 +00:00
package net.shadowfacts.ekt
import java.io.File
import javax.script.ScriptContext
import javax.script.ScriptEngineManager
2017-08-05 18:11:19 +00:00
import javax.script.SimpleScriptContext
2017-08-04 19:38:25 +00:00
/**
* @author shadowfacts
*/
object EKT {
2017-08-04 20:56:49 +00:00
private val startControlCodes: Map<String, (String) -> String> = mapOf(
":" to { s -> s },
"=" to { s -> ")" + s },
"#" to { s -> "*/" + s }
)
private val endControlCodes: Map<String, (String) -> String> = mapOf(
":" to { s -> s },
"=" to { s -> s + "echo(" },
"#" to { s -> s + "/*" }
)
2017-08-06 18:54:28 +00:00
private val startStringRegex = Regex("([:=#])]")
2017-08-04 20:56:49 +00:00
private val endStringRegex = Regex("\\[([:=#])")
2017-08-04 19:38:25 +00:00
private val scriptPrefix = """
2017-08-06 14:55:35 +00:00
val _env = bindings["_env"] as net.shadowfacts.ekt.EKT.TemplateEnvironment
2017-08-04 19:38:25 +00:00
val _result = StringBuilder()
2017-08-06 14:55:35 +00:00
fun echo(it: Any) { _result.append(it) }
fun include(include: String) {
2017-08-06 18:59:19 +00:00
val env = net.shadowfacts.ekt.EKT.TemplateEnvironment(include, _env, cacheDir = null)
2017-08-06 14:55:35 +00:00
echo(net.shadowfacts.ekt.EKT.render(env, env.include))
}
2017-08-04 19:38:25 +00:00
"""
private val scriptSuffix = """
_result.toString()
"""
2017-08-05 18:11:19 +00:00
private val engine by lazy {
2017-08-06 18:54:28 +00:00
ScriptEngineManager().getEngineByExtension("kts")
2017-08-05 18:11:19 +00:00
}
2017-08-06 14:55:35 +00:00
fun render(env: TemplateEnvironment, template: String = env.template): String {
if (env.cacheDir != null && env.cacheFile.exists()) {
2017-08-06 18:54:28 +00:00
return eval(env.cacheFile.readText(Charsets.UTF_8), env)
2017-08-06 14:55:35 +00:00
}
2017-08-04 19:38:25 +00:00
@Suppress("NAME_SHADOWING")
var template = template
template = template.replace("$", "\${'$'}")
2017-08-04 20:56:49 +00:00
template = ":]$template[:"
2017-08-04 19:38:25 +00:00
template = template.replace(startStringRegex, {
2017-08-04 20:56:49 +00:00
val c = it.groups[1]!!.value
if (c in startControlCodes) {
startControlCodes[c]!!("\necho(\"\"\"")
} else {
throw RuntimeException("Unknown control code: [$c")
}
2017-08-04 19:38:25 +00:00
})
template = template.replace(endStringRegex, {
2017-08-04 20:56:49 +00:00
val c = it.groups[1]!!.value
if (c in endControlCodes) {
endControlCodes[c]!!("\"\"\")\n")
} else {
throw RuntimeException("Unknown control code: $c]")
}
2017-08-04 19:38:25 +00:00
})
2017-08-05 18:59:15 +00:00
// Hack to allow data to be accessed by name from template instead of via bindings map
2017-08-06 14:55:35 +00:00
val unwrapBindings = env.data.keys.map {
val type = env.data[it]!!.type
2017-08-05 18:59:15 +00:00
"val $it = (bindings[\"$it\"] as net.shadowfacts.ekt.EKT.TypedValue).value as $type"
}.joinToString("\n")
2017-08-04 19:38:25 +00:00
2017-08-05 18:59:15 +00:00
val script = unwrapBindings + scriptPrefix + template + scriptSuffix
2017-08-06 14:55:35 +00:00
if (env.cacheDir != null) {
env.cacheFile.apply {
if (!parentFile.exists()) parentFile.mkdirs()
if (!exists()) createNewFile()
writeText(script, Charsets.UTF_8)
}
2017-08-04 19:38:25 +00:00
}
2017-08-06 18:54:28 +00:00
return eval(script, env)
2017-08-05 18:59:15 +00:00
}
2017-08-06 14:55:35 +00:00
fun render(name: String, templateDir: File, includeDir: File, cacheDir: File? = null, data: Map<String, TypedValue>): String {
return render(TemplateEnvironment(name, templateDir, includeDir, cacheDir, data))
2017-08-05 18:59:15 +00:00
}
2017-08-06 14:55:35 +00:00
fun render(name: String, templateDir: File, includeDir: File, cacheDir: File? = null, init: DataProvider.() -> Unit): String {
return render(TemplateEnvironment(name, templateDir, includeDir, cacheDir, init))
}
2017-08-05 17:41:24 +00:00
2017-08-06 14:55:35 +00:00
fun render(name: String, dir: File, cacheScripts: Boolean = false, data: Map<String, TypedValue>): String {
return render(name, dir, File(dir, "includes"), if (cacheScripts) File(dir, "cache") else null, data)
2017-08-04 19:38:25 +00:00
}
2017-08-06 14:55:35 +00:00
fun render(name: String, dir: File, cacheScripts: Boolean = false, init: DataProvider.() -> Unit): String {
return render(name, dir, File(dir, "includes"), if (cacheScripts) File(dir, "cache") else null, init)
2017-08-04 19:38:25 +00:00
}
2017-08-06 18:54:28 +00:00
internal fun eval(script: String, env: TemplateEnvironment): String {
2017-08-05 18:11:19 +00:00
engine.context = SimpleScriptContext()
2017-08-04 19:38:25 +00:00
val bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE)
2017-08-06 14:55:35 +00:00
bindings.putAll(env.data)
bindings.put("_env", env)
2017-08-04 19:38:25 +00:00
2017-08-06 18:54:28 +00:00
return engine.eval(script) as String
2017-08-04 19:38:25 +00:00
}
2017-08-06 14:55:35 +00:00
class TemplateEnvironment {
val rootName: String
val name: String
val templateDir: File
val includeDir: File
val cacheDir: File?
val data: Map<String, TypedValue>
val template: String
get() = File(templateDir, "$name.ekt").readText(Charsets.UTF_8)
val include: String
get() = File(includeDir, "$name.ekt").readText(Charsets.UTF_8)
val cacheFile: File
get() = File(cacheDir!!, "$name.kts")
constructor(name: String, templateDir: File, includeDir: File, cacheDir: File?, data: Map<String, TypedValue>) {
this.rootName = name
this.name = name
this.templateDir = templateDir
this.includeDir = includeDir
this.cacheDir = cacheDir
this.data = data
}
constructor(name: String, templateDir: File, includeDir: File, cacheDir: File?, init: DataProvider.() -> Unit):
this(name, templateDir, includeDir, cacheDir, DataProvider.init(init))
2017-08-06 18:59:19 +00:00
constructor(name: String, parent: TemplateEnvironment, cacheDir: File? = parent.cacheDir) {
2017-08-06 14:55:35 +00:00
this.rootName = parent.rootName
this.name = name
this.templateDir = parent.templateDir
this.includeDir = parent.includeDir
2017-08-06 18:59:19 +00:00
this.cacheDir = cacheDir
2017-08-06 14:55:35 +00:00
this.data = parent.data
}
}
class DataProvider {
2017-08-05 17:41:24 +00:00
internal val map = mutableMapOf<String, TypedValue>()
infix fun String.to(value: Any) {
if (value is TypedValue) {
map[this] = value
} else {
map[this] = TypedValue(value, value::class.qualifiedName!!)
}
}
infix fun Any.asType(type: String): TypedValue {
return TypedValue(this, type)
}
2017-08-06 14:55:35 +00:00
companion object {
fun init(init: DataProvider.() -> Unit): Map<String, TypedValue> {
val ctx = DataProvider()
ctx.init()
return ctx.map
}
}
2017-08-05 17:41:24 +00:00
}
data class TypedValue(val value: Any, val type: String)
2017-08-04 19:38:25 +00:00
}