diff --git a/data/magprog/assets/js/main.js b/data/magprog/assets/js/main.js index 84526ba..9f48e4d 100644 --- a/data/magprog/assets/js/main.js +++ b/data/magprog/assets/js/main.js @@ -20,7 +20,7 @@ }); // Hack: Enable IE flexbox workarounds. - if (browser.name == 'ie') + if (browser.name === 'ie') $body.addClass('is-ie'); // Play initial animations on page load. @@ -47,7 +47,8 @@ // Sidebar. if ($sidebar.length > 0) { - var $sidebar_a = $sidebar.find('a'); + //adding exclusion for home link + var $sidebar_a = $sidebar.find('a').not('.spc-home'); $sidebar_a .addClass('scrolly') @@ -56,7 +57,7 @@ var $this = $(this); // External link? Bail. - if ($this.attr('href').charAt(0) != '#') + if ($this.attr('href').charAt(0) !== '#') return; // Deactivate all links. @@ -95,7 +96,7 @@ $section.removeClass('inactive'); // No locked links? Deactivate all links and activate this section's one. - if ($sidebar_a.filter('.active-locked').length == 0) { + if ($sidebar_a.filter('.active-locked').length === 0) { $sidebar_a.removeClass('active'); $this.addClass('active'); @@ -159,7 +160,7 @@ $image.css('background-image', 'url(' + $img.attr('src') + ')'); // Set background position. - if (x = $img.data('position')) + if (x === $img.data('position')) $image.css('background-position', x); // Hide . diff --git a/src/main/kotlin/ru/mipt/spc/Application.kt b/src/main/kotlin/ru/mipt/spc/Application.kt index 1908458..7f34687 100644 --- a/src/main/kotlin/ru/mipt/spc/Application.kt +++ b/src/main/kotlin/ru/mipt/spc/Application.kt @@ -6,7 +6,9 @@ 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.mountSnark import java.net.URI import java.nio.file.FileSystems import java.nio.file.Files @@ -48,6 +50,7 @@ fun Application.spcModule() { val context = Context("spc-site") { plugin(SnarkPlugin) } + val snarkPlugin = context.fetch(SnarkPlugin) val dataPath = Path.of("data") @@ -83,21 +86,21 @@ fun Application.spcModule() { dataPath.resolve(DEPLOY_DATE_FILE).writeText(date) } + mountSnark(snarkPlugin) { + val homeDataPath = resolveData( + javaClass.getResource("/home")!!.toURI(), + dataPath / "home" + ) - val homeDataPath = resolveData( - javaClass.getResource("/home")!!.toURI(), - dataPath / "home" - ) + spcHome(rootPath = homeDataPath) - spcHome(context, rootPath = homeDataPath) + val mastersDataPath = resolveData( + javaClass.getResource("/magprog")!!.toURI(), + dataPath / "magprog" + ) - - val mastersDataPath = resolveData( - javaClass.getResource("/magprog")!!.toURI(), - dataPath / "magprog" - ) - - spcMaster(context, dataPath = mastersDataPath) + spcMaster(dataPath = mastersDataPath) + } } diff --git a/src/main/kotlin/ru/mipt/spc/master.kt b/src/main/kotlin/ru/mipt/spc/master.kt index d2cf0f6..d088b15 100644 --- a/src/main/kotlin/ru/mipt/spc/master.kt +++ b/src/main/kotlin/ru/mipt/spc/master.kt @@ -1,17 +1,7 @@ package ru.mipt.spc -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.html.respondHtml -import io.ktor.server.http.content.files -import io.ktor.server.http.content.static -import io.ktor.server.routing.get -import io.ktor.server.routing.route -import io.ktor.server.routing.routing import kotlinx.coroutines.runBlocking import kotlinx.html.* -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.fetch import space.kscience.dataforge.data.await import space.kscience.dataforge.data.getByType import space.kscience.dataforge.meta.Meta @@ -219,11 +209,6 @@ context(SiteData) private fun FlowContent.mentors() { } } -context(SiteData) internal fun FlowContent.contacts() { - -} - - context(SiteData) internal fun HTML.magProgHead(title: String) { head { this.title = title @@ -292,147 +277,133 @@ context(SiteData) internal fun BODY.magProgFooter() { private val HtmlData.mentorPageId get() = "mentor-${id}" -internal fun Application.spcMaster(context: Context, dataPath: Path, prefix: String = "/magprog") { +internal fun SiteBuilder.spcMaster(dataPath: Path, prefix: Name = "magprog".asName()) { - val snark = context.fetch(SnarkPlugin) + val magProgSiteContext = snark.readDirectory(dataPath.resolve("content")) - val magProgSiteContext: SiteData = snark.readDirectory(dataPath.resolve("content"), prefix) + mountSite(prefix, magProgSiteContext) { + assetDirectory("", dataPath.resolve("assets")) + assetDirectory("images", dataPath.resolve("images")) - routing { - route(prefix) { - with(magProgSiteContext) { - static { - files(dataPath.resolve("assets").toFile()) - - static("images") { - files(dataPath.resolve("images").toFile()) - } + page { + val sections = listOf( + wrapSection(resolveHtml(INTRO_PATH)!!, "intro"), + MagProgSection( + id = "partners", + title = "Партнеры", + style = "wrapper style3 fullscreen fade-up" + ) { + partners() + }, + // section(props.data.partners), + MagProgSection( + id = "mentors", + title = "Научные руководители", + style = "wrapper style2 spotlights", + ) { + mentors() + }, + MagProgSection( + id = "program", + title = "Учебная программа", + style = "wrapper style3 fullscreen fade-up" + ) { + programSection() + }, + wrapSection(resolveHtml(ENROLL_PATH)!!, "enroll"), + wrapSection(id = "contacts", title = "Контакты") { + htmlData(resolveHtml(CONTACTS_PATH)!!) + team() } - - - get { - call.respondHtml { - val sections = listOf( - wrapSection(resolveHtml(INTRO_PATH)!!, "intro"), - MagProgSection( - id = "partners", - title = "Партнеры", - style = "wrapper style3 fullscreen fade-up" - ) { - partners() - }, - // section(props.data.partners), - MagProgSection( - id = "mentors", - title = "Научные руководители", - style = "wrapper style2 spotlights", - ) { - mentors() - }, - MagProgSection( - id = "program", - title = "Учебная программа", - style = "wrapper style3 fullscreen fade-up" - ) { - programSection() - }, - wrapSection(resolveHtml(ENROLL_PATH)!!, "enroll"), - wrapSection(id = "contacts", title = "Контакты") { - htmlData(resolveHtml(CONTACTS_PATH)!!) - team() - } - ) - magProgHead("Магистратура \"Научное программирование\"") - body("is-preload magprog-body") { - section { - id = "sidebar" - div("inner") { - nav { - ul { - li { - a(href = "/"){ - i("fa fa-home") { - attributes["aria-hidden"] = "true" - } - +"SPC" - } - } - sections.forEach { section -> - li { - a(href = "#${section.id}") { - +section.title - } - } - } + ) + magProgHead("Магистратура \"Научное программирование\"") + body("is-preload magprog-body") { + section { + id = "sidebar" + div("inner") { + nav { + ul { + li { + a(classes = "spc-home", href = "/") { + i("fa fa-home") { + attributes["aria-hidden"] = "true" + } + +"SPC" + } + } + sections.forEach { section -> + li { + a(href = "#${section.id}") { + +section.title } } } } - div { - id = "wrapper" - sections.forEach { sec -> - section(sec.style) { - id = sec.id - with(sec) { content() } - } - } - } - magProgFooter() } } } - - val mentors = findByType("magprog_mentor").values.sortedBy { - it.order + div { + id = "wrapper" + sections.forEach { sec -> + section(sec.style) { + id = sec.id + with(sec) { content() } + } + } } + magProgFooter() + } + } - mentors.forEach { mentor -> - get(mentor.mentorPageId) { - call.respondHtml { - magProgHead("Научное программирование: ${mentor.name}") - body("is-preload") { - header { - id = "header" - a(classes = "title") { - href = "$homeRef#mentors" - +"Научные руководители" - } - nav { - ul { - mentors.forEach { - li { - a { - href = resolveRef(it.mentorPageId) - +it.name.substringAfterLast(" ") - } - } - } + + val mentors = data.findByType("magprog_mentor").values.sortedBy { + it.order + } + + 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 = resolveRef(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() } } } diff --git a/src/main/kotlin/ru/mipt/spc/spcCollection.kt b/src/main/kotlin/ru/mipt/spc/spcCollection.kt index 5cadd52..0a36e5f 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) private fun FlowContent.spcSpotlightContent( +context(SiteData, FlowContent) private fun spcSpotlightContent( landing: HtmlData, content: Map, ) { @@ -88,13 +88,13 @@ context(SiteData) private fun FlowContent.spcSpotlightContent( } -context(SiteData) internal fun SiteBuilder.spcSpotlight( +internal fun SiteBuilder.spcSpotlight( address: String, contentFilter: (Name, Meta) -> Boolean, ) { val name = address.parseAsName() - val body = resolveHtml(name) ?: error("Could not find body for $name") - val content = resolveAllHtml(contentFilter) + val body = data.resolveHtml(name) ?: error("Could not find body for $name") + val content = data.resolveAllHtml(contentFilter) val meta = body.meta page(name) { @@ -110,7 +110,7 @@ context(SiteData) internal fun SiteBuilder.spcSpotlight( } content.forEach { (name, contentBody) -> - page(name){ + page(name) { spcPageContent(contentBody.meta) { htmlData(contentBody) } diff --git a/src/main/kotlin/ru/mipt/spc/spcHome.kt b/src/main/kotlin/ru/mipt/spc/spcHome.kt index da4ca09..54e4f28 100644 --- a/src/main/kotlin/ru/mipt/spc/spcHome.kt +++ b/src/main/kotlin/ru/mipt/spc/spcHome.kt @@ -1,14 +1,7 @@ package ru.mipt.spc import html5up.forty.fortyScripts -import io.ktor.server.application.Application -import io.ktor.server.routing.route -import io.ktor.server.routing.routing import kotlinx.html.* -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.error -import space.kscience.dataforge.context.fetch -import space.kscience.dataforge.context.logger import space.kscience.dataforge.data.Data import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get @@ -59,32 +52,9 @@ context(SiteData) internal fun HTML.spcPageContent( } } - -internal fun SiteBuilder.spcPage(subRoute: Name, meta: Meta, fragment: FlowContent.() -> Unit) { - page(subRoute) { - spcPageContent(meta, fragment = fragment) - } -} - -internal fun SiteBuilder.spcPage( - subRoute: Name, - dataPath: Name = subRoute, - more: FlowContent.() -> Unit = {}, -) { - val data = data.resolveHtml(dataPath) - if (data != null) { - spcPage(subRoute, data.meta) { - htmlData(data) - more() - } - } else { - logger.error { "Content for page with path $dataPath not found" } - } -} - @Suppress("UNCHECKED_CAST") internal val FortyDataRenderer: SiteBuilder.(Data<*>) -> Unit = { data -> - if(data.type == typeOf()) { + if (data.type == typeOf()) { data as Data page { spcPageContent(data.meta) { @@ -94,32 +64,6 @@ internal val FortyDataRenderer: SiteBuilder.(Data<*>) -> Unit = { data -> } } -///** -// * Route a directory -// */ -//internal fun SiteBuilder.spcDirectory( -// subRoute: String, -// dataPath: Name = subRoute.replace("/", ".").parseAsName(), -//) { -// data.filterByType { name, _ -> name.startsWith(dataPath) }.forEach { html -> -// val pageName = if (html.name.lastOrNull()?.body == SiteData.INDEX_PAGE_NAME) { -// html.name.cutLast() -// } else { -// html.name -// } -// -// spcPage(pageName.tokens.joinToString(separator = "/"), html.meta) { -// htmlData(html) -// } -// } -//} - -internal fun SiteBuilder.spcPage( - name: Name, - more: FlowContent.() -> Unit = {}, -) { - spcPage(name, name, more) -} context(SiteData, HTML) private fun HTML.spcHome() { spcHead() @@ -308,26 +252,25 @@ context(SiteData, HTML) private fun HTML.spcHome() { } -internal fun Application.spcHome(context: Context, rootPath: Path, prefix: String = "") { +internal fun SiteBuilder.spcHome(rootPath: Path, prefix: Name = Name.EMPTY) { - val snark = context.fetch(SnarkPlugin) + val homePageData = snark.readDirectory(rootPath.resolve("content")) - val homePageContext = snark.readDirectory(rootPath.resolve("content"), prefix) + mountSite(prefix, homePageData) { + assetDirectory("assets", rootPath.resolve("assets")) + assetDirectory("images", rootPath.resolve("images")) - routing { - route(prefix) { - snarkSite(homePageContext) { - assetDirectory("assets", rootPath.resolve("assets")) - assetDirectory("images", rootPath.resolve("images")) + page { spcHome() } - page { spcHome() } + pages("consulting", dataRenderer = FortyDataRenderer) + //pages("ru.consulting".parseAsName(), dataRenderer = FortyDataRenderer) - pages("consulting", dataRenderer = FortyDataRenderer) - //pages("ru.consulting".parseAsName(), dataRenderer = FortyDataRenderer) - - spcSpotlight("team") { _, m -> m["type"].string == "team" } - spcSpotlight("research") { name, m -> name.startsWith("projects".asName()) && m["type"].string == "project" } - } + spcSpotlight("team") { _, m -> + m["type"].string == "team" + } + spcSpotlight("research") { name, m -> + name.startsWith("projects".asName()) && m["type"].string == "project" } } + } \ 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 d76c32a..dccd6d5 100644 --- a/src/main/kotlin/space/kscience/snark/SiteBuilder.kt +++ b/src/main/kotlin/space/kscience/snark/SiteBuilder.kt @@ -1,19 +1,23 @@ package space.kscience.snark +import io.ktor.server.application.Application import io.ktor.server.application.call import io.ktor.server.html.respondHtml import io.ktor.server.http.content.* import io.ktor.server.routing.Route 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.context.ContextAware +import space.kscience.dataforge.data.DataTree import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName import java.nio.file.Path internal fun Name.toWebPath() = tokens.joinToString(separator = "/") + /** * An abstraction, which is used to render sites to the different rendering engines */ @@ -33,12 +37,19 @@ interface SiteBuilder : ContextAware { fun page(route: Name = Name.EMPTY, content: context(SiteData, HTML) () -> Unit) + /** + * Create a new branch builder with replaced [data] + */ + fun withData(newData: SiteData): 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) } @@ -47,7 +58,22 @@ 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.baseUrlPath.removeSuffix("/") + "/" + route.toWebPath(), + meta = dataRoot.meta // TODO consider meshing sub-site meta with the parent site + ) + route(route) { + withData(mountedData).block() + } +} + class KtorSiteRoute(override val data: SiteData, private val ktorRoute: Route) : SiteBuilder { + override fun assetFile(remotePath: String, file: Path) { ktorRoute.file(remotePath, file.toFile()) } @@ -61,7 +87,8 @@ class KtorSiteRoute(override val data: SiteData, private val ktorRoute: Route) : override fun page(route: Name, content: context(SiteData, HTML)() -> Unit) { ktorRoute.get(route.toWebPath()) { call.respondHtml { - content(data.copyWithRequestHost(call.request), this) + val dataWithUrl = data.copyWithRequestHost(call.request) + content(dataWithUrl, this) } } } @@ -76,11 +103,30 @@ class KtorSiteRoute(override val data: SiteData, private val ktorRoute: Route) : override fun assetResourceDirectory(resourcesPath: String) { ktorRoute.resources(resourcesPath) } + + override fun withData(newData: SiteData): SiteBuilder = KtorSiteRoute(newData, ktorRoute) } -inline fun Route.snarkSite( - siteContext: SiteData, +inline fun Route.mountSnark( + data: SiteData, block: context(SiteData, SiteBuilder)() -> Unit, ) { - block(siteContext, KtorSiteRoute(siteContext, this@snarkSite)) -} \ No newline at end of file + block(data, KtorSiteRoute(data, this@mountSnark)) +} + +fun Application.mountSnark( + data: SiteData, + block: context(SiteData, SiteBuilder)() -> Unit, +) { + routing { + mountSnark(data, block) + } +} + +fun Application.mountSnark( + snarkPlugin: SnarkPlugin, + block: context(SiteData, SiteBuilder)() -> Unit, +) { + mountSnark(SiteData.empty(snarkPlugin), block) +} + diff --git a/src/main/kotlin/space/kscience/snark/SiteData.kt b/src/main/kotlin/space/kscience/snark/SiteData.kt index e4b5f72..becdcd5 100644 --- a/src/main/kotlin/space/kscience/snark/SiteData.kt +++ b/src/main/kotlin/space/kscience/snark/SiteData.kt @@ -13,16 +13,18 @@ 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.NameToken import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.startsWith import space.kscience.snark.SiteData.Companion.INDEX_PAGE_NAME -import java.nio.file.Path +import kotlin.reflect.KType +import kotlin.reflect.typeOf data class SiteData( val snark: SnarkPlugin, val data: DataTree<*>, - val urlPath: String, - override val meta: Meta = data.meta + val baseUrlPath: String, + override val meta: Meta = data.meta, ) : ContextAware, DataTree by data { override val context: Context get() = snark.context @@ -30,6 +32,19 @@ data class SiteData( val language: String? by meta.string() companion object { + fun empty( + snark: SnarkPlugin, + baseUrlPath: String = "/", + meta: Meta = Meta.EMPTY, + ): SiteData { + 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) + } + const val INDEX_PAGE_NAME: String = "index" } } @@ -37,9 +52,9 @@ data class SiteData( /** * Resolve a resource full path by its name */ -fun SiteData.resolveRef(name: String): String = "${urlPath.removeSuffix("/")}/$name" +fun SiteData.resolveRef(name: String): String = "${baseUrlPath.removeSuffix("/")}/$name" -fun SiteData.resolveRef(name: Name): String = "${urlPath.removeSuffix("/")}/${name.tokens.joinToString("/")}" +fun SiteData.resolveRef(name: Name): String = "${baseUrlPath.removeSuffix("/")}/${name.tokens.joinToString("/")}" /** * Resolve a Html builder by its full name @@ -70,15 +85,15 @@ fun SiteData.findByType(contentType: String, baseName: Name = Name.EMPTY) = reso } 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) -} +// +//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) +//} @PublishedApi internal fun SiteData.copyWithRequestHost(request: ApplicationRequest): SiteData { @@ -86,14 +101,7 @@ internal fun SiteData.copyWithRequestHost(request: ApplicationRequest): SiteData protocol = URLProtocol.createOrDefault(request.origin.scheme), host = request.host(), port = request.port(), - pathSegments = urlPath.split("/"), + pathSegments = baseUrlPath.split("/"), ) - return copy(urlPath = uri.buildString()) + return copy(baseUrlPath = uri.buildString()) } - -/** - * Substitute uri in [SiteData] with uri in the call to properly resolve relative refs. Only host properties are substituted. - */ -context(SiteData) inline fun withRequest(request: ApplicationRequest, block: context(SiteData) () -> Unit) { - block(copyWithRequestHost(request)) -} \ No newline at end of file