1
0
forked from SPC/spc-site

Complete refactor to new routing API

This commit is contained in:
Alexander Nozik 2022-06-24 16:39:10 +03:00
parent 43bf8e8e96
commit c15a0ea948
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
14 changed files with 337 additions and 286 deletions

View File

@ -1,8 +1,7 @@
package html5up.forty package html5up.forty
import kotlinx.html.* import kotlinx.html.*
import space.kscience.snark.SiteData import space.kscience.snark.PageBuilder
import space.kscience.snark.resolveRef
internal fun FlowContent.fortyMenu() { 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 { script {
src = resolveRef("assets/js/jquery.min.js") src = resolveRef("assets/js/jquery.min.js")
} }

View File

@ -1,9 +1,9 @@
package html5up.forty package html5up.forty
import kotlinx.html.* 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 { head {
title { title {
} }

View File

@ -1,10 +1,9 @@
package html5up.forty package html5up.forty
import kotlinx.html.* import kotlinx.html.*
import space.kscience.snark.SiteData import space.kscience.snark.PageBuilder
import space.kscience.snark.resolveRef
context(SiteData) internal fun HTML.fortyPage(){ context(PageBuilder) internal fun HTML.fortyPage(){
head { head {
title { title {
} }

View File

@ -1,16 +1,14 @@
package ru.mipt.spc package ru.mipt.spc
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.application.log import io.ktor.server.application.log
import io.ktor.server.plugins.httpsredirect.HttpsRedirect
import kotlinx.css.CssBuilder import kotlinx.css.CssBuilder
import kotlinx.html.CommonAttributeGroupFacade import kotlinx.html.CommonAttributeGroupFacade
import kotlinx.html.style import kotlinx.html.style
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
import space.kscience.snark.SnarkPlugin import space.kscience.snark.SnarkPlugin
import space.kscience.snark.site import space.kscience.snark.snarkSite
import java.net.URI import java.net.URI
import java.nio.file.FileSystems import java.nio.file.FileSystems
import java.nio.file.Files import java.nio.file.Files
@ -49,7 +47,7 @@ const val BUILD_DATE_FILE = "/buildDate"
@Suppress("unused") @Suppress("unused")
fun Application.spcModule() { fun Application.spcModule() {
install(HttpsRedirect) // install(HttpsRedirect)
val context = Context("spc-site") { val context = Context("spc-site") {
plugin(SnarkPlugin) plugin(SnarkPlugin)
@ -65,7 +63,7 @@ fun Application.spcModule() {
val inProduction: Boolean = environment.config.propertyOrNull("ktor.environment.production") != null val inProduction: Boolean = environment.config.propertyOrNull("ktor.environment.production") != null
if(inProduction){ if (inProduction) {
log.info("Production mode activated") log.info("Production mode activated")
log.info("Build date: $buildDate") log.info("Build date: $buildDate")
log.info("Deploy date: $deployDate") log.info("Deploy date: $deployDate")
@ -90,7 +88,7 @@ fun Application.spcModule() {
dataPath.resolve(DEPLOY_DATE_FILE).writeText(date) dataPath.resolve(DEPLOY_DATE_FILE).writeText(date)
} }
snark.site { snarkSite(snark) {
val homeDataPath = resolveData( val homeDataPath = resolveData(
this@spcModule.javaClass.getResource("/home")!!.toURI(), this@spcModule.javaClass.getResource("/home")!!.toURI(),
dataPath / "home" dataPath / "home"

View File

@ -34,14 +34,14 @@ import kotlin.collections.set
private val HtmlData.imagePath: String? get() = meta["image"]?.string ?: meta["image.path"].string 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") private val HtmlData.name: String get() = meta["name"].string ?: error("Name not found")
class MagProgSection( context(PageBuilder) class MagProgSection(
val id: String, val id: String,
val title: String, val title: String,
val style: String, val style: String,
val content: FlowContent.() -> Unit, val content: FlowContent.() -> Unit,
) )
private fun wrapSection( context(PageBuilder) private fun wrapSection(
id: String, id: String,
title: String, title: String,
sectionContent: FlowContent.() -> Unit, sectionContent: FlowContent.() -> Unit,
@ -52,7 +52,7 @@ private fun wrapSection(
} }
} }
private fun wrapSection( context(PageBuilder) private fun wrapSection(
block: HtmlData, block: HtmlData,
idOverride: String? = null, idOverride: String? = null,
): MagProgSection = wrapSection( ): 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 RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses"
private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners" private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners"
context(SiteData) private fun FlowContent.programSection() { context(PageBuilder) private fun FlowContent.programSection() {
val programBlock = resolveHtml(PROGRAM_PATH)!! val programBlock = data.resolveHtml(PROGRAM_PATH)!!
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!! val recommendedBlock = data.resolveHtml(RECOMMENDED_COURSES_PATH)!!
div("inner") { div("inner") {
h2 { +"Учебная программа" } h2 { +"Учебная программа" }
htmlData(programBlock) 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<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY //val partnersData: Meta = resolve<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
val partnersData: Meta = runBlocking { data.getByType<Meta>(PARTNERS_PATH)?.await() } ?: Meta.EMPTY val partnersData: Meta = runBlocking { data.getByType<Meta>(PARTNERS_PATH)?.await() } ?: Meta.EMPTY
div("inner") { div("inner") {
@ -117,8 +117,8 @@ context(SiteData) private fun FlowContent.partners() {
// val photo: String? by meta.string() // val photo: String? by meta.string()
//} //}
context(SiteData) private fun FlowContent.team() { context(PageBuilder) private fun FlowContent.team() {
val team = findByType("magprog_team").values.sortedBy { it.order } val team = data.findByContentType("magprog_team").values.sortedBy { it.order }
div("inner") { div("inner") {
h2 { +"Команда" } h2 { +"Команда" }
@ -172,8 +172,8 @@ context(SiteData) private fun FlowContent.team() {
// } // }
} }
context(SiteData) private fun FlowContent.mentors() { context(PageBuilder) private fun FlowContent.mentors() {
val mentors = findByType("magprog_mentor").entries.sortedBy { it.value.id } val mentors = data.findByContentType("magprog_mentor").entries.sortedBy { it.value.id }
div("inner") { div("inner") {
h2 { h2 {
@ -183,7 +183,7 @@ context(SiteData) private fun FlowContent.mentors() {
mentors.forEach { (name, mentor) -> mentors.forEach { (name, mentor) ->
section { section {
id = mentor.id id = mentor.id
val ref = resolvePage("mentor-${mentor.id}") val ref = resolvePageRef("mentor-${mentor.id}")
a(classes = "image", href = ref) { a(classes = "image", href = ref) {
mentor.imagePath?.let { photoPath -> mentor.imagePath?.let { photoPath ->
img( img(
@ -200,7 +200,7 @@ context(SiteData) private fun FlowContent.mentors() {
h2 { h2 {
a(href = ref) { +mentor.name } a(href = ref) { +mentor.name }
} }
val info = resolveHtml(name.withIndex("info")) val info = data.resolveHtml(name.withIndex("info"))
if (info != null) { if (info != null) {
htmlData(info) 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 { head {
this.title = title this.title = title
meta { 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") { footer("wrapper style1-alt") {
id = "footer" id = "footer"
div("inner") { div("inner") {
@ -282,13 +282,13 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
val magProgSiteContext = snark.readDirectory(dataPath.resolve("content")) val magProgSiteContext = snark.readDirectory(dataPath.resolve("content"))
mountSite(prefix, magProgSiteContext) { route(prefix, magProgSiteContext, setAsRoot = true) {
assetDirectory("assets", dataPath.resolve("assets")) assetDirectory("assets", dataPath.resolve("assets"))
assetDirectory("images", dataPath.resolve("images")) assetDirectory("images", dataPath.resolve("images"))
page { page {
val sections = listOf<MagProgSection>( val sections = listOf<MagProgSection>(
wrapSection(resolveHtml(INTRO_PATH)!!, "intro"), wrapSection(data.resolveHtml(INTRO_PATH)!!, "intro"),
MagProgSection( MagProgSection(
id = "partners", id = "partners",
title = "Партнеры", title = "Партнеры",
@ -311,12 +311,13 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
) { ) {
programSection() programSection()
}, },
wrapSection(resolveHtml(ENROLL_PATH)!!, "enroll"), wrapSection(data.resolveHtml(ENROLL_PATH)!!, "enroll"),
wrapSection(id = "contacts", title = "Контакты") { wrapSection(id = "contacts", title = "Контакты") {
htmlData(resolveHtml(CONTACTS_PATH)!!) htmlData(data.resolveHtml(CONTACTS_PATH)!!)
team() team()
} }
) )
magProgHead("Магистратура \"Научное программирование\"") magProgHead("Магистратура \"Научное программирование\"")
body("is-preload magprog-body") { body("is-preload magprog-body") {
section { section {
@ -355,9 +356,10 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
magProgFooter() magProgFooter()
} }
} }
}
val mentors = data.findByType("magprog_mentor").values.sortedBy { val mentors = data.findByContentType("magprog_mentor").values.sortedBy {
it.order it.order
} }
@ -377,7 +379,7 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
mentors.forEach { mentors.forEach {
li { li {
a { a {
href = resolvePage(it.mentorPageId) href = resolvePageRef(it.mentorPageId)
+it.name.substringAfterLast(" ") +it.name.substringAfterLast(" ")
} }
} }
@ -408,5 +410,4 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
} }
} }
} }
}
} }

View File

@ -15,7 +15,7 @@ import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
context(SiteData, FlowContent) private fun spcSpotlightContent( context(PageBuilder) private fun FlowContent.spcSpotlightContent(
landing: HtmlData, landing: HtmlData,
content: Map<Name, HtmlData>, content: Map<Name, HtmlData>,
) { ) {
@ -44,11 +44,11 @@ context(SiteData, FlowContent) private fun spcSpotlightContent(
id = "main" id = "main"
//TODO add smart SNARK ordering //TODO add smart SNARK ordering
section("spotlights") { section("spotlights") {
content.entries.sortedBy { it.value.meta["order"].int ?: Int.MAX_VALUE }.forEach { (name, data) -> content.entries.sortedBy { it.value.meta["order"].int ?: Int.MAX_VALUE }.forEach { (name, entry) ->
val ref = resolvePage(name) val ref = resolvePageRef(name)
section { section {
id = data.meta["id"].string ?: name.toString() id = entry.meta["id"].string ?: name.toString()
data.meta["image"]?.let { imageMeta: Meta -> entry.meta["image"]?.let { imageMeta: Meta ->
val imagePath = val imagePath =
imageMeta.value?.string ?: imageMeta["path"].string ?: error("Image path not provided") imageMeta.value?.string ?: imageMeta["path"].string ?: error("Image path not provided")
a(classes = "image") { a(classes = "image") {
@ -63,11 +63,11 @@ context(SiteData, FlowContent) private fun spcSpotlightContent(
div("content") { div("content") {
div("inner") { div("inner") {
header("major") { 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) { if (infoData == null) {
htmlData(data) htmlData(entry)
} else { } else {
htmlData(infoData) htmlData(infoData)
} }

View File

@ -15,7 +15,7 @@ import java.nio.file.Path
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
context(SiteData) internal fun HTML.spcPageContent( context(PageBuilder) internal fun HTML.spcPageContent(
meta: Meta, meta: Meta,
title: String = meta["title"].string ?: SPC_TITLE, title: String = meta["title"].string ?: SPC_TITLE,
fragment: FlowContent.() -> Unit, 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() spcHead()
body("is-preload") { body("is-preload") {
wrapper { wrapper {
@ -150,7 +150,7 @@ context(SiteData, HTML) private fun spcHome() {
header("major") { header("major") {
h3 { h3 {
a(classes = "link") { a(classes = "link") {
href = resolvePage("magprog") href = resolvePageRef("magprog")
+"""Master's program""" +"""Master's program"""
} }
} }
@ -167,7 +167,7 @@ context(SiteData, HTML) private fun spcHome() {
header("major") { header("major") {
h3 { h3 {
a(classes = "link") { a(classes = "link") {
href = resolvePage("research") href = resolvePageRef("research")
+"""Research""" +"""Research"""
} }
} }
@ -186,7 +186,7 @@ context(SiteData, HTML) private fun spcHome() {
header("major") { header("major") {
h3 { h3 {
a(classes = "link") { a(classes = "link") {
href = resolvePage("consulting") href = resolvePageRef("consulting")
+"""Consulting""" +"""Consulting"""
} }
} }
@ -203,7 +203,7 @@ context(SiteData, HTML) private fun spcHome() {
header("major") { header("major") {
h3 { h3 {
a(classes = "link") { a(classes = "link") {
href = resolvePage("team") href = resolvePageRef("team")
+"""Team""" +"""Team"""
} }
} }
@ -256,7 +256,7 @@ internal fun SiteBuilder.spcHome(rootPath: Path, prefix: Name = Name.EMPTY) {
val homePageData = snark.readDirectory(rootPath.resolve("content")) val homePageData = snark.readDirectory(rootPath.resolve("content"))
mountSite(prefix, homePageData) { route(prefix, homePageData, setAsRoot = true) {
assetDirectory("assets", rootPath.resolve("assets")) assetDirectory("assets", rootPath.resolve("assets"))
assetDirectory("images", rootPath.resolve("images")) assetDirectory("images", rootPath.resolve("images"))

View File

@ -1,15 +1,14 @@
package ru.mipt.spc package ru.mipt.spc
import kotlinx.html.* import kotlinx.html.*
import space.kscience.snark.SiteData import space.kscience.snark.PageBuilder
import space.kscience.snark.homeRef import space.kscience.snark.homeRef
import space.kscience.snark.resolvePage import space.kscience.snark.resolvePageRef
import space.kscience.snark.resolveRef
internal const val SPC_TITLE = "Scientific Programming Centre" 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 { head {
title { title {
+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 { nav {
id = "menu" id = "menu"
ul("links") { ul("links") {
@ -40,25 +39,25 @@ context(SiteData) internal fun FlowContent.spcHomeMenu() {
} }
li { li {
a { a {
href = resolvePage("magprog") href = resolvePageRef("magprog")
+"""Master""" +"""Master"""
} }
} }
li { li {
a { a {
href = resolvePage("research") href = resolvePageRef("research")
+"""Research""" +"""Research"""
} }
} }
li { li {
a { a {
href = resolvePage("consulting") href = resolvePageRef("consulting")
+"""Consulting""" +"""Consulting"""
} }
} }
li { li {
a { a {
href = resolvePage("team") href = resolvePageRef("team")
+"""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 { footer {
id = "footer" id = "footer"
div("inner") { 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 { div {
id = "wrapper" id = "wrapper"
// Header // Header

View File

@ -7,7 +7,6 @@ import io.ktor.server.application.call
import io.ktor.server.html.respondHtml import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.* import io.ktor.server.http.content.*
import io.ktor.server.plugins.origin import io.ktor.server.plugins.origin
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 io.ktor.server.routing.Route 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.get
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import kotlinx.html.HTML 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 space.kscience.dataforge.names.Name
import java.nio.file.Path 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) { override fun assetFile(remotePath: String, file: Path) {
ktorRoute.file(remotePath, file.toFile()) 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()) { ktorRoute.get(route.toWebPath()) {
call.respondHtml { call.respondHtml {
val dataWithUrl = data.copyWithRequestHost(call.request) val request = call.request
content(dataWithUrl, this) //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 = override fun route(
KtorSiteBuilder(data, ktorRoute.createRouteFromPath(subRoute.toWebPath())) 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) { override fun assetResourceFile(remotePath: String, resourcesPath: String) {
ktorRoute.resource(resourcesPath, resourcesPath) ktorRoute.resource(resourcesPath, resourcesPath)
@ -49,39 +101,27 @@ class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route)
override fun assetResourceDirectory(resourcesPath: String) { override fun assetResourceDirectory(resourcesPath: String) {
ktorRoute.resources(resourcesPath) 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( inline fun Route.snarkSite(
data: SiteData, snark: SnarkPlugin,
data: DataTree<*>,
meta: Meta = data.meta,
block: SiteBuilder.() -> Unit, block: SiteBuilder.() -> Unit,
) { ) {
block(KtorSiteBuilder(data, this@snarkSite)) contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite))
} }
fun Application.snarkSite( fun Application.snarkSite(
data: SiteData, snark: SnarkPlugin,
data: DataTree<*> = DataTree.empty(),
meta: Meta = data.meta,
block: SiteBuilder.() -> Unit, block: SiteBuilder.() -> Unit,
) { ) {
routing { routing {
snarkSite(data, block) snarkSite(snark, data, meta, block)
} }
} }
context (Application) fun SnarkPlugin.site(
block: SiteBuilder.() -> Unit,
) {
snarkSite(SiteData.empty(this), block)
}

View File

@ -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<HtmlFragment>(name) ?: getByType<HtmlFragment>(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<Name, HtmlData> =
filterByType<HtmlFragment> { 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"

View File

@ -4,21 +4,28 @@ import kotlinx.html.HTML
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.data.DataTree 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.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import java.nio.file.Path 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 * An abstraction, which is used to render sites to the different rendering engines
*/ */
interface SiteBuilder : ContextAware { 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) fun assetFile(remotePath: String, file: Path)
@ -28,39 +35,62 @@ interface SiteBuilder : ContextAware {
fun assetResourceDirectory(resourcesPath: String) 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
/** companion object {
* Create a route val INDEX_PAGE_TOKEN: NameToken = NameToken("index")
*/
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()
} }
} }
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<Any> {
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
override val dataType: KType get() = typeOf<Any>()
override val meta: Meta get() = meta
}

View File

@ -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<Any> 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<Any> {
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
override val dataType: KType get() = typeOf<Any>()
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<HtmlFragment>(name) ?: getByType<HtmlFragment>(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<Name, HtmlData> =
filterByType<HtmlFragment> { 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<Any> = readDirectory(path)
//
// return readData(parsedData, rootUrl)
//}

View File

@ -60,7 +60,7 @@ fun SiteBuilder.pages(
val layoutMeta = data.meta[LAYOUT_KEY] val layoutMeta = data.meta[LAYOUT_KEY]
if (layoutMeta != null) { if (layoutMeta != null) {
//use layout if it is defined //use layout if it is defined
this.data.snark.layout(layoutMeta).render(data) snark.layout(layoutMeta).render(data)
} else { } else {
when (data) { when (data) {
is DataTreeItem.Node -> { is DataTreeItem.Node -> {

View File

@ -3,19 +3,30 @@ package space.kscience.snark
import kotlinx.html.HTML import kotlinx.html.HTML
import kotlinx.html.html import kotlinx.html.html
import kotlinx.html.stream.createHTML 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.Meta
import space.kscience.dataforge.meta.withDefault
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty import space.kscience.dataforge.names.isEmpty
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.io.path.* 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) { private fun Path.copyRecursively(target: Path) {
Files.walk(this).forEach { source: Path -> Files.walk(this).forEach { source: Path ->
val destination: Path = target.resolve(source.relativeTo(this)) val destination: Path = target.resolve(source.relativeTo(this))
if(!destination.isDirectory()) { if (!destination.isDirectory()) {
//avoid re-creating directories //avoid re-creating directories
source.copyTo(destination, true) 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) { override fun assetResourceFile(remotePath: String, resourcesPath: String) {
val targetPath = path.resolve(remotePath) val targetPath = path.resolve(remotePath)
targetPath.parent.createDirectories() 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) { 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) 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() val htmlBuilder = createHTML()
htmlBuilder.html { htmlBuilder.html {
content(data, this) content(StaticPageBuilder(), this)
} }
val newPath = if (route.isEmpty()) { val newPath = if (route.isEmpty()) {
@ -62,19 +89,23 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
newPath.writeText(htmlBuilder.finalize()) newPath.writeText(htmlBuilder.finalize())
} }
override fun route(subRoute: Name): SiteBuilder = StaticSiteBuilder(data, path.resolve(subRoute.toWebPath())) override fun route(
routeName: Name,
override fun withData(newData: SiteData): SiteBuilder = StaticSiteBuilder(newData, path) dataOverride: DataTree<*>?,
metaOverride: Meta?,
} setAsRoot: Boolean,
): SiteBuilder = StaticSiteBuilder(
fun SnarkPlugin.static(path: Path, block: SiteBuilder.() -> Unit) { snark = snark,
val base = SiteData.empty( data = dataOverride ?: data,
this, meta = metaOverride?.withDefault(meta) ?: meta,
baseUrlPath = path.absolutePathString(), baseUrl = baseUrl,
meta = Meta { path = path.resolve(routeName.toWebPath())
"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()
} }