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
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")
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

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

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.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,57 +356,57 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
magProgFooter()
}
}
}
val mentors = data.findByType("magprog_mentor").values.sortedBy {
it.order
}
val mentors = data.findByContentType("magprog_mentor").values.sortedBy {
it.order
}
mentors.forEach { mentor ->
page(mentor.mentorPageId.asName()) {
mentors.forEach { mentor ->
page(mentor.mentorPageId.asName()) {
magProgHead("Научное программирование: ${mentor.name}")
body("is-preload") {
header {
id = "header"
a(classes = "title") {
href = "$homeRef#mentors"
+"Научные руководители"
}
nav {
ul {
mentors.forEach {
li {
a {
href = resolvePage(it.mentorPageId)
+it.name.substringAfterLast(" ")
}
magProgHead("Научное программирование: ${mentor.name}")
body("is-preload") {
header {
id = "header"
a(classes = "title") {
href = "$homeRef#mentors"
+"Научные руководители"
}
nav {
ul {
mentors.forEach {
li {
a {
href = resolvePageRef(it.mentorPageId)
+it.name.substringAfterLast(" ")
}
}
}
}
}
div {
id = "wrapper"
section("wrapper") {
id = "main"
div("inner") {
h1("major") { +mentor.name }
val imageClass = mentor.meta["image.position"].string ?: "left"
span("image $imageClass") {
mentor.imagePath?.let { photoPath ->
img(
src = resolveRef(photoPath),
alt = mentor.name
)
}
}
htmlData(mentor)
}
}
}
magProgFooter()
}
div {
id = "wrapper"
section("wrapper") {
id = "main"
div("inner") {
h1("major") { +mentor.name }
val imageClass = mentor.meta["image.position"].string ?: "left"
span("image $imageClass") {
mentor.imagePath?.let { photoPath ->
img(
src = resolveRef(photoPath),
alt = mentor.name
)
}
}
htmlData(mentor)
}
}
}
magProgFooter()
}
}
}

View File

@ -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)
}

View File

@ -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"))

View File

@ -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

View File

@ -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)
}

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.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
}

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]
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 -> {

View File

@ -3,19 +3,30 @@ package space.kscience.snark
import kotlinx.html.HTML
import kotlinx.html.html
import kotlinx.html.stream.createHTML
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.withDefault
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty
import java.nio.file.Files
import java.nio.file.Path
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.io.path.*
class StaticSiteBuilder(override val data: SiteData, private val path: Path) : SiteBuilder {
internal class StaticSiteBuilder(
override val snark: SnarkPlugin,
override val data: DataTree<*>,
override val meta: Meta,
private val baseUrl: String,
private val path: Path,
) : SiteBuilder {
private fun Path.copyRecursively(target: Path) {
Files.walk(this).forEach { source: Path ->
val destination: Path = target.resolve(source.relativeTo(this))
if(!destination.isDirectory()) {
if (!destination.isDirectory()) {
//avoid re-creating directories
source.copyTo(destination, true)
}
@ -37,7 +48,7 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
val targetPath = path.resolve(remotePath)
targetPath.parent.createDirectories()
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath,true)
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true)
}
override fun assetResourceDirectory(resourcesPath: String) {
@ -45,11 +56,27 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path)
}
override fun page(route: Name, content: context(SiteData, HTML) () -> Unit) {
inner class StaticPageBuilder : PageBuilder {
override val data: DataTree<*> get() = this@StaticSiteBuilder.data
override val meta: Meta get() = this@StaticSiteBuilder.meta
override val context: Context get() = this@StaticSiteBuilder.context
override fun resolveRef(ref: String): String {
TODO("Not yet implemented")
}
override fun resolvePageRef(pageName: Name): String {
TODO("Not yet implemented")
}
}
override fun page(route: Name, content: context(PageBuilder, HTML) () -> Unit) {
val htmlBuilder = createHTML()
htmlBuilder.html {
content(data, this)
content(StaticPageBuilder(), this)
}
val newPath = if (route.isEmpty()) {
@ -62,19 +89,23 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
newPath.writeText(htmlBuilder.finalize())
}
override fun route(subRoute: Name): SiteBuilder = StaticSiteBuilder(data, path.resolve(subRoute.toWebPath()))
override fun withData(newData: SiteData): SiteBuilder = StaticSiteBuilder(newData, path)
}
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()
}