diff --git a/src/main/kotlin/html5up/forty/common.kt b/src/main/kotlin/html5up/forty/common.kt
index e0378ad..753294f 100644
--- a/src/main/kotlin/html5up/forty/common.kt
+++ b/src/main/kotlin/html5up/forty/common.kt
@@ -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")
}
diff --git a/src/main/kotlin/html5up/forty/landing.kt b/src/main/kotlin/html5up/forty/landing.kt
index 1974c49..6bd9f8d 100644
--- a/src/main/kotlin/html5up/forty/landing.kt
+++ b/src/main/kotlin/html5up/forty/landing.kt
@@ -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 {
}
diff --git a/src/main/kotlin/html5up/forty/page.kt b/src/main/kotlin/html5up/forty/page.kt
index 6dcc315..d7986df 100644
--- a/src/main/kotlin/html5up/forty/page.kt
+++ b/src/main/kotlin/html5up/forty/page.kt
@@ -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 {
}
diff --git a/src/main/kotlin/ru/mipt/spc/Application.kt b/src/main/kotlin/ru/mipt/spc/Application.kt
index 84b3fe8..cdb0feb 100644
--- a/src/main/kotlin/ru/mipt/spc/Application.kt
+++ b/src/main/kotlin/ru/mipt/spc/Application.kt
@@ -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"
diff --git a/src/main/kotlin/ru/mipt/spc/master.kt b/src/main/kotlin/ru/mipt/spc/master.kt
index dc678af..77b32e2 100644
--- a/src/main/kotlin/ru/mipt/spc/master.kt
+++ b/src/main/kotlin/ru/mipt/spc/master.kt
@@ -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(PARTNERS_PATH)?.meta ?: Meta.EMPTY
val partnersData: Meta = runBlocking { data.getByType(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(
- 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,58 +356,58 @@ 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()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/ru/mipt/spc/spcCollection.kt b/src/main/kotlin/ru/mipt/spc/spcCollection.kt
index 3b28e2a..74518c0 100644
--- a/src/main/kotlin/ru/mipt/spc/spcCollection.kt
+++ b/src/main/kotlin/ru/mipt/spc/spcCollection.kt
@@ -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,
) {
@@ -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)
}
diff --git a/src/main/kotlin/ru/mipt/spc/spcHome.kt b/src/main/kotlin/ru/mipt/spc/spcHome.kt
index 3391f87..b3c89f4 100644
--- a/src/main/kotlin/ru/mipt/spc/spcHome.kt
+++ b/src/main/kotlin/ru/mipt/spc/spcHome.kt
@@ -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"))
diff --git a/src/main/kotlin/ru/mipt/spc/spcMisc.kt b/src/main/kotlin/ru/mipt/spc/spcMisc.kt
index ea98dd8..ffb9a10 100644
--- a/src/main/kotlin/ru/mipt/spc/spcMisc.kt
+++ b/src/main/kotlin/ru/mipt/spc/spcMisc.kt
@@ -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
diff --git a/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt b/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt
index 9f3bb84..f435855 100644
--- a/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt
+++ b/src/main/kotlin/space/kscience/snark/KtorSiteBuilder.kt
@@ -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)
}
\ No newline at end of file
diff --git a/src/main/kotlin/space/kscience/snark/PageBuilder.kt b/src/main/kotlin/space/kscience/snark/PageBuilder.kt
new file mode 100644
index 0000000..71333a4
--- /dev/null
+++ b/src/main/kotlin/space/kscience/snark/PageBuilder.kt
@@ -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(name) ?: getByType(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 =
+ filterByType { 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"
\ No newline at end of file
diff --git a/src/main/kotlin/space/kscience/snark/SiteBuilder.kt b/src/main/kotlin/space/kscience/snark/SiteBuilder.kt
index 7586e8e..b1bcf5c 100644
--- a/src/main/kotlin/space/kscience/snark/SiteBuilder.kt
+++ b/src/main/kotlin/space/kscience/snark/SiteBuilder.kt
@@ -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 {
+ override val items: Map> get() = emptyMap()
+ override val dataType: KType get() = typeOf()
+ override val meta: Meta get() = meta
+}
\ No newline at end of file
diff --git a/src/main/kotlin/space/kscience/snark/SiteData.kt b/src/main/kotlin/space/kscience/snark/SiteData.kt
deleted file mode 100644
index 981ca2d..0000000
--- a/src/main/kotlin/space/kscience/snark/SiteData.kt
+++ /dev/null
@@ -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 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 {
- override val items: Map> get() = emptyMap()
- override val dataType: KType get() = typeOf()
- 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(name) ?: getByType(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 =
- filterByType { 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 = readDirectory(path)
-//
-// return readData(parsedData, rootUrl)
-//}
diff --git a/src/main/kotlin/space/kscience/snark/SiteLayout.kt b/src/main/kotlin/space/kscience/snark/SiteLayout.kt
index cf82290..9062a78 100644
--- a/src/main/kotlin/space/kscience/snark/SiteLayout.kt
+++ b/src/main/kotlin/space/kscience/snark/SiteLayout.kt
@@ -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 -> {
diff --git a/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt b/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt
index c580fa1..15e252d 100644
--- a/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt
+++ b/src/main/kotlin/space/kscience/snark/StaticSiteBuilder.kt
@@ -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)
-
+ 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())
+ )
}
-fun SnarkPlugin.static(path: Path, block: SiteBuilder.() -> Unit) {
- val base = SiteData.empty(
- this,
- baseUrlPath = path.absolutePathString(),
- meta = Meta {
- "pageSuffix" put ".html"
- }
- )
- StaticSiteBuilder(base, path).block()
+fun SnarkPlugin.static(outputPath: Path, data: DataTree<*> = DataTree.empty(), block: SiteBuilder.() -> Unit) {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
+ StaticSiteBuilder(this, data, meta, "", outputPath).block()
}
\ No newline at end of file