From 96a240100e1c1efa5ebe6e8377c401b0749599fd Mon Sep 17 00:00:00 2001 From: limebeck Date: Wed, 31 Jan 2024 23:11:18 +0300 Subject: [PATCH] V0.2.0: Implement static renderer to html --- gradle.properties | 7 +- reveal-kt/app/build.gradle.kts | 11 +++- .../app/src/jvmMain/kotlin/Application.kt | 2 +- reveal-kt/app/src/jvmMain/kotlin/Commands.kt | 66 +++++++++++++++++-- reveal-kt/app/src/jvmMain/kotlin/Utils.kt | 18 +++++ .../app/src/jvmMain/kotlin/server/Server.kt | 22 +++---- 6 files changed, 101 insertions(+), 25 deletions(-) diff --git a/gradle.properties b/gradle.properties index 7189b40..941ffe1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,12 +5,11 @@ yarn.ignoreScripts = false kotlinVersion=1.9.20 -revealKtVersion=0.1.4 +revealKtVersion=0.2.0 kotlinCoroutinesVersion=1.7.3 ktorVersion=2.1.3 dokkaVersion=1.8.20 -repo.uri=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 -repo.username=limebeck -repo.password=***REMOVED*** + +repo.uri=https://maven.pkg.github.com/LimeBeck/reveal-kt diff --git a/reveal-kt/app/build.gradle.kts b/reveal-kt/app/build.gradle.kts index 7dfdbee..f40f6c9 100644 --- a/reveal-kt/app/build.gradle.kts +++ b/reveal-kt/app/build.gradle.kts @@ -95,9 +95,12 @@ application { mainClass.set("dev.limebeck.application.ApplicationKt") } -val copyJsTask = tasks.named("jvmProcessResources") { +val jvmProcessResources = tasks.named("jvmProcessResources") + +val jsCopyTask = tasks.create("jsCopyTask") { val jsBrowserDistribution = tasks.named("jsBrowserDistribution") from(jsBrowserDistribution) + into(jvmProcessResources.get().destinationDir.resolve("js")) excludes.add("*.zip") excludes.add("*.tar") } @@ -113,12 +116,16 @@ val stubJavaDocJar by tasks.registering(Jar::class) { // tasks to create an executable jar with all components of the app val shadow = tasks.getByName("shadowJar") { - dependsOn(copyJsTask) // make sure JS gets compiled first + dependsOn(jsCopyTask) // make sure JS gets compiled first archiveClassifier.set("") mergeServiceFiles() finalizedBy(stubJavaDocJar) } +tasks.named("jvmJar") { + dependsOn(jsCopyTask) +} + tasks.named("publish") { dependsOn(shadow) } diff --git a/reveal-kt/app/src/jvmMain/kotlin/Application.kt b/reveal-kt/app/src/jvmMain/kotlin/Application.kt index 01c68ad..7664423 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/Application.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/Application.kt @@ -4,6 +4,6 @@ import com.github.ajalt.clikt.completion.completionOption import com.github.ajalt.clikt.core.subcommands fun main(args: Array) = RevealKtApplication() - .subcommands(RunServer(), InitTemplate()) + .subcommands(RunServer(), InitTemplate(), RenderToFile()) .completionOption() .main(args) diff --git a/reveal-kt/app/src/jvmMain/kotlin/Commands.kt b/reveal-kt/app/src/jvmMain/kotlin/Commands.kt index c319c33..6b7585b 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/Commands.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/Commands.kt @@ -9,15 +9,13 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.file import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.path -import dev.limebeck.application.server.Config -import dev.limebeck.application.server.ServerConfig -import dev.limebeck.application.server.runServer +import dev.limebeck.application.server.* import dev.limebeck.application.templates.generatePresentationTemplate import dev.limebeck.revealkt.RevealkConfig +import dev.limebeck.revealkt.scripts.RevealKtScriptLoader import java.io.File import java.nio.file.Path -import kotlin.io.path.div -import kotlin.io.path.pathString +import kotlin.io.path.* class RevealKtApplication : CliktCommand( printHelpOnEmptyArgs = true, @@ -58,6 +56,64 @@ class RunServer : CliktCommand(name = "run", help = "Serve presentation with liv } } +class RenderToFile : CliktCommand(name = "render", help = "Render to file") { + val outputDir: Path? by option(help = "Output dir").path( + mustBeWritable = true, + canBeDir = true, + canBeFile = false + ) + val script: File by argument(help = "Script file").file(canBeDir = false, mustBeReadable = true) + + init { + context { + helpFormatter = { + MordantHelpFormatter( + showDefaultValues = true, + context = it + ) + } + } + } + + @OptIn(ExperimentalPathApi::class) + override fun run() { + val scriptLoader = RevealKtScriptLoader() + when (val loadResult = scriptLoader.loadScript(script)) { + is RevealKtScriptLoader.LoadResult.Success -> { + val outputDir = outputDir ?: Path.of("./out") + if (outputDir.notExists()) { + outputDir.createDirectories() + } + + val resources = getResourcesList("js").filter { + it.isFont() || it.name in listOf("revealkt.js", "favicon.ico") + } + + resources.forEach { + it.copyTo(outputDir.resolve(it.name)) + } + + val assetsPath = script.parentFile.resolve("assets").toPath() + if (assetsPath.exists()) { +// println("<4a6f2742> Copy assets from $assetsPath to $outputDir") + assetsPath.copyToRecursively(outputDir.resolve("assets"), followLinks = true, overwrite = true) + } + + val result = renderLoadResult(loadResult) + outputDir.resolve("index.html") + .createFile() + .writeText(result) + } + + is RevealKtScriptLoader.LoadResult.Error -> { + loadResult.diagnostic.forEach { + logger.error("$it") + } + } + } + } +} + class InitTemplate : CliktCommand(name = "init", help = "Create presentation template") { val name: String by argument(help = "Presentation name") val basePath: Path by option(help = "Template dir") diff --git a/reveal-kt/app/src/jvmMain/kotlin/Utils.kt b/reveal-kt/app/src/jvmMain/kotlin/Utils.kt index 6fb7f7e..3215be1 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/Utils.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/Utils.kt @@ -1,5 +1,10 @@ package dev.limebeck.application +import java.nio.file.FileSystems +import java.nio.file.Path +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name + fun printMessage(message: String, symbol: String = "*", minRowLength: Int = 40, borderSize: Int = 2) { val lines = message.lines() val rowLength = maxOf(lines.maxOf { it.length }, minRowLength) @@ -21,3 +26,16 @@ fun printMessage(message: String, symbol: String = "*", minRowLength: Int = 40, fun String.printToConsole(symbol: String = "*", minRowLength: Int = 40, borderSize: Int = 2) = printMessage(this, symbol, minRowLength, borderSize) + +fun getResourcesList(path: String): List { + val classLoader = {}::class.java.classLoader + val resource = classLoader.getResource(path).toURI() + FileSystems.newFileSystem(resource, mapOf("create" to "true")) + return Path.of(resource).listDirectoryEntries().map { + it + } +} + +fun Path.isFont() = name.endsWith(".woff") + || name.endsWith(".eot") + || name.endsWith(".ttf") diff --git a/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt b/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt index 1795a82..de6c02c 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt @@ -15,16 +15,12 @@ import io.ktor.server.plugins.statuspages.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.produceIn +import kotlinx.coroutines.flow.* import org.slf4j.LoggerFactory import java.awt.Desktop import java.io.File import java.net.URI import java.util.* -import kotlin.time.ExperimentalTime import kotlin.time.TimeSource import kotlin.time.measureTimedValue @@ -42,7 +38,7 @@ data class ServerConfig( val logger = LoggerFactory.getLogger("ServerLogger") -@OptIn(ExperimentalTime::class, FlowPreview::class) +@OptIn(FlowPreview::class) fun runServer(config: Config) { println("Starting application...") val startTime = TimeSource.Monotonic.markNow() @@ -72,12 +68,12 @@ fun runServer(config: Config) { } coroutineScope.launch { - updatedFilesStateFlow.collect { sf -> - if (sf != null && ( - sf.any { it.path.contains(config.script.name) } - || sf.any { it.path.contains("assets") } - ) - ) { + updatedFilesStateFlow + .filterNotNull() + .filter { sf -> + sf.any { it.path.contains(config.script.name) } + || sf.any { it.path.contains("assets") } + }.collect { val result = measureTimedValue { val loadResult = scriptLoader.loadScript(config.script) renderLoadResult(loadResult) @@ -85,7 +81,6 @@ fun runServer(config: Config) { logger.info("<00596867> Render time: ${result.duration}") renderedTemplateStateFlow.emit(result.value) } - } } embeddedServer(CIO, environment = applicationEngineEnvironment { @@ -124,6 +119,7 @@ fun runServer(config: Config) { ) { withContext(Dispatchers.IO) { this::class.java.classLoader.getResourceAsStream(resourceName)?.readAllBytes() + ?: this::class.java.classLoader.getResourceAsStream("js/$resourceName")?.readAllBytes() ?: throw NotFoundException() } }