diff --git a/README.md b/README.md index 9dc14b2..23342f3 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,4 @@ The idea is the following: ## References -Currently we use two different designs from https://html5up.net/ \ No newline at end of file +Currently, we use two different designs from https://html5up.net/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5fb6820..43da64a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,18 +1,18 @@ import ru.mipt.npm.gradle.KScienceVersions -import java.time.LocalDateTime +import space.kscience.snark.plugin.JSch +import space.kscience.snark.plugin.execute +import space.kscience.snark.plugin.uploadDirectory +import space.kscience.snark.plugin.useSession plugins { id("ru.mipt.npm.gradle.project") id("ru.mipt.npm.gradle.jvm") + id("space.kscience.snark") application } -repositories { - mavenLocal() -} - group = "ru.mipt.npm" -version = "0.1.0-SNAPSHOT" +version = "0.1.0" application { mainClass.set("io.ktor.server.netty.EngineMain") @@ -21,23 +21,15 @@ application { applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment", "-Xmx200M") } - -val dataforgeVersion by extra("0.6.0-dev-9") +val snarkVersion: String by extra val ktorVersion = KScienceVersions.ktorVersion dependencies { - implementation("io.ktor:ktor-server-core:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-html:0.7.5") - implementation("io.ktor:ktor-server-html-builder:$ktorVersion") - implementation("org.jetbrains.kotlin-wrappers:kotlin-css") - implementation("io.ktor:ktor-server-host-common:$ktorVersion") - implementation("io.ktor:ktor-server-status-pages:$ktorVersion") + implementation("space.kscience:snark-ktor:$snarkVersion") + implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-http-redirect:$ktorVersion") implementation("ch.qos.logback:logback-classic:1.2.11") - implementation("space.kscience:dataforge-workspace:$dataforgeVersion") - implementation("space.kscience:dataforge-io-yaml:$dataforgeVersion") - implementation("org.jetbrains:markdown:0.3.1") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } @@ -46,9 +38,13 @@ kotlin { explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Disabled } +apiValidation{ + validationDisabled = true +} + tasks.withType { kotlinOptions { - freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers" + freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers" } } @@ -58,20 +54,6 @@ sourceSets { } } -val writeBuildDate: Task by tasks.creating { - doLast { - val deployDate = LocalDateTime.now() - val file = File(project.buildDir, "resources/main/buildDate") - file.parentFile.mkdirs() - file.writeText(deployDate.toString()) - } - outputs.file("resources/main/buildDate") - outputs.upToDateWhen { false } -} - -//write build time in build to check outdated external data directory -tasks.getByName("processResources").dependsOn(writeBuildDate) - /* Upload with JSch */ val host = System.getenv("SPC_HOST") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index 48b2cf5..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -plugins{ - `kotlin-dsl` -} - -repositories{ - mavenCentral() - gradlePluginPortal() -} - -dependencies{ - implementation("com.github.mwiede:jsch:0.2.1") -} \ No newline at end of file diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts deleted file mode 100644 index 10374ea..0000000 --- a/buildSrc/settings.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -dependencyResolutionManagement { - repositories { - maven("https://repo.kotlin.link") - mavenCentral() - gradlePluginPortal() - } -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/uploads.kt b/buildSrc/src/main/kotlin/uploads.kt deleted file mode 100644 index 19df10a..0000000 --- a/buildSrc/src/main/kotlin/uploads.kt +++ /dev/null @@ -1,99 +0,0 @@ -import com.jcraft.jsch.* -import java.io.File -import java.io.FileInputStream -import java.util.* - - -/** - * https://kodehelp.com/java-program-uploading-folder-content-recursively-from-local-to-sftp-server/ - */ -private fun ChannelSftp.recursiveFolderUpload(sourceFile: File, destinationPath: String) { - if (sourceFile.isFile) { - // copy if it is a file - cd(destinationPath) - if (!sourceFile.name.startsWith(".")) put( - FileInputStream(sourceFile), - sourceFile.getName(), - ChannelSftp.OVERWRITE - ) - } else { - val files = sourceFile.listFiles() - if (files != null && !sourceFile.getName().startsWith(".")) { - cd(destinationPath) - var attrs: SftpATTRS? = null - // check if the directory is already existing - val directoryPath = destinationPath + "/" + sourceFile.getName() - try { - attrs = stat(directoryPath) - } catch (e: Exception) { - println("$directoryPath does not exist") - } - - // else create a directory - if (attrs != null) { - println("Directory $directoryPath exists IsDir=${attrs.isDir()}") - } else { - println("Creating directory $directoryPath") - mkdir(sourceFile.getName()) - } - for (f in files) { - recursiveFolderUpload(f, destinationPath + "/" + sourceFile.getName()) - } - } - } -} - -fun Session.uploadDirectory( - file: File, - targetDirectory: String, -) { - var channel: ChannelSftp? = null - try { - val config = Properties() - config["StrictHostKeyChecking"] = "no" - channel = openChannel("sftp") as ChannelSftp // Open SFTP Channel - channel.connect() - channel.cd(targetDirectory) // Change Directory on SFTP Server - channel.recursiveFolderUpload(file, targetDirectory) - } finally { - channel?.disconnect() - } -} - -fun Session.execute( - command: String, -): String { - var channel: ChannelExec? = null - try { - channel = openChannel("exec") as ChannelExec - channel.setCommand(command) - channel.inputStream = null - channel.setErrStream(System.err) - val input = channel.inputStream - channel.connect() - return input.use { it.readAllBytes().decodeToString() } - } finally { - channel?.disconnect() - } -} - -inline fun JSch.useSession( - host: String, - user: String, - port: Int = 22, - block: Session.() -> Unit, -) { - var session: Session? = null - try { - session = getSession(user, host, port) - val config = Properties() - config["StrictHostKeyChecking"] = "no" - session.setConfig(config) - session.connect() - session.block() - } finally { - session?.disconnect() - } -} - -fun JSch(configuration: JSch.() -> Unit): JSch = JSch().apply(configuration) diff --git a/data/common/android-chrome-192x192.png b/data/common/android-chrome-192x192.png new file mode 100644 index 0000000..6b59b36 Binary files /dev/null and b/data/common/android-chrome-192x192.png differ diff --git a/data/common/android-chrome-512x512.png b/data/common/android-chrome-512x512.png new file mode 100644 index 0000000..0eece2d Binary files /dev/null and b/data/common/android-chrome-512x512.png differ diff --git a/data/common/apple-touch-icon.png b/data/common/apple-touch-icon.png new file mode 100644 index 0000000..5c4b1ed Binary files /dev/null and b/data/common/apple-touch-icon.png differ diff --git a/data/common/favicon-16x16.png b/data/common/favicon-16x16.png new file mode 100644 index 0000000..eb5e0e5 Binary files /dev/null and b/data/common/favicon-16x16.png differ diff --git a/data/common/favicon-32x32.png b/data/common/favicon-32x32.png new file mode 100644 index 0000000..7296b2e Binary files /dev/null and b/data/common/favicon-32x32.png differ diff --git a/data/common/favicon.ico b/data/common/favicon.ico new file mode 100644 index 0000000..cef4411 Binary files /dev/null and b/data/common/favicon.ico differ diff --git a/data/common/site.webmanifest b/data/common/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/data/common/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e2e0fd8..26b15c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ kotlin.code.style=official -toolsVersion=0.11.7-kotlin-1.7.0 \ No newline at end of file +toolsVersion=0.11.7-kotlin-1.7.0 +snarkVersion=0.1.0-dev-1 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5841487..ea2d6ee 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,9 +6,9 @@ enableFeaturePreview("VERSION_CATALOGS") pluginManagement { val toolsVersion: String by extra + val snarkVersion: String by extra repositories { - mavenLocal() maven("https://repo.kotlin.link") mavenCentral() gradlePluginPortal() @@ -19,6 +19,7 @@ pluginManagement { id("ru.mipt.npm.gradle.mpp") version toolsVersion id("ru.mipt.npm.gradle.jvm") version toolsVersion id("ru.mipt.npm.gradle.js") version toolsVersion + id("space.kscience.snark") version snarkVersion } } @@ -27,7 +28,6 @@ dependencyResolutionManagement { val toolsVersion: String by extra repositories { - mavenLocal() maven("https://repo.kotlin.link") mavenCentral() } @@ -37,4 +37,9 @@ dependencyResolutionManagement { from("ru.mipt.npm:version-catalog:$toolsVersion") } } +} + +val snarkProjectDirectory = File("../snark") +if(snarkProjectDirectory.exists()) { + includeBuild("../snark") } \ No newline at end of file diff --git a/src/main/kotlin/html5up/forty/common.kt b/src/main/kotlin/html5up/forty/common.kt index 753294f..93e8ad0 100644 --- a/src/main/kotlin/html5up/forty/common.kt +++ b/src/main/kotlin/html5up/forty/common.kt @@ -1,7 +1,7 @@ package html5up.forty import kotlinx.html.* -import space.kscience.snark.PageBuilder +import space.kscience.snark.html.WebPage internal fun FlowContent.fortyMenu() { @@ -200,7 +200,7 @@ internal fun FlowContent.fortyFooter() { } } -context(PageBuilder) internal fun BODY.fortyScripts() { +context(WebPage) internal fun BODY.fortyScripts() { script { src = resolveRef("assets/js/jquery.min.js") } diff --git a/src/main/kotlin/html5up/forty/landing.kt b/src/main/kotlin/html5up/forty/landing.kt index 6bd9f8d..530242b 100644 --- a/src/main/kotlin/html5up/forty/landing.kt +++ b/src/main/kotlin/html5up/forty/landing.kt @@ -1,9 +1,9 @@ package html5up.forty import kotlinx.html.* -import space.kscience.snark.PageBuilder +import space.kscience.snark.html.WebPage -context(PageBuilder) internal fun HTML.landing(){ +context(WebPage) internal fun HTML.landing(){ head { title { } diff --git a/src/main/kotlin/html5up/forty/page.kt b/src/main/kotlin/html5up/forty/page.kt index d7986df..81d7128 100644 --- a/src/main/kotlin/html5up/forty/page.kt +++ b/src/main/kotlin/html5up/forty/page.kt @@ -1,9 +1,9 @@ package html5up.forty import kotlinx.html.* -import space.kscience.snark.PageBuilder +import space.kscience.snark.html.WebPage -context(PageBuilder) internal fun HTML.fortyPage(){ +context(WebPage) internal fun HTML.fortyPage(){ head { title { } diff --git a/src/main/kotlin/ru/mipt/spc/Application.kt b/src/main/kotlin/ru/mipt/spc/Application.kt index cdb0feb..61e33eb 100644 --- a/src/main/kotlin/ru/mipt/spc/Application.kt +++ b/src/main/kotlin/ru/mipt/spc/Application.kt @@ -5,10 +5,8 @@ import io.ktor.server.application.log import kotlinx.css.CssBuilder import kotlinx.html.CommonAttributeGroupFacade import kotlinx.html.style -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.fetch -import space.kscience.snark.SnarkPlugin -import space.kscience.snark.snarkSite +import space.kscience.snark.SnarkEnvironment +import space.kscience.snark.ktor.site import java.net.URI import java.nio.file.FileSystems import java.nio.file.Files @@ -49,11 +47,6 @@ const val BUILD_DATE_FILE = "/buildDate" fun Application.spcModule() { // install(HttpsRedirect) - val context = Context("spc-site") { - plugin(SnarkPlugin) - } - val snark = context.fetch(SnarkPlugin) - val dataPath = Path.of("data") // Clear data directory if it is outdated @@ -88,20 +81,26 @@ fun Application.spcModule() { dataPath.resolve(DEPLOY_DATE_FILE).writeText(date) } - snarkSite(snark) { + SnarkEnvironment.default.site { + + resolveData( + this@spcModule.javaClass.getResource("/common")!!.toURI(), + dataPath / "common" + ) + val homeDataPath = resolveData( this@spcModule.javaClass.getResource("/home")!!.toURI(), dataPath / "home" ) - spcHome(rootPath = homeDataPath) + spcHome(dataPath = homeDataPath) val mastersDataPath = resolveData( this@spcModule.javaClass.getResource("/magprog")!!.toURI(), dataPath / "magprog" ) - spcMaster(dataPath = mastersDataPath) + spcMasters(dataPath = mastersDataPath) } } diff --git a/src/main/kotlin/ru/mipt/spc/spcCollection.kt b/src/main/kotlin/ru/mipt/spc/spcCollection.kt index 74518c0..aaf88b0 100644 --- a/src/main/kotlin/ru/mipt/spc/spcCollection.kt +++ b/src/main/kotlin/ru/mipt/spc/spcCollection.kt @@ -10,12 +10,12 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.withIndex import space.kscience.dataforge.values.string -import space.kscience.snark.* +import space.kscience.snark.html.* import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set -context(PageBuilder) private fun FlowContent.spcSpotlightContent( +context(WebPage) private fun FlowContent.spcSpotlightContent( landing: HtmlData, content: Map, ) { diff --git a/src/main/kotlin/ru/mipt/spc/spcHome.kt b/src/main/kotlin/ru/mipt/spc/spcHome.kt index 2c22d09..b9c1120 100644 --- a/src/main/kotlin/ru/mipt/spc/spcHome.kt +++ b/src/main/kotlin/ru/mipt/spc/spcHome.kt @@ -10,12 +10,12 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.startsWith import space.kscience.dataforge.values.string -import space.kscience.snark.* +import space.kscience.snark.html.* import java.nio.file.Path import kotlin.reflect.typeOf -context(PageBuilder) internal fun HTML.spcPageContent( +context(WebPage) internal fun HTML.spcPageContent( meta: Meta, title: String = meta["title"].string ?: SPC_TITLE, fragment: FlowContent.() -> Unit, @@ -65,7 +65,7 @@ internal val FortyDataRenderer: DataRenderer = { name, data -> } -context(PageBuilder) private fun HTML.spcHome() { +context(WebPage) private fun HTML.spcHome() { spcHead() body("is-preload") { wrapper { @@ -252,13 +252,14 @@ context(PageBuilder) private fun HTML.spcHome() { } -internal fun SiteBuilder.spcHome(rootPath: Path, prefix: Name = Name.EMPTY) { +internal fun SiteBuilder.spcHome(dataPath: Path, prefix: Name = Name.EMPTY) { - val homePageData = snark.readDirectory(rootPath.resolve("content")) + val homePageData = snark.readDirectory(dataPath.resolve("content")) route(prefix, homePageData, setAsRoot = true) { - assetDirectory("assets", rootPath.resolve("assets")) - assetDirectory("images", rootPath.resolve("images")) + file(dataPath.resolve("assets")) + file(dataPath.resolve("images")) + file(dataPath.resolve("../common"), "") page { spcHome() } diff --git a/src/main/kotlin/ru/mipt/spc/master.kt b/src/main/kotlin/ru/mipt/spc/spcMasters.kt similarity index 90% rename from src/main/kotlin/ru/mipt/spc/master.kt rename to src/main/kotlin/ru/mipt/spc/spcMasters.kt index f873a52..19b702f 100644 --- a/src/main/kotlin/ru/mipt/spc/master.kt +++ b/src/main/kotlin/ru/mipt/spc/spcMasters.kt @@ -14,6 +14,8 @@ import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.withIndex import space.kscience.snark.* +import space.kscience.snark.html.* +import space.kscience.snark.html.WebPage import java.nio.file.Path import kotlin.collections.component1 import kotlin.collections.component2 @@ -35,14 +37,14 @@ import kotlin.collections.set private val HtmlData.imagePath: String? get() = meta["image"]?.string ?: meta["image.path"].string private val HtmlData.name: String get() = meta["name"].string ?: error("Name not found") -context(PageBuilder) class MagProgSection( +context(WebPage) class MagProgSection( val id: String, val title: String, val style: String, val content: FlowContent.() -> Unit, ) -context(PageBuilder) private fun wrapSection( +context(WebPage) private fun wrapSection( id: String, title: String, sectionContent: FlowContent.() -> Unit, @@ -53,7 +55,7 @@ context(PageBuilder) private fun wrapSection( } } -context(PageBuilder) private fun wrapSection( +context(WebPage) private fun wrapSection( block: HtmlData, idOverride: String? = null, ): MagProgSection = wrapSection( @@ -71,7 +73,7 @@ private val PROGRAM_PATH: Name = CONTENT_NODE_NAME + "program" private val RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses" private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners" -context(PageBuilder) private fun FlowContent.programSection() { +context(WebPage) private fun FlowContent.programSection() { val programBlock = data.resolveHtml(PROGRAM_PATH)!! val recommendedBlock = data.resolveHtml(RECOMMENDED_COURSES_PATH)!! div("inner") { @@ -88,7 +90,7 @@ context(PageBuilder) private fun FlowContent.programSection() { } } -context(PageBuilder) private fun FlowContent.partners() { +context(WebPage) private fun FlowContent.partners() { //val partnersData: Meta = resolve(PARTNERS_PATH)?.meta ?: Meta.EMPTY val partnersData: Meta = runBlocking { data.getByType(PARTNERS_PATH)?.await() } ?: Meta.EMPTY div("inner") { @@ -118,7 +120,7 @@ context(PageBuilder) private fun FlowContent.partners() { // val photo: String? by meta.string() //} -context(PageBuilder) private fun FlowContent.team() { +context(WebPage) private fun FlowContent.team() { val team = data.findByContentType("magprog_team").values.sortedBy { it.order } div("inner") { @@ -173,7 +175,7 @@ context(PageBuilder) private fun FlowContent.team() { // } } -context(PageBuilder) private fun FlowContent.mentors() { +context(WebPage) private fun FlowContent.mentors() { val mentors = data.findByContentType("magprog_mentor").entries.sortedBy { it.value.id } div("inner") { @@ -211,7 +213,7 @@ context(PageBuilder) private fun FlowContent.mentors() { } } -context(PageBuilder) internal fun HTML.magProgHead(title: String) { +context(WebPage) internal fun HTML.magProgHead(title: String) { head { this.title = title meta { @@ -235,10 +237,31 @@ context(PageBuilder) internal fun HTML.magProgHead(title: String) { href = resolveRef("assets/css/noscript.css") } } + link { + rel = "apple-touch-icon" + sizes = "180x180" + href = "/apple-touch-icon.png" + } + link { + rel = "icon" + type = "image/png" + sizes = "32x32" + href = "/favicon-32x32.png" + } + link { + rel = "icon" + type = "image/png" + sizes = "16x16" + href = "/favicon-16x16.png" + } + link { + rel = "manifest" + href = "/site.webmanifest" + } } } -context(PageBuilder) internal fun BODY.magProgFooter() { +context(WebPage) internal fun BODY.magProgFooter() { footer("wrapper style1-alt") { id = "footer" div("inner") { @@ -277,15 +300,16 @@ context(PageBuilder) internal fun BODY.magProgFooter() { } } -private val HtmlData.mentorPageId get() = "mentor-${id}" +context(SnarkContext) private val HtmlData.mentorPageId get() = "mentor-${id}" -internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asName()) { +internal fun SiteBuilder.spcMasters(dataPath: Path, prefix: Name = "magprog".asName()) { val magProgData: DataTree = snark.readDirectory(dataPath.resolve("content")) route(prefix, magProgData, setAsRoot = true) { - assetDirectory("assets", dataPath.resolve("assets")) - assetDirectory("images", dataPath.resolve("images")) + file(dataPath.resolve("assets")) + file(dataPath.resolve("images")) + file(dataPath.resolve("../common"), "") page { val sections = listOf( diff --git a/src/main/kotlin/ru/mipt/spc/spcMisc.kt b/src/main/kotlin/ru/mipt/spc/spcMisc.kt index ffb9a10..36ca434 100644 --- a/src/main/kotlin/ru/mipt/spc/spcMisc.kt +++ b/src/main/kotlin/ru/mipt/spc/spcMisc.kt @@ -1,14 +1,14 @@ package ru.mipt.spc import kotlinx.html.* -import space.kscience.snark.PageBuilder -import space.kscience.snark.homeRef -import space.kscience.snark.resolvePageRef +import space.kscience.snark.html.WebPage +import space.kscience.snark.html.homeRef +import space.kscience.snark.html.resolvePageRef internal const val SPC_TITLE = "Scientific Programming Centre" -context(PageBuilder) internal fun HTML.spcHead(title: String = SPC_TITLE) { +context(WebPage) internal fun HTML.spcHead(title: String = SPC_TITLE) { head { title { +title @@ -24,10 +24,31 @@ context(PageBuilder) internal fun HTML.spcHead(title: String = SPC_TITLE) { noScript { link(rel = "stylesheet", href = resolveRef("assets/css/noscript.css")) } + link { + rel = "apple-touch-icon" + sizes = "180x180" + href = "/apple-touch-icon.png" + } + link { + rel = "icon" + type = "image/png" + sizes = "32x32" + href = "/favicon-32x32.png" + } + link { + rel = "icon" + type = "image/png" + sizes = "16x16" + href = "/favicon-16x16.png" + } + link { + rel = "manifest" + href = "/site.webmanifest" + } } } -context(PageBuilder) internal fun FlowContent.spcHomeMenu() { +context(WebPage) internal fun FlowContent.spcHomeMenu() { nav { id = "menu" ul("links") { @@ -39,7 +60,7 @@ context(PageBuilder) internal fun FlowContent.spcHomeMenu() { } li { a { - href = resolvePageRef("magprog") + href = resolvePageRef("magprog.index") +"""Master""" } } @@ -51,7 +72,7 @@ context(PageBuilder) internal fun FlowContent.spcHomeMenu() { } li { a { - href = resolvePageRef("consulting") + href = resolvePageRef("consulting.index") +"""Consulting""" } } @@ -79,7 +100,7 @@ context(PageBuilder) internal fun FlowContent.spcHomeMenu() { } } -context(PageBuilder) internal fun FlowContent.spcFooter() { +context(WebPage) internal fun FlowContent.spcFooter() { footer { id = "footer" div("inner") { @@ -129,7 +150,7 @@ context(PageBuilder) internal fun FlowContent.spcFooter() { } } -context(PageBuilder) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) { +context(WebPage) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) { div { id = "wrapper" // Header diff --git a/src/main/kotlin/ru/mipt/spc/staticRender.kt b/src/main/kotlin/ru/mipt/spc/staticRender.kt index 46dc7ad..4af783e 100644 --- a/src/main/kotlin/ru/mipt/spc/staticRender.kt +++ b/src/main/kotlin/ru/mipt/spc/staticRender.kt @@ -1,15 +1,13 @@ package ru.mipt.spc -import space.kscience.dataforge.context.Global -import space.kscience.dataforge.context.fetch -import space.kscience.snark.SnarkPlugin -import space.kscience.snark.static +import space.kscience.snark.SnarkEnvironment +import space.kscience.snark.html.static import java.nio.file.Path import kotlin.io.path.toPath fun main() { - Global.fetch(SnarkPlugin).static(Path.of("build/out")) { - spcHome(rootPath = javaClass.getResource("/home")!!.toURI().toPath()) - spcMaster(dataPath = javaClass.getResource("/magprog")!!.toURI().toPath()) + SnarkEnvironment.default.static(Path.of("build/out")) { + spcHome(dataPath = javaClass.getResource("/home")!!.toURI().toPath()) + spcMasters(dataPath = javaClass.getResource("/magprog")!!.toURI().toPath()) } } \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/HtmlData.kt b/src/main/kotlin/space/kscience/snark/HtmlData.kt deleted file mode 100644 index b64c3a1..0000000 --- a/src/main/kotlin/space/kscience/snark/HtmlData.kt +++ /dev/null @@ -1,34 +0,0 @@ -package space.kscience.snark - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.html.FlowContent -import kotlinx.html.TagConsumer -import space.kscience.dataforge.data.Data -import space.kscience.dataforge.data.await -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.int -import space.kscience.dataforge.meta.string - - -//TODO replace by VisionForge type -//typealias HtmlFragment = context(PageBuilder, TagConsumer<*>) () -> Unit - -fun interface HtmlFragment { - fun TagConsumer<*>.renderFragment(page: PageBuilder) - //TODO move pageBuilder to a context receiver after KT-52967 is fixed -} - -typealias HtmlData = Data - -//fun HtmlData(meta: Meta, content: context(PageBuilder) TagConsumer<*>.() -> Unit): HtmlData = -// Data(HtmlFragment(content), meta) - -internal val HtmlData.id: String get() = meta["id"]?.string ?: "block[${hashCode()}]" -internal val HtmlData.language: String? get() = meta["language"].string?.lowercase() - -internal val HtmlData.order: Int? get() = meta["order"]?.int - -context(PageBuilder) fun FlowContent.htmlData(data: HtmlData) = runBlocking(Dispatchers.IO) { - with(data.await()) { consumer.renderFragment(this@PageBuilder) } -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt b/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt deleted file mode 100644 index ad575f1..0000000 --- a/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt +++ /dev/null @@ -1,135 +0,0 @@ -package space.kscience.snark - -import io.ktor.http.URLBuilder -import io.ktor.http.URLProtocol -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.html.respondHtml -import io.ktor.server.http.content.* -import io.ktor.server.plugins.origin -import io.ktor.server.request.host -import io.ktor.server.request.port -import io.ktor.server.routing.Route -import io.ktor.server.routing.createRouteFromPath -import io.ktor.server.routing.get -import io.ktor.server.routing.routing -import kotlinx.html.HTML -import space.kscience.dataforge.data.DataTree -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.withDefault -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.cutLast -import space.kscience.dataforge.names.endsWith -import java.nio.file.Path -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@PublishedApi -internal class KtorSiteBuilder( - override val snark: SnarkPlugin, - override val data: DataTree<*>, - override val meta: Meta, - private val baseUrl: String, - private val ktorRoute: Route, -) : SiteBuilder { - - override fun assetFile(remotePath: String, file: Path) { - ktorRoute.file(remotePath, file.toFile()) - } - - override fun assetDirectory(remotePath: String, directory: Path) { - ktorRoute.static(remotePath) { - files(directory.toFile()) - } - } - - private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) { - ref - } else if (ref.isEmpty()) { - baseUrl - } else { - "${baseUrl.removeSuffix("/")}/$ref" - } - - - inner class KtorPageBuilder( - val pageBaseUrl: String, - override val meta: Meta = this@KtorSiteBuilder.meta, - ) : PageBuilder { - override val snark: SnarkPlugin get() = this@KtorSiteBuilder.snark - override val data: DataTree<*> get() = this@KtorSiteBuilder.data - - override fun resolveRef(ref: String): String = resolveRef(pageBaseUrl, ref) - - override fun resolvePageRef(pageName: Name): String = if (pageName.endsWith(SiteBuilder.INDEX_PAGE_TOKEN)) { - resolveRef(pageName.cutLast().toWebPath()) - } else { - resolveRef(pageName.toWebPath()) - } - } - - override fun page(route: Name, content: context(PageBuilder, HTML)() -> Unit) { - ktorRoute.get(route.toWebPath()) { - call.respondHtml { - val request = call.request - //substitute host for url for backwards calls - val url = URLBuilder(baseUrl).apply { - protocol = URLProtocol.createOrDefault(request.origin.scheme) - host = request.host() - port = request.port() - } - val pageBuilder = KtorPageBuilder(url.buildString()) - content(pageBuilder, this) - } - } - } - - override fun route( - routeName: Name, - dataOverride: DataTree<*>?, - metaOverride: Meta?, - setAsRoot: Boolean, - ): SiteBuilder = KtorSiteBuilder( - snark = snark, - data = dataOverride ?: data, - meta = metaOverride?.withDefault(meta) ?: meta, - baseUrl = if (setAsRoot) { - resolveRef(baseUrl, routeName.toWebPath()) - } else { - baseUrl - }, - ktorRoute = ktorRoute.createRouteFromPath(routeName.toWebPath()) - ) - - - override fun assetResourceFile(remotePath: String, resourcesPath: String) { - ktorRoute.resource(resourcesPath, resourcesPath) - } - - override fun assetResourceDirectory(resourcesPath: String) { - ktorRoute.resources(resourcesPath) - } -} - -inline fun Route.snarkSite( - snark: SnarkPlugin, - data: DataTree<*>, - meta: Meta = data.meta, - block: SiteBuilder.() -> Unit, -) { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite)) -} - -fun Application.snarkSite( - snark: SnarkPlugin, - data: DataTree<*> = DataTree.empty(), - meta: Meta = data.meta, - block: SiteBuilder.() -> Unit, -) { - routing { - snarkSite(snark, data, meta, block) - } -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/PageBuilder.kt b/src/main/kotlin/space/kscience/snark/PageBuilder.kt deleted file mode 100644 index d675f9e..0000000 --- a/src/main/kotlin/space/kscience/snark/PageBuilder.kt +++ /dev/null @@ -1,65 +0,0 @@ -package space.kscience.snark - -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.ContextAware -import space.kscience.dataforge.data.* -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.names.* - -internal fun Name.toWebPath() = tokens.joinToString(separator = "/"){ - if (it.hasIndex()) { - "${it.body}[${it.index}]" - } else { - it.body - } -} - -interface PageBuilder : ContextAware { - - val snark: SnarkPlugin - - override val context: Context get() = snark.context - - val data: DataTree<*> - - val meta: Meta - - fun resolveRef(ref: String): String - - fun resolvePageRef(pageName: Name): String -} - - -fun PageBuilder.resolvePageRef(pageName: String) = resolvePageRef(pageName.parseAsName()) - -val PageBuilder.homeRef get() = resolvePageRef(SiteBuilder.INDEX_PAGE_TOKEN.asName()) - -/** - * Resolve a Html builder by its full name - */ -fun DataTree<*>.resolveHtml(name: Name): HtmlData? { - val resolved = (getByType(name) ?: getByType(name + SiteBuilder.INDEX_PAGE_TOKEN)) - - return resolved?.takeIf { - it.published //TODO add language confirmation - } -} - -/** - * Find all Html blocks using given name/meta filter - */ -fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map = - filterByType { name, meta -> - predicate(name, meta) - && meta["published"].string != "false" - //TODO add language confirmation - }.asSequence().associate { it.name to it.data } - - -fun DataTree<*>.findByContentType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta -> - name.startsWith(baseName) && meta["content_type"].string == contentType -} - -internal val Data<*>.published: Boolean get() = meta["published"].string != "false" \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/SiteBuilder.kt b/src/main/kotlin/space/kscience/snark/SiteBuilder.kt deleted file mode 100644 index e6e96aa..0000000 --- a/src/main/kotlin/space/kscience/snark/SiteBuilder.kt +++ /dev/null @@ -1,97 +0,0 @@ -package space.kscience.snark - -import kotlinx.html.HTML -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.ContextAware -import space.kscience.dataforge.data.DataTree -import space.kscience.dataforge.data.DataTreeItem -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.NameToken -import space.kscience.dataforge.names.parseAsName -import java.nio.file.Path -import kotlin.reflect.KType -import kotlin.reflect.typeOf - - -/** - * An abstraction, which is used to render sites to the different rendering engines - */ -interface SiteBuilder : ContextAware { - - val data: DataTree<*> - - val snark: SnarkPlugin - - override val context: Context get() = snark.context - - val meta: Meta - - fun assetFile(remotePath: String, file: Path) - - fun assetDirectory(remotePath: String, directory: Path) - - fun assetResourceFile(remotePath: String, resourcesPath: String) - - fun assetResourceDirectory(resourcesPath: String) - - fun page(route: Name = Name.EMPTY, content: context(PageBuilder, HTML) () -> Unit) - - /** - * Create a route with optional data tree override. For example one could use a subtree of the initial tree. - * By default, the same data tree is used for route - */ - fun route( - routeName: Name, - dataOverride: DataTree<*>? = null, - metaOverride: Meta? = null, - setAsRoot: Boolean = false, - ): SiteBuilder - - companion object { - val INDEX_PAGE_TOKEN: NameToken = NameToken("index") - val UP_PAGE_TOKEN: NameToken = NameToken("..") - } -} - -public inline fun SiteBuilder.route( - route: Name, - dataOverride: DataTree<*>? = null, - metaOverride: Meta? = null, - setAsRoot: Boolean = false, - block: SiteBuilder.() -> Unit, -) { - route(route, dataOverride, metaOverride, setAsRoot).apply(block) -} - -public inline fun SiteBuilder.route( - route: String, - dataOverride: DataTree<*>? = null, - metaOverride: Meta? = null, - setAsRoot: Boolean = false, - block: SiteBuilder.() -> Unit, -) { - route(route.parseAsName(), dataOverride, metaOverride, setAsRoot).apply(block) -} - - -///** -// * Create a stand-alone site at a given node -// */ -//public fun SiteBuilder.site(route: Name, dataRoot: DataTree<*>, block: SiteBuilder.() -> Unit) { -// val mountedData = data.copy( -// data = dataRoot, -// baseUrlPath = data.resolveRef(route.tokens.joinToString(separator = "/")), -// meta = Laminate(dataRoot.meta, data.meta) //layering dataRoot meta over existing data -// ) -// route(route) { -// withData(mountedData).block() -// } -//} - -//TODO move to DF -fun DataTree.Companion.empty(meta: Meta = Meta.EMPTY) = object : DataTree { - override val items: Map> get() = emptyMap() - override val dataType: KType get() = typeOf() - override val meta: Meta get() = meta -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/SiteLayout.kt b/src/main/kotlin/space/kscience/snark/SiteLayout.kt deleted file mode 100644 index f04f4f1..0000000 --- a/src/main/kotlin/space/kscience/snark/SiteLayout.kt +++ /dev/null @@ -1,150 +0,0 @@ -package space.kscience.snark - -import kotlinx.html.body -import kotlinx.html.head -import kotlinx.html.title -import space.kscience.dataforge.data.Data -import space.kscience.dataforge.data.DataTreeItem -import space.kscience.dataforge.data.getItem -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.getIndexed -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.misc.Type -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.NameToken -import space.kscience.dataforge.names.asName -import space.kscience.dataforge.names.parseAsName -import space.kscience.snark.SiteLayout.Companion.ASSETS_KEY -import space.kscience.snark.SiteLayout.Companion.INDEX_PAGE_TOKEN -import space.kscience.snark.SiteLayout.Companion.LAYOUT_KEY -import java.nio.file.Path -import kotlin.reflect.typeOf - -internal fun SiteBuilder.assetsFrom(rootMeta: Meta) { - rootMeta.getIndexed("resource".asName()).forEach { (_, meta) -> - - val path by meta.string() - val remotePath by meta.string() - - path?.let { resourcePath -> - //If remote path provided, use a single resource - remotePath?.let { - assetResourceFile(it, resourcePath) - return@forEach - } - - //otherwise use package resources - assetResourceDirectory(resourcePath) - } - } - - rootMeta.getIndexed("file".asName()).forEach { (_, meta) -> - val remotePath by meta.string { error("File remote path is not provided") } - val path by meta.string { error("File path is not provided") } - assetFile(remotePath, Path.of(path)) - } - - rootMeta.getIndexed("directory".asName()).forEach { (_, meta) -> - val path by meta.string { error("Directory path is not provided") } - assetDirectory("", Path.of(path)) - } -} - -typealias DataRenderer = SiteBuilder.(name: Name, data: Data) -> Unit - -/** - * Recursively renders the data items in [data]. If [LAYOUT_KEY] is defined in an item, use it to load - * layout from the context, otherwise render children nodes as name segments and individual data items using [dataRenderer]. - */ -fun SiteBuilder.pages( - data: DataTreeItem<*>, - dataRenderer: DataRenderer = SiteLayout.defaultDataRenderer, -) { - val layoutMeta = data.meta[LAYOUT_KEY] - if (layoutMeta != null) { - //use layout if it is defined - snark.layout(layoutMeta).render(data) - } else { - when (data) { - is DataTreeItem.Node -> { - data.tree.items.forEach { (token, item) -> - //Don't apply index token - if (token == INDEX_PAGE_TOKEN) { - pages(item, dataRenderer) - } else if (item is DataTreeItem.Leaf) { - dataRenderer(this, token.asName(), item.data) - } else { - route(token.asName()) { - pages(item, dataRenderer) - } - } - } - } - is DataTreeItem.Leaf -> { - dataRenderer.invoke(this, Name.EMPTY, data.data) - } - } - data.meta[ASSETS_KEY]?.let { - assetsFrom(it) - } - } - //TODO watch for changes -} - -/** - * Render all pages in a node with given name - */ -fun SiteBuilder.pages( - dataPath: Name, - remotePath: Name = dataPath, - dataRenderer: DataRenderer = SiteLayout.defaultDataRenderer, -) { - val item = data.getItem(dataPath) ?: error("No data found by name $dataPath") - route(remotePath) { - pages(item, dataRenderer) - } -} - -fun SiteBuilder.pages( - dataPath: String, - remotePath: Name = dataPath.parseAsName(), - dataRenderer: DataRenderer = SiteLayout.defaultDataRenderer, -) { - pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer) -} - - -@Type(SiteLayout.TYPE) -fun interface SiteLayout { - - context(SiteBuilder) fun render(item: DataTreeItem<*>) - - companion object { - const val TYPE = "snark.layout" - const val LAYOUT_KEY = "layout" - const val ASSETS_KEY = "assets" - val INDEX_PAGE_TOKEN = NameToken("index") - - val defaultDataRenderer: SiteBuilder.(name: Name, data: Data<*>) -> Unit = { name: Name, data: Data<*> -> - if (data.type == typeOf()) { - page(name) { - head { - title = data.meta["title"].string ?: "Untitled page" - } - body { - @Suppress("UNCHECKED_CAST") - htmlData(data as HtmlData) - } - } - } - } - } -} - - -object DefaultSiteLayout : SiteLayout { - context(SiteBuilder) override fun render(item: DataTreeItem<*>) { - pages(item) - } -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt b/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt deleted file mode 100644 index 547b2ad..0000000 --- a/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt +++ /dev/null @@ -1,146 +0,0 @@ -package space.kscience.snark - -import io.ktor.util.extension -import io.ktor.utils.io.core.Input -import io.ktor.utils.io.core.readBytes -import space.kscience.dataforge.context.* -import space.kscience.dataforge.data.DataTree -import space.kscience.dataforge.io.IOReader -import space.kscience.dataforge.io.JsonMetaFormat -import space.kscience.dataforge.io.asBinary -import space.kscience.dataforge.io.readWith -import space.kscience.dataforge.io.yaml.YamlMetaFormat -import space.kscience.dataforge.io.yaml.YamlPlugin -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.misc.DFExperimental -import space.kscience.dataforge.misc.Type -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.asName -import space.kscience.dataforge.names.parseAsName -import space.kscience.dataforge.workspace.FileData -import space.kscience.dataforge.workspace.readDataDirectory -import java.nio.file.Path -import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -/** - * A parser of binary content including priority flag and file extensions - */ -@Type(SnarkParser.TYPE) -interface SnarkParser { - val type: KType - - val fileExtensions: Set - - val priority: Int get() = DEFAULT_PRIORITY - - fun parse(context: Context, meta: Meta, bytes: ByteArray): R - - fun reader(context: Context, meta: Meta) = object : IOReader { - override val type: KType get() = this@SnarkParser.type - - override fun readObject(input: Input): R = parse(context, meta, input.readBytes()) - } - - companion object { - const val TYPE = "snark.parser" - const val DEFAULT_PRIORITY = 10 - } -} - -@PublishedApi -internal class SnarkParserWrapper( - val reader: IOReader, - override val type: KType, - override val fileExtensions: Set, -) : SnarkParser { - override fun parse(context: Context, meta: Meta, bytes: ByteArray): R = bytes.asBinary().readWith(reader) -} - -/** - * Create a generic parser from reader - */ -@Suppress("FunctionName") -inline fun SnarkParser( - reader: IOReader, - vararg fileExtensions: String, -): SnarkParser = SnarkParserWrapper(reader, typeOf(), fileExtensions.toSet()) - -@OptIn(DFExperimental::class) -class SnarkPlugin : AbstractPlugin() { - private val yaml by require(YamlPlugin) - val io get() = yaml.io - - override val tag: PluginTag get() = Companion.tag - - private val parsers: Map> by lazy { - context.gather(SnarkParser.TYPE, true) - } - - private val layouts: Map by lazy { - context.gather(SiteLayout.TYPE, true) - } - - private val textTransformations: Map by lazy { - context.gather(TextTransformation.TYPE, true) - } - - fun readDirectory(path: Path): DataTree = io.readDataDirectory(path) { dataPath, meta -> - val fileExtension = meta[FileData.META_FILE_EXTENSION_KEY].string ?: dataPath.extension - val parser: SnarkParser = parsers.values.filter { parser -> - fileExtension in parser.fileExtensions - }.maxByOrNull { - it.priority - } ?: run { - logger.warn { "The parser is not found for file $dataPath with meta $meta" } - byteArraySnarkParser - } - - parser.reader(context, meta) - } - - internal fun layout(layoutMeta: Meta): SiteLayout { - val layoutName = layoutMeta.string - ?: layoutMeta["name"].string ?: error("Layout name not defined in $layoutMeta") - return layouts[layoutName.parseAsName()] ?: error("Layout with name $layoutName not found in $this") - } - - internal fun textTransformation(transformationMeta: Meta): TextTransformation { - val transformationName = transformationMeta.string - ?: transformationMeta["name"].string ?: error("Transformation name not defined in $transformationMeta") - return textTransformations[transformationName.parseAsName()] - ?: error("Text transformation with name $transformationName not found in $this") - } - - override fun content(target: String): Map = when (target) { - SnarkParser.TYPE -> mapOf( - "html".asName() to SnarkHtmlParser, - "markdown".asName() to SnarkMarkdownParser, - "json".asName() to SnarkParser(JsonMetaFormat, "json"), - "yaml".asName() to SnarkParser(YamlMetaFormat, "yaml", "yml"), - "png".asName() to SnarkParser(ImageIOReader, "png"), - "jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"), - "gif".asName() to SnarkParser(ImageIOReader, "gif"), - ) - TextTransformation.TYPE -> mapOf( - "basic".asName() to BasicTextTransformation - ) - else -> super.content(target) - } - - companion object : PluginFactory { - override val tag: PluginTag = PluginTag("snark") - override val type: KClass = SnarkPlugin::class - - override fun build(context: Context, meta: Meta): SnarkPlugin = SnarkPlugin() - - private val byteArrayIOReader = IOReader { - readBytes() - } - - private val byteArraySnarkParser = SnarkParser(byteArrayIOReader) - } -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/SnarkTextParser.kt b/src/main/kotlin/space/kscience/snark/SnarkTextParser.kt deleted file mode 100644 index ee8a0f7..0000000 --- a/src/main/kotlin/space/kscience/snark/SnarkTextParser.kt +++ /dev/null @@ -1,68 +0,0 @@ -package space.kscience.snark - -import io.ktor.util.asStream -import io.ktor.utils.io.core.Input -import kotlinx.html.div -import kotlinx.html.unsafe -import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor -import org.intellij.markdown.html.HtmlGenerator -import org.intellij.markdown.parser.MarkdownParser -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.io.IOReader -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.get -import java.awt.image.BufferedImage -import javax.imageio.ImageIO -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -abstract class SnarkTextParser : SnarkParser { - abstract fun parseText(text: String, meta: Meta): R - - override fun parse(context: Context, meta: Meta, bytes: ByteArray): R = - parseText(bytes.decodeToString(), meta) - - fun transformText(text: String, meta: Meta, page: PageBuilder): String = - meta[TextTransformation.TEXT_TRANSFORMATION_KEY]?.let { - with(page){ page.snark.textTransformation(it).transform(text)} - } ?: text -} - - -internal object SnarkHtmlParser : SnarkTextParser() { - override val fileExtensions: Set = setOf("html") - override val type: KType = typeOf() - - override fun parseText(text: String, meta: Meta): HtmlFragment = HtmlFragment { page -> - div { - unsafe { +transformText(text, meta, page) } - } - } -} - -internal object SnarkMarkdownParser : SnarkTextParser() { - override val fileExtensions: Set = setOf("markdown", "mdown", "mkdn", "mkd", "md") - override val type: KType = typeOf() - - private val markdownFlavor = CommonMarkFlavourDescriptor() - private val markdownParser = MarkdownParser(markdownFlavor) - - override fun parseText(text: String, meta: Meta): HtmlFragment { - val parsedTree = markdownParser.buildMarkdownTreeFromString(text) - val htmlString = HtmlGenerator(text, parsedTree, markdownFlavor).generateHtml() - - return HtmlFragment { page -> - div { - unsafe { - +SnarkHtmlParser.transformText(htmlString, meta, page) - } - } - } - } -} - -internal object ImageIOReader : IOReader { - override val type: KType get() = typeOf() - - override fun readObject(input: Input): BufferedImage = ImageIO.read(input.asStream()) -} diff --git a/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt b/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt deleted file mode 100644 index dbc796b..0000000 --- a/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt +++ /dev/null @@ -1,126 +0,0 @@ -package space.kscience.snark - -import kotlinx.html.HTML -import kotlinx.html.html -import kotlinx.html.stream.createHTML -import space.kscience.dataforge.data.DataTree -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.withDefault -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.isEmpty -import java.nio.file.Files -import java.nio.file.Path -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.io.path.* - - -internal class StaticSiteBuilder( - override val snark: SnarkPlugin, - override val data: DataTree<*>, - override val meta: Meta, - private val baseUrl: String, - private val path: Path, -) : SiteBuilder { - private fun Path.copyRecursively(target: Path) { - Files.walk(this).forEach { source: Path -> - val destination: Path = target.resolve(source.relativeTo(this)) - if (!destination.isDirectory()) { - //avoid re-creating directories - source.copyTo(destination, true) - } - } - } - - override fun assetFile(remotePath: String, file: Path) { - val targetPath = path.resolve(remotePath) - targetPath.parent.createDirectories() - file.copyTo(targetPath, true) - } - - override fun assetDirectory(remotePath: String, directory: Path) { - val targetPath = path.resolve(remotePath) - targetPath.parent.createDirectories() - directory.copyRecursively(targetPath) - } - - override fun assetResourceFile(remotePath: String, resourcesPath: String) { - val targetPath = path.resolve(remotePath) - targetPath.parent.createDirectories() - javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true) - } - - override fun assetResourceDirectory(resourcesPath: String) { - path.parent.createDirectories() - javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path) - } - - private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) { - ref - } else if (ref.isEmpty()) { - baseUrl - } else { - "${baseUrl.removeSuffix("/")}/$ref" - } - - inner class StaticPageBuilder : PageBuilder { - override val data: DataTree<*> get() = this@StaticSiteBuilder.data - override val meta: Meta get() = this@StaticSiteBuilder.meta - override val snark: SnarkPlugin get() = this@StaticSiteBuilder.snark - - - - override fun resolveRef(ref: String): String = resolveRef(baseUrl, ref) - - override fun resolvePageRef(pageName: Name): String = resolveRef( - pageName.toWebPath() + ".html" - ) - } - - - override fun page(route: Name, content: context(PageBuilder, HTML) () -> Unit) { - val htmlBuilder = createHTML() - - htmlBuilder.html { - content(StaticPageBuilder(), this) - } - - val newPath = if (route.isEmpty()) { - path.resolve("index.html") - } else { - path.resolve(route.toWebPath() + ".html") - } - - newPath.parent.createDirectories() - newPath.writeText(htmlBuilder.finalize()) - } - - override fun route( - routeName: Name, - dataOverride: DataTree<*>?, - metaOverride: Meta?, - setAsRoot: Boolean, - ): SiteBuilder = StaticSiteBuilder( - snark = snark, - data = dataOverride ?: data, - meta = metaOverride?.withDefault(meta) ?: meta, - baseUrl = if (setAsRoot) { - resolveRef(baseUrl, routeName.toWebPath()) - } else { - baseUrl - }, - path = path.resolve(routeName.toWebPath()) - ) -} - -fun SnarkPlugin.static( - outputPath: Path, - data: DataTree<*> = DataTree.empty(), - siteUrl: String = outputPath.absolutePathString().replace("\\", "/"), - block: SiteBuilder.() -> Unit, -) { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - StaticSiteBuilder(this, data, meta, siteUrl, outputPath).block() -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/TextTransformation.kt b/src/main/kotlin/space/kscience/snark/TextTransformation.kt deleted file mode 100644 index 09f453c..0000000 --- a/src/main/kotlin/space/kscience/snark/TextTransformation.kt +++ /dev/null @@ -1,38 +0,0 @@ -package space.kscience.snark - -import space.kscience.dataforge.misc.Type -import space.kscience.dataforge.names.NameToken - -@Type(TextTransformation.TYPE) -fun interface TextTransformation { - context(PageBuilder) fun transform(text: String): String - - companion object { - const val TYPE = "snark.textTransformation" - val TEXT_TRANSFORMATION_KEY = NameToken("transformation") - } -} - -object BasicTextTransformation : TextTransformation { - - private val regex = "\\\$\\{(\\w*)(?>\\(\"(.*)\"\\))?\\}".toRegex() - - context(PageBuilder) override fun transform(text: String): String { - return text.replace(regex) { match -> - when (match.groups[1]!!.value) { - "homeRef" -> homeRef - "resolveRef" -> { - val refString = match.groups[2]?.value ?: error("resolveRef requires a string (quoted) argument") - resolveRef(refString) - } - "resolvePageRef" -> { - val refString = match.groups[2]?.value ?: error("resolvePageRef requires a string (quoted) argument") - resolvePageRef(refString) - } - else -> match.value - } - } - } -} - -