From: Lanius Trolling Date: Sun, 12 Mar 2023 16:28:37 +0000 (-0400) Subject: Add Groovy-scripted JMustache template lambda functions X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=5bd473c1bfbc7f3541dbc1ad5076075c36326b81;p=factbooks Add Groovy-scripted JMustache template lambda functions --- diff --git a/build.gradle.kts b/build.gradle.kts index 62c0d7b..6a6f8f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0") implementation("com.samskivert:jmustache:1.15") + implementation("org.apache.groovy:groovy-jsr223:4.0.10") implementation(files("libs/nsapi4j.jar")) diff --git a/src/main/kotlin/info/mechyrdia/Configuration.kt b/src/main/kotlin/info/mechyrdia/Configuration.kt index cf389c8..adbffa5 100644 --- a/src/main/kotlin/info/mechyrdia/Configuration.kt +++ b/src/main/kotlin/info/mechyrdia/Configuration.kt @@ -10,10 +10,13 @@ data class Configuration( val host: String = "127.0.0.1", val port: Int = 8080, + val isDevMode: Boolean = false, + val articleDir: String = "../lore", - val templateDir: String = "../tpl", val assetDir: String = "../assets", + val templateDir: String = "../tpl", val jsonDocDir: String = "../data", + val scriptDir: String = "../funcs", val dbName: String = "nslore", val dbConn: String = "mongodb://localhost:27017", diff --git a/src/main/kotlin/info/mechyrdia/lore/article_listing.kt b/src/main/kotlin/info/mechyrdia/lore/article_listing.kt index e578b5d..e337163 100644 --- a/src/main/kotlin/info/mechyrdia/lore/article_listing.kt +++ b/src/main/kotlin/info/mechyrdia/lore/article_listing.kt @@ -1,9 +1,7 @@ package info.mechyrdia.lore -import kotlinx.html.UL -import kotlinx.html.a -import kotlinx.html.li -import kotlinx.html.ul +import info.mechyrdia.Configuration +import kotlinx.html.* import java.io.File data class ArticleNode(val name: String, val subNodes: List) @@ -26,6 +24,9 @@ fun List.renderInto(list: UL, base: String? = null) { val prefix = base?.let { "$it/" } ?: "" forEach { node -> list.li { + if (!Configuration.CurrentConfiguration.isDevMode && (node.name.endsWith(".wip") || node.name.endsWith(".old"))) + style = "display:none" + a(href = "/lore/$prefix${node.name}") { +node.name } if (node.subNodes.isNotEmpty()) ul { diff --git a/src/main/kotlin/info/mechyrdia/lore/preparser.kt b/src/main/kotlin/info/mechyrdia/lore/preparser.kt index 32b28ec..756be77 100644 --- a/src/main/kotlin/info/mechyrdia/lore/preparser.kt +++ b/src/main/kotlin/info/mechyrdia/lore/preparser.kt @@ -4,9 +4,12 @@ import com.samskivert.mustache.Escapers import com.samskivert.mustache.Mustache import info.mechyrdia.Configuration import info.mechyrdia.JsonFileCodec +import info.mechyrdia.application +import io.ktor.server.application.* import io.ktor.util.* import kotlinx.serialization.json.* import java.io.File +import javax.script.ScriptException @JvmInline value class JsonPath private constructor(private val pathElements: List) { @@ -91,7 +94,12 @@ object PreParser { File(Configuration.CurrentConfiguration.jsonDocDir).combineSafe("$name.json") .takeIf { it.isFile } ?.let { file -> - convertJson(JsonFileCodec.parseToJsonElement(file.readText()), file) + val text = file.readText() + val data = convertJson(JsonFileCodec.parseToJsonElement(text), file) + if (data !is Map<*, *>) + error("JSON Object expected in file $name, got $text") + + data + PreParserFunctions.getFunctions() } fun preparse(name: String, content: String): String { @@ -100,7 +108,11 @@ object PreParser { val context = loadJsonContext(name) template.execute(context) } catch (ex: RuntimeException) { - "[h1]Error[/h1]\n\nThere was an error pre-parsing this factbook: ${ex.message}" + application.log.warn("Runtime error pre-parsing factbook $name", ex) + "[h1]Error[/h1]\n\nThere was a runtime error pre-parsing this factbook: ${ex.message}" + } catch (ex: ScriptException) { + application.log.warn("Scripting error pre-parsing factbook $name", ex) + "[h1]Error[/h1]\n\nThere was a scripting error pre-parsing this factbook: ${ex.message}" } } } diff --git a/src/main/kotlin/info/mechyrdia/lore/preparser_functions.kt b/src/main/kotlin/info/mechyrdia/lore/preparser_functions.kt new file mode 100644 index 0000000..bdc6302 --- /dev/null +++ b/src/main/kotlin/info/mechyrdia/lore/preparser_functions.kt @@ -0,0 +1,46 @@ +package info.mechyrdia.lore + +import com.samskivert.mustache.Mustache +import com.samskivert.mustache.Template +import info.mechyrdia.Configuration +import io.ktor.util.* +import java.io.File +import java.io.Writer +import java.security.MessageDigest +import javax.script.Compilable +import javax.script.CompiledScript +import javax.script.ScriptEngineManager +import javax.script.SimpleBindings + +@JvmInline +value class ScriptedMustacheLambda(private val script: CompiledScript) : Mustache.Lambda { + override fun execute(frag: Template.Fragment, out: Writer) { + val bindings = SimpleBindings() + bindings["frag"] = frag + bindings["out"] = out + script.eval(bindings) + } +} + +object PreParserFunctions { + private val scriptEngine = ThreadLocal.withInitial { ScriptEngineManager().getEngineByExtension("groovy") } + private val hasher = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") } + private val cache = mutableMapOf() + + private fun loadFunction(name: String): Mustache.Lambda { + val scriptFile = File(Configuration.CurrentConfiguration.scriptDir).combineSafe("$name.groovy") + val script = scriptFile.readText() + val digest = hasher.get().digest(script.toByteArray()).joinToString(separator = "") { it.toUByte().toString(16) } + cache[digest]?.let { return ScriptedMustacheLambda(it) } + + val compiledScript = (scriptEngine.get() as Compilable).compile(script) + cache[digest] = compiledScript + return ScriptedMustacheLambda(compiledScript) + } + + fun getFunctions(): Map { + val scriptDir = File(Configuration.CurrentConfiguration.scriptDir) + val scripts = scriptDir.listFiles() ?: return emptyMap() + return scripts.associate { it.nameWithoutExtension to loadFunction(it.nameWithoutExtension) } + } +}