forked from SPC/spc-site
Refactor page placement.
This commit is contained in:
parent
600a9b5529
commit
4966bfc9b3
@ -1,7 +1,7 @@
|
|||||||
package html5up.forty
|
package html5up.forty
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteContext
|
import space.kscience.snark.SiteData
|
||||||
import space.kscience.snark.resolveRef
|
import space.kscience.snark.resolveRef
|
||||||
|
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ internal fun FlowContent.fortyFooter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun BODY.fortyScripts() {
|
context(SiteData) internal fun BODY.fortyScripts() {
|
||||||
script {
|
script {
|
||||||
src = resolveRef("assets/js/jquery.min.js")
|
src = resolveRef("assets/js/jquery.min.js")
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package html5up.forty
|
package html5up.forty
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteContext
|
import space.kscience.snark.SiteData
|
||||||
|
|
||||||
context(SiteContext) internal fun HTML.landing(){
|
context(SiteData) internal fun HTML.landing(){
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package html5up.forty
|
package html5up.forty
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteContext
|
import space.kscience.snark.SiteData
|
||||||
import space.kscience.snark.resolveRef
|
import space.kscience.snark.resolveRef
|
||||||
|
|
||||||
context(SiteContext) internal fun HTML.fortyPage(){
|
context(SiteData) internal fun HTML.fortyPage(){
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ 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(SiteContext) private fun FlowContent.programSection() {
|
context(SiteData) private fun FlowContent.programSection() {
|
||||||
val programBlock = resolveHtml(PROGRAM_PATH)!!
|
val programBlock = resolveHtml(PROGRAM_PATH)!!
|
||||||
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!!
|
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!!
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -97,7 +97,7 @@ context(SiteContext) private fun FlowContent.programSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) private fun FlowContent.partners() {
|
context(SiteData) 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") {
|
||||||
@ -127,7 +127,7 @@ context(SiteContext) private fun FlowContent.partners() {
|
|||||||
// val photo: String? by meta.string()
|
// val photo: String? by meta.string()
|
||||||
//}
|
//}
|
||||||
|
|
||||||
context(SiteContext) private fun FlowContent.team() {
|
context(SiteData) private fun FlowContent.team() {
|
||||||
val team = findByType("magprog_team").values.sortedBy { it.order }
|
val team = findByType("magprog_team").values.sortedBy { it.order }
|
||||||
|
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -182,7 +182,7 @@ context(SiteContext) private fun FlowContent.team() {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) private fun FlowContent.mentors() {
|
context(SiteData) private fun FlowContent.mentors() {
|
||||||
val mentors = findByType("magprog_mentor").entries.sortedBy { it.value.id }
|
val mentors = findByType("magprog_mentor").entries.sortedBy { it.value.id }
|
||||||
|
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -219,12 +219,12 @@ context(SiteContext) private fun FlowContent.mentors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun FlowContent.contacts() {
|
context(SiteData) internal fun FlowContent.contacts() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
context(SiteContext) internal fun HTML.magProgHead(title: String) {
|
context(SiteData) internal fun HTML.magProgHead(title: String) {
|
||||||
head {
|
head {
|
||||||
this.title = title
|
this.title = title
|
||||||
meta {
|
meta {
|
||||||
@ -251,7 +251,7 @@ context(SiteContext) internal fun HTML.magProgHead(title: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun BODY.magProgFooter() {
|
context(SiteData) internal fun BODY.magProgFooter() {
|
||||||
footer("wrapper style1-alt") {
|
footer("wrapper style1-alt") {
|
||||||
id = "footer"
|
id = "footer"
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -296,7 +296,7 @@ internal fun Application.spcMaster(context: Context, dataPath: Path, prefix: Str
|
|||||||
|
|
||||||
val snark = context.fetch(SnarkPlugin)
|
val snark = context.fetch(SnarkPlugin)
|
||||||
|
|
||||||
val magProgSiteContext: SiteContext = snark.read(dataPath.resolve("content"), prefix)
|
val magProgSiteContext: SiteData = snark.readDirectory(dataPath.resolve("content"), prefix)
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
route(prefix) {
|
route(prefix) {
|
||||||
|
@ -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(SiteContext) private fun FlowContent.spcSpotlightContent(
|
context(SiteData) private fun FlowContent.spcSpotlightContent(
|
||||||
landing: HtmlData,
|
landing: HtmlData,
|
||||||
content: Map<Name, HtmlData>,
|
content: Map<Name, HtmlData>,
|
||||||
) {
|
) {
|
||||||
@ -88,11 +88,12 @@ context(SiteContext) private fun FlowContent.spcSpotlightContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
context(SiteContext) internal fun SiteBuilder.spcSpotlight(
|
context(SiteData) internal fun SiteBuilder.spcSpotlight(
|
||||||
name: String,
|
address: String,
|
||||||
contentFilter: (Name, Meta) -> Boolean,
|
contentFilter: (Name, Meta) -> Boolean,
|
||||||
) {
|
) {
|
||||||
val body = resolveHtml(name.parseAsName()) ?: error("Could not find body for $name")
|
val name = address.parseAsName()
|
||||||
|
val body = resolveHtml(name) ?: error("Could not find body for $name")
|
||||||
val content = resolveAllHtml(contentFilter)
|
val content = resolveAllHtml(contentFilter)
|
||||||
|
|
||||||
val meta = body.meta
|
val meta = body.meta
|
||||||
@ -109,7 +110,7 @@ context(SiteContext) internal fun SiteBuilder.spcSpotlight(
|
|||||||
}
|
}
|
||||||
|
|
||||||
content.forEach { (name, contentBody) ->
|
content.forEach { (name, contentBody) ->
|
||||||
page(name.tokens.joinToString("/")){
|
page(name){
|
||||||
spcPageContent(contentBody.meta) {
|
spcPageContent(contentBody.meta) {
|
||||||
htmlData(contentBody)
|
htmlData(contentBody)
|
||||||
}
|
}
|
||||||
|
@ -9,18 +9,20 @@ import space.kscience.dataforge.context.Context
|
|||||||
import space.kscience.dataforge.context.error
|
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.context.logger
|
||||||
import space.kscience.dataforge.data.filterByType
|
import space.kscience.dataforge.data.Data
|
||||||
import space.kscience.dataforge.data.forEach
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
import space.kscience.dataforge.names.*
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.asName
|
||||||
|
import space.kscience.dataforge.names.startsWith
|
||||||
import space.kscience.dataforge.values.string
|
import space.kscience.dataforge.values.string
|
||||||
import space.kscience.snark.*
|
import space.kscience.snark.*
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
|
||||||
context(SiteContext) internal fun HTML.spcPageContent(
|
context(SiteData) 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,
|
||||||
@ -58,18 +60,18 @@ context(SiteContext) internal fun HTML.spcPageContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
context(SiteContext) internal fun SiteBuilder.spcPage(subRoute: String, meta: Meta, fragment: FlowContent.() -> Unit) {
|
internal fun SiteBuilder.spcPage(subRoute: Name, meta: Meta, fragment: FlowContent.() -> Unit) {
|
||||||
page(subRoute) {
|
page(subRoute) {
|
||||||
spcPageContent(meta, fragment = fragment)
|
spcPageContent(meta, fragment = fragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun SiteBuilder.spcPage(
|
internal fun SiteBuilder.spcPage(
|
||||||
subRoute: String,
|
subRoute: Name,
|
||||||
dataPath: Name = subRoute.replace("/", ".").parseAsName(),
|
dataPath: Name = subRoute,
|
||||||
more: FlowContent.() -> Unit = {},
|
more: FlowContent.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val data = resolveHtml(dataPath)
|
val data = data.resolveHtml(dataPath)
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
spcPage(subRoute, data.meta) {
|
spcPage(subRoute, data.meta) {
|
||||||
htmlData(data)
|
htmlData(data)
|
||||||
@ -80,34 +82,46 @@ context(SiteContext) internal fun SiteBuilder.spcPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Suppress("UNCHECKED_CAST")
|
||||||
* Route a directory
|
internal val FortyDataRenderer: SiteBuilder.(Data<*>) -> Unit = { data ->
|
||||||
*/
|
if(data.type == typeOf<HtmlFragment>()) {
|
||||||
context(SiteContext) internal fun SiteBuilder.spcDirectory(
|
data as Data<HtmlFragment>
|
||||||
subRoute: String,
|
page {
|
||||||
dataPath: Name = subRoute.replace("/", ".").parseAsName(),
|
spcPageContent(data.meta) {
|
||||||
) {
|
htmlData(data)
|
||||||
data.filterByType<HtmlFragment> { name, _ -> name.startsWith(dataPath) }.forEach { html ->
|
|
||||||
val pageName = if (html.name.lastOrNull()?.body == SiteContext.INDEX_PAGE_NAME) {
|
|
||||||
html.name.cutLast()
|
|
||||||
} else {
|
|
||||||
html.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spcPage(pageName.tokens.joinToString(separator = "/"), html.meta) {
|
|
||||||
htmlData(html)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun SiteBuilder.spcPage(
|
///**
|
||||||
|
// * Route a directory
|
||||||
|
// */
|
||||||
|
//internal fun SiteBuilder.spcDirectory(
|
||||||
|
// subRoute: String,
|
||||||
|
// dataPath: Name = subRoute.replace("/", ".").parseAsName(),
|
||||||
|
//) {
|
||||||
|
// data.filterByType<HtmlFragment> { name, _ -> name.startsWith(dataPath) }.forEach { html ->
|
||||||
|
// val pageName = if (html.name.lastOrNull()?.body == SiteData.INDEX_PAGE_NAME) {
|
||||||
|
// html.name.cutLast()
|
||||||
|
// } else {
|
||||||
|
// html.name
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// spcPage(pageName.tokens.joinToString(separator = "/"), html.meta) {
|
||||||
|
// htmlData(html)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
internal fun SiteBuilder.spcPage(
|
||||||
name: Name,
|
name: Name,
|
||||||
more: FlowContent.() -> Unit = {},
|
more: FlowContent.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
spcPage(name.tokens.joinToString("/"), name, more)
|
spcPage(name, name, more)
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext, HTML) private fun HTML.spcHome() {
|
context(SiteData, HTML) private fun HTML.spcHome() {
|
||||||
spcHead()
|
spcHead()
|
||||||
body("is-preload") {
|
body("is-preload") {
|
||||||
wrapper {
|
wrapper {
|
||||||
@ -298,18 +312,18 @@ internal fun Application.spcHome(context: Context, rootPath: Path, prefix: Strin
|
|||||||
|
|
||||||
val snark = context.fetch(SnarkPlugin)
|
val snark = context.fetch(SnarkPlugin)
|
||||||
|
|
||||||
val homePageContext = snark.read(rootPath.resolve("content"), prefix)
|
val homePageContext = snark.readDirectory(rootPath.resolve("content"), prefix)
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
route(prefix) {
|
route(prefix) {
|
||||||
snarkSite(homePageContext) {
|
snarkSite(homePageContext) {
|
||||||
staticDirectory("assets", rootPath.resolve("assets"))
|
assetDirectory("assets", rootPath.resolve("assets"))
|
||||||
staticDirectory("images", rootPath.resolve("images"))
|
assetDirectory("images", rootPath.resolve("images"))
|
||||||
|
|
||||||
page { spcHome() }
|
page { spcHome() }
|
||||||
|
|
||||||
spcDirectory("consulting")
|
pages("consulting", dataRenderer = FortyDataRenderer)
|
||||||
spcDirectory("ru/consulting")
|
//pages("ru.consulting".parseAsName(), dataRenderer = FortyDataRenderer)
|
||||||
|
|
||||||
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" }
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package ru.mipt.spc
|
package ru.mipt.spc
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteContext
|
import space.kscience.snark.SiteData
|
||||||
import space.kscience.snark.homeRef
|
import space.kscience.snark.homeRef
|
||||||
import space.kscience.snark.resolveRef
|
import space.kscience.snark.resolveRef
|
||||||
|
|
||||||
|
|
||||||
internal const val SPC_TITLE = "Scientific Programming Centre"
|
internal const val SPC_TITLE = "Scientific Programming Centre"
|
||||||
|
|
||||||
context(SiteContext) internal fun HTML.spcHead(title: String = SPC_TITLE) {
|
context(SiteData) internal fun HTML.spcHead(title: String = SPC_TITLE) {
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
+title
|
+title
|
||||||
@ -27,7 +27,7 @@ context(SiteContext) internal fun HTML.spcHead(title: String = SPC_TITLE) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun FlowContent.spcHomeMenu() {
|
context(SiteData) internal fun FlowContent.spcHomeMenu() {
|
||||||
nav {
|
nav {
|
||||||
id = "menu"
|
id = "menu"
|
||||||
ul("links") {
|
ul("links") {
|
||||||
@ -79,7 +79,7 @@ context(SiteContext) internal fun FlowContent.spcHomeMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun FlowContent.spcFooter() {
|
context(SiteData) internal fun FlowContent.spcFooter() {
|
||||||
footer {
|
footer {
|
||||||
id = "footer"
|
id = "footer"
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -129,7 +129,7 @@ context(SiteContext) internal fun FlowContent.spcFooter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteContext) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) {
|
context(SiteData) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) {
|
||||||
div {
|
div {
|
||||||
id = "wrapper"
|
id = "wrapper"
|
||||||
// Header
|
// Header
|
||||||
|
@ -9,71 +9,78 @@ import io.ktor.server.routing.get
|
|||||||
import kotlinx.html.HTML
|
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.names.Name
|
||||||
|
import space.kscience.dataforge.names.parseAsName
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
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 siteContext: SiteContext
|
val data: SiteData
|
||||||
|
|
||||||
override val context: Context get() = siteContext.context
|
override val context: Context get() = data.context
|
||||||
|
|
||||||
fun staticFile(remotePath: String, file: Path)
|
fun assetFile(remotePath: String, file: Path)
|
||||||
|
|
||||||
fun staticDirectory(remotePath: String, directory: Path)
|
fun assetDirectory(remotePath: String, directory: Path)
|
||||||
|
|
||||||
fun staticResourceFile(remotePath: String, resourcesPath: String)
|
fun assetResourceFile(remotePath: String, resourcesPath: String)
|
||||||
|
|
||||||
fun staticResourceDirectory(resourcesPath: String)
|
fun assetResourceDirectory(resourcesPath: String)
|
||||||
|
|
||||||
fun page(route: String = "", content: context(SiteContext, HTML) () -> Unit)
|
fun page(route: Name = Name.EMPTY, content: context(SiteData, HTML) () -> Unit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a route
|
* Create a route
|
||||||
*/
|
*/
|
||||||
fun route(subRoute: String): SiteBuilder
|
fun route(subRoute: Name): SiteBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
public inline fun SiteBuilder.route(route: String, block: SiteBuilder.() -> Unit) {
|
public inline fun SiteBuilder.route(route: Name, block: SiteBuilder.() -> Unit) {
|
||||||
route(route).apply(block)
|
route(route).apply(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
class KtorSiteRoute(override val siteContext: SiteContext, private val ktorRoute: Route) : SiteBuilder {
|
public inline fun SiteBuilder.route(route: String, block: SiteBuilder.() -> Unit) {
|
||||||
override fun staticFile(remotePath: String, file: Path) {
|
route(route.parseAsName()).apply(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
class KtorSiteRoute(override val data: SiteData, private val ktorRoute: Route) : SiteBuilder {
|
||||||
|
override fun assetFile(remotePath: String, file: Path) {
|
||||||
ktorRoute.file(remotePath, file.toFile())
|
ktorRoute.file(remotePath, file.toFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun staticDirectory(remotePath: String, directory: Path) {
|
override fun assetDirectory(remotePath: String, directory: Path) {
|
||||||
ktorRoute.static(remotePath) {
|
ktorRoute.static(remotePath) {
|
||||||
files(directory.toFile())
|
files(directory.toFile())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun page(route: String, content: context(SiteContext, HTML)() -> Unit) {
|
override fun page(route: Name, content: context(SiteData, HTML)() -> Unit) {
|
||||||
ktorRoute.get(route) {
|
ktorRoute.get(route.toWebPath()) {
|
||||||
call.respondHtml {
|
call.respondHtml {
|
||||||
content(siteContext.copyWithRequestHost(call.request), this)
|
content(data.copyWithRequestHost(call.request), this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun route(subRoute: String): SiteBuilder =
|
override fun route(subRoute: Name): SiteBuilder =
|
||||||
KtorSiteRoute(siteContext, ktorRoute.createRouteFromPath(subRoute))
|
KtorSiteRoute(data, ktorRoute.createRouteFromPath(subRoute.toWebPath()))
|
||||||
|
|
||||||
override fun staticResourceFile(remotePath: String, resourcesPath: String) {
|
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
||||||
ktorRoute.resource(resourcesPath, resourcesPath)
|
ktorRoute.resource(resourcesPath, resourcesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun staticResourceDirectory(resourcesPath: String) {
|
override fun assetResourceDirectory(resourcesPath: String) {
|
||||||
ktorRoute.resources(resourcesPath)
|
ktorRoute.resources(resourcesPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun Route.snarkSite(
|
inline fun Route.snarkSite(
|
||||||
siteContext: SiteContext,
|
siteContext: SiteData,
|
||||||
block: context(SiteContext, SiteBuilder)() -> Unit,
|
block: context(SiteData, SiteBuilder)() -> Unit,
|
||||||
) {
|
) {
|
||||||
block(siteContext, KtorSiteRoute(siteContext, this@snarkSite))
|
block(siteContext, KtorSiteRoute(siteContext, this@snarkSite))
|
||||||
}
|
}
|
@ -15,15 +15,15 @@ import space.kscience.dataforge.meta.string
|
|||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
import space.kscience.dataforge.names.startsWith
|
import space.kscience.dataforge.names.startsWith
|
||||||
import space.kscience.snark.SiteContext.Companion.INDEX_PAGE_NAME
|
import space.kscience.snark.SiteData.Companion.INDEX_PAGE_NAME
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
data class SiteContext(
|
data class SiteData(
|
||||||
val snark: SnarkPlugin,
|
val snark: SnarkPlugin,
|
||||||
val path: String,
|
|
||||||
val meta: Meta,
|
|
||||||
val data: DataTree<*>,
|
val data: DataTree<*>,
|
||||||
) : ContextAware {
|
val urlPath: String,
|
||||||
|
override val meta: Meta = data.meta
|
||||||
|
) : ContextAware, DataTree<Any> by data {
|
||||||
|
|
||||||
override val context: Context get() = snark.context
|
override val context: Context get() = snark.context
|
||||||
|
|
||||||
@ -37,15 +37,15 @@ data class SiteContext(
|
|||||||
/**
|
/**
|
||||||
* Resolve a resource full path by its name
|
* Resolve a resource full path by its name
|
||||||
*/
|
*/
|
||||||
fun SiteContext.resolveRef(name: String): String = "${path.removeSuffix("/")}/$name"
|
fun SiteData.resolveRef(name: String): String = "${urlPath.removeSuffix("/")}/$name"
|
||||||
|
|
||||||
fun SiteContext.resolveRef(name: Name): String = "${path.removeSuffix("/")}/${name.tokens.joinToString("/")}"
|
fun SiteData.resolveRef(name: Name): String = "${urlPath.removeSuffix("/")}/${name.tokens.joinToString("/")}"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a Html builder by its full name
|
* Resolve a Html builder by its full name
|
||||||
*/
|
*/
|
||||||
fun SiteContext.resolveHtml(name: Name): HtmlData? {
|
fun DataTree<*>.resolveHtml(name: Name): HtmlData? {
|
||||||
val resolved = (data.getByType<HtmlFragment>(name) ?: data.getByType<HtmlFragment>(name + INDEX_PAGE_NAME))
|
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + INDEX_PAGE_NAME))
|
||||||
|
|
||||||
return resolved?.takeIf {
|
return resolved?.takeIf {
|
||||||
it.published //TODO add language confirmation
|
it.published //TODO add language confirmation
|
||||||
@ -55,45 +55,45 @@ fun SiteContext.resolveHtml(name: Name): HtmlData? {
|
|||||||
/**
|
/**
|
||||||
* Find all Html blocks using given name/meta filter
|
* Find all Html blocks using given name/meta filter
|
||||||
*/
|
*/
|
||||||
fun SiteContext.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
|
fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
|
||||||
data.filterByType<HtmlFragment> { name, meta ->
|
filterByType<HtmlFragment> { name, meta ->
|
||||||
predicate(name, meta)
|
predicate(name, meta)
|
||||||
&& meta["published"].string != "false"
|
&& meta["published"].string != "false"
|
||||||
//TODO add language confirmation
|
//TODO add language confirmation
|
||||||
}.asSequence().associate { it.name to it.data }
|
}.asSequence().associate { it.name to it.data }
|
||||||
|
|
||||||
val SiteContext.homeRef get() = resolveRef("").removeSuffix("/")
|
val SiteData.homeRef get() = resolveRef("").removeSuffix("/")
|
||||||
|
|
||||||
|
|
||||||
fun SiteContext.findByType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
|
fun SiteData.findByType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
|
||||||
name.startsWith(baseName) && meta["content_type"].string == contentType
|
name.startsWith(baseName) && meta["content_type"].string == contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val Data<*>.published: Boolean get() = meta["published"].string != "false"
|
internal val Data<*>.published: Boolean get() = meta["published"].string != "false"
|
||||||
|
|
||||||
fun SnarkPlugin.siteContext(rootUrl: String, data: DataTree<*>): SiteContext =
|
fun SnarkPlugin.readData(data: DataTree<*>, rootUrl: String = "/"): SiteData =
|
||||||
SiteContext(this, rootUrl, data.meta, data)
|
SiteData(this, data, rootUrl)
|
||||||
|
|
||||||
fun SnarkPlugin.read(path: Path, rootUrl: String = "/"): SiteContext {
|
fun SnarkPlugin.readDirectory(path: Path, rootUrl: String = "/"): SiteData {
|
||||||
val parsedData: DataTree<Any> = readDirectory(path)
|
val parsedData: DataTree<Any> = readDirectory(path)
|
||||||
|
|
||||||
return siteContext(rootUrl, parsedData)
|
return readData(parsedData, rootUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal fun SiteContext.copyWithRequestHost(request: ApplicationRequest): SiteContext {
|
internal fun SiteData.copyWithRequestHost(request: ApplicationRequest): SiteData {
|
||||||
val uri = URLBuilder(
|
val uri = URLBuilder(
|
||||||
protocol = URLProtocol.createOrDefault(request.origin.scheme),
|
protocol = URLProtocol.createOrDefault(request.origin.scheme),
|
||||||
host = request.host(),
|
host = request.host(),
|
||||||
port = request.port(),
|
port = request.port(),
|
||||||
pathSegments = path.split("/"),
|
pathSegments = urlPath.split("/"),
|
||||||
)
|
)
|
||||||
return copy(path = uri.buildString())
|
return copy(urlPath = uri.buildString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Substitute uri in [SiteContext] with uri in the call to properly resolve relative refs. Only host properties are substituted.
|
* Substitute uri in [SiteData] with uri in the call to properly resolve relative refs. Only host properties are substituted.
|
||||||
*/
|
*/
|
||||||
context(SiteContext) inline fun withRequest(request: ApplicationRequest, block: context(SiteContext) () -> Unit) {
|
context(SiteData) inline fun withRequest(request: ApplicationRequest, block: context(SiteData) () -> Unit) {
|
||||||
block(copyWithRequestHost(request))
|
block(copyWithRequestHost(request))
|
||||||
}
|
}
|
@ -1,22 +1,25 @@
|
|||||||
package space.kscience.snark
|
package space.kscience.snark
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import space.kscience.dataforge.data.DataTree
|
import space.kscience.dataforge.data.Data
|
||||||
import space.kscience.dataforge.data.DataTreeItem
|
import space.kscience.dataforge.data.DataTreeItem
|
||||||
import space.kscience.dataforge.data.await
|
import space.kscience.dataforge.data.await
|
||||||
|
import space.kscience.dataforge.data.getItem
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.getIndexed
|
import space.kscience.dataforge.meta.getIndexed
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.NameToken
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.parseAsName
|
||||||
import space.kscience.snark.SiteLayout.Companion.DESIGNATION_KEY
|
import space.kscience.snark.SiteLayout.Companion.ASSETS_KEY
|
||||||
|
import space.kscience.snark.SiteLayout.Companion.INDEX_PAGE_TOKEN
|
||||||
import space.kscience.snark.SiteLayout.Companion.LAYOUT_KEY
|
import space.kscience.snark.SiteLayout.Companion.LAYOUT_KEY
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
internal fun SiteBuilder.staticFrom(rootMeta: Meta) {
|
internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
|
||||||
rootMeta.getIndexed("resource".asName()).forEach { (_, meta) ->
|
rootMeta.getIndexed("resource".asName()).forEach { (_, meta) ->
|
||||||
|
|
||||||
val path by meta.string()
|
val path by meta.string()
|
||||||
@ -25,75 +28,109 @@ internal fun SiteBuilder.staticFrom(rootMeta: Meta) {
|
|||||||
path?.let { resourcePath ->
|
path?.let { resourcePath ->
|
||||||
//If remote path provided, use a single resource
|
//If remote path provided, use a single resource
|
||||||
remotePath?.let {
|
remotePath?.let {
|
||||||
staticResourceFile(it, resourcePath)
|
assetResourceFile(it, resourcePath)
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//otherwise use package resources
|
//otherwise use package resources
|
||||||
staticResourceDirectory(resourcePath)
|
assetResourceDirectory(resourcePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
|
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
|
||||||
val remotePath by meta.string { error("File remote path is not provided") }
|
val remotePath by meta.string { error("File remote path is not provided") }
|
||||||
val path by meta.string { error("File path is not provided") }
|
val path by meta.string { error("File path is not provided") }
|
||||||
staticFile(remotePath, Path.of(path))
|
assetFile(remotePath, Path.of(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
|
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
|
||||||
val path by meta.string { error("Directory path is not provided") }
|
val path by meta.string { error("Directory path is not provided") }
|
||||||
staticDirectory("", Path.of(path))
|
assetDirectory("", Path.of(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represent pages in a [DataTree]
|
* Recursively renders the data items in [data]. If [LAYOUT_KEY] is defined in an item, use it to load
|
||||||
|
* layout from the context, otherwise render children nodes as name segments and individual data items using [dataRenderer].
|
||||||
*/
|
*/
|
||||||
fun SiteBuilder.data(data: DataTreeItem<*>, prefix: Name = Name.EMPTY) {
|
fun SiteBuilder.pages(
|
||||||
|
data: DataTreeItem<*>,
|
||||||
|
dataRenderer: SiteBuilder.(Data<*>) -> Unit = SiteLayout.defaultDataRenderer,
|
||||||
|
) {
|
||||||
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
|
||||||
siteContext.snark.layout(layoutMeta).render(data)
|
this.data.snark.layout(layoutMeta).render(data)
|
||||||
} else {
|
} else {
|
||||||
when (data) {
|
when (data) {
|
||||||
is DataTreeItem.Node -> {
|
is DataTreeItem.Node -> {
|
||||||
data.tree.items.forEach { (token, item) ->
|
data.tree.items.forEach { (token, item) ->
|
||||||
data(item, prefix + token)
|
//Don't apply index token
|
||||||
|
if (token == INDEX_PAGE_TOKEN) {
|
||||||
|
pages(item, dataRenderer)
|
||||||
|
}
|
||||||
|
route(token.toString()) {
|
||||||
|
pages(item, dataRenderer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is DataTreeItem.Leaf -> {
|
is DataTreeItem.Leaf -> {
|
||||||
val item = data.data
|
dataRenderer.invoke(this, data.data)
|
||||||
if (item.type == typeOf<HtmlData>() && item.meta[DESIGNATION_KEY].string == "page") {
|
|
||||||
route(prefix.tokens.joinToString(separator = "/")) {
|
|
||||||
page {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val pageFragment: HtmlFragment = runBlocking { item.await() as HtmlFragment }
|
|
||||||
pageFragment.invoke(consumer)
|
|
||||||
}
|
|
||||||
staticFrom(item.meta)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
data.meta[ASSETS_KEY]?.let {
|
||||||
|
assetsFrom(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//TODO watch for changes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render all pages in a node with given name
|
||||||
|
*/
|
||||||
|
fun SiteBuilder.pages(
|
||||||
|
dataPath: Name,
|
||||||
|
remotePath: Name = dataPath,
|
||||||
|
dataRenderer: SiteBuilder.(Data<*>) -> Unit = SiteLayout.defaultDataRenderer,
|
||||||
|
) {
|
||||||
|
val item = data.getItem(dataPath) ?: error("No data found by name $dataPath")
|
||||||
|
route(remotePath) {
|
||||||
|
pages(item, dataRenderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun SiteBuilder.pages(
|
||||||
//TODO watch for changes
|
dataPath: String,
|
||||||
|
remotePath: Name = dataPath.parseAsName(),
|
||||||
|
dataRenderer: SiteBuilder.(Data<*>) -> Unit = SiteLayout.defaultDataRenderer,
|
||||||
|
) {
|
||||||
|
pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun interface SiteLayout {
|
fun interface SiteLayout {
|
||||||
|
|
||||||
context(SiteBuilder) fun render(data: DataTreeItem<*>)
|
context(SiteBuilder) fun render(item: DataTreeItem<*>)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val DESIGNATION_KEY = "designation"
|
|
||||||
const val LAYOUT_KEY = "layout"
|
const val LAYOUT_KEY = "layout"
|
||||||
|
const val ASSETS_KEY = "assets"
|
||||||
|
val INDEX_PAGE_TOKEN = NameToken("index")
|
||||||
|
|
||||||
|
val defaultDataRenderer: SiteBuilder.(Data<*>) -> Unit = { data ->
|
||||||
|
if (data.type == typeOf<HtmlData>()) {
|
||||||
|
page {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val pageFragment: HtmlFragment = runBlocking { data.await() as HtmlFragment }
|
||||||
|
pageFragment.invoke(consumer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object DefaultSiteLayout : SiteLayout {
|
object DefaultSiteLayout : SiteLayout {
|
||||||
context(SiteBuilder) override fun render(data: DataTreeItem<*>) {
|
context(SiteBuilder) override fun render(item: DataTreeItem<*>) {
|
||||||
data(data)
|
pages(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,26 +0,0 @@
|
|||||||
package ru.mipt.spc
|
|
||||||
|
|
||||||
//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) {
|
|
||||||
// val prefix = name.tokens.joinToString(separator = "/", prefix = "/")
|
|
||||||
// pageBuilders[name] = { dataset ->
|
|
||||||
// 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))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
Loading…
Reference in New Issue
Block a user