Complete refactor to new routing API
This commit is contained in:
parent
43bf8e8e96
commit
c15a0ea948
@ -1,8 +1,7 @@
|
|||||||
package html5up.forty
|
package html5up.forty
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteData
|
import space.kscience.snark.PageBuilder
|
||||||
import space.kscience.snark.resolveRef
|
|
||||||
|
|
||||||
|
|
||||||
internal fun FlowContent.fortyMenu() {
|
internal fun FlowContent.fortyMenu() {
|
||||||
@ -201,7 +200,7 @@ internal fun FlowContent.fortyFooter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) internal fun BODY.fortyScripts() {
|
context(PageBuilder) internal fun BODY.fortyScripts() {
|
||||||
script {
|
script {
|
||||||
src = resolveRef("assets/js/jquery.min.js")
|
src = resolveRef("assets/js/jquery.min.js")
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package html5up.forty
|
package html5up.forty
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteData
|
import space.kscience.snark.PageBuilder
|
||||||
|
|
||||||
context(SiteData) internal fun HTML.landing(){
|
context(PageBuilder) internal fun HTML.landing(){
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package html5up.forty
|
package html5up.forty
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteData
|
import space.kscience.snark.PageBuilder
|
||||||
import space.kscience.snark.resolveRef
|
|
||||||
|
|
||||||
context(SiteData) internal fun HTML.fortyPage(){
|
context(PageBuilder) internal fun HTML.fortyPage(){
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
package ru.mipt.spc
|
package ru.mipt.spc
|
||||||
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.application.log
|
import io.ktor.server.application.log
|
||||||
import io.ktor.server.plugins.httpsredirect.HttpsRedirect
|
|
||||||
import kotlinx.css.CssBuilder
|
import kotlinx.css.CssBuilder
|
||||||
import kotlinx.html.CommonAttributeGroupFacade
|
import kotlinx.html.CommonAttributeGroupFacade
|
||||||
import kotlinx.html.style
|
import kotlinx.html.style
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.fetch
|
import space.kscience.dataforge.context.fetch
|
||||||
import space.kscience.snark.SnarkPlugin
|
import space.kscience.snark.SnarkPlugin
|
||||||
import space.kscience.snark.site
|
import space.kscience.snark.snarkSite
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@ -49,7 +47,7 @@ const val BUILD_DATE_FILE = "/buildDate"
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun Application.spcModule() {
|
fun Application.spcModule() {
|
||||||
install(HttpsRedirect)
|
// install(HttpsRedirect)
|
||||||
|
|
||||||
val context = Context("spc-site") {
|
val context = Context("spc-site") {
|
||||||
plugin(SnarkPlugin)
|
plugin(SnarkPlugin)
|
||||||
@ -65,7 +63,7 @@ fun Application.spcModule() {
|
|||||||
|
|
||||||
val inProduction: Boolean = environment.config.propertyOrNull("ktor.environment.production") != null
|
val inProduction: Boolean = environment.config.propertyOrNull("ktor.environment.production") != null
|
||||||
|
|
||||||
if(inProduction){
|
if (inProduction) {
|
||||||
log.info("Production mode activated")
|
log.info("Production mode activated")
|
||||||
log.info("Build date: $buildDate")
|
log.info("Build date: $buildDate")
|
||||||
log.info("Deploy date: $deployDate")
|
log.info("Deploy date: $deployDate")
|
||||||
@ -90,7 +88,7 @@ fun Application.spcModule() {
|
|||||||
dataPath.resolve(DEPLOY_DATE_FILE).writeText(date)
|
dataPath.resolve(DEPLOY_DATE_FILE).writeText(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
snark.site {
|
snarkSite(snark) {
|
||||||
val homeDataPath = resolveData(
|
val homeDataPath = resolveData(
|
||||||
this@spcModule.javaClass.getResource("/home")!!.toURI(),
|
this@spcModule.javaClass.getResource("/home")!!.toURI(),
|
||||||
dataPath / "home"
|
dataPath / "home"
|
||||||
|
@ -34,14 +34,14 @@ import kotlin.collections.set
|
|||||||
private val HtmlData.imagePath: String? get() = meta["image"]?.string ?: meta["image.path"].string
|
private val HtmlData.imagePath: String? get() = meta["image"]?.string ?: meta["image.path"].string
|
||||||
private val HtmlData.name: String get() = meta["name"].string ?: error("Name not found")
|
private val HtmlData.name: String get() = meta["name"].string ?: error("Name not found")
|
||||||
|
|
||||||
class MagProgSection(
|
context(PageBuilder) class MagProgSection(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val style: String,
|
val style: String,
|
||||||
val content: FlowContent.() -> Unit,
|
val content: FlowContent.() -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun wrapSection(
|
context(PageBuilder) private fun wrapSection(
|
||||||
id: String,
|
id: String,
|
||||||
title: String,
|
title: String,
|
||||||
sectionContent: FlowContent.() -> Unit,
|
sectionContent: FlowContent.() -> Unit,
|
||||||
@ -52,7 +52,7 @@ private fun wrapSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun wrapSection(
|
context(PageBuilder) private fun wrapSection(
|
||||||
block: HtmlData,
|
block: HtmlData,
|
||||||
idOverride: String? = null,
|
idOverride: String? = null,
|
||||||
): MagProgSection = wrapSection(
|
): MagProgSection = wrapSection(
|
||||||
@ -70,9 +70,9 @@ private val PROGRAM_PATH: Name = CONTENT_NODE_NAME + "program"
|
|||||||
private val RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses"
|
private val RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses"
|
||||||
private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners"
|
private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners"
|
||||||
|
|
||||||
context(SiteData) private fun FlowContent.programSection() {
|
context(PageBuilder) private fun FlowContent.programSection() {
|
||||||
val programBlock = resolveHtml(PROGRAM_PATH)!!
|
val programBlock = data.resolveHtml(PROGRAM_PATH)!!
|
||||||
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!!
|
val recommendedBlock = data.resolveHtml(RECOMMENDED_COURSES_PATH)!!
|
||||||
div("inner") {
|
div("inner") {
|
||||||
h2 { +"Учебная программа" }
|
h2 { +"Учебная программа" }
|
||||||
htmlData(programBlock)
|
htmlData(programBlock)
|
||||||
@ -87,7 +87,7 @@ context(SiteData) private fun FlowContent.programSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) private fun FlowContent.partners() {
|
context(PageBuilder) private fun FlowContent.partners() {
|
||||||
//val partnersData: Meta = resolve<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
|
//val partnersData: Meta = resolve<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
|
||||||
val partnersData: Meta = runBlocking { data.getByType<Meta>(PARTNERS_PATH)?.await() } ?: Meta.EMPTY
|
val partnersData: Meta = runBlocking { data.getByType<Meta>(PARTNERS_PATH)?.await() } ?: Meta.EMPTY
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -117,8 +117,8 @@ context(SiteData) private fun FlowContent.partners() {
|
|||||||
// val photo: String? by meta.string()
|
// val photo: String? by meta.string()
|
||||||
//}
|
//}
|
||||||
|
|
||||||
context(SiteData) private fun FlowContent.team() {
|
context(PageBuilder) private fun FlowContent.team() {
|
||||||
val team = findByType("magprog_team").values.sortedBy { it.order }
|
val team = data.findByContentType("magprog_team").values.sortedBy { it.order }
|
||||||
|
|
||||||
div("inner") {
|
div("inner") {
|
||||||
h2 { +"Команда" }
|
h2 { +"Команда" }
|
||||||
@ -172,8 +172,8 @@ context(SiteData) private fun FlowContent.team() {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) private fun FlowContent.mentors() {
|
context(PageBuilder) private fun FlowContent.mentors() {
|
||||||
val mentors = findByType("magprog_mentor").entries.sortedBy { it.value.id }
|
val mentors = data.findByContentType("magprog_mentor").entries.sortedBy { it.value.id }
|
||||||
|
|
||||||
div("inner") {
|
div("inner") {
|
||||||
h2 {
|
h2 {
|
||||||
@ -183,7 +183,7 @@ context(SiteData) private fun FlowContent.mentors() {
|
|||||||
mentors.forEach { (name, mentor) ->
|
mentors.forEach { (name, mentor) ->
|
||||||
section {
|
section {
|
||||||
id = mentor.id
|
id = mentor.id
|
||||||
val ref = resolvePage("mentor-${mentor.id}")
|
val ref = resolvePageRef("mentor-${mentor.id}")
|
||||||
a(classes = "image", href = ref) {
|
a(classes = "image", href = ref) {
|
||||||
mentor.imagePath?.let { photoPath ->
|
mentor.imagePath?.let { photoPath ->
|
||||||
img(
|
img(
|
||||||
@ -200,7 +200,7 @@ context(SiteData) private fun FlowContent.mentors() {
|
|||||||
h2 {
|
h2 {
|
||||||
a(href = ref) { +mentor.name }
|
a(href = ref) { +mentor.name }
|
||||||
}
|
}
|
||||||
val info = resolveHtml(name.withIndex("info"))
|
val info = data.resolveHtml(name.withIndex("info"))
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
htmlData(info)
|
htmlData(info)
|
||||||
}
|
}
|
||||||
@ -210,7 +210,7 @@ context(SiteData) private fun FlowContent.mentors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) internal fun HTML.magProgHead(title: String) {
|
context(PageBuilder) internal fun HTML.magProgHead(title: String) {
|
||||||
head {
|
head {
|
||||||
this.title = title
|
this.title = title
|
||||||
meta {
|
meta {
|
||||||
@ -237,7 +237,7 @@ context(SiteData) internal fun HTML.magProgHead(title: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) internal fun BODY.magProgFooter() {
|
context(PageBuilder) internal fun BODY.magProgFooter() {
|
||||||
footer("wrapper style1-alt") {
|
footer("wrapper style1-alt") {
|
||||||
id = "footer"
|
id = "footer"
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -282,13 +282,13 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
|
|||||||
|
|
||||||
val magProgSiteContext = snark.readDirectory(dataPath.resolve("content"))
|
val magProgSiteContext = snark.readDirectory(dataPath.resolve("content"))
|
||||||
|
|
||||||
mountSite(prefix, magProgSiteContext) {
|
route(prefix, magProgSiteContext, setAsRoot = true) {
|
||||||
assetDirectory("assets", dataPath.resolve("assets"))
|
assetDirectory("assets", dataPath.resolve("assets"))
|
||||||
assetDirectory("images", dataPath.resolve("images"))
|
assetDirectory("images", dataPath.resolve("images"))
|
||||||
|
|
||||||
page {
|
page {
|
||||||
val sections = listOf<MagProgSection>(
|
val sections = listOf<MagProgSection>(
|
||||||
wrapSection(resolveHtml(INTRO_PATH)!!, "intro"),
|
wrapSection(data.resolveHtml(INTRO_PATH)!!, "intro"),
|
||||||
MagProgSection(
|
MagProgSection(
|
||||||
id = "partners",
|
id = "partners",
|
||||||
title = "Партнеры",
|
title = "Партнеры",
|
||||||
@ -311,12 +311,13 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
|
|||||||
) {
|
) {
|
||||||
programSection()
|
programSection()
|
||||||
},
|
},
|
||||||
wrapSection(resolveHtml(ENROLL_PATH)!!, "enroll"),
|
wrapSection(data.resolveHtml(ENROLL_PATH)!!, "enroll"),
|
||||||
wrapSection(id = "contacts", title = "Контакты") {
|
wrapSection(id = "contacts", title = "Контакты") {
|
||||||
htmlData(resolveHtml(CONTACTS_PATH)!!)
|
htmlData(data.resolveHtml(CONTACTS_PATH)!!)
|
||||||
team()
|
team()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
magProgHead("Магистратура \"Научное программирование\"")
|
magProgHead("Магистратура \"Научное программирование\"")
|
||||||
body("is-preload magprog-body") {
|
body("is-preload magprog-body") {
|
||||||
section {
|
section {
|
||||||
@ -355,57 +356,57 @@ internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asNa
|
|||||||
magProgFooter()
|
magProgFooter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val mentors = data.findByType("magprog_mentor").values.sortedBy {
|
val mentors = data.findByContentType("magprog_mentor").values.sortedBy {
|
||||||
it.order
|
it.order
|
||||||
}
|
}
|
||||||
|
|
||||||
mentors.forEach { mentor ->
|
mentors.forEach { mentor ->
|
||||||
page(mentor.mentorPageId.asName()) {
|
page(mentor.mentorPageId.asName()) {
|
||||||
|
|
||||||
magProgHead("Научное программирование: ${mentor.name}")
|
magProgHead("Научное программирование: ${mentor.name}")
|
||||||
body("is-preload") {
|
body("is-preload") {
|
||||||
header {
|
header {
|
||||||
id = "header"
|
id = "header"
|
||||||
a(classes = "title") {
|
a(classes = "title") {
|
||||||
href = "$homeRef#mentors"
|
href = "$homeRef#mentors"
|
||||||
+"Научные руководители"
|
+"Научные руководители"
|
||||||
}
|
}
|
||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
mentors.forEach {
|
mentors.forEach {
|
||||||
li {
|
li {
|
||||||
a {
|
a {
|
||||||
href = resolvePage(it.mentorPageId)
|
href = resolvePageRef(it.mentorPageId)
|
||||||
+it.name.substringAfterLast(" ")
|
+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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import kotlin.collections.component1
|
|||||||
import kotlin.collections.component2
|
import kotlin.collections.component2
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
context(SiteData, FlowContent) private fun spcSpotlightContent(
|
context(PageBuilder) private fun FlowContent.spcSpotlightContent(
|
||||||
landing: HtmlData,
|
landing: HtmlData,
|
||||||
content: Map<Name, HtmlData>,
|
content: Map<Name, HtmlData>,
|
||||||
) {
|
) {
|
||||||
@ -44,11 +44,11 @@ context(SiteData, FlowContent) private fun spcSpotlightContent(
|
|||||||
id = "main"
|
id = "main"
|
||||||
//TODO add smart SNARK ordering
|
//TODO add smart SNARK ordering
|
||||||
section("spotlights") {
|
section("spotlights") {
|
||||||
content.entries.sortedBy { it.value.meta["order"].int ?: Int.MAX_VALUE }.forEach { (name, data) ->
|
content.entries.sortedBy { it.value.meta["order"].int ?: Int.MAX_VALUE }.forEach { (name, entry) ->
|
||||||
val ref = resolvePage(name)
|
val ref = resolvePageRef(name)
|
||||||
section {
|
section {
|
||||||
id = data.meta["id"].string ?: name.toString()
|
id = entry.meta["id"].string ?: name.toString()
|
||||||
data.meta["image"]?.let { imageMeta: Meta ->
|
entry.meta["image"]?.let { imageMeta: Meta ->
|
||||||
val imagePath =
|
val imagePath =
|
||||||
imageMeta.value?.string ?: imageMeta["path"].string ?: error("Image path not provided")
|
imageMeta.value?.string ?: imageMeta["path"].string ?: error("Image path not provided")
|
||||||
a(classes = "image") {
|
a(classes = "image") {
|
||||||
@ -63,11 +63,11 @@ context(SiteData, FlowContent) private fun spcSpotlightContent(
|
|||||||
div("content") {
|
div("content") {
|
||||||
div("inner") {
|
div("inner") {
|
||||||
header("major") {
|
header("major") {
|
||||||
h3 { +(data.meta["title"].string ?: "???") }
|
h3 { +(entry.meta["title"].string ?: "???") }
|
||||||
}
|
}
|
||||||
val infoData = resolveHtml(name.withIndex("info"))
|
val infoData = data.resolveHtml(name.withIndex("info"))
|
||||||
if (infoData == null) {
|
if (infoData == null) {
|
||||||
htmlData(data)
|
htmlData(entry)
|
||||||
} else {
|
} else {
|
||||||
htmlData(infoData)
|
htmlData(infoData)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import java.nio.file.Path
|
|||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
|
||||||
context(SiteData) internal fun HTML.spcPageContent(
|
context(PageBuilder) internal fun HTML.spcPageContent(
|
||||||
meta: Meta,
|
meta: Meta,
|
||||||
title: String = meta["title"].string ?: SPC_TITLE,
|
title: String = meta["title"].string ?: SPC_TITLE,
|
||||||
fragment: FlowContent.() -> Unit,
|
fragment: FlowContent.() -> Unit,
|
||||||
@ -65,7 +65,7 @@ internal val FortyDataRenderer: SiteBuilder.(Data<*>) -> Unit = { data ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
context(SiteData, HTML) private fun spcHome() {
|
context(PageBuilder) private fun HTML.spcHome() {
|
||||||
spcHead()
|
spcHead()
|
||||||
body("is-preload") {
|
body("is-preload") {
|
||||||
wrapper {
|
wrapper {
|
||||||
@ -150,7 +150,7 @@ context(SiteData, HTML) private fun spcHome() {
|
|||||||
header("major") {
|
header("major") {
|
||||||
h3 {
|
h3 {
|
||||||
a(classes = "link") {
|
a(classes = "link") {
|
||||||
href = resolvePage("magprog")
|
href = resolvePageRef("magprog")
|
||||||
+"""Master's program"""
|
+"""Master's program"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,7 +167,7 @@ context(SiteData, HTML) private fun spcHome() {
|
|||||||
header("major") {
|
header("major") {
|
||||||
h3 {
|
h3 {
|
||||||
a(classes = "link") {
|
a(classes = "link") {
|
||||||
href = resolvePage("research")
|
href = resolvePageRef("research")
|
||||||
+"""Research"""
|
+"""Research"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ context(SiteData, HTML) private fun spcHome() {
|
|||||||
header("major") {
|
header("major") {
|
||||||
h3 {
|
h3 {
|
||||||
a(classes = "link") {
|
a(classes = "link") {
|
||||||
href = resolvePage("consulting")
|
href = resolvePageRef("consulting")
|
||||||
+"""Consulting"""
|
+"""Consulting"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,7 +203,7 @@ context(SiteData, HTML) private fun spcHome() {
|
|||||||
header("major") {
|
header("major") {
|
||||||
h3 {
|
h3 {
|
||||||
a(classes = "link") {
|
a(classes = "link") {
|
||||||
href = resolvePage("team")
|
href = resolvePageRef("team")
|
||||||
+"""Team"""
|
+"""Team"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,7 +256,7 @@ internal fun SiteBuilder.spcHome(rootPath: Path, prefix: Name = Name.EMPTY) {
|
|||||||
|
|
||||||
val homePageData = snark.readDirectory(rootPath.resolve("content"))
|
val homePageData = snark.readDirectory(rootPath.resolve("content"))
|
||||||
|
|
||||||
mountSite(prefix, homePageData) {
|
route(prefix, homePageData, setAsRoot = true) {
|
||||||
assetDirectory("assets", rootPath.resolve("assets"))
|
assetDirectory("assets", rootPath.resolve("assets"))
|
||||||
assetDirectory("images", rootPath.resolve("images"))
|
assetDirectory("images", rootPath.resolve("images"))
|
||||||
|
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package ru.mipt.spc
|
package ru.mipt.spc
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import space.kscience.snark.SiteData
|
import space.kscience.snark.PageBuilder
|
||||||
import space.kscience.snark.homeRef
|
import space.kscience.snark.homeRef
|
||||||
import space.kscience.snark.resolvePage
|
import space.kscience.snark.resolvePageRef
|
||||||
import space.kscience.snark.resolveRef
|
|
||||||
|
|
||||||
|
|
||||||
internal const val SPC_TITLE = "Scientific Programming Centre"
|
internal const val SPC_TITLE = "Scientific Programming Centre"
|
||||||
|
|
||||||
context(SiteData) internal fun HTML.spcHead(title: String = SPC_TITLE) {
|
context(PageBuilder) internal fun HTML.spcHead(title: String = SPC_TITLE) {
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
+title
|
+title
|
||||||
@ -28,7 +27,7 @@ context(SiteData) internal fun HTML.spcHead(title: String = SPC_TITLE) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) internal fun FlowContent.spcHomeMenu() {
|
context(PageBuilder) internal fun FlowContent.spcHomeMenu() {
|
||||||
nav {
|
nav {
|
||||||
id = "menu"
|
id = "menu"
|
||||||
ul("links") {
|
ul("links") {
|
||||||
@ -40,25 +39,25 @@ context(SiteData) internal fun FlowContent.spcHomeMenu() {
|
|||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a {
|
a {
|
||||||
href = resolvePage("magprog")
|
href = resolvePageRef("magprog")
|
||||||
+"""Master"""
|
+"""Master"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a {
|
a {
|
||||||
href = resolvePage("research")
|
href = resolvePageRef("research")
|
||||||
+"""Research"""
|
+"""Research"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a {
|
a {
|
||||||
href = resolvePage("consulting")
|
href = resolvePageRef("consulting")
|
||||||
+"""Consulting"""
|
+"""Consulting"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a {
|
a {
|
||||||
href = resolvePage("team")
|
href = resolvePageRef("team")
|
||||||
+"""Team"""
|
+"""Team"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +79,7 @@ context(SiteData) internal fun FlowContent.spcHomeMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) internal fun FlowContent.spcFooter() {
|
context(PageBuilder) internal fun FlowContent.spcFooter() {
|
||||||
footer {
|
footer {
|
||||||
id = "footer"
|
id = "footer"
|
||||||
div("inner") {
|
div("inner") {
|
||||||
@ -130,7 +129,7 @@ context(SiteData) internal fun FlowContent.spcFooter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SiteData) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) {
|
context(PageBuilder) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) {
|
||||||
div {
|
div {
|
||||||
id = "wrapper"
|
id = "wrapper"
|
||||||
// Header
|
// Header
|
||||||
|
@ -7,7 +7,6 @@ import io.ktor.server.application.call
|
|||||||
import io.ktor.server.html.respondHtml
|
import io.ktor.server.html.respondHtml
|
||||||
import io.ktor.server.http.content.*
|
import io.ktor.server.http.content.*
|
||||||
import io.ktor.server.plugins.origin
|
import io.ktor.server.plugins.origin
|
||||||
import io.ktor.server.request.ApplicationRequest
|
|
||||||
import io.ktor.server.request.host
|
import io.ktor.server.request.host
|
||||||
import io.ktor.server.request.port
|
import io.ktor.server.request.port
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
@ -15,10 +14,23 @@ import io.ktor.server.routing.createRouteFromPath
|
|||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
import kotlinx.html.HTML
|
import kotlinx.html.HTML
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.data.DataTree
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.withDefault
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route) : SiteBuilder {
|
@PublishedApi
|
||||||
|
internal class KtorSiteBuilder(
|
||||||
|
override val snark: SnarkPlugin,
|
||||||
|
override val data: DataTree<*>,
|
||||||
|
override val meta: Meta,
|
||||||
|
private val baseUrl: String,
|
||||||
|
private val ktorRoute: Route,
|
||||||
|
) : SiteBuilder {
|
||||||
|
|
||||||
override fun assetFile(remotePath: String, file: Path) {
|
override fun assetFile(remotePath: String, file: Path) {
|
||||||
ktorRoute.file(remotePath, file.toFile())
|
ktorRoute.file(remotePath, file.toFile())
|
||||||
@ -30,17 +42,57 @@ class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun page(route: Name, content: context(SiteData, HTML)() -> Unit) {
|
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
|
||||||
|
ref
|
||||||
|
} else {
|
||||||
|
"${baseUrl.removeSuffix("/")}/$ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class KtorPageBuilder(
|
||||||
|
val pageBaseUrl: String,
|
||||||
|
override val meta: Meta = this@KtorSiteBuilder.meta,
|
||||||
|
) : PageBuilder {
|
||||||
|
override val context: Context get() = this@KtorSiteBuilder.context
|
||||||
|
override val data: DataTree<*> get() = this@KtorSiteBuilder.data
|
||||||
|
|
||||||
|
override fun resolveRef(ref: String): String = resolveRef(pageBaseUrl, ref)
|
||||||
|
|
||||||
|
override fun resolvePageRef(pageName: Name): String = resolveRef(pageName.tokens.joinToString(separator = "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun page(route: Name, content: context(PageBuilder, HTML)() -> Unit) {
|
||||||
ktorRoute.get(route.toWebPath()) {
|
ktorRoute.get(route.toWebPath()) {
|
||||||
call.respondHtml {
|
call.respondHtml {
|
||||||
val dataWithUrl = data.copyWithRequestHost(call.request)
|
val request = call.request
|
||||||
content(dataWithUrl, this)
|
//substitute host for url for backwards calls
|
||||||
|
val url = URLBuilder(baseUrl).apply {
|
||||||
|
protocol = URLProtocol.createOrDefault(request.origin.scheme)
|
||||||
|
host = request.host()
|
||||||
|
port = request.port()
|
||||||
|
}
|
||||||
|
val pageBuilder = KtorPageBuilder(url.buildString())
|
||||||
|
content(pageBuilder, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun route(subRoute: Name): SiteBuilder =
|
override fun route(
|
||||||
KtorSiteBuilder(data, ktorRoute.createRouteFromPath(subRoute.toWebPath()))
|
routeName: Name,
|
||||||
|
dataOverride: DataTree<*>?,
|
||||||
|
metaOverride: Meta?,
|
||||||
|
setAsRoot: Boolean,
|
||||||
|
): SiteBuilder = KtorSiteBuilder(
|
||||||
|
snark = snark,
|
||||||
|
data = dataOverride ?: data,
|
||||||
|
meta = metaOverride?.withDefault(meta) ?: meta,
|
||||||
|
baseUrl = if (setAsRoot) {
|
||||||
|
resolveRef(baseUrl, routeName.toWebPath())
|
||||||
|
} else {
|
||||||
|
baseUrl
|
||||||
|
},
|
||||||
|
ktorRoute = ktorRoute.createRouteFromPath(routeName.toWebPath())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
||||||
ktorRoute.resource(resourcesPath, resourcesPath)
|
ktorRoute.resource(resourcesPath, resourcesPath)
|
||||||
@ -49,39 +101,27 @@ class KtorSiteBuilder(override val data: SiteData, private val ktorRoute: Route)
|
|||||||
override fun assetResourceDirectory(resourcesPath: String) {
|
override fun assetResourceDirectory(resourcesPath: String) {
|
||||||
ktorRoute.resources(resourcesPath)
|
ktorRoute.resources(resourcesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun withData(newData: SiteData): SiteBuilder = KtorSiteBuilder(newData, ktorRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PublishedApi
|
|
||||||
internal fun SiteData.copyWithRequestHost(request: ApplicationRequest): SiteData {
|
|
||||||
val uri = URLBuilder(
|
|
||||||
protocol = URLProtocol.createOrDefault(request.origin.scheme),
|
|
||||||
host = request.host(),
|
|
||||||
port = request.port(),
|
|
||||||
pathSegments = baseUrlPath.split("/"),
|
|
||||||
)
|
|
||||||
return copy(baseUrlPath = uri.buildString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun Route.snarkSite(
|
inline fun Route.snarkSite(
|
||||||
data: SiteData,
|
snark: SnarkPlugin,
|
||||||
|
data: DataTree<*>,
|
||||||
|
meta: Meta = data.meta,
|
||||||
block: SiteBuilder.() -> Unit,
|
block: SiteBuilder.() -> Unit,
|
||||||
) {
|
) {
|
||||||
block(KtorSiteBuilder(data, this@snarkSite))
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Application.snarkSite(
|
fun Application.snarkSite(
|
||||||
data: SiteData,
|
snark: SnarkPlugin,
|
||||||
|
data: DataTree<*> = DataTree.empty(),
|
||||||
|
meta: Meta = data.meta,
|
||||||
block: SiteBuilder.() -> Unit,
|
block: SiteBuilder.() -> Unit,
|
||||||
) {
|
) {
|
||||||
routing {
|
routing {
|
||||||
snarkSite(data, block)
|
snarkSite(snark, data, meta, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context (Application) fun SnarkPlugin.site(
|
|
||||||
block: SiteBuilder.() -> Unit,
|
|
||||||
) {
|
|
||||||
snarkSite(SiteData.empty(this), block)
|
|
||||||
}
|
|
56
src/main/kotlin/space/kscience/snark/PageBuilder.kt
Normal file
56
src/main/kotlin/space/kscience/snark/PageBuilder.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package space.kscience.snark
|
||||||
|
|
||||||
|
import space.kscience.dataforge.context.ContextAware
|
||||||
|
import space.kscience.dataforge.data.*
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.meta.string
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.parseAsName
|
||||||
|
import space.kscience.dataforge.names.plus
|
||||||
|
import space.kscience.dataforge.names.startsWith
|
||||||
|
|
||||||
|
internal fun Name.toWebPath() = tokens.joinToString(separator = "/")
|
||||||
|
|
||||||
|
interface PageBuilder : ContextAware {
|
||||||
|
val data: DataTree<*>
|
||||||
|
|
||||||
|
val meta: Meta
|
||||||
|
|
||||||
|
fun resolveRef(ref: String): String
|
||||||
|
|
||||||
|
fun resolvePageRef(pageName: Name): String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun PageBuilder.resolvePageRef(pageName: String) = resolvePageRef(pageName.parseAsName())
|
||||||
|
|
||||||
|
val PageBuilder.homeRef get() = resolvePageRef(Name.EMPTY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a Html builder by its full name
|
||||||
|
*/
|
||||||
|
fun DataTree<*>.resolveHtml(name: Name): HtmlData? {
|
||||||
|
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + SiteBuilder.INDEX_PAGE_TOKEN))
|
||||||
|
|
||||||
|
return resolved?.takeIf {
|
||||||
|
it.published //TODO add language confirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all Html blocks using given name/meta filter
|
||||||
|
*/
|
||||||
|
fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
|
||||||
|
filterByType<HtmlFragment> { name, meta ->
|
||||||
|
predicate(name, meta)
|
||||||
|
&& meta["published"].string != "false"
|
||||||
|
//TODO add language confirmation
|
||||||
|
}.asSequence().associate { it.name to it.data }
|
||||||
|
|
||||||
|
|
||||||
|
fun DataTree<*>.findByContentType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
|
||||||
|
name.startsWith(baseName) && meta["content_type"].string == contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Data<*>.published: Boolean get() = meta["published"].string != "false"
|
@ -4,21 +4,28 @@ import kotlinx.html.HTML
|
|||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.data.DataTree
|
import space.kscience.dataforge.data.DataTree
|
||||||
import space.kscience.dataforge.meta.Laminate
|
import space.kscience.dataforge.data.DataTreeItem
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.NameToken
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
internal fun Name.toWebPath() = tokens.joinToString(separator = "/")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstraction, which is used to render sites to the different rendering engines
|
* An abstraction, which is used to render sites to the different rendering engines
|
||||||
*/
|
*/
|
||||||
interface SiteBuilder : ContextAware {
|
interface SiteBuilder : ContextAware {
|
||||||
|
|
||||||
val data: SiteData
|
val data: DataTree<*>
|
||||||
|
|
||||||
override val context: Context get() = data.context
|
val snark: SnarkPlugin
|
||||||
|
|
||||||
|
override val context: Context get() = snark.context
|
||||||
|
|
||||||
|
val meta: Meta
|
||||||
|
|
||||||
fun assetFile(remotePath: String, file: Path)
|
fun assetFile(remotePath: String, file: Path)
|
||||||
|
|
||||||
@ -28,39 +35,62 @@ interface SiteBuilder : ContextAware {
|
|||||||
|
|
||||||
fun assetResourceDirectory(resourcesPath: String)
|
fun assetResourceDirectory(resourcesPath: String)
|
||||||
|
|
||||||
fun page(route: Name = Name.EMPTY, content: context(SiteData, HTML) () -> Unit)
|
fun page(route: Name = Name.EMPTY, content: context(PageBuilder, HTML) () -> Unit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new branch builder with replaced [data]
|
* Create a route with optional data tree override. For example one could use a subtree of the initial tree.
|
||||||
|
* By default, the same data tree is used for route
|
||||||
*/
|
*/
|
||||||
fun withData(newData: SiteData): SiteBuilder
|
fun route(
|
||||||
|
routeName: Name,
|
||||||
|
dataOverride: DataTree<*>? = null,
|
||||||
|
metaOverride: Meta? = null,
|
||||||
|
setAsRoot: Boolean = false,
|
||||||
|
): SiteBuilder
|
||||||
|
|
||||||
/**
|
companion object {
|
||||||
* Create a route
|
val INDEX_PAGE_TOKEN: NameToken = NameToken("index")
|
||||||
*/
|
|
||||||
fun route(subRoute: Name): SiteBuilder
|
|
||||||
}
|
|
||||||
|
|
||||||
val SiteBuilder.snark get() = data.snark
|
|
||||||
|
|
||||||
public inline fun SiteBuilder.route(route: Name, block: SiteBuilder.() -> Unit) {
|
|
||||||
route(route).apply(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
public inline fun SiteBuilder.route(route: String, block: SiteBuilder.() -> Unit) {
|
|
||||||
route(route.parseAsName()).apply(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a stand-alone site at a given node
|
|
||||||
*/
|
|
||||||
public fun SiteBuilder.mountSite(route: Name, dataRoot: DataTree<*>, block: SiteBuilder.() -> Unit) {
|
|
||||||
val mountedData = data.copy(
|
|
||||||
data = dataRoot,
|
|
||||||
baseUrlPath = data.resolveRef(route.tokens.joinToString(separator = "/")),
|
|
||||||
meta = Laminate(dataRoot.meta, data.meta) //layering dataRoot meta over existing data
|
|
||||||
)
|
|
||||||
route(route) {
|
|
||||||
withData(mountedData).block()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public inline fun SiteBuilder.route(
|
||||||
|
route: Name,
|
||||||
|
dataOverride: DataTree<*>? = null,
|
||||||
|
metaOverride: Meta? = null,
|
||||||
|
setAsRoot: Boolean = false,
|
||||||
|
block: SiteBuilder.() -> Unit,
|
||||||
|
) {
|
||||||
|
route(route, dataOverride, metaOverride, setAsRoot).apply(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
public inline fun SiteBuilder.route(
|
||||||
|
route: String,
|
||||||
|
dataOverride: DataTree<*>? = null,
|
||||||
|
metaOverride: Meta? = null,
|
||||||
|
setAsRoot: Boolean = false,
|
||||||
|
block: SiteBuilder.() -> Unit,
|
||||||
|
) {
|
||||||
|
route(route.parseAsName(), dataOverride, metaOverride, setAsRoot).apply(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///**
|
||||||
|
// * Create a stand-alone site at a given node
|
||||||
|
// */
|
||||||
|
//public fun SiteBuilder.site(route: Name, dataRoot: DataTree<*>, block: SiteBuilder.() -> Unit) {
|
||||||
|
// val mountedData = data.copy(
|
||||||
|
// data = dataRoot,
|
||||||
|
// baseUrlPath = data.resolveRef(route.tokens.joinToString(separator = "/")),
|
||||||
|
// meta = Laminate(dataRoot.meta, data.meta) //layering dataRoot meta over existing data
|
||||||
|
// )
|
||||||
|
// route(route) {
|
||||||
|
// withData(mountedData).block()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//TODO move to DF
|
||||||
|
fun DataTree.Companion.empty(meta: Meta = Meta.EMPTY) = object : DataTree<Any> {
|
||||||
|
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
|
||||||
|
override val dataType: KType get() = typeOf<Any>()
|
||||||
|
override val meta: Meta get() = meta
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
package space.kscience.snark
|
|
||||||
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.dataforge.context.ContextAware
|
|
||||||
import space.kscience.dataforge.data.*
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import space.kscience.dataforge.names.*
|
|
||||||
import space.kscience.snark.SiteData.Companion.INDEX_PAGE_TOKEN
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
|
|
||||||
data class SiteData(
|
|
||||||
val snark: SnarkPlugin,
|
|
||||||
val data: DataTree<*>,
|
|
||||||
val baseUrlPath: String,
|
|
||||||
override val meta: Meta,
|
|
||||||
) : ContextAware, DataTree<Any> by data {
|
|
||||||
|
|
||||||
override val context: Context get() = snark.context
|
|
||||||
|
|
||||||
val language: String? by meta.string()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun empty(
|
|
||||||
snark: SnarkPlugin,
|
|
||||||
baseUrlPath: String = "",
|
|
||||||
meta: Meta = Meta.EMPTY,
|
|
||||||
): SiteData {
|
|
||||||
//TODO use empty data from DF
|
|
||||||
val emptyData = object : DataTree<Any> {
|
|
||||||
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
|
|
||||||
override val dataType: KType get() = typeOf<Any>()
|
|
||||||
override val meta: Meta get() = meta
|
|
||||||
}
|
|
||||||
return SiteData(snark, emptyData, baseUrlPath, meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
val INDEX_PAGE_TOKEN: NameToken = NameToken("index")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a resource full path by its name
|
|
||||||
*/
|
|
||||||
fun SiteData.resolveRef(name: String): String = if (baseUrlPath.isEmpty()) {
|
|
||||||
name
|
|
||||||
} else {
|
|
||||||
"${baseUrlPath.removeSuffix("/")}/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a page designated by given name. Depending on rendering specifics, some prefixes or suffixes could be added.
|
|
||||||
*/
|
|
||||||
fun SiteData.resolvePage(name: Name): String {
|
|
||||||
return resolveRef(name.tokens.joinToString("/")) + (meta["pageSuffix"].string ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun SiteData.resolvePage(name: String): String = resolvePage(name.parseAsName())
|
|
||||||
|
|
||||||
val SiteData.homeRef get() = resolvePage(Name.EMPTY)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a Html builder by its full name
|
|
||||||
*/
|
|
||||||
fun DataTree<*>.resolveHtml(name: Name): HtmlData? {
|
|
||||||
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + INDEX_PAGE_TOKEN))
|
|
||||||
|
|
||||||
return resolved?.takeIf {
|
|
||||||
it.published //TODO add language confirmation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all Html blocks using given name/meta filter
|
|
||||||
*/
|
|
||||||
fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
|
|
||||||
filterByType<HtmlFragment> { name, meta ->
|
|
||||||
predicate(name, meta)
|
|
||||||
&& meta["published"].string != "false"
|
|
||||||
//TODO add language confirmation
|
|
||||||
}.asSequence().associate { it.name to it.data }
|
|
||||||
|
|
||||||
|
|
||||||
fun SiteData.findByType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
|
|
||||||
name.startsWith(baseName) && meta["content_type"].string == contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val Data<*>.published: Boolean get() = meta["published"].string != "false"
|
|
||||||
//
|
|
||||||
//fun SnarkPlugin.readData(data: DataTree<*>, rootUrl: String = "/"): SiteData =
|
|
||||||
// SiteData(this, data, rootUrl)
|
|
||||||
//
|
|
||||||
//fun SnarkPlugin.readDirectory(path: Path, rootUrl: String = "/"): SiteData {
|
|
||||||
// val parsedData: DataTree<Any> = readDirectory(path)
|
|
||||||
//
|
|
||||||
// return readData(parsedData, rootUrl)
|
|
||||||
//}
|
|
@ -60,7 +60,7 @@ fun SiteBuilder.pages(
|
|||||||
val layoutMeta = data.meta[LAYOUT_KEY]
|
val layoutMeta = data.meta[LAYOUT_KEY]
|
||||||
if (layoutMeta != null) {
|
if (layoutMeta != null) {
|
||||||
//use layout if it is defined
|
//use layout if it is defined
|
||||||
this.data.snark.layout(layoutMeta).render(data)
|
snark.layout(layoutMeta).render(data)
|
||||||
} else {
|
} else {
|
||||||
when (data) {
|
when (data) {
|
||||||
is DataTreeItem.Node -> {
|
is DataTreeItem.Node -> {
|
||||||
|
@ -3,19 +3,30 @@ package space.kscience.snark
|
|||||||
import kotlinx.html.HTML
|
import kotlinx.html.HTML
|
||||||
import kotlinx.html.html
|
import kotlinx.html.html
|
||||||
import kotlinx.html.stream.createHTML
|
import kotlinx.html.stream.createHTML
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.data.DataTree
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.withDefault
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.isEmpty
|
import space.kscience.dataforge.names.isEmpty
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
import kotlin.io.path.*
|
import kotlin.io.path.*
|
||||||
|
|
||||||
|
|
||||||
class StaticSiteBuilder(override val data: SiteData, private val path: Path) : SiteBuilder {
|
internal class StaticSiteBuilder(
|
||||||
|
override val snark: SnarkPlugin,
|
||||||
|
override val data: DataTree<*>,
|
||||||
|
override val meta: Meta,
|
||||||
|
private val baseUrl: String,
|
||||||
|
private val path: Path,
|
||||||
|
) : SiteBuilder {
|
||||||
private fun Path.copyRecursively(target: Path) {
|
private fun Path.copyRecursively(target: Path) {
|
||||||
Files.walk(this).forEach { source: Path ->
|
Files.walk(this).forEach { source: Path ->
|
||||||
val destination: Path = target.resolve(source.relativeTo(this))
|
val destination: Path = target.resolve(source.relativeTo(this))
|
||||||
if(!destination.isDirectory()) {
|
if (!destination.isDirectory()) {
|
||||||
//avoid re-creating directories
|
//avoid re-creating directories
|
||||||
source.copyTo(destination, true)
|
source.copyTo(destination, true)
|
||||||
}
|
}
|
||||||
@ -37,7 +48,7 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
|
|||||||
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
||||||
val targetPath = path.resolve(remotePath)
|
val targetPath = path.resolve(remotePath)
|
||||||
targetPath.parent.createDirectories()
|
targetPath.parent.createDirectories()
|
||||||
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath,true)
|
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun assetResourceDirectory(resourcesPath: String) {
|
override fun assetResourceDirectory(resourcesPath: String) {
|
||||||
@ -45,11 +56,27 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
|
|||||||
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path)
|
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun page(route: Name, content: context(SiteData, HTML) () -> Unit) {
|
inner class StaticPageBuilder : PageBuilder {
|
||||||
|
override val data: DataTree<*> get() = this@StaticSiteBuilder.data
|
||||||
|
override val meta: Meta get() = this@StaticSiteBuilder.meta
|
||||||
|
override val context: Context get() = this@StaticSiteBuilder.context
|
||||||
|
|
||||||
|
|
||||||
|
override fun resolveRef(ref: String): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolvePageRef(pageName: Name): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun page(route: Name, content: context(PageBuilder, HTML) () -> Unit) {
|
||||||
val htmlBuilder = createHTML()
|
val htmlBuilder = createHTML()
|
||||||
|
|
||||||
htmlBuilder.html {
|
htmlBuilder.html {
|
||||||
content(data, this)
|
content(StaticPageBuilder(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val newPath = if (route.isEmpty()) {
|
val newPath = if (route.isEmpty()) {
|
||||||
@ -62,19 +89,23 @@ class StaticSiteBuilder(override val data: SiteData, private val path: Path) : S
|
|||||||
newPath.writeText(htmlBuilder.finalize())
|
newPath.writeText(htmlBuilder.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun route(subRoute: Name): SiteBuilder = StaticSiteBuilder(data, path.resolve(subRoute.toWebPath()))
|
override fun route(
|
||||||
|
routeName: Name,
|
||||||
override fun withData(newData: SiteData): SiteBuilder = StaticSiteBuilder(newData, path)
|
dataOverride: DataTree<*>?,
|
||||||
|
metaOverride: Meta?,
|
||||||
}
|
setAsRoot: Boolean,
|
||||||
|
): SiteBuilder = StaticSiteBuilder(
|
||||||
fun SnarkPlugin.static(path: Path, block: SiteBuilder.() -> Unit) {
|
snark = snark,
|
||||||
val base = SiteData.empty(
|
data = dataOverride ?: data,
|
||||||
this,
|
meta = metaOverride?.withDefault(meta) ?: meta,
|
||||||
baseUrlPath = path.absolutePathString(),
|
baseUrl = baseUrl,
|
||||||
meta = Meta {
|
path = path.resolve(routeName.toWebPath())
|
||||||
"pageSuffix" put ".html"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
StaticSiteBuilder(base, path).block()
|
}
|
||||||
|
|
||||||
|
fun SnarkPlugin.static(outputPath: Path, data: DataTree<*> = DataTree.empty(), block: SiteBuilder.() -> Unit) {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
StaticSiteBuilder(this, data, meta, "", outputPath).block()
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user