From c15a0ea9487d4f57e7fd75674b6de8b74afc1ffe Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 24 Jun 2022 16:39:10 +0300 Subject: [PATCH] Complete refactor to new routing API --- src/main/kotlin/html5up/forty/common.kt | 5 +- src/main/kotlin/html5up/forty/landing.kt | 4 +- src/main/kotlin/html5up/forty/page.kt | 5 +- src/main/kotlin/ru/mipt/spc/Application.kt | 10 +- src/main/kotlin/ru/mipt/spc/master.kt | 123 +++++++++--------- src/main/kotlin/ru/mipt/spc/spcCollection.kt | 16 +-- src/main/kotlin/ru/mipt/spc/spcHome.kt | 14 +- src/main/kotlin/ru/mipt/spc/spcMisc.kt | 21 ++- .../space/kscience/snark/KtorSiteBuilder.kt | 100 +++++++++----- .../space/kscience/snark/PageBuilder.kt | 56 ++++++++ .../space/kscience/snark/SiteBuilder.kt | 98 +++++++++----- .../kotlin/space/kscience/snark/SiteData.kt | 102 --------------- .../kotlin/space/kscience/snark/SiteLayout.kt | 2 +- .../space/kscience/snark/StaticSiteBuilder.kt | 67 +++++++--- 14 files changed, 337 insertions(+), 286 deletions(-) create mode 100644 src/main/kotlin/space/kscience/snark/PageBuilder.kt delete mode 100644 src/main/kotlin/space/kscience/snark/SiteData.kt diff --git a/src/main/kotlin/html5up/forty/common.kt b/src/main/kotlin/html5up/forty/common.kt index e0378ad..753294f 100644 --- a/src/main/kotlin/html5up/forty/common.kt +++ b/src/main/kotlin/html5up/forty/common.kt @@ -1,8 +1,7 @@ package html5up.forty import kotlinx.html.* -import space.kscience.snark.SiteData -import space.kscience.snark.resolveRef +import space.kscience.snark.PageBuilder internal fun FlowContent.fortyMenu() { @@ -201,7 +200,7 @@ internal fun FlowContent.fortyFooter() { } } -context(SiteData) internal fun BODY.fortyScripts() { +context(PageBuilder) 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 1974c49..6bd9f8d 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.SiteData +import space.kscience.snark.PageBuilder -context(SiteData) internal fun HTML.landing(){ +context(PageBuilder) 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 6dcc315..d7986df 100644 --- a/src/main/kotlin/html5up/forty/page.kt +++ b/src/main/kotlin/html5up/forty/page.kt @@ -1,10 +1,9 @@ package html5up.forty import kotlinx.html.* -import space.kscience.snark.SiteData -import space.kscience.snark.resolveRef +import space.kscience.snark.PageBuilder -context(SiteData) internal fun HTML.fortyPage(){ +context(PageBuilder) 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 84b3fe8..cdb0feb 100644 --- a/src/main/kotlin/ru/mipt/spc/Application.kt +++ b/src/main/kotlin/ru/mipt/spc/Application.kt @@ -1,16 +1,14 @@ package ru.mipt.spc import io.ktor.server.application.Application -import io.ktor.server.application.install import io.ktor.server.application.log -import io.ktor.server.plugins.httpsredirect.HttpsRedirect 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.site +import space.kscience.snark.snarkSite import java.net.URI import java.nio.file.FileSystems import java.nio.file.Files @@ -49,7 +47,7 @@ const val BUILD_DATE_FILE = "/buildDate" @Suppress("unused") fun Application.spcModule() { - install(HttpsRedirect) +// install(HttpsRedirect) val context = Context("spc-site") { plugin(SnarkPlugin) @@ -65,7 +63,7 @@ fun Application.spcModule() { val inProduction: Boolean = environment.config.propertyOrNull("ktor.environment.production") != null - if(inProduction){ + if (inProduction) { log.info("Production mode activated") log.info("Build date: $buildDate") log.info("Deploy date: $deployDate") @@ -90,7 +88,7 @@ fun Application.spcModule() { dataPath.resolve(DEPLOY_DATE_FILE).writeText(date) } - snark.site { + snarkSite(snark) { val homeDataPath = resolveData( this@spcModule.javaClass.getResource("/home")!!.toURI(), dataPath / "home" diff --git a/src/main/kotlin/ru/mipt/spc/master.kt b/src/main/kotlin/ru/mipt/spc/master.kt index dc678af..77b32e2 100644 --- a/src/main/kotlin/ru/mipt/spc/master.kt +++ b/src/main/kotlin/ru/mipt/spc/master.kt @@ -34,14 +34,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") -class MagProgSection( +context(PageBuilder) class MagProgSection( val id: String, val title: String, val style: String, val content: FlowContent.() -> Unit, ) -private fun wrapSection( +context(PageBuilder) private fun wrapSection( id: String, title: String, sectionContent: FlowContent.() -> Unit, @@ -52,7 +52,7 @@ private fun wrapSection( } } -private fun wrapSection( +context(PageBuilder) private fun wrapSection( block: HtmlData, idOverride: String? = null, ): MagProgSection = wrapSection( @@ -70,9 +70,9 @@ 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(SiteData) private fun FlowContent.programSection() { - val programBlock = resolveHtml(PROGRAM_PATH)!! - val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!! +context(PageBuilder) private fun FlowContent.programSection() { + val programBlock = data.resolveHtml(PROGRAM_PATH)!! + val recommendedBlock = data.resolveHtml(RECOMMENDED_COURSES_PATH)!! div("inner") { h2 { +"Учебная программа" } htmlData(programBlock) @@ -87,7 +87,7 @@ context(SiteData) private fun FlowContent.programSection() { } } -context(SiteData) private fun FlowContent.partners() { +context(PageBuilder) 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") { @@ -117,8 +117,8 @@ context(SiteData) private fun FlowContent.partners() { // val photo: String? by meta.string() //} -context(SiteData) private fun FlowContent.team() { - val team = findByType("magprog_team").values.sortedBy { it.order } +context(PageBuilder) private fun FlowContent.team() { + val team = data.findByContentType("magprog_team").values.sortedBy { it.order } div("inner") { h2 { +"Команда" } @@ -172,8 +172,8 @@ context(SiteData) private fun FlowContent.team() { // } } -context(SiteData) private fun FlowContent.mentors() { - val mentors = findByType("magprog_mentor").entries.sortedBy { it.value.id } +context(PageBuilder) private fun FlowContent.mentors() { + val mentors = data.findByContentType("magprog_mentor").entries.sortedBy { it.value.id } div("inner") { h2 { @@ -183,7 +183,7 @@ context(SiteData) private fun FlowContent.mentors() { mentors.forEach { (name, mentor) -> section { id = mentor.id - val ref = resolvePage("mentor-${mentor.id}") + val ref = resolvePageRef("mentor-${mentor.id}") a(classes = "image", href = ref) { mentor.imagePath?.let { photoPath -> img( @@ -200,7 +200,7 @@ context(SiteData) private fun FlowContent.mentors() { h2 { a(href = ref) { +mentor.name } } - val info = resolveHtml(name.withIndex("info")) + val info = data.resolveHtml(name.withIndex("info")) if (info != null) { htmlData(info) } @@ -210,7 +210,7 @@ context(SiteData) private fun FlowContent.mentors() { } } -context(SiteData) internal fun HTML.magProgHead(title: String) { +context(PageBuilder) internal fun HTML.magProgHead(title: String) { head { this.title = title meta { @@ -237,7 +237,7 @@ context(SiteData) internal fun HTML.magProgHead(title: String) { } } -context(SiteData) internal fun BODY.magProgFooter() { +context(PageBuilder) internal fun BODY.magProgFooter() { footer("wrapper style1-alt") { id = "footer" div("inner") { @@ -282,13 +282,13 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa val magProgSiteContext = snark.readDirectory(dataPath.resolve("content")) - mountSite(prefix, magProgSiteContext) { + route(prefix, magProgSiteContext, setAsRoot = true) { assetDirectory("assets", dataPath.resolve("assets")) assetDirectory("images", dataPath.resolve("images")) page { val sections = listOf( - wrapSection(resolveHtml(INTRO_PATH)!!, "intro"), + wrapSection(data.resolveHtml(INTRO_PATH)!!, "intro"), MagProgSection( id = "partners", title = "Партнеры", @@ -311,12 +311,13 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa ) { programSection() }, - wrapSection(resolveHtml(ENROLL_PATH)!!, "enroll"), + wrapSection(data.resolveHtml(ENROLL_PATH)!!, "enroll"), wrapSection(id = "contacts", title = "Контакты") { - htmlData(resolveHtml(CONTACTS_PATH)!!) + htmlData(data.resolveHtml(CONTACTS_PATH)!!) team() } ) + magProgHead("Магистратура \"Научное программирование\"") body("is-preload magprog-body") { section { @@ -355,58 +356,58 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa magProgFooter() } } + } - val mentors = data.findByType("magprog_mentor").values.sortedBy { - it.order - } + val mentors = data.findByContentType("magprog_mentor").values.sortedBy { + it.order + } - mentors.forEach { mentor -> - page(mentor.mentorPageId.asName()) { + mentors.forEach { mentor -> + page(mentor.mentorPageId.asName()) { - magProgHead("Научное программирование: ${mentor.name}") - body("is-preload") { - header { - id = "header" - a(classes = "title") { - href = "$homeRef#mentors" - +"Научные руководители" - } - nav { - ul { - mentors.forEach { - li { - a { - href = resolvePage(it.mentorPageId) - +it.name.substringAfterLast(" ") - } + magProgHead("Научное программирование: ${mentor.name}") + body("is-preload") { + header { + id = "header" + a(classes = "title") { + href = "$homeRef#mentors" + +"Научные руководители" + } + nav { + ul { + mentors.forEach { + li { + a { + href = resolvePageRef(it.mentorPageId) + +it.name.substringAfterLast(" ") } } } } } - div { - id = "wrapper" - section("wrapper") { - id = "main" - div("inner") { - h1("major") { +mentor.name } - val imageClass = mentor.meta["image.position"].string ?: "left" - span("image $imageClass") { - mentor.imagePath?.let { photoPath -> - img( - src = resolveRef(photoPath), - alt = mentor.name - ) - } - } - htmlData(mentor) - } - } - } - magProgFooter() } + div { + id = "wrapper" + section("wrapper") { + id = "main" + div("inner") { + h1("major") { +mentor.name } + val imageClass = mentor.meta["image.position"].string ?: "left" + span("image $imageClass") { + mentor.imagePath?.let { photoPath -> + img( + src = resolveRef(photoPath), + alt = mentor.name + ) + } + } + htmlData(mentor) + } + } + } + magProgFooter() } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ru/mipt/spc/spcCollection.kt b/src/main/kotlin/ru/mipt/spc/spcCollection.kt index 3b28e2a..74518c0 100644 --- a/src/main/kotlin/ru/mipt/spc/spcCollection.kt +++ b/src/main/kotlin/ru/mipt/spc/spcCollection.kt @@ -15,7 +15,7 @@ import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set -context(SiteData, FlowContent) private fun spcSpotlightContent( +context(PageBuilder) private fun FlowContent.spcSpotlightContent( landing: HtmlData, content: Map, ) { @@ -44,11 +44,11 @@ context(SiteData, FlowContent) private fun spcSpotlightContent( id = "main" //TODO add smart SNARK ordering section("spotlights") { - content.entries.sortedBy { it.value.meta["order"].int ?: Int.MAX_VALUE }.forEach { (name, data) -> - val ref = resolvePage(name) + content.entries.sortedBy { it.value.meta["order"].int ?: Int.MAX_VALUE }.forEach { (name, entry) -> + val ref = resolvePageRef(name) section { - id = data.meta["id"].string ?: name.toString() - data.meta["image"]?.let { imageMeta: Meta -> + id = entry.meta["id"].string ?: name.toString() + entry.meta["image"]?.let { imageMeta: Meta -> val imagePath = imageMeta.value?.string ?: imageMeta["path"].string ?: error("Image path not provided") a(classes = "image") { @@ -63,11 +63,11 @@ context(SiteData, FlowContent) private fun spcSpotlightContent( div("content") { div("inner") { header("major") { - h3 { +(data.meta["title"].string ?: "???") } + h3 { +(entry.meta["title"].string ?: "???") } } - val infoData = resolveHtml(name.withIndex("info")) + val infoData = data.resolveHtml(name.withIndex("info")) if (infoData == null) { - htmlData(data) + htmlData(entry) } else { htmlData(infoData) } diff --git a/src/main/kotlin/ru/mipt/spc/spcHome.kt b/src/main/kotlin/ru/mipt/spc/spcHome.kt index 3391f87..b3c89f4 100644 --- a/src/main/kotlin/ru/mipt/spc/spcHome.kt +++ b/src/main/kotlin/ru/mipt/spc/spcHome.kt @@ -15,7 +15,7 @@ import java.nio.file.Path import kotlin.reflect.typeOf -context(SiteData) internal fun HTML.spcPageContent( +context(PageBuilder) internal fun HTML.spcPageContent( meta: Meta, title: String = meta["title"].string ?: SPC_TITLE, fragment: FlowContent.() -> Unit, @@ -65,7 +65,7 @@ internal val FortyDataRenderer: SiteBuilder.(Data<*>) -> Unit = { data -> } -context(SiteData, HTML) private fun spcHome() { +context(PageBuilder) private fun HTML.spcHome() { spcHead() body("is-preload") { wrapper { @@ -150,7 +150,7 @@ context(SiteData, HTML) private fun spcHome() { header("major") { h3 { a(classes = "link") { - href = resolvePage("magprog") + href = resolvePageRef("magprog") +"""Master's program""" } } @@ -167,7 +167,7 @@ context(SiteData, HTML) private fun spcHome() { header("major") { h3 { a(classes = "link") { - href = resolvePage("research") + href = resolvePageRef("research") +"""Research""" } } @@ -186,7 +186,7 @@ context(SiteData, HTML) private fun spcHome() { header("major") { h3 { a(classes = "link") { - href = resolvePage("consulting") + href = resolvePageRef("consulting") +"""Consulting""" } } @@ -203,7 +203,7 @@ context(SiteData, HTML) private fun spcHome() { header("major") { h3 { a(classes = "link") { - href = resolvePage("team") + href = resolvePageRef("team") +"""Team""" } } @@ -256,7 +256,7 @@ internal fun SiteBuilder.spcHome(rootPath: Path, prefix: Name = Name.EMPTY) { val homePageData = snark.readDirectory(rootPath.resolve("content")) - mountSite(prefix, homePageData) { + route(prefix, homePageData, setAsRoot = true) { assetDirectory("assets", rootPath.resolve("assets")) assetDirectory("images", rootPath.resolve("images")) diff --git a/src/main/kotlin/ru/mipt/spc/spcMisc.kt b/src/main/kotlin/ru/mipt/spc/spcMisc.kt index ea98dd8..ffb9a10 100644 --- a/src/main/kotlin/ru/mipt/spc/spcMisc.kt +++ b/src/main/kotlin/ru/mipt/spc/spcMisc.kt @@ -1,15 +1,14 @@ package ru.mipt.spc import kotlinx.html.* -import space.kscience.snark.SiteData +import space.kscience.snark.PageBuilder import space.kscience.snark.homeRef -import space.kscience.snark.resolvePage -import space.kscience.snark.resolveRef +import space.kscience.snark.resolvePageRef internal const val SPC_TITLE = "Scientific Programming Centre" -context(SiteData) internal fun HTML.spcHead(title: String = SPC_TITLE) { +context(PageBuilder) internal fun HTML.spcHead(title: String = SPC_TITLE) { head { title { +title @@ -28,7 +27,7 @@ context(SiteData) internal fun HTML.spcHead(title: String = SPC_TITLE) { } } -context(SiteData) internal fun FlowContent.spcHomeMenu() { +context(PageBuilder) internal fun FlowContent.spcHomeMenu() { nav { id = "menu" ul("links") { @@ -40,25 +39,25 @@ context(SiteData) internal fun FlowContent.spcHomeMenu() { } li { a { - href = resolvePage("magprog") + href = resolvePageRef("magprog") +"""Master""" } } li { a { - href = resolvePage("research") + href = resolvePageRef("research") +"""Research""" } } li { a { - href = resolvePage("consulting") + href = resolvePageRef("consulting") +"""Consulting""" } } li { a { - href = resolvePage("team") + href = resolvePageRef("team") +"""Team""" } } @@ -80,7 +79,7 @@ context(SiteData) internal fun FlowContent.spcHomeMenu() { } } -context(SiteData) internal fun FlowContent.spcFooter() { +context(PageBuilder) internal fun FlowContent.spcFooter() { footer { id = "footer" div("inner") { @@ -130,7 +129,7 @@ context(SiteData) internal fun FlowContent.spcFooter() { } } -context(SiteData) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) { +context(PageBuilder) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) { div { id = "wrapper" // Header diff --git a/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt b/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt index 9f3bb84..f435855 100644 --- a/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt +++ b/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt @@ -7,7 +7,6 @@ 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.ApplicationRequest import io.ktor.server.request.host import io.ktor.server.request.port import io.ktor.server.routing.Route @@ -15,10 +14,23 @@ 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.context.Context +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 java.nio.file.Path +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract -class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route) : SiteBuilder { +@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()) @@ -30,17 +42,57 @@ class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route) } } - override fun page(route: Name, content: context(SiteData, HTML)() -> Unit) { + private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) { + ref + } else { + "${baseUrl.removeSuffix("/")}/$ref" + } + + inner class KtorPageBuilder( + val pageBaseUrl: String, + override val meta: Meta = this@KtorSiteBuilder.meta, + ) : PageBuilder { + override val context: Context get() = this@KtorSiteBuilder.context + override val data: DataTree<*> get() = this@KtorSiteBuilder.data + + override fun resolveRef(ref: String): String = resolveRef(pageBaseUrl, ref) + + override fun resolvePageRef(pageName: Name): String = resolveRef(pageName.tokens.joinToString(separator = "/")) + } + + override fun page(route: Name, content: context(PageBuilder, HTML)() -> Unit) { ktorRoute.get(route.toWebPath()) { call.respondHtml { - val dataWithUrl = data.copyWithRequestHost(call.request) - content(dataWithUrl, this) + 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(subRoute: Name): SiteBuilder = - KtorSiteBuilder(data, ktorRoute.createRouteFromPath(subRoute.toWebPath())) + 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) @@ -49,39 +101,27 @@ class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route) override fun assetResourceDirectory(resourcesPath: String) { ktorRoute.resources(resourcesPath) } - - override fun withData(newData: SiteData): SiteBuilder = KtorSiteBuilder(newData, ktorRoute) -} - -@PublishedApi -internal fun SiteData.copyWithRequestHost(request: ApplicationRequest): SiteData { - val uri = URLBuilder( - protocol = URLProtocol.createOrDefault(request.origin.scheme), - host = request.host(), - port = request.port(), - pathSegments = baseUrlPath.split("/"), - ) - return copy(baseUrlPath = uri.buildString()) } inline fun Route.snarkSite( - data: SiteData, + snark: SnarkPlugin, + data: DataTree<*>, + meta: Meta = data.meta, block: SiteBuilder.() -> Unit, ) { - block(KtorSiteBuilder(data, this@snarkSite)) + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite)) } fun Application.snarkSite( - data: SiteData, + snark: SnarkPlugin, + data: DataTree<*> = DataTree.empty(), + meta: Meta = data.meta, block: SiteBuilder.() -> Unit, ) { routing { - snarkSite(data, block) + snarkSite(snark, data, meta, block) } -} - -context (Application) fun SnarkPlugin.site( - block: SiteBuilder.() -> Unit, -) { - snarkSite(SiteData.empty(this), 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 new file mode 100644 index 0000000..71333a4 --- /dev/null +++ b/src/main/kotlin/space/kscience/snark/PageBuilder.kt @@ -0,0 +1,56 @@ +package space.kscience.snark + +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.Name +import space.kscience.dataforge.names.parseAsName +import space.kscience.dataforge.names.plus +import space.kscience.dataforge.names.startsWith + +internal fun Name.toWebPath() = tokens.joinToString(separator = "/") + +interface PageBuilder : ContextAware { + 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(Name.EMPTY) + +/** + * 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 index 7586e8e..b1bcf5c 100644 --- a/src/main/kotlin/space/kscience/snark/SiteBuilder.kt +++ b/src/main/kotlin/space/kscience/snark/SiteBuilder.kt @@ -4,21 +4,28 @@ 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.meta.Laminate +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 -internal fun Name.toWebPath() = tokens.joinToString(separator = "/") /** * An abstraction, which is used to render sites to the different rendering engines */ interface SiteBuilder : ContextAware { - val data: SiteData + val data: DataTree<*> - override val context: Context get() = data.context + val snark: SnarkPlugin + + override val context: Context get() = snark.context + + val meta: Meta fun assetFile(remotePath: String, file: Path) @@ -28,39 +35,62 @@ interface SiteBuilder : ContextAware { fun assetResourceDirectory(resourcesPath: String) - fun page(route: Name = Name.EMPTY, content: context(SiteData, HTML) () -> Unit) + fun page(route: Name = Name.EMPTY, content: context(PageBuilder, HTML) () -> Unit) /** - * Create a new branch builder with replaced [data] + * 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 withData(newData: SiteData): SiteBuilder + fun route( + routeName: Name, + dataOverride: DataTree<*>? = null, + metaOverride: Meta? = null, + setAsRoot: Boolean = false, + ): SiteBuilder - /** - * Create a route - */ - fun route(subRoute: Name): SiteBuilder -} - -val SiteBuilder.snark get() = data.snark - -public inline fun SiteBuilder.route(route: Name, block: SiteBuilder.() -> Unit) { - route(route).apply(block) -} - -public inline fun SiteBuilder.route(route: String, block: SiteBuilder.() -> Unit) { - route(route.parseAsName()).apply(block) -} - -/** - * Create a stand-alone site at a given node - */ -public fun SiteBuilder.mountSite(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() + companion object { + val INDEX_PAGE_TOKEN: NameToken = NameToken("index") } } + +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/SiteData.kt b/src/main/kotlin/space/kscience/snark/SiteData.kt deleted file mode 100644 index 981ca2d..0000000 --- a/src/main/kotlin/space/kscience/snark/SiteData.kt +++ /dev/null @@ -1,102 +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.* -import space.kscience.snark.SiteData.Companion.INDEX_PAGE_TOKEN -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -data class SiteData( - val snark: SnarkPlugin, - val data: DataTree<*>, - val baseUrlPath: String, - override val meta: Meta, -) : ContextAware, DataTree by data { - - override val context: Context get() = snark.context - - val language: String? by meta.string() - - companion object { - fun empty( - snark: SnarkPlugin, - baseUrlPath: String = "", - meta: Meta = Meta.EMPTY, - ): SiteData { - //TODO use empty data from DF - val emptyData = object : DataTree { - override val items: Map> get() = emptyMap() - override val dataType: KType get() = typeOf() - override val meta: Meta get() = meta - } - return SiteData(snark, emptyData, baseUrlPath, meta) - } - - val INDEX_PAGE_TOKEN: NameToken = NameToken("index") - } -} - -/** - * Resolve a resource full path by its name - */ -fun SiteData.resolveRef(name: String): String = if (baseUrlPath.isEmpty()) { - name -} else { - "${baseUrlPath.removeSuffix("/")}/$name" -} - -/** - * Resolve a page designated by given name. Depending on rendering specifics, some prefixes or suffixes could be added. - */ -fun SiteData.resolvePage(name: Name): String { - return resolveRef(name.tokens.joinToString("/")) + (meta["pageSuffix"].string ?: "") -} - -/** - * - */ -fun SiteData.resolvePage(name: String): String = resolvePage(name.parseAsName()) - -val SiteData.homeRef get() = resolvePage(Name.EMPTY) - -/** - * Resolve a Html builder by its full name - */ -fun DataTree<*>.resolveHtml(name: Name): HtmlData? { - val resolved = (getByType(name) ?: getByType(name + 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 SiteData.findByType(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" -// -//fun SnarkPlugin.readData(data: DataTree<*>, rootUrl: String = "/"): SiteData = -// SiteData(this, data, rootUrl) -// -//fun SnarkPlugin.readDirectory(path: Path, rootUrl: String = "/"): SiteData { -// val parsedData: DataTree = readDirectory(path) -// -// return readData(parsedData, rootUrl) -//} diff --git a/src/main/kotlin/space/kscience/snark/SiteLayout.kt b/src/main/kotlin/space/kscience/snark/SiteLayout.kt index cf82290..9062a78 100644 --- a/src/main/kotlin/space/kscience/snark/SiteLayout.kt +++ b/src/main/kotlin/space/kscience/snark/SiteLayout.kt @@ -60,7 +60,7 @@ fun SiteBuilder.pages( val layoutMeta = data.meta[LAYOUT_KEY] if (layoutMeta != null) { //use layout if it is defined - this.data.snark.layout(layoutMeta).render(data) + snark.layout(layoutMeta).render(data) } else { when (data) { is DataTreeItem.Node -> { diff --git a/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt b/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt index c580fa1..15e252d 100644 --- a/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt +++ b/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt @@ -3,19 +3,30 @@ package space.kscience.snark import kotlinx.html.HTML import kotlinx.html.html import kotlinx.html.stream.createHTML +import space.kscience.dataforge.context.Context +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.* -class StaticSiteBuilder(override val data: SiteData, private val path: Path) : SiteBuilder { +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()) { + if (!destination.isDirectory()) { //avoid re-creating directories source.copyTo(destination, true) } @@ -37,7 +48,7 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S 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) + javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true) } override fun assetResourceDirectory(resourcesPath: String) { @@ -45,11 +56,27 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path) } - override fun page(route: Name, content: context(SiteData, HTML) () -> Unit) { + inner class StaticPageBuilder : PageBuilder { + override val data: DataTree<*> get() = this@StaticSiteBuilder.data + override val meta: Meta get() = this@StaticSiteBuilder.meta + override val context: Context get() = this@StaticSiteBuilder.context + + + override fun resolveRef(ref: String): String { + TODO("Not yet implemented") + } + + override fun resolvePageRef(pageName: Name): String { + TODO("Not yet implemented") + } + } + + + override fun page(route: Name, content: context(PageBuilder, HTML) () -> Unit) { val htmlBuilder = createHTML() htmlBuilder.html { - content(data, this) + content(StaticPageBuilder(), this) } val newPath = if (route.isEmpty()) { @@ -62,19 +89,23 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S newPath.writeText(htmlBuilder.finalize()) } - override fun route(subRoute: Name): SiteBuilder = StaticSiteBuilder(data, path.resolve(subRoute.toWebPath())) - - override fun withData(newData: SiteData): SiteBuilder = StaticSiteBuilder(newData, path) - + override fun route( + routeName: Name, + dataOverride: DataTree<*>?, + metaOverride: Meta?, + setAsRoot: Boolean, + ): SiteBuilder = StaticSiteBuilder( + snark = snark, + data = dataOverride ?: data, + meta = metaOverride?.withDefault(meta) ?: meta, + baseUrl = baseUrl, + path = path.resolve(routeName.toWebPath()) + ) } -fun SnarkPlugin.static(path: Path, block: SiteBuilder.() -> Unit) { - val base = SiteData.empty( - this, - baseUrlPath = path.absolutePathString(), - meta = Meta { - "pageSuffix" put ".html" - } - ) - StaticSiteBuilder(base, path).block() +fun SnarkPlugin.static(outputPath: Path, data: DataTree<*> = DataTree.empty(), block: SiteBuilder.() -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + StaticSiteBuilder(this, data, meta, "", outputPath).block() } \ No newline at end of file