1
0
forked from SPC/spc-site

Refactor snark part

This commit is contained in:
Alexander Nozik 2022-06-21 10:52:24 +03:00
parent 285057fbb0
commit 600a9b5529
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
13 changed files with 240 additions and 216 deletions

View File

@ -1,7 +1,7 @@
package html5up.forty
import kotlinx.html.*
import space.kscience.snark.PageContext
import space.kscience.snark.SiteContext
import space.kscience.snark.resolveRef
@ -201,7 +201,7 @@ internal fun FlowContent.fortyFooter() {
}
}
context(PageContext) internal fun BODY.fortyScripts() {
context(SiteContext) internal fun BODY.fortyScripts() {
script {
src = resolveRef("assets/js/jquery.min.js")
}

View File

@ -1,9 +1,9 @@
package html5up.forty
import kotlinx.html.*
import space.kscience.snark.PageContext
import space.kscience.snark.SiteContext
context(PageContext) internal fun HTML.landing(){
context(SiteContext) internal fun HTML.landing(){
head {
title {
}

View File

@ -1,10 +1,10 @@
package html5up.forty
import kotlinx.html.*
import space.kscience.snark.PageContext
import space.kscience.snark.SiteContext
import space.kscience.snark.resolveRef
context(PageContext) internal fun HTML.fortyPage(){
context(SiteContext) internal fun HTML.fortyPage(){
head {
title {
}

View File

@ -80,7 +80,7 @@ private val PROGRAM_PATH: Name = CONTENT_NODE_NAME + "program"
private val RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses"
private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners"
context(PageContext) private fun FlowContent.programSection() {
context(SiteContext) private fun FlowContent.programSection() {
val programBlock = resolveHtml(PROGRAM_PATH)!!
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!!
div("inner") {
@ -97,7 +97,7 @@ context(PageContext) private fun FlowContent.programSection() {
}
}
context(PageContext) private fun FlowContent.partners() {
context(SiteContext) private fun FlowContent.partners() {
//val partnersData: Meta = resolve<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
val partnersData: Meta = runBlocking { data.getByType<Meta>(PARTNERS_PATH)?.await() } ?: Meta.EMPTY
div("inner") {
@ -127,7 +127,7 @@ context(PageContext) private fun FlowContent.partners() {
// val photo: String? by meta.string()
//}
context(PageContext) private fun FlowContent.team() {
context(SiteContext) private fun FlowContent.team() {
val team = findByType("magprog_team").values.sortedBy { it.order }
div("inner") {
@ -182,7 +182,7 @@ context(PageContext) private fun FlowContent.team() {
// }
}
context(PageContext) private fun FlowContent.mentors() {
context(SiteContext) private fun FlowContent.mentors() {
val mentors = findByType("magprog_mentor").entries.sortedBy { it.value.id }
div("inner") {
@ -219,12 +219,12 @@ context(PageContext) private fun FlowContent.mentors() {
}
}
context(PageContext) internal fun FlowContent.contacts() {
context(SiteContext) internal fun FlowContent.contacts() {
}
context(PageContext) internal fun HTML.magProgHead(title: String) {
context(SiteContext) internal fun HTML.magProgHead(title: String) {
head {
this.title = title
meta {
@ -251,7 +251,7 @@ context(PageContext) internal fun HTML.magProgHead(title: String) {
}
}
context(PageContext) internal fun BODY.magProgFooter() {
context(SiteContext) internal fun BODY.magProgFooter() {
footer("wrapper style1-alt") {
id = "footer"
div("inner") {
@ -296,11 +296,11 @@ internal fun Application.spcMaster(context: Context, dataPath: Path, prefix: Str
val snark = context.fetch(SnarkPlugin)
val magProgPageContext: PageContext = snark.read(dataPath.resolve("content"), prefix)
val magProgSiteContext: SiteContext = snark.read(dataPath.resolve("content"), prefix)
routing {
route(prefix) {
with(magProgPageContext) {
with(magProgSiteContext) {
static {
files(dataPath.resolve("assets").toFile())

View File

@ -11,15 +11,11 @@ import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.withIndex
import space.kscience.dataforge.values.string
import space.kscience.snark.*
import kotlin.collections.Map
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.forEach
import kotlin.collections.joinToString
import kotlin.collections.set
import kotlin.collections.sortedBy
context(PageContext) private fun FlowContent.spcSpotlightContent(
context(SiteContext) private fun FlowContent.spcSpotlightContent(
landing: HtmlData,
content: Map<Name, HtmlData>,
) {
@ -92,7 +88,7 @@ context(PageContext) private fun FlowContent.spcSpotlightContent(
}
context(PageContext) internal fun SnarkRoute.spcSpotlight(
context(SiteContext) internal fun SiteBuilder.spcSpotlight(
name: String,
contentFilter: (Name, Meta) -> Boolean,
) {

View File

@ -20,7 +20,7 @@ import space.kscience.snark.*
import java.nio.file.Path
context(PageContext) internal fun HTML.spcPageContent(
context(SiteContext) internal fun HTML.spcPageContent(
meta: Meta,
title: String = meta["title"].string ?: SPC_TITLE,
fragment: FlowContent.() -> Unit,
@ -58,13 +58,13 @@ context(PageContext) internal fun HTML.spcPageContent(
}
context(PageContext) internal fun SnarkRoute.spcPage(subRoute: String, meta: Meta, fragment: FlowContent.() -> Unit) {
context(SiteContext) internal fun SiteBuilder.spcPage(subRoute: String, meta: Meta, fragment: FlowContent.() -> Unit) {
page(subRoute) {
spcPageContent(meta, fragment = fragment)
}
}
context(PageContext) internal fun SnarkRoute.spcPage(
context(SiteContext) internal fun SiteBuilder.spcPage(
subRoute: String,
dataPath: Name = subRoute.replace("/", ".").parseAsName(),
more: FlowContent.() -> Unit = {},
@ -83,12 +83,12 @@ context(PageContext) internal fun SnarkRoute.spcPage(
/**
* Route a directory
*/
context(PageContext) internal fun SnarkRoute.spcDirectory(
context(SiteContext) internal fun SiteBuilder.spcDirectory(
subRoute: String,
dataPath: Name = subRoute.replace("/", ".").parseAsName(),
) {
data.filterByType<HtmlFragment> { name, _ -> name.startsWith(dataPath) }.forEach { html ->
val pageName = if (html.name.lastOrNull()?.body == PageContext.INDEX_PAGE_NAME) {
val pageName = if (html.name.lastOrNull()?.body == SiteContext.INDEX_PAGE_NAME) {
html.name.cutLast()
} else {
html.name
@ -100,14 +100,14 @@ context(PageContext) internal fun SnarkRoute.spcDirectory(
}
}
context(PageContext) internal fun SnarkRoute.spcPage(
context(SiteContext) internal fun SiteBuilder.spcPage(
name: Name,
more: FlowContent.() -> Unit = {},
) {
spcPage(name.tokens.joinToString("/"), name, more)
}
context(PageContext, HTML) private fun HTML.spcHome() {
context(SiteContext, HTML) private fun HTML.spcHome() {
spcHead()
body("is-preload") {
wrapper {
@ -302,7 +302,7 @@ internal fun Application.spcHome(context: Context, rootPath: Path, prefix: Strin
routing {
route(prefix) {
snark(homePageContext) {
snarkSite(homePageContext) {
staticDirectory("assets", rootPath.resolve("assets"))
staticDirectory("images", rootPath.resolve("images"))

View File

@ -1,14 +1,14 @@
package ru.mipt.spc
import kotlinx.html.*
import space.kscience.snark.PageContext
import space.kscience.snark.SiteContext
import space.kscience.snark.homeRef
import space.kscience.snark.resolveRef
internal const val SPC_TITLE = "Scientific Programming Centre"
context(PageContext) internal fun HTML.spcHead(title: String = SPC_TITLE) {
context(SiteContext) internal fun HTML.spcHead(title: String = SPC_TITLE) {
head {
title {
+title
@ -27,7 +27,7 @@ context(PageContext) internal fun HTML.spcHead(title: String = SPC_TITLE) {
}
}
context(PageContext) internal fun FlowContent.spcHomeMenu() {
context(SiteContext) internal fun FlowContent.spcHomeMenu() {
nav {
id = "menu"
ul("links") {
@ -79,7 +79,7 @@ context(PageContext) internal fun FlowContent.spcHomeMenu() {
}
}
context(PageContext) internal fun FlowContent.spcFooter() {
context(SiteContext) internal fun FlowContent.spcFooter() {
footer {
id = "footer"
div("inner") {
@ -129,7 +129,7 @@ context(PageContext) internal fun FlowContent.spcFooter() {
}
}
context(PageContext) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) {
context(SiteContext) internal fun FlowContent.wrapper(contentBody: FlowContent.() -> Unit) {
div {
id = "wrapper"
// Header

View File

@ -0,0 +1,79 @@
package space.kscience.snark
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 kotlinx.html.HTML
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import java.nio.file.Path
/**
* An abstraction, which is used to render sites to the different rendering engines
*/
interface SiteBuilder : ContextAware {
val siteContext: SiteContext
override val context: Context get() = siteContext.context
fun staticFile(remotePath: String, file: Path)
fun staticDirectory(remotePath: String, directory: Path)
fun staticResourceFile(remotePath: String, resourcesPath: String)
fun staticResourceDirectory(resourcesPath: String)
fun page(route: String = "", content: context(SiteContext, HTML) () -> Unit)
/**
* Create a route
*/
fun route(subRoute: String): SiteBuilder
}
public inline fun SiteBuilder.route(route: String, block: SiteBuilder.() -> Unit) {
route(route).apply(block)
}
class KtorSiteRoute(override val siteContext: SiteContext, private val ktorRoute: Route) : SiteBuilder {
override fun staticFile(remotePath: String, file: Path) {
ktorRoute.file(remotePath, file.toFile())
}
override fun staticDirectory(remotePath: String, directory: Path) {
ktorRoute.static(remotePath) {
files(directory.toFile())
}
}
override fun page(route: String, content: context(SiteContext, HTML)() -> Unit) {
ktorRoute.get(route) {
call.respondHtml {
content(siteContext.copyWithRequestHost(call.request), this)
}
}
}
override fun route(subRoute: String): SiteBuilder =
KtorSiteRoute(siteContext, ktorRoute.createRouteFromPath(subRoute))
override fun staticResourceFile(remotePath: String, resourcesPath: String) {
ktorRoute.resource(resourcesPath, resourcesPath)
}
override fun staticResourceDirectory(resourcesPath: String) {
ktorRoute.resources(resourcesPath)
}
}
inline fun Route.snarkSite(
siteContext: SiteContext,
block: context(SiteContext, SiteBuilder)() -> Unit,
) {
block(siteContext, KtorSiteRoute(siteContext, this@snarkSite))
}

View File

@ -15,17 +15,19 @@ import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.names.startsWith
import space.kscience.snark.PageContext.Companion.INDEX_PAGE_NAME
import space.kscience.snark.SiteContext.Companion.INDEX_PAGE_NAME
import java.nio.file.Path
data class PageContext(
override val context: Context,
data class SiteContext(
val snark: SnarkPlugin,
val path: String,
val pageMeta: Meta,
val data: DataSet<*>,
val meta: Meta,
val data: DataTree<*>,
) : ContextAware {
val language: String? by pageMeta.string()
override val context: Context get() = snark.context
val language: String? by meta.string()
companion object {
const val INDEX_PAGE_NAME: String = "index"
@ -35,14 +37,14 @@ data class PageContext(
/**
* Resolve a resource full path by its name
*/
fun PageContext.resolveRef(name: String): String = "${path.removeSuffix("/")}/$name"
fun SiteContext.resolveRef(name: String): String = "${path.removeSuffix("/")}/$name"
fun PageContext.resolveRef(name: Name): String = "${path.removeSuffix("/")}/${name.tokens.joinToString("/")}"
fun SiteContext.resolveRef(name: Name): String = "${path.removeSuffix("/")}/${name.tokens.joinToString("/")}"
/**
* Resolve a Html builder by its full name
*/
fun PageContext.resolveHtml(name: Name): HtmlData? {
fun SiteContext.resolveHtml(name: Name): HtmlData? {
val resolved = (data.getByType<HtmlFragment>(name) ?: data.getByType<HtmlFragment>(name + INDEX_PAGE_NAME))
return resolved?.takeIf {
@ -53,40 +55,45 @@ fun PageContext.resolveHtml(name: Name): HtmlData? {
/**
* Find all Html blocks using given name/meta filter
*/
fun PageContext.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
fun SiteContext.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
data.filterByType<HtmlFragment> { name, meta ->
predicate(name, meta)
&& meta["published"].string != "false"
//TODO add language confirmation
}.asSequence().associate { it.name to it.data }
val PageContext.homeRef get() = resolveRef("").removeSuffix("/")
val SiteContext.homeRef get() = resolveRef("").removeSuffix("/")
fun PageContext.findByType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
fun SiteContext.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 PageContext(dfContext: Context, rootUrl: String, data: DataSet<*>): PageContext =
PageContext(dfContext, rootUrl, data.meta, data)
fun SnarkPlugin.siteContext(rootUrl: String, data: DataTree<*>): SiteContext =
SiteContext(this, rootUrl, data.meta, data)
fun SnarkPlugin.read(path: Path, rootUrl: String = "/"): PageContext {
val parsedData: DataSet<Any> = readDirectory(path)
fun SnarkPlugin.read(path: Path, rootUrl: String = "/"): SiteContext {
val parsedData: DataTree<Any> = readDirectory(path)
return PageContext(context, rootUrl, parsedData)
return siteContext(rootUrl, parsedData)
}
/**
* Substitute uri in [PageContext] with uri in the call to properly resolve relative refs. Only host properties are substituted.
*/
context(PageContext) inline fun withRequest(request: ApplicationRequest, block: context(PageContext) () -> Unit) {
@PublishedApi
internal fun SiteContext.copyWithRequestHost(request: ApplicationRequest): SiteContext {
val uri = URLBuilder(
protocol = URLProtocol.createOrDefault(request.origin.scheme),
host = request.host(),
port = request.port(),
pathSegments = path.split("/"),
)
block(copy(path = uri.buildString()))
return copy(path = uri.buildString())
}
/**
* Substitute uri in [SiteContext] with uri in the call to properly resolve relative refs. Only host properties are substituted.
*/
context(SiteContext) inline fun withRequest(request: ApplicationRequest, block: context(SiteContext) () -> Unit) {
block(copyWithRequestHost(request))
}

View File

@ -0,0 +1,99 @@
package space.kscience.snark
import kotlinx.coroutines.runBlocking
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.DataTreeItem
import space.kscience.dataforge.data.await
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.getIndexed
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus
import space.kscience.snark.SiteLayout.Companion.DESIGNATION_KEY
import space.kscience.snark.SiteLayout.Companion.LAYOUT_KEY
import java.nio.file.Path
import kotlin.reflect.typeOf
internal fun SiteBuilder.staticFrom(rootMeta: Meta) {
rootMeta.getIndexed("resource".asName()).forEach { (_, meta) ->
val path by meta.string()
val remotePath by meta.string()
path?.let { resourcePath ->
//If remote path provided, use a single resource
remotePath?.let {
staticResourceFile(it, resourcePath)
return@forEach
}
//otherwise use package resources
staticResourceDirectory(resourcePath)
}
}
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
val remotePath by meta.string { error("File remote path is not provided") }
val path by meta.string { error("File path is not provided") }
staticFile(remotePath, Path.of(path))
}
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
val path by meta.string { error("Directory path is not provided") }
staticDirectory("", Path.of(path))
}
}
/**
* Represent pages in a [DataTree]
*/
fun SiteBuilder.data(data: DataTreeItem<*>, prefix: Name = Name.EMPTY) {
val layoutMeta = data.meta[LAYOUT_KEY]
if (layoutMeta != null) {
//use layout if it is defined
siteContext.snark.layout(layoutMeta).render(data)
} else {
when (data) {
is DataTreeItem.Node -> {
data.tree.items.forEach { (token, item) ->
data(item, prefix + token)
}
}
is DataTreeItem.Leaf -> {
val item = data.data
if (item.type == typeOf<HtmlData>() && item.meta[DESIGNATION_KEY].string == "page") {
route(prefix.tokens.joinToString(separator = "/")) {
page {
@Suppress("UNCHECKED_CAST")
val pageFragment: HtmlFragment = runBlocking { item.await() as HtmlFragment }
pageFragment.invoke(consumer)
}
staticFrom(item.meta)
}
}
}
}
}
//TODO watch for changes
}
fun interface SiteLayout {
context(SiteBuilder) fun render(data: DataTreeItem<*>)
companion object {
internal const val DESIGNATION_KEY = "designation"
const val LAYOUT_KEY = "layout"
}
}
object DefaultSiteLayout : SiteLayout {
context(SiteBuilder) override fun render(data: DataTreeItem<*>) {
data(data)
}
}

View File

@ -69,7 +69,6 @@ class SnarkPlugin : AbstractPlugin() {
context.gather(SnarkParser.TYPE, true)
}
fun readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path) { dataPath, meta ->
val fileExtension = meta[FileData.META_FILE_EXTENSION_KEY].string ?: dataPath.extension
val parser: SnarkParser<*>? = parsers.values.filter { parser ->
@ -84,6 +83,9 @@ class SnarkPlugin : AbstractPlugin() {
}
}
fun layout(meta: Meta): SiteLayout = when(meta[SiteLayout.LAYOUT_KEY]){
else -> DefaultSiteLayout
}
override fun content(target: String): Map<Name, Any> = when (target) {
SnarkParser.TYPE -> mapOf(

View File

@ -1,64 +0,0 @@
package space.kscience.snark
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.file
import io.ktor.server.http.content.files
import io.ktor.server.http.content.static
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.html.HTML
import java.nio.file.Path
/**
* An abstraction, which is used to render sites to the different rendering engines
*/
interface SnarkRoute {
fun staticFile(remotePath: String, file: Path)
fun staticDirectory(remotePath: String, directory: Path)
context(PageContext) fun page(route: String = "", content: context(PageContext, HTML) () -> Unit)
context(PageContext) fun route(route: String, block: context(PageContext, SnarkRoute) () -> Unit)
}
class KtorRouteBuilder(private val ktorRoute: Route) : SnarkRoute {
override fun staticFile(remotePath: String, file: Path) {
ktorRoute.file(remotePath, file.toFile())
}
override fun staticDirectory(remotePath: String, directory: Path) {
ktorRoute.static(remotePath) {
files(directory.toFile())
}
}
context(PageContext) override fun page(route: String, content: context(PageContext, HTML)() -> Unit) {
ktorRoute.get(route) {
withRequest(call.request) {
call.respondHtml {
content(this@PageContext, this)
}
}
}
}
context(PageContext) override fun route(
route: String,
block: context(PageContext, SnarkRoute)() -> Unit,
) {
ktorRoute.route(route) {
block(this@PageContext, KtorRouteBuilder(this))
}
}
}
inline fun Route.snark(
pageContext: PageContext,
block: context(PageContext, SnarkRoute)() -> Unit,
) {
block(pageContext, KtorRouteBuilder(this@snark))
}

View File

@ -1,95 +0,0 @@
package space.kscience.snark
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.routing.get
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.DataTreeItem
import space.kscience.dataforge.data.await
import space.kscience.dataforge.data.type
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.getIndexed
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus
import java.nio.file.Path
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.reflect.typeOf
internal const val DESIGNATION_KEY = "designation"
private fun SnarkRoute.staticFrom(rootMeta: Meta) {
// rootMeta.getIndexed("resource".asName()).forEach { (_, meta) ->
// val resourcePackage by meta.string()
// val remotePath by meta.string()
// val resourceName by meta.string()
//
// //If remote path provided, use a single resource
// remotePath?.let {
// resource(it, resourceName ?: it, resourcePackage)
// return@forEach
// }
//
// //otherwise use package resources
// resources(resourcePackage)
// }
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
val remotePath by meta.string { error("File remote path is not provided") }
val path by meta.string { error("File path is not provided") }
staticFile(remotePath, Path.of(path))
}
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
val path by meta.string { error("Directory path is not provided") }
staticDirectory("", Path.of(path))
}
}
/**
* Represent pages in a [DataTree]
*/
context(PageContext) fun SnarkRoute.pagesFrom(prefix: Name, data: DataTree<*>) {
if (data.meta[DESIGNATION_KEY].string == "page") {
TODO("Implement node-based pages")
// route(prefix.tokens.joinToString(separator = "/")) {
// get {
// val headFragment = data.getByType<HtmlFragment>("head")?.await()
// call.respondHtml {
// head {
// headFragment?.invoke(consumer)
// data.meta["title"].string?.let { title(it) }
// }
// body {
// data.filterByType<HtmlFragment> { name, meta ->
// name.first().body == "section" && meta["published"].boolean != false
// }.traverse().sortedBy { }
// }
// }
// }
// staticFrom(data.meta)
// }
} else {
data.items.forEach { (token, item) ->
when (item) {
is DataTreeItem.Node -> pagesFrom(prefix + token, item.tree)
is DataTreeItem.Leaf -> if (item.type == typeOf<HtmlData>() && item.meta[DESIGNATION_KEY].string == "page") {
route(prefix.tokens.joinToString(separator = "/")) {
get {
@Suppress("UNCHECKED_CAST")
val pageFragment: HtmlFragment = item.data.await() as HtmlFragment
call.respondHtml {
pageFragment.invoke(consumer)
}
}
staticFrom(item.meta)
}
}
}
}
}
//TODO watch for changes
}