Complete refactor to new routing API
This commit is contained in:
parent
43bf8e8e96
commit
c15a0ea948
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
|
@ -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)
|
||||
@ -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"
|
||||
|
@ -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<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
|
||||
val partnersData: Meta = runBlocking { data.getByType<Meta>(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<MagProgSection>(
|
||||
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,9 +356,10 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
|
||||
magProgFooter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val mentors = data.findByType("magprog_mentor").values.sortedBy {
|
||||
val mentors = data.findByContentType("magprog_mentor").values.sortedBy {
|
||||
it.order
|
||||
}
|
||||
|
||||
@ -377,7 +379,7 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
|
||||
mentors.forEach {
|
||||
li {
|
||||
a {
|
||||
href = resolvePage(it.mentorPageId)
|
||||
href = resolvePageRef(it.mentorPageId)
|
||||
+it.name.substringAfterLast(" ")
|
||||
}
|
||||
}
|
||||
@ -409,4 +411,3 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Name, HtmlData>,
|
||||
) {
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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"))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
56
src/main/kotlin/space/kscience/snark/PageBuilder.kt
Normal file
56
src/main/kotlin/space/kscience/snark/PageBuilder.kt
Normal 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"
|
@ -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<Any> {
|
||||
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
|
||||
override val dataType: KType get() = typeOf<Any>()
|
||||
override val meta: Meta get() = meta
|
||||
}
|
@ -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)
|
||||
//}
|
@ -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 -> {
|
||||
|
@ -3,15 +3,26 @@ 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))
|
||||
@ -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)
|
||||
|
||||
}
|
||||
|
||||
fun SnarkPlugin.static(path: Path, block: SiteBuilder.() -> Unit) {
|
||||
val base = SiteData.empty(
|
||||
this,
|
||||
baseUrlPath = path.absolutePathString(),
|
||||
meta = Meta {
|
||||
"pageSuffix" put ".html"
|
||||
}
|
||||
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())
|
||||
)
|
||||
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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user