2017-08-04 19:38:25 +00:00
|
|
|
package net.shadowfacts.ekt
|
|
|
|
|
2017-10-07 19:36:22 +00:00
|
|
|
import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase
|
|
|
|
import org.jetbrains.kotlin.cli.common.repl.ReplCompileResult
|
|
|
|
import java.io.*
|
|
|
|
import javax.script.*
|
2017-08-30 23:35:26 +00:00
|
|
|
import kotlin.concurrent.getOrSet
|
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
|
|
|
|
2017-08-19 20:42:04 +00:00
|
|
|
private val lastImportRegex = Regex("(import (.*?)\\n)+")
|
|
|
|
|
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-19 15:45:27 +00:00
|
|
|
fun echo(it: Any?) { _result.append(it) }
|
2017-08-22 21:41:40 +00:00
|
|
|
fun include(include: String, data: Map<String, net.shadowfacts.ekt.EKT.TypedValue>? = null) {
|
|
|
|
val env = _env.createChild(include, data)
|
2017-08-06 14:55:35 +00:00
|
|
|
echo(net.shadowfacts.ekt.EKT.render(env, env.include))
|
|
|
|
}
|
2017-08-22 21:41:40 +00:00
|
|
|
fun include(include: String, init: net.shadowfacts.ekt.EKT.DataProvider.() -> Unit) {
|
|
|
|
include(include, data = net.shadowfacts.ekt.EKT.DataProvider.init(init))
|
|
|
|
}
|
2017-08-04 19:38:25 +00:00
|
|
|
"""
|
|
|
|
private val scriptSuffix = """
|
|
|
|
_result.toString()
|
|
|
|
"""
|
|
|
|
|
2017-10-07 19:36:22 +00:00
|
|
|
private val engine = ThreadLocal<KotlinJsr223JvmScriptEngineBase>()
|
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 {
|
2017-10-07 19:36:22 +00:00
|
|
|
if (env.cacheDir != null && env.cacheScript.exists()) {
|
|
|
|
return eval(env.cacheScript.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
|
2017-08-04 21:05:25 +00:00
|
|
|
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-19 20:42:04 +00:00
|
|
|
val lines = template.split("\n")
|
|
|
|
val imports = lines.filter { it.trim().startsWith("import") }.joinToString("\n")
|
|
|
|
template = lines.filterNot { it.trim().startsWith("import") }.joinToString("\n")
|
|
|
|
|
|
|
|
val script = imports + scriptPrefix + template + scriptSuffix
|
2017-08-05 18:59:15 +00:00
|
|
|
|
2017-08-06 14:55:35 +00:00
|
|
|
if (env.cacheDir != null) {
|
2017-10-07 19:36:22 +00:00
|
|
|
env.cacheScript.apply {
|
2017-08-06 14:55:35 +00:00
|
|
|
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-19 15:45:27 +00:00
|
|
|
fun renderFile(name: String, templateDir: File, includeDir: File, cacheDir: File? = null, data: Map<String, TypedValue>): String {
|
|
|
|
return render(FileTemplateEnvironment(name, templateDir, includeDir, cacheDir, data))
|
2017-08-05 18:59:15 +00:00
|
|
|
}
|
|
|
|
|
2017-08-19 15:45:27 +00:00
|
|
|
fun renderFile(name: String, templateDir: File, includeDir: File, cacheDir: File? = null, init: DataProvider.() -> Unit): String {
|
|
|
|
return render(FileTemplateEnvironment(name, templateDir, includeDir, cacheDir, init))
|
2017-08-06 14:55:35 +00:00
|
|
|
}
|
2017-08-05 17:41:24 +00:00
|
|
|
|
2017-08-19 15:45:27 +00:00
|
|
|
fun renderFile(name: String, dir: File, cacheScripts: Boolean = false, data: Map<String, TypedValue>): String {
|
|
|
|
return renderFile(name, dir, File(dir, "includes"), if (cacheScripts) File(dir, "cache") else null, data)
|
2017-08-04 19:38:25 +00:00
|
|
|
}
|
|
|
|
|
2017-08-19 15:45:27 +00:00
|
|
|
fun renderFile(name: String, dir: File, cacheScripts: Boolean = false, init: DataProvider.() -> Unit): String {
|
|
|
|
return renderFile(name, dir, File(dir, "includes"), if (cacheScripts) File(dir, "cache") else null, init)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun renderClasspath(name: String, templatePath: String, includePath: String, cacheDir: File? = null, data: Map<String, TypedValue>): String {
|
|
|
|
return render(ClasspathTemplateEnvironment(name, templatePath, includePath, cacheDir, data))
|
|
|
|
}
|
|
|
|
|
|
|
|
fun renderClasspath(name: String, templatePath: String, includePath: String, cacheDir: File? = null, init: DataProvider.() -> Unit): String {
|
|
|
|
return render(ClasspathTemplateEnvironment(name, templatePath, includePath, cacheDir, init))
|
|
|
|
}
|
|
|
|
|
|
|
|
fun renderClasspath(name: String, path: String, cacheDir: File? = null, data: Map<String, TypedValue>): String {
|
|
|
|
return renderClasspath(name, path, "$path/includes", cacheDir, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun renderClasspath(name: String, path: String, cacheDir: File? = null, init: DataProvider.() -> Unit): String {
|
|
|
|
return renderClasspath(name, path, "$path/includes", cacheDir, init)
|
2017-08-04 19:38:25 +00:00
|
|
|
}
|
|
|
|
|
2017-10-07 19:36:22 +00:00
|
|
|
private fun eval(script: String, env: TemplateEnvironment): String {
|
|
|
|
val engine = engine.getOrSet { ScriptEngineManager().getEngineByExtension("kts") as KotlinJsr223JvmScriptEngineBase }
|
|
|
|
|
|
|
|
val context = createContext(engine, env)
|
|
|
|
|
|
|
|
if (env.cacheDir != null) {
|
|
|
|
val cacheCompiled = env.cacheCompiled
|
|
|
|
val compiled = if (cacheCompiled.exists()) {
|
|
|
|
val fis = FileInputStream(cacheCompiled)
|
|
|
|
val ois = ObjectInputStream(fis)
|
|
|
|
val data = ois.readObject() as ReplCompileResult.CompiledClasses
|
|
|
|
ois.close()
|
|
|
|
fis.close()
|
|
|
|
KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript(engine, engine.nextCodeLine(context, env.cacheScript.readText(Charsets.UTF_8)), data)
|
|
|
|
} else {
|
|
|
|
val compiled = engine.compile(script, context) as KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript
|
|
|
|
val data = compiled.compiledData
|
|
|
|
val fos = FileOutputStream(cacheCompiled)
|
|
|
|
val oos = ObjectOutputStream(fos)
|
|
|
|
oos.writeObject(data)
|
|
|
|
oos.close()
|
|
|
|
fos.close()
|
|
|
|
compiled
|
|
|
|
}
|
|
|
|
return engine.eval(compiled, context) as String
|
|
|
|
} else {
|
|
|
|
return engine.eval(script, context) as String
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun eval(script: KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript, env: TemplateEnvironment): String {
|
|
|
|
return script.engine.eval(script, createContext(script.engine, env)) as String
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun createContext(engine: ScriptEngine, env: TemplateEnvironment): ScriptContext {
|
|
|
|
val bindings = engine.createBindings().apply {
|
|
|
|
putAll(env.data)
|
|
|
|
put("_env", env)
|
|
|
|
}
|
|
|
|
|
|
|
|
val context = SimpleScriptContext().apply {
|
|
|
|
setBindings(bindings, ScriptContext.ENGINE_SCOPE)
|
|
|
|
}
|
2017-08-04 19:38:25 +00:00
|
|
|
|
2017-08-07 21:22:43 +00:00
|
|
|
// Hack to allow data to be accessed by name from template instead of via bindings map
|
2017-10-07 19:36:22 +00:00
|
|
|
val unwrapBindings = env.data.keys.joinToString("\n") {
|
2017-08-07 21:22:43 +00:00
|
|
|
val type = env.data[it]!!.type
|
|
|
|
"val $it = (bindings[\"$it\"] as net.shadowfacts.ekt.EKT.TypedValue).value as $type"
|
2017-10-07 19:36:22 +00:00
|
|
|
}
|
|
|
|
engine.eval(unwrapBindings, context)
|
2017-08-07 21:22:43 +00:00
|
|
|
|
2017-10-07 19:36:22 +00:00
|
|
|
return context
|
2017-08-04 19:38:25 +00:00
|
|
|
}
|
|
|
|
|
2017-08-19 15:45:27 +00:00
|
|
|
interface TemplateEnvironment {
|
2017-08-06 14:55:35 +00:00
|
|
|
val rootName: String
|
|
|
|
val name: String
|
|
|
|
val cacheDir: File?
|
|
|
|
val data: Map<String, TypedValue>
|
|
|
|
|
|
|
|
val template: String
|
|
|
|
val include: String
|
2017-10-07 19:36:22 +00:00
|
|
|
val cacheScript: File
|
2017-08-06 14:55:35 +00:00
|
|
|
get() = File(cacheDir!!, "$name.kts")
|
2017-10-07 19:36:22 +00:00
|
|
|
val cacheCompiled: File
|
|
|
|
get() = File(cacheDir!!, "$name.kts.compiled")
|
2017-08-06 14:55:35 +00:00
|
|
|
|
2017-08-22 21:41:40 +00:00
|
|
|
fun createChild(name: String, data: Map<String, TypedValue>? = null): TemplateEnvironment
|
2017-08-19 15:45:27 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
class FileTemplateEnvironment: TemplateEnvironment {
|
|
|
|
override val rootName: String
|
|
|
|
override val name: String
|
|
|
|
override val cacheDir: File?
|
|
|
|
override val data: Map<String, TypedValue>
|
|
|
|
|
|
|
|
val templateDir: File
|
|
|
|
val includeDir: File
|
|
|
|
|
|
|
|
override val template: String
|
|
|
|
get() = File(templateDir, "$name.ekt").readText(Charsets.UTF_8)
|
|
|
|
override val include: String
|
|
|
|
get() = File(includeDir, "$name.ekt").readText(Charsets.UTF_8)
|
|
|
|
|
2017-08-06 14:55:35 +00:00
|
|
|
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-22 21:41:40 +00:00
|
|
|
constructor(name: String, parent: FileTemplateEnvironment, data: Map<String, TypedValue>?) {
|
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-22 21:41:40 +00:00
|
|
|
this.cacheDir = parent.cacheDir
|
|
|
|
this.data = data ?: parent.data
|
2017-08-06 14:55:35 +00:00
|
|
|
}
|
|
|
|
|
2017-08-22 21:41:40 +00:00
|
|
|
override fun createChild(name: String, data: Map<String, TypedValue>?): TemplateEnvironment {
|
|
|
|
return FileTemplateEnvironment(name, this, data)
|
2017-08-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
class ClasspathTemplateEnvironment: TemplateEnvironment {
|
|
|
|
override val rootName: String
|
|
|
|
override val name: String
|
|
|
|
override val cacheDir: File?
|
|
|
|
override val data: Map<String, TypedValue>
|
|
|
|
|
|
|
|
val templatePath: String
|
|
|
|
val includePath: String
|
|
|
|
|
|
|
|
override val template: String
|
|
|
|
get() = EKT::class.java.getResourceAsStream("$templatePath/$name.ekt").bufferedReader(Charsets.UTF_8).readText()
|
|
|
|
override val include: String
|
|
|
|
get() = EKT::class.java.getResourceAsStream("$includePath/$name.ekt").bufferedReader(Charsets.UTF_8).readText()
|
|
|
|
|
|
|
|
constructor(name: String, templatePath: String, includePath: String, cacheDir: File?, data: Map<String, TypedValue>) {
|
|
|
|
this.rootName = name
|
|
|
|
this.name = name
|
|
|
|
this.templatePath = templatePath
|
|
|
|
this.includePath = includePath
|
|
|
|
this.cacheDir = cacheDir
|
|
|
|
this.data = data
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(name: String, templatePath: String, includePath: String, cacheDir: File?, init: DataProvider.() -> Unit):
|
|
|
|
this(name, templatePath, includePath, cacheDir, DataProvider.init(init))
|
|
|
|
|
2017-08-22 21:41:40 +00:00
|
|
|
constructor(name: String, parent: ClasspathTemplateEnvironment, data: Map<String, TypedValue>?) {
|
2017-08-19 15:45:27 +00:00
|
|
|
this.rootName = parent.rootName
|
|
|
|
this.name = name
|
|
|
|
this.templatePath = parent.templatePath
|
|
|
|
this.includePath = parent.includePath
|
2017-08-22 21:41:40 +00:00
|
|
|
this.cacheDir = parent.cacheDir
|
|
|
|
this.data = data ?: parent.data
|
2017-08-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
2017-08-22 21:41:40 +00:00
|
|
|
override fun createChild(name: String, data: Map<String, TypedValue>?): TemplateEnvironment {
|
|
|
|
return ClasspathTemplateEnvironment(name, this, data)
|
2017-08-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
2017-08-06 14:55:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class DataProvider {
|
2017-08-05 17:41:24 +00:00
|
|
|
internal val map = mutableMapOf<String, TypedValue>()
|
|
|
|
|
2017-08-20 20:17:04 +00:00
|
|
|
infix fun String.to(value: Any?) {
|
2017-08-05 17:41:24 +00:00
|
|
|
if (value is TypedValue) {
|
|
|
|
map[this] = value
|
|
|
|
} else {
|
2017-08-20 20:17:04 +00:00
|
|
|
if (value == null) {
|
|
|
|
throw RuntimeException("Must provide explicit type for 'null' value")
|
|
|
|
} else {
|
|
|
|
map[this] = TypedValue(value, value::class.qualifiedName!!)
|
|
|
|
}
|
2017-08-05 17:41:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-20 20:17:04 +00:00
|
|
|
infix fun Any?.asType(type: String): TypedValue {
|
2017-08-05 17:41:24 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-08-20 20:17:04 +00:00
|
|
|
data class TypedValue(val value: Any?, val type: String)
|
2017-08-05 17:31:09 +00:00
|
|
|
|
2017-08-04 19:38:25 +00:00
|
|
|
}
|