Refactor SPC home page to use decoupled representation from KTor

This commit is contained in:
Alexander Nozik 2022-06-20 17:10:37 +03:00
parent a907c57134
commit 23a736a012
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
6 changed files with 134 additions and 145 deletions

View File

@ -1,10 +1,6 @@
package ru.mipt.spc package ru.mipt.spc
import html5up.forty.fortyScripts import html5up.forty.fortyScripts
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import kotlinx.html.* import kotlinx.html.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -15,6 +11,13 @@ import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.withIndex import space.kscience.dataforge.names.withIndex
import space.kscience.dataforge.values.string import space.kscience.dataforge.values.string
import space.kscience.snark.* import space.kscience.snark.*
import kotlin.collections.Map
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.forEach
import kotlin.collections.joinToString
import kotlin.collections.set
import kotlin.collections.sortedBy
context(PageContext) private fun FlowContent.spcSpotlightContent( context(PageContext) private fun FlowContent.spcSpotlightContent(
landing: HtmlData, landing: HtmlData,
@ -89,7 +92,7 @@ context(PageContext) private fun FlowContent.spcSpotlightContent(
} }
context(PageContext) internal fun Route.spcSpotlight( context(PageContext) internal fun SnarkRoute.spcSpotlight(
name: String, name: String,
contentFilter: (Name, Meta) -> Boolean, contentFilter: (Name, Meta) -> Boolean,
) { ) {
@ -97,29 +100,22 @@ context(PageContext) internal fun Route.spcSpotlight(
val content = resolveAllHtml(contentFilter) val content = resolveAllHtml(contentFilter)
val meta = body.meta val meta = body.meta
get(name) { page(name) {
withRequest(call.request) { val title = meta["title"].string ?: SPC_TITLE
call.respondHtml { spcHead(title)
val title = meta["title"].string ?: SPC_TITLE body("is-preload") {
spcHead(title) wrapper {
body("is-preload") { spcSpotlightContent(body, content)
wrapper {
spcSpotlightContent(body, content)
}
fortyScripts()
}
} }
fortyScripts()
} }
} }
content.forEach { (name, contentBody) -> content.forEach { (name, contentBody) ->
get(name.tokens.joinToString("/")) { page(name.tokens.joinToString("/")){
withRequest(call.request) { spcPageContent(contentBody.meta) {
call.respondHtml { htmlData(contentBody)
spcPageContent(contentBody.meta) {
htmlData(contentBody)
}
}
} }
} }
} }

View File

@ -2,13 +2,13 @@ package ru.mipt.spc
import html5up.forty.fortyScripts import html5up.forty.fortyScripts
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.call import io.ktor.server.routing.route
import io.ktor.server.application.log import io.ktor.server.routing.routing
import io.ktor.server.html.respondHtml
import io.ktor.server.routing.*
import kotlinx.html.* import kotlinx.html.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.data.filterByType import space.kscience.dataforge.data.filterByType
import space.kscience.dataforge.data.forEach import space.kscience.dataforge.data.forEach
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
@ -58,17 +58,13 @@ context(PageContext) internal fun HTML.spcPageContent(
} }
context(PageContext) internal fun Route.spcPage(subRoute: String, meta: Meta, fragment: FlowContent.() -> Unit) { context(PageContext) internal fun SnarkRoute.spcPage(subRoute: String, meta: Meta, fragment: FlowContent.() -> Unit) {
get(subRoute) { page(subRoute) {
withRequest(call.request) { spcPageContent(meta, fragment = fragment)
call.respondHtml {
spcPageContent(meta, fragment = fragment)
}
}
} }
} }
context(PageContext) internal fun Route.spcPage( context(PageContext) internal fun SnarkRoute.spcPage(
subRoute: String, subRoute: String,
dataPath: Name = subRoute.replace("/", ".").parseAsName(), dataPath: Name = subRoute.replace("/", ".").parseAsName(),
more: FlowContent.() -> Unit = {}, more: FlowContent.() -> Unit = {},
@ -80,14 +76,14 @@ context(PageContext) internal fun Route.spcPage(
more() more()
} }
} else { } else {
application.log.error("Content for page with path $dataPath not found") logger.error { "Content for page with path $dataPath not found" }
} }
} }
/** /**
* Route a directory * Route a directory
*/ */
context(PageContext) internal fun Route.spcDirectory( context(PageContext) internal fun SnarkRoute.spcDirectory(
subRoute: String, subRoute: String,
dataPath: Name = subRoute.replace("/", ".").parseAsName(), dataPath: Name = subRoute.replace("/", ".").parseAsName(),
) { ) {
@ -104,14 +100,14 @@ context(PageContext) internal fun Route.spcDirectory(
} }
} }
context(PageContext) internal fun Route.spcPage( context(PageContext) internal fun SnarkRoute.spcPage(
name: Name, name: Name,
more: FlowContent.() -> Unit = {}, more: FlowContent.() -> Unit = {},
) { ) {
spcPage(name.tokens.joinToString("/"), name, more) spcPage(name.tokens.joinToString("/"), name, more)
} }
context(PageContext) private fun HTML.spcHome() { context(PageContext, HTML) private fun HTML.spcHome() {
spcHead() spcHead()
body("is-preload") { body("is-preload") {
wrapper { wrapper {
@ -309,13 +305,11 @@ internal fun Application.spcHome(context: Context, rootPath: Path, prefix: Strin
snark(homePageContext) { snark(homePageContext) {
staticDirectory("assets", rootPath.resolve("assets")) staticDirectory("assets", rootPath.resolve("assets"))
staticDirectory("images", rootPath.resolve("images")) staticDirectory("images", rootPath.resolve("images"))
page { spcHome() }
}
with(homePageContext) { page { spcHome() }
spcDirectory("consulting") spcDirectory("consulting")
spcPage("ru/consulting") spcDirectory("ru/consulting")
spcSpotlight("team") { _, m -> m["type"].string == "team" } spcSpotlight("team") { _, m -> m["type"].string == "team" }
spcSpotlight("research") { name, m -> name.startsWith("projects".asName()) && m["type"].string == "project" } spcSpotlight("research") { name, m -> name.startsWith("projects".asName()) && m["type"].string == "project" }

View File

@ -6,6 +6,8 @@ import io.ktor.server.plugins.origin
import io.ktor.server.request.ApplicationRequest import io.ktor.server.request.ApplicationRequest
import io.ktor.server.request.host import io.ktor.server.request.host
import io.ktor.server.request.port import io.ktor.server.request.port
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.data.* import space.kscience.dataforge.data.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -16,7 +18,13 @@ import space.kscience.dataforge.names.startsWith
import space.kscience.snark.PageContext.Companion.INDEX_PAGE_NAME import space.kscience.snark.PageContext.Companion.INDEX_PAGE_NAME
import java.nio.file.Path import java.nio.file.Path
data class PageContext(val path: String, val pageMeta: Meta, val data: DataSet<*>) { data class PageContext(
override val context: Context,
val path: String,
val pageMeta: Meta,
val data: DataSet<*>,
) : ContextAware {
val language: String? by pageMeta.string() val language: String? by pageMeta.string()
companion object { companion object {
@ -61,12 +69,13 @@ fun PageContext.findByType(contentType: String, baseName: Name = Name.EMPTY) = r
internal val Data<*>.published: Boolean get() = meta["published"].string != "false" internal val Data<*>.published: Boolean get() = meta["published"].string != "false"
fun PageContext(rootUrl: String, data: DataSet<*>): PageContext = PageContext(rootUrl, data.meta, data) fun PageContext(dfContext: Context, rootUrl: String, data: DataSet<*>): PageContext =
PageContext(dfContext, rootUrl, data.meta, data)
fun SnarkPlugin.read(path: Path, rootUrl: String = "/"): PageContext { fun SnarkPlugin.read(path: Path, rootUrl: String = "/"): PageContext {
val parsedData: DataSet<Any> = readDirectory(path) val parsedData: DataSet<Any> = readDirectory(path)
return PageContext(rootUrl, parsedData) return PageContext(context, rootUrl, parsedData)
} }
/** /**

View File

@ -1,61 +0,0 @@
package space.kscience.snark
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.file
import io.ktor.server.http.content.files
import io.ktor.server.http.content.static
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.html.HTML
import java.nio.file.Path
interface RouteBuilder {
val pageContext: PageContext
fun staticFile(remotePath: String, file: Path)
fun staticDirectory(remotePath: String, directory: Path)
fun page(route: String = "", content: context(PageContext) HTML.() -> Unit)
fun route(route: String, block: RouteBuilder.() -> Unit)
}
class KtorRouteBuilder(override val pageContext: PageContext, private val ktorRoute: Route) : RouteBuilder {
override fun staticFile(remotePath: String, file: Path) {
ktorRoute.file(remotePath, file.toFile())
}
override fun staticDirectory(remotePath: String, directory: Path) {
ktorRoute.static(remotePath) {
files(directory.toFile())
}
}
override fun page(route: String, content: context(PageContext) HTML.() -> Unit) {
ktorRoute.get(route) {
with(pageContext) {
withRequest(call.request) {
val innerContext = this
call.respondHtml {
content(innerContext, this)
}
}
}
}
}
override fun route(route: String, block: RouteBuilder.() -> Unit) {
ktorRoute.route(route) {
block(KtorRouteBuilder(pageContext, this))
}
}
}
fun Route.snark(pageContext: PageContext, block: context(PageContext) RouteBuilder.() -> Unit) {
with(pageContext){
block(KtorRouteBuilder(pageContext, this@snark))
}
}

View File

@ -0,0 +1,64 @@
package space.kscience.snark
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.file
import io.ktor.server.http.content.files
import io.ktor.server.http.content.static
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.html.HTML
import java.nio.file.Path
/**
* An abstraction, which is used to render sites to the different rendering engines
*/
interface SnarkRoute {
fun staticFile(remotePath: String, file: Path)
fun staticDirectory(remotePath: String, directory: Path)
context(PageContext) fun page(route: String = "", content: context(PageContext, HTML) () -> Unit)
context(PageContext) fun route(route: String, block: context(PageContext, SnarkRoute) () -> Unit)
}
class KtorRouteBuilder(private val ktorRoute: Route) : SnarkRoute {
override fun staticFile(remotePath: String, file: Path) {
ktorRoute.file(remotePath, file.toFile())
}
override fun staticDirectory(remotePath: String, directory: Path) {
ktorRoute.static(remotePath) {
files(directory.toFile())
}
}
context(PageContext) override fun page(route: String, content: context(PageContext, HTML)() -> Unit) {
ktorRoute.get(route) {
withRequest(call.request) {
call.respondHtml {
content(this@PageContext, this)
}
}
}
}
context(PageContext) override fun route(
route: String,
block: context(PageContext, SnarkRoute)() -> Unit,
) {
ktorRoute.route(route) {
block(this@PageContext, KtorRouteBuilder(this))
}
}
}
inline fun Route.snark(
pageContext: PageContext,
block: context(PageContext, SnarkRoute)() -> Unit,
) {
block(pageContext, KtorRouteBuilder(this@snark))
}

View File

@ -1,39 +1,26 @@
package ru.mipt.spc package ru.mipt.spc
import kotlinx.html.TagConsumer //class SiteBuilderAction : AbstractAction<Any, HtmlFragment>(typeOf<HtmlFragment>()) {
import space.kscience.dataforge.actions.AbstractAction //
import space.kscience.dataforge.data.Data // private val pageBuilders = HashMap<Name, (DataSet<*>) -> HtmlData>()
import space.kscience.dataforge.data.DataSet //
import space.kscience.dataforge.data.DataSetBuilder // fun page(name: Name, meta: Meta = Meta.EMPTY, builder: context(PageContext) TagConsumer<*>.() -> Unit) {
import space.kscience.dataforge.meta.Meta // val prefix = name.tokens.joinToString(separator = "/", prefix = "/")
import space.kscience.dataforge.meta.copy // pageBuilders[name] = { dataset ->
import space.kscience.dataforge.names.Name // val fragment: HtmlFragment = {
import space.kscience.snark.HtmlData // builder.invoke(PageContext(prefix, dataset), this)
import space.kscience.snark.HtmlFragment // }
import space.kscience.snark.PageContext // Data(fragment, meta.copy {
import kotlin.reflect.typeOf // "name" put name.toString()
// })
class SiteBuilderAction : AbstractAction<Any, HtmlFragment>(typeOf<HtmlFragment>()) { // }
// }
private val pageBuilders = HashMap<Name, (DataSet<*>) -> HtmlData>() //
//
fun page(name: Name, meta: Meta = Meta.EMPTY, builder: context(PageContext) TagConsumer<*>.() -> Unit) { // override fun DataSetBuilder<HtmlFragment>.generate(data: DataSet<Any>, meta: Meta) {
val prefix = name.tokens.joinToString(separator = "/", prefix = "/") // pageBuilders.forEach { (name, builder) ->
pageBuilders[name] = { dataset -> // data(name, builder(data))
val fragment: HtmlFragment = { // }
builder.invoke(PageContext(prefix, dataset), this) // }
} //
Data(fragment, meta.copy { //}
"name" put name.toString()
})
}
}
override fun DataSetBuilder<HtmlFragment>.generate(data: DataSet<Any>, meta: Meta) {
pageBuilders.forEach { (name, builder) ->
data(name, builder(data))
}
}
}