Full refactoring
This commit is contained in:
parent
eeaa080a88
commit
738f41265f
@ -21,8 +21,8 @@ public class Snark : WorkspacePlugin() {
|
||||
public val io: IOPlugin by require(IOPlugin)
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
|
||||
public val readers: Map<Name, SnarkIOReader<Any>> by lazy {
|
||||
context.gather<SnarkIOReader<Any>>(SnarkIOReader.DF_TYPE, inherit = true)
|
||||
public val readers: Map<Name, SnarkReader<Any>> by lazy {
|
||||
context.gather<SnarkReader<Any>>(SnarkReader.DF_TYPE, inherit = true)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,7 +50,7 @@ public class Snark : WorkspacePlugin() {
|
||||
readByteArray()
|
||||
}
|
||||
|
||||
internal val byteArraySnarkParser = SnarkIOReader(byteArrayIOReader)
|
||||
internal val byteArraySnarkParser = SnarkReader(byteArrayIOReader)
|
||||
|
||||
}
|
||||
}
|
@ -3,11 +3,11 @@ package space.kscience.snark
|
||||
import space.kscience.dataforge.io.IOReader
|
||||
import space.kscience.dataforge.io.asBinary
|
||||
import space.kscience.dataforge.misc.DfId
|
||||
import space.kscience.snark.SnarkIOReader.Companion.DEFAULT_PRIORITY
|
||||
import space.kscience.snark.SnarkIOReader.Companion.DF_TYPE
|
||||
import space.kscience.snark.SnarkReader.Companion.DEFAULT_PRIORITY
|
||||
import space.kscience.snark.SnarkReader.Companion.DF_TYPE
|
||||
|
||||
@DfId(DF_TYPE)
|
||||
public interface SnarkIOReader<out T>: IOReader<T> {
|
||||
public interface SnarkReader<out T>: IOReader<T> {
|
||||
public val types: Set<String>
|
||||
public val priority: Int get() = DEFAULT_PRIORITY
|
||||
public fun readFrom(source: String): T
|
||||
@ -27,17 +27,17 @@ public interface SnarkIOReader<out T>: IOReader<T> {
|
||||
* @property priority The priority of the SnarkIOReader. Higher priority SnarkIOReader instances will be preferred over lower priority ones.
|
||||
*/
|
||||
|
||||
private class SnarkIOReaderWrapper<out T>(
|
||||
private class SnarkReaderWrapper<out T>(
|
||||
private val reader: IOReader<T>,
|
||||
override val types: Set<String>,
|
||||
override val priority: Int = DEFAULT_PRIORITY,
|
||||
) : IOReader<T> by reader, SnarkIOReader<T> {
|
||||
) : IOReader<T> by reader, SnarkReader<T> {
|
||||
|
||||
override fun readFrom(source: String): T = readFrom(source.encodeToByteArray().asBinary())
|
||||
}
|
||||
|
||||
public fun <T : Any> SnarkIOReader(
|
||||
public fun <T : Any> SnarkReader(
|
||||
reader: IOReader<T>,
|
||||
vararg types: String,
|
||||
priority: Int = DEFAULT_PRIORITY
|
||||
): SnarkIOReader<T> = SnarkIOReaderWrapper(reader, types.toSet(), priority)
|
||||
): SnarkReader<T> = SnarkReaderWrapper(reader, types.toSet(), priority)
|
@ -1,48 +0,0 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import kotlinx.html.body
|
||||
import kotlinx.html.head
|
||||
import kotlinx.html.title
|
||||
import space.kscience.dataforge.data.Data
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.Name
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
* Render (or don't) given data piece
|
||||
*/
|
||||
public interface DataRenderer {
|
||||
|
||||
context(SiteBuilder)
|
||||
public operator fun invoke(name: Name, data: Data<Any>)
|
||||
|
||||
public companion object {
|
||||
public val DEFAULT: DataRenderer = object : DataRenderer {
|
||||
|
||||
context(SiteBuilder)
|
||||
override fun invoke(name: Name, data: Data<Any>) {
|
||||
if (data.type == typeOf<HtmlData>()) {
|
||||
val languageMeta: Meta = Language.forName(name)
|
||||
|
||||
val dataMeta: Meta = if (languageMeta.isEmpty()) {
|
||||
data.meta
|
||||
} else {
|
||||
data.meta.toMutableMeta().apply {
|
||||
"languages" put languageMeta
|
||||
}
|
||||
}
|
||||
|
||||
page(name, dataMeta) {
|
||||
head {
|
||||
title = dataMeta["title"].string ?: "Untitled page"
|
||||
}
|
||||
body {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
htmlData(data as HtmlData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,8 +20,8 @@ public fun interface HtmlFragment {
|
||||
|
||||
public typealias HtmlData = Data<HtmlFragment>
|
||||
|
||||
context(WebPage)
|
||||
public fun FlowContent.htmlData(data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
|
||||
|
||||
public fun FlowContent.htmlData(page: PageContext, data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
|
||||
withSnarkPage(page) {
|
||||
with(data.await()) { consumer.renderFragment() }
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import kotlinx.html.HTML
|
||||
import space.kscience.dataforge.data.DataSet
|
||||
import space.kscience.dataforge.data.DataSetBuilder
|
||||
import space.kscience.dataforge.data.node
|
||||
import space.kscience.dataforge.data.static
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.names.Name
|
||||
|
||||
public fun interface HtmlPage {
|
||||
public fun HTML.renderPage(pageContext: PageContext, pageData: DataSet<*>)
|
||||
}
|
||||
|
||||
|
||||
// data builders
|
||||
|
||||
public fun DataSetBuilder<Any>.page(name: Name, pageMeta: Meta = Meta.EMPTY, block: HTML.(pageContext: PageContext, pageData: DataSet<Any>) -> Unit) {
|
||||
val page = HtmlPage(block)
|
||||
static<HtmlPage>(name, page, pageMeta)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// if (data.type == typeOf<HtmlData>()) {
|
||||
// val languageMeta: Meta = Language.forName(name)
|
||||
//
|
||||
// val dataMeta: Meta = if (languageMeta.isEmpty()) {
|
||||
// data.meta
|
||||
// } else {
|
||||
// data.meta.toMutableMeta().apply {
|
||||
// "languages" put languageMeta
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// page(name, dataMeta) { pageContext->
|
||||
// head {
|
||||
// title = dataMeta["title"].string ?: "Untitled page"
|
||||
// }
|
||||
// body {
|
||||
// @Suppress("UNCHECKED_CAST")
|
||||
// htmlData(pageContext, data as HtmlData)
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -0,0 +1,36 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import space.kscience.dataforge.data.DataSet
|
||||
import space.kscience.dataforge.data.DataSetBuilder
|
||||
import space.kscience.dataforge.data.static
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
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.parseAsName
|
||||
|
||||
public fun interface HtmlSite {
|
||||
public fun renderSite(siteContext: SiteContext, siteData: DataSet<Any>)
|
||||
}
|
||||
|
||||
public fun DataSetBuilder<Any>.site(
|
||||
name: Name,
|
||||
siteMeta: Meta,
|
||||
block: (siteContext: SiteContext, siteData: DataSet<Any>) -> Unit,
|
||||
) {
|
||||
static(name, HtmlSite(block), siteMeta)
|
||||
}
|
||||
|
||||
//public fun DataSetBuilder<Any>.site(name: Name, block: DataSetBuilder<Any>.() -> Unit) {
|
||||
// node(name, block)
|
||||
//}
|
||||
|
||||
internal fun DataSetBuilder<Any>.assetsFrom(rootMeta: Meta) {
|
||||
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
|
||||
val webName: String? by meta.string()
|
||||
val name by meta.string { error("File path is not provided") }
|
||||
val fileName = name.parseAsName()
|
||||
static(fileName, webName?.parseAsName() ?: fileName)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import space.kscience.dataforge.data.getItem
|
||||
import space.kscience.dataforge.data.DataSet
|
||||
import space.kscience.dataforge.data.branch
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.*
|
||||
import space.kscience.snark.SnarkBuilder
|
||||
@ -8,7 +9,6 @@ import space.kscience.snark.html.Language.Companion.SITE_LANGUAGES_KEY
|
||||
import space.kscience.snark.html.Language.Companion.SITE_LANGUAGE_KEY
|
||||
|
||||
|
||||
|
||||
public class Language : Scheme() {
|
||||
/**
|
||||
* Language key override
|
||||
@ -35,49 +35,50 @@ public class Language : Scheme() {
|
||||
|
||||
public val LANGUAGES_KEY: Name = "languages".asName()
|
||||
|
||||
public val SITE_LANGUAGE_KEY: Name = SiteBuilder.SITE_META_KEY + LANGUAGE_KEY
|
||||
public val SITE_LANGUAGE_KEY: Name = SiteContext.SITE_META_KEY + LANGUAGE_KEY
|
||||
|
||||
public val SITE_LANGUAGES_KEY: Name = SiteBuilder.SITE_META_KEY + LANGUAGES_KEY
|
||||
public val SITE_LANGUAGES_KEY: Name = SiteContext.SITE_META_KEY + LANGUAGES_KEY
|
||||
|
||||
public const val DEFAULT_LANGUAGE: String = "en"
|
||||
|
||||
/**
|
||||
* Automatically build a language map for a data piece with given [name] based on existence of appropriate data nodes.
|
||||
*/
|
||||
context(SiteBuilder)
|
||||
public fun forName(name: Name): Meta = Meta {
|
||||
val currentLanguagePrefix = languages[language]?.get(Language::prefix.name)?.string ?: language
|
||||
val fullName = (route.removeFirstOrNull(currentLanguagePrefix.asName()) ?: route) + name
|
||||
languages.forEach { (key, meta) ->
|
||||
val languagePrefix: String = meta[Language::prefix.name].string ?: key
|
||||
val nameWithLanguage: Name = if (languagePrefix.isBlank()) {
|
||||
fullName
|
||||
} else {
|
||||
languagePrefix.asName() + fullName
|
||||
}
|
||||
if (data.getItem(name) != null) {
|
||||
key put meta.asMutableMeta().apply {
|
||||
Language::target.name put nameWithLanguage.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * Automatically build a language map for a data piece with given [name] based on existence of appropriate data nodes.
|
||||
// */
|
||||
// context(SiteContext)
|
||||
// public fun forName(name: Name): Meta = Meta {
|
||||
// val currentLanguagePrefix = languages[language]?.get(Language::prefix.name)?.string ?: language
|
||||
// val fullName = (route.removeFirstOrNull(currentLanguagePrefix.asName()) ?: route) + name
|
||||
// languages.forEach { (key, meta) ->
|
||||
// val languagePrefix: String = meta[Language::prefix.name].string ?: key
|
||||
// val nameWithLanguage: Name = if (languagePrefix.isBlank()) {
|
||||
// fullName
|
||||
// } else {
|
||||
// languagePrefix.asName() + fullName
|
||||
// }
|
||||
// if (resolveData.getItem(name) != null) {
|
||||
// key put meta.asMutableMeta().apply {
|
||||
// Language::target.name put nameWithLanguage.toString()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
public val SiteBuilder.languages: Map<String, Meta>
|
||||
public val SiteContext.languages: Map<String, Meta>
|
||||
get() = siteMeta[SITE_LANGUAGES_KEY]?.items?.mapKeys { it.key.toStringUnescaped() } ?: emptyMap()
|
||||
|
||||
public val SiteBuilder.language: String
|
||||
public val SiteContext.language: String
|
||||
get() = siteMeta[SITE_LANGUAGE_KEY].string ?: Language.DEFAULT_LANGUAGE
|
||||
|
||||
public val SiteBuilder.languagePrefix: Name
|
||||
public val SiteContext.languagePrefix: Name
|
||||
get() = languages[language]?.let { it[Language::prefix.name].string ?: language }?.parseAsName() ?: Name.EMPTY
|
||||
|
||||
public fun SiteBuilder.withLanguages(languageMap: Map<String, Meta>, block: SiteBuilder.(language: String) -> Unit) {
|
||||
@SnarkBuilder
|
||||
public suspend fun SiteContext.multiLanguageSite(data: DataSet<Any>, languageMap: Map<String, Meta>, site: HtmlSite) {
|
||||
languageMap.forEach { (languageKey, languageMeta) ->
|
||||
val prefix = languageMeta[Language::prefix.name].string ?: languageKey
|
||||
val routeMeta = Meta {
|
||||
val languageSiteMeta = Meta {
|
||||
SITE_LANGUAGE_KEY put languageKey
|
||||
SITE_LANGUAGES_KEY put Meta {
|
||||
languageMap.forEach {
|
||||
@ -85,65 +86,48 @@ public fun SiteBuilder.withLanguages(languageMap: Map<String, Meta>, block: Site
|
||||
}
|
||||
}
|
||||
}
|
||||
route(prefix, routeMeta = routeMeta) {
|
||||
block(languageKey)
|
||||
}
|
||||
site(prefix.parseAsName(), data.branch(prefix), siteMeta = Laminate(languageSiteMeta, siteMeta), site)
|
||||
}
|
||||
}
|
||||
|
||||
@SnarkBuilder
|
||||
public fun SiteBuilder.withLanguages(
|
||||
vararg language: Pair<String, String>,
|
||||
block: SiteBuilder.(language: String) -> Unit,
|
||||
) {
|
||||
val languageMap = language.associate {
|
||||
it.first to Meta {
|
||||
Language::prefix.name put it.second
|
||||
}
|
||||
}
|
||||
withLanguages(languageMap, block)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The language key of this page
|
||||
*/
|
||||
public val WebPage.language: String
|
||||
public val PageContext.language: String
|
||||
get() = pageMeta[Language.LANGUAGE_KEY]?.string ?: pageMeta[SITE_LANGUAGE_KEY]?.string ?: Language.DEFAULT_LANGUAGE
|
||||
|
||||
/**
|
||||
* Mapping of language keys to other language versions of this page
|
||||
*/
|
||||
public val WebPage.languages: Map<String, Meta>
|
||||
public val PageContext.languages: Map<String, Meta>
|
||||
get() = pageMeta[Language.LANGUAGES_KEY]?.items?.mapKeys { it.key.toStringUnescaped() } ?: emptyMap()
|
||||
|
||||
public fun WebPage.localisedPageRef(pageName: Name, relative: Boolean = false): String {
|
||||
public fun PageContext.localisedPageRef(pageName: Name, relative: Boolean = false): String {
|
||||
val prefix = languages[language]?.get(Language::prefix.name)?.string?.parseAsName() ?: Name.EMPTY
|
||||
return resolvePageRef(prefix + pageName, relative)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render all pages in a node with given name. Use localization prefix if appropriate data is available.
|
||||
*/
|
||||
public fun SiteBuilder.localizedPages(
|
||||
dataPath: Name,
|
||||
remotePath: Name = dataPath,
|
||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
) {
|
||||
val item = data.getItem(languagePrefix + dataPath)
|
||||
?: data.getItem(dataPath)
|
||||
?: error("No data found by name $dataPath")
|
||||
route(remotePath) {
|
||||
pages(item, dataRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
public fun SiteBuilder.localizedPages(
|
||||
dataPath: String,
|
||||
remotePath: Name = dataPath.parseAsName(),
|
||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
) {
|
||||
localizedPages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||
}
|
||||
//
|
||||
///**
|
||||
// * Render all pages in a node with given name. Use localization prefix if appropriate data is available.
|
||||
// */
|
||||
//public fun SiteContext.localizedPages(
|
||||
// dataPath: Name,
|
||||
// remotePath: Name = dataPath,
|
||||
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
//) {
|
||||
// val item = resolveData.getItem(languagePrefix + dataPath)
|
||||
// ?: resolveData.getItem(dataPath)
|
||||
// ?: error("No data found by name $dataPath")
|
||||
// route(remotePath) {
|
||||
// pages(item, dataRenderer)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//public fun SiteContext.localizedPages(
|
||||
// dataPath: String,
|
||||
// remotePath: Name = dataPath.parseAsName(),
|
||||
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
//) {
|
||||
// localizedPages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||
//}
|
@ -1,5 +1,6 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import kotlinx.html.HTML
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.data.*
|
||||
@ -10,7 +11,6 @@ import space.kscience.dataforge.names.*
|
||||
import space.kscience.snark.SnarkBuilder
|
||||
import space.kscience.snark.SnarkContext
|
||||
|
||||
context(SnarkContext)
|
||||
public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
|
||||
if (it.hasIndex()) {
|
||||
"${it.body}[${it.index}]"
|
||||
@ -23,13 +23,7 @@ public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
|
||||
* A context for building a single page
|
||||
*/
|
||||
@SnarkBuilder
|
||||
public interface WebPage : ContextAware, SnarkContext {
|
||||
|
||||
public val snark: SnarkHtml
|
||||
|
||||
override val context: Context get() = snark.context
|
||||
|
||||
public val data: DataTree<*>
|
||||
public interface PageContext : SnarkContext {
|
||||
|
||||
/**
|
||||
* A metadata for a page. It should include site metadata
|
||||
@ -45,27 +39,27 @@ public interface WebPage : ContextAware, SnarkContext {
|
||||
/**
|
||||
* Resolve absolute url for a page with given [pageName].
|
||||
*
|
||||
* @param relative if true, add [SiteBuilder] route to the absolute page name
|
||||
* @param relative if true, add [SiteContext] route to the absolute page name
|
||||
*/
|
||||
public fun resolvePageRef(pageName: Name, relative: Boolean = false): String
|
||||
}
|
||||
|
||||
context(WebPage)
|
||||
public val page: WebPage
|
||||
get() = this@WebPage
|
||||
context(PageContext)
|
||||
public val page: PageContext
|
||||
get() = this@PageContext
|
||||
|
||||
public fun WebPage.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
|
||||
public fun PageContext.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
|
||||
|
||||
public val WebPage.homeRef: String get() = resolvePageRef(SiteBuilder.INDEX_PAGE_TOKEN.asName())
|
||||
public val PageContext.homeRef: String get() = resolvePageRef(SiteContext.INDEX_PAGE_TOKEN.asName())
|
||||
|
||||
public val WebPage.name: Name? get() = pageMeta["name"].string?.parseAsName()
|
||||
public val PageContext.name: Name? get() = pageMeta["name"].string?.parseAsName()
|
||||
|
||||
/**
|
||||
* Resolve a Html builder by its full name
|
||||
*/
|
||||
context(SnarkContext)
|
||||
public fun DataTree<*>.resolveHtmlOrNull(name: Name): HtmlData? {
|
||||
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + SiteBuilder.INDEX_PAGE_TOKEN))
|
||||
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + SiteContext.INDEX_PAGE_TOKEN))
|
||||
|
||||
return resolved?.takeIf {
|
||||
it.published //TODO add language confirmation
|
@ -1,248 +0,0 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
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.data.DataTreeItem
|
||||
import space.kscience.dataforge.data.branch
|
||||
import space.kscience.dataforge.data.getItem
|
||||
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.NameToken
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import space.kscience.snark.SnarkBuilder
|
||||
import space.kscience.snark.SnarkContext
|
||||
import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY
|
||||
|
||||
|
||||
/**
|
||||
* An abstraction, which is used to render sites to the different rendering engines
|
||||
*/
|
||||
@SnarkBuilder
|
||||
public interface SiteBuilder : ContextAware, SnarkContext {
|
||||
|
||||
/**
|
||||
* Route name of this [SiteBuilder] relative to the site root
|
||||
*/
|
||||
public val route: Name
|
||||
|
||||
/**
|
||||
* Data used for site construction. The type of the data is not limited
|
||||
*/
|
||||
public val data: DataTree<*>
|
||||
|
||||
/**
|
||||
* Snark plugin and context used for layout resolution, preprocessors, etc
|
||||
*/
|
||||
public val snark: SnarkHtml
|
||||
|
||||
override val context: Context get() = snark.context
|
||||
|
||||
/**
|
||||
* Site configuration
|
||||
*/
|
||||
public val siteMeta: Meta
|
||||
|
||||
/**
|
||||
* Serve static data as a file from [data] with given [dataName] at given [routeName].
|
||||
*/
|
||||
public fun static(dataName: Name, routeName: Name = dataName)
|
||||
|
||||
|
||||
/**
|
||||
* Create a single page at given [route]. If route is empty, create an index page at current route.
|
||||
*
|
||||
* @param pageMeta additional page meta. [WebPage] will use both it and [siteMeta]
|
||||
*/
|
||||
@SnarkBuilder
|
||||
public fun page(route: Name = Name.EMPTY, pageMeta: Meta = Meta.EMPTY, content: context(HTML, WebPage) () -> Unit)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public fun route(
|
||||
routeName: Name,
|
||||
dataOverride: DataTree<*>? = null,
|
||||
routeMeta: Meta = Meta.EMPTY,
|
||||
): SiteBuilder
|
||||
|
||||
/**
|
||||
* Creates a route and sets it as site base url
|
||||
*/
|
||||
public fun site(
|
||||
routeName: Name,
|
||||
dataOverride: DataTree<*>? = null,
|
||||
routeMeta: Meta = Meta.EMPTY,
|
||||
): SiteBuilder
|
||||
|
||||
|
||||
public companion object {
|
||||
public val SITE_META_KEY: Name = "site".asName()
|
||||
public val INDEX_PAGE_TOKEN: NameToken = NameToken("index")
|
||||
public val UP_PAGE_TOKEN: NameToken = NameToken("..")
|
||||
}
|
||||
}
|
||||
|
||||
context(SiteBuilder)
|
||||
public val site: SiteBuilder
|
||||
get() = this@SiteBuilder
|
||||
|
||||
@SnarkBuilder
|
||||
public inline fun SiteBuilder.route(
|
||||
route: Name,
|
||||
dataOverride: DataTree<*>? = null,
|
||||
routeMeta: Meta = Meta.EMPTY,
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
route(route, dataOverride, routeMeta).apply(block)
|
||||
}
|
||||
|
||||
@SnarkBuilder
|
||||
public inline fun SiteBuilder.route(
|
||||
route: String,
|
||||
dataOverride: DataTree<*>? = null,
|
||||
routeMeta: Meta = Meta.EMPTY,
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
route(route.parseAsName(), dataOverride, routeMeta).apply(block)
|
||||
}
|
||||
|
||||
@SnarkBuilder
|
||||
public inline fun SiteBuilder.site(
|
||||
route: Name,
|
||||
dataOverride: DataTree<*>? = null,
|
||||
routeMeta: Meta = Meta.EMPTY,
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
site(route, dataOverride, routeMeta).apply(block)
|
||||
}
|
||||
|
||||
@SnarkBuilder
|
||||
public inline fun SiteBuilder.site(
|
||||
route: String,
|
||||
dataOverride: DataTree<*>? = null,
|
||||
routeMeta: Meta = Meta.EMPTY,
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
site(route.parseAsName(), dataOverride, routeMeta).apply(block)
|
||||
}
|
||||
|
||||
public inline fun SiteBuilder.withData(
|
||||
data: DataTree<*>,
|
||||
block: SiteBuilder.() -> Unit
|
||||
){
|
||||
route(Name.EMPTY, data).apply(block)
|
||||
}
|
||||
|
||||
public inline fun SiteBuilder.withDataBranch(
|
||||
name: Name,
|
||||
block: SiteBuilder.() -> Unit
|
||||
){
|
||||
route(Name.EMPTY, data.branch(name)).apply(block)
|
||||
}
|
||||
|
||||
public inline fun SiteBuilder.withDataBranch(
|
||||
name: String,
|
||||
block: SiteBuilder.() -> Unit
|
||||
){
|
||||
route(Name.EMPTY, data.branch(name)).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()
|
||||
// }
|
||||
//}
|
||||
|
||||
public fun SiteBuilder.static(dataName: String): Unit = static(dataName.parseAsName())
|
||||
|
||||
public fun SiteBuilder.static(dataName: String, routeName: String): Unit = static(
|
||||
dataName.parseAsName(),
|
||||
routeName.parseAsName()
|
||||
)
|
||||
|
||||
internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
|
||||
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
|
||||
val webName: String? by meta.string()
|
||||
val name by meta.string { error("File path is not provided") }
|
||||
val fileName = name.parseAsName()
|
||||
static(fileName, webName?.parseAsName() ?: fileName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively renders the data items in [data]. If [LAYOUT_KEY] is defined in an item, use it to load
|
||||
* layout from the context, otherwise render children nodes as name segments and individual data items using [dataRenderer].
|
||||
*/
|
||||
public fun SiteBuilder.pages(
|
||||
data: DataTreeItem<*>,
|
||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
) {
|
||||
val layoutMeta = data.meta[LAYOUT_KEY]
|
||||
if (layoutMeta != null) {
|
||||
//use layout if it is defined
|
||||
snark.siteLayout(layoutMeta).render(data)
|
||||
} else {
|
||||
when (data) {
|
||||
is DataTreeItem.Node -> {
|
||||
data.tree.items.forEach { (token, item) ->
|
||||
//Don't apply index token
|
||||
if (token == SiteLayout.INDEX_PAGE_TOKEN) {
|
||||
pages(item, dataRenderer)
|
||||
} else if (item is DataTreeItem.Leaf) {
|
||||
dataRenderer(token.asName(), item.data)
|
||||
} else {
|
||||
route(token.asName()) {
|
||||
pages(item, dataRenderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DataTreeItem.Leaf -> {
|
||||
dataRenderer(Name.EMPTY, data.data)
|
||||
}
|
||||
}
|
||||
data.meta[SiteLayout.ASSETS_KEY]?.let {
|
||||
assetsFrom(it)
|
||||
}
|
||||
}
|
||||
//TODO watch for changes
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all pages in a node with given name
|
||||
*/
|
||||
public fun SiteBuilder.pages(
|
||||
dataPath: Name,
|
||||
remotePath: Name = dataPath,
|
||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
) {
|
||||
val item = data.getItem(dataPath) ?: error("No data found by name $dataPath")
|
||||
route(remotePath) {
|
||||
pages(item, dataRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
public fun SiteBuilder.pages(
|
||||
dataPath: String,
|
||||
remotePath: Name = dataPath.parseAsName(),
|
||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
) {
|
||||
pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import kotlinx.html.HTML
|
||||
import space.kscience.dataforge.data.*
|
||||
import space.kscience.dataforge.io.Binary
|
||||
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.asName
|
||||
import space.kscience.dataforge.workspace.Workspace
|
||||
import space.kscience.snark.SnarkBuilder
|
||||
import space.kscience.snark.SnarkContext
|
||||
|
||||
|
||||
/**
|
||||
* An abstraction, which is used to render sites to the different rendering engines
|
||||
*/
|
||||
@SnarkBuilder
|
||||
public interface SiteContext : SnarkContext {
|
||||
|
||||
/**
|
||||
* Route name of this [SiteContext] relative to the site root
|
||||
*/
|
||||
public val route: Name
|
||||
|
||||
/**
|
||||
* Site configuration
|
||||
*/
|
||||
public val siteMeta: Meta
|
||||
|
||||
/**
|
||||
* Renders a static file or resource for the given route and data.
|
||||
*
|
||||
* @param route The route name of the static file relative to the site root.
|
||||
* @param data The data object containing the binary data for the static file.
|
||||
*/
|
||||
public suspend fun static(route: Name, data: Data<Binary>)
|
||||
|
||||
|
||||
/**
|
||||
* Create a single page at given [route]. If route is empty, create an index page at current route.
|
||||
*
|
||||
* @param pageMeta additional page meta. [PageContext] will use both it and [siteMeta]
|
||||
*/
|
||||
@SnarkBuilder
|
||||
public suspend fun page(
|
||||
route: Name,
|
||||
data: DataSet<Any>,
|
||||
pageMeta: Meta = Meta.EMPTY,
|
||||
htmlPage: HtmlPage,
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Creates a sub-site and sets it as site base url
|
||||
* @param route mount site at [rootName]
|
||||
* @param dataPrefix prefix path for data used in this site
|
||||
*/
|
||||
@SnarkBuilder
|
||||
public suspend fun site(
|
||||
route: Name,
|
||||
data: DataSet<Any>,
|
||||
siteMeta: Meta = Meta.EMPTY,
|
||||
htmlSite: HtmlSite,
|
||||
)
|
||||
|
||||
|
||||
public companion object {
|
||||
public val SITE_META_KEY: Name = "site".asName()
|
||||
public val INDEX_PAGE_TOKEN: NameToken = NameToken("index")
|
||||
public val UP_PAGE_TOKEN: NameToken = NameToken("..")
|
||||
}
|
||||
}
|
||||
|
||||
@SnarkBuilder
|
||||
public suspend fun SiteContext.page(
|
||||
route: Name,
|
||||
data: DataSet<Any>,
|
||||
pageMeta: Meta = Meta.EMPTY,
|
||||
htmlPage: HTML.(page: PageContext, data: DataSet<Any>) -> Unit,
|
||||
): Unit = page(route, data, pageMeta, HtmlPage(htmlPage))
|
||||
|
||||
context(SiteContext)
|
||||
public val site: SiteContext
|
||||
get() = this@SiteContext
|
||||
|
||||
|
||||
public suspend fun SiteContext.renderPages(data: DataSet<Any>): Unit {
|
||||
|
||||
// Render all sub-sites
|
||||
data.filterByType<HtmlSite>().forEach { siteData: NamedData<HtmlSite> ->
|
||||
// generate a sub-site context and render the data in sub-site context
|
||||
val dataPrefix = siteData.meta["site.dataPath"].string?.asName() ?: Name.EMPTY
|
||||
site(
|
||||
route = siteData.meta["site.route"].string?.asName() ?: siteData.name,
|
||||
data.branch(dataPrefix),
|
||||
siteMeta = siteData.meta,
|
||||
siteData.await()
|
||||
)
|
||||
}
|
||||
|
||||
// Render all stand-alone pages in default site
|
||||
data.filterByType<HtmlPage>().forEach { pageData: NamedData<HtmlPage> ->
|
||||
val dataPrefix = pageData.meta["page.dataPath"].string?.asName() ?: Name.EMPTY
|
||||
page(
|
||||
route = pageData.meta["page.route"].string?.asName() ?: pageData.name,
|
||||
data.branch(dataPrefix),
|
||||
pageMeta = pageData.meta,
|
||||
pageData.await()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
///**
|
||||
// * Recursively renders the data items in [data]. If [LAYOUT_KEY] is defined in an item, use it to load
|
||||
// * layout from the context, otherwise render children nodes as name segments and individual data items using [dataRenderer].
|
||||
// */
|
||||
//public fun SiteContext.pages(
|
||||
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
//) {
|
||||
// val layoutMeta = siteData().meta[LAYOUT_KEY]
|
||||
// if (layoutMeta != null) {
|
||||
// //use layout if it is defined
|
||||
// snark.siteLayout(layoutMeta).render(siteData())
|
||||
// } else {
|
||||
// when (siteData()) {
|
||||
// is DataTreeItem.Node -> {
|
||||
// siteData().tree.items.forEach { (token, item) ->
|
||||
// //Don't apply index token
|
||||
// if (token == SiteLayout.INDEX_PAGE_TOKEN) {
|
||||
// pages(item, dataRenderer)
|
||||
// } else if (item is DataTreeItem.Leaf) {
|
||||
// dataRenderer(token.asName(), item.data)
|
||||
// } else {
|
||||
// route(token.asName()) {
|
||||
// pages(item, dataRenderer)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// is DataTreeItem.Leaf -> {
|
||||
// dataRenderer(Name.EMPTY, siteData().data)
|
||||
// }
|
||||
// }
|
||||
// siteData().meta[SiteLayout.ASSETS_KEY]?.let {
|
||||
// assetsFrom(it)
|
||||
// }
|
||||
// }
|
||||
// //TODO watch for changes
|
||||
//}
|
||||
//
|
||||
///**
|
||||
// * Render all pages in a node with given name
|
||||
// */
|
||||
//public fun SiteContext.pages(
|
||||
// dataPath: Name,
|
||||
// remotePath: Name = dataPath,
|
||||
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
//) {
|
||||
// val item = resolveData.getItem(dataPath) ?: error("No data found by name $dataPath")
|
||||
// route(remotePath) {
|
||||
// pages(item, dataRenderer)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//public fun SiteContext.pages(
|
||||
// dataPath: String,
|
||||
// remotePath: Name = dataPath.parseAsName(),
|
||||
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||
//) {
|
||||
// pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||
//}
|
@ -1,32 +0,0 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import space.kscience.dataforge.data.DataTreeItem
|
||||
import space.kscience.dataforge.misc.DfId
|
||||
import space.kscience.dataforge.names.NameToken
|
||||
|
||||
/**
|
||||
* An abstraction to render singular data or a data tree.
|
||||
*/
|
||||
@DfId(SiteLayout.TYPE)
|
||||
public fun interface SiteLayout {
|
||||
|
||||
context(SiteBuilder)
|
||||
public fun render(item: DataTreeItem<*>)
|
||||
|
||||
public companion object {
|
||||
public const val TYPE: String = "snark.layout"
|
||||
public const val LAYOUT_KEY: String = "layout"
|
||||
public const val ASSETS_KEY: String = "assets"
|
||||
public val INDEX_PAGE_TOKEN: NameToken = NameToken("index")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The default [SiteLayout]. It renders all [HtmlData] pages with simple headers via [SiteLayout.defaultDataRenderer]
|
||||
*/
|
||||
public object DefaultSiteLayout : SiteLayout {
|
||||
context(SiteBuilder) override fun render(item: DataTreeItem<*>) {
|
||||
pages(item)
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import io.ktor.http.ContentType
|
||||
import kotlinx.io.readByteArray
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.data.*
|
||||
import space.kscience.dataforge.io.Binary
|
||||
import space.kscience.dataforge.io.IOPlugin
|
||||
import space.kscience.dataforge.io.IOReader
|
||||
import space.kscience.dataforge.io.JsonMetaFormat
|
||||
@ -21,18 +20,12 @@ import space.kscience.dataforge.provider.dfId
|
||||
import space.kscience.dataforge.workspace.*
|
||||
import space.kscience.snark.ImageIOReader
|
||||
import space.kscience.snark.Snark
|
||||
import space.kscience.snark.SnarkIOReader
|
||||
import space.kscience.snark.SnarkReader
|
||||
import space.kscience.snark.TextProcessor
|
||||
import java.net.URLConnection
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.extension
|
||||
|
||||
public fun <T : Any> SnarkIOReader(
|
||||
reader: IOReader<T>,
|
||||
vararg types: ContentType,
|
||||
priority: Int = SnarkIOReader.DEFAULT_PRIORITY,
|
||||
): SnarkIOReader<T> = SnarkIOReader(reader, *types.map { it.toString() }.toTypedArray(), priority = priority)
|
||||
|
||||
|
||||
/**
|
||||
* A plugin used for rendering a [DataTree] as HTML
|
||||
@ -44,33 +37,17 @@ public class SnarkHtml : WorkspacePlugin() {
|
||||
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
|
||||
/**
|
||||
* Lazy-initialized variable that holds a map of site layouts.
|
||||
*
|
||||
* @property siteLayouts The map of site layouts, where the key is the layout name and the value is the corresponding SiteLayout object.
|
||||
*/
|
||||
private val siteLayouts: Map<Name, SiteLayout> by lazy {
|
||||
context.gather(SiteLayout.TYPE, true)
|
||||
}
|
||||
|
||||
|
||||
internal fun siteLayout(layoutMeta: Meta): SiteLayout {
|
||||
val layoutName = layoutMeta.string
|
||||
?: layoutMeta["name"].string ?: error("Layout name not defined in $layoutMeta")
|
||||
return siteLayouts[layoutName.parseAsName()] ?: error("Layout with name $layoutName not found in $this")
|
||||
}
|
||||
|
||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||
SnarkIOReader::class.dfId -> mapOf(
|
||||
SnarkReader::class.dfId -> mapOf(
|
||||
"html".asName() to HtmlReader,
|
||||
"markdown".asName() to MarkdownReader,
|
||||
"json".asName() to SnarkIOReader(JsonMetaFormat, ContentType.Application.Json),
|
||||
"yaml".asName() to SnarkIOReader(YamlMetaFormat, "text/yaml", "yaml"),
|
||||
"png".asName() to SnarkIOReader(ImageIOReader, ContentType.Image.PNG),
|
||||
"jpg".asName() to SnarkIOReader(ImageIOReader, ContentType.Image.JPEG),
|
||||
"gif".asName() to SnarkIOReader(ImageIOReader, ContentType.Image.GIF),
|
||||
"svg".asName() to SnarkIOReader(IOReader.binary, ContentType.Image.SVG, ContentType.parse("svg")),
|
||||
"raw".asName() to SnarkIOReader(
|
||||
"json".asName() to SnarkReader(JsonMetaFormat, ContentType.Application.Json.toString()),
|
||||
"yaml".asName() to SnarkReader(YamlMetaFormat, "text/yaml", "yaml"),
|
||||
"png".asName() to SnarkReader(ImageIOReader, ContentType.Image.PNG.toString()),
|
||||
"jpg".asName() to SnarkReader(ImageIOReader, ContentType.Image.JPEG.toString()),
|
||||
"gif".asName() to SnarkReader(ImageIOReader, ContentType.Image.GIF.toString()),
|
||||
"svg".asName() to SnarkReader(IOReader.binary, ContentType.Image.SVG.toString(), "svg"),
|
||||
"raw".asName() to SnarkReader(
|
||||
IOReader.binary,
|
||||
"css",
|
||||
"js",
|
||||
@ -86,13 +63,6 @@ public class SnarkHtml : WorkspacePlugin() {
|
||||
else -> super.content(target)
|
||||
}
|
||||
|
||||
// public val assets: TaskReference<Binary> by task<Binary> {
|
||||
// node(Name.EMPTY, from(allData).filter { name, meta ->
|
||||
//
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
public val preprocess: TaskReference<String> by task<String> {
|
||||
pipeFrom<String, String>(dataByType<String>()) { text, _, meta ->
|
||||
meta[TextProcessor.TEXT_TRANSFORMATION_KEY]?.let {
|
||||
@ -128,6 +98,10 @@ public class SnarkHtml : WorkspacePlugin() {
|
||||
}
|
||||
|
||||
|
||||
// public val site by task<Any> {
|
||||
//
|
||||
// }
|
||||
|
||||
// public val textTransformationAction: Action<String, String> = Action.map<String, String> {
|
||||
// val transformations = actionMeta.getIndexed("transformation").entries.sortedBy {
|
||||
// it.key?.toIntOrNull() ?: 0
|
||||
@ -144,7 +118,7 @@ public class SnarkHtml : WorkspacePlugin() {
|
||||
readByteArray()
|
||||
}
|
||||
|
||||
internal val byteArraySnarkParser = SnarkIOReader(byteArrayIOReader)
|
||||
internal val byteArraySnarkParser = SnarkReader(byteArrayIOReader)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,200 +0,0 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.html.HTML
|
||||
import kotlinx.html.html
|
||||
import kotlinx.html.stream.createHTML
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.buffered
|
||||
import space.kscience.dataforge.data.DataTree
|
||||
import space.kscience.dataforge.data.DataTreeItem
|
||||
import space.kscience.dataforge.data.await
|
||||
import space.kscience.dataforge.data.getItem
|
||||
import space.kscience.dataforge.io.Binary
|
||||
import space.kscience.dataforge.io.writeBinary
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.isEmpty
|
||||
import space.kscience.dataforge.names.plus
|
||||
import space.kscience.dataforge.workspace.FileData
|
||||
import java.nio.file.Path
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.io.path.*
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of [SiteBuilder] to render site as a static directory [outputPath]
|
||||
*/
|
||||
internal class StaticSiteBuilder(
|
||||
override val snark: SnarkHtml,
|
||||
override val data: DataTree<*>,
|
||||
override val siteMeta: Meta,
|
||||
private val baseUrl: String,
|
||||
override val route: Name,
|
||||
private val outputPath: 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()) {
|
||||
// //avoid re-creating directories
|
||||
// source.copyTo(destination, true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private suspend fun files(item: DataTreeItem<Any>, routeName: Name) {
|
||||
//try using direct file rendering
|
||||
item.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
||||
val file = Path.of(it)
|
||||
val targetPath = outputPath.resolve(routeName.toWebPath())
|
||||
targetPath.parent.createDirectories()
|
||||
file.copyToRecursively(targetPath, followLinks = false)
|
||||
//success, don't do anything else
|
||||
return@files
|
||||
}
|
||||
|
||||
when (item) {
|
||||
is DataTreeItem.Leaf -> {
|
||||
val datum = item.data
|
||||
if (datum.type != typeOf<Binary>()) error("Can't directly serve file of type ${item.data.type}")
|
||||
val targetPath = outputPath.resolve(routeName.toWebPath())
|
||||
val binary = datum.await() as Binary
|
||||
targetPath.outputStream().asSink().buffered().use {
|
||||
it.writeBinary(binary)
|
||||
}
|
||||
}
|
||||
|
||||
is DataTreeItem.Node -> {
|
||||
item.tree.items.forEach { (token, childItem) ->
|
||||
files(childItem, routeName + token)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun static(dataName: Name, routeName: Name) {
|
||||
val item: DataTreeItem<Any> = data.getItem(dataName) ?: error("Data with name $dataName is not resolved")
|
||||
runBlocking {
|
||||
files(item, routeName)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// override fun file(file: Path, webPath: String) {
|
||||
// val targetPath = outputPath.resolve(webPath)
|
||||
// if (file.isDirectory()) {
|
||||
// targetPath.parent.createDirectories()
|
||||
// file.copyRecursively(targetPath)
|
||||
// } else if (webPath.isBlank()) {
|
||||
// error("Can't mount file to an empty route")
|
||||
// } else {
|
||||
// targetPath.parent.createDirectories()
|
||||
// file.copyTo(targetPath, true)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun resourceFile(resourcesPath: String, webPath: String) {
|
||||
// val targetPath = outputPath.resolve(webPath)
|
||||
// targetPath.parent.createDirectories()
|
||||
// javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true)
|
||||
// }
|
||||
//
|
||||
// override fun resourceDirectory(resourcesPath: String) {
|
||||
// outputPath.parent.createDirectories()
|
||||
// javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(outputPath)
|
||||
// }
|
||||
|
||||
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
|
||||
ref
|
||||
} else if (ref.isEmpty()) {
|
||||
baseUrl
|
||||
} else {
|
||||
"${baseUrl.removeSuffix("/")}/$ref"
|
||||
}
|
||||
|
||||
inner class StaticWebPage(override val pageMeta: Meta) : WebPage {
|
||||
override val data: DataTree<*> get() = this@StaticSiteBuilder.data
|
||||
|
||||
override val snark: SnarkHtml get() = this@StaticSiteBuilder.snark
|
||||
|
||||
|
||||
override fun resolveRef(ref: String): String =
|
||||
this@StaticSiteBuilder.resolveRef(this@StaticSiteBuilder.baseUrl, ref)
|
||||
|
||||
override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef(
|
||||
(if (relative) this@StaticSiteBuilder.route + pageName else pageName).toWebPath() + ".html"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override fun page(route: Name, pageMeta: Meta, content: context(HTML) WebPage.() -> Unit) {
|
||||
val htmlBuilder = createHTML()
|
||||
|
||||
val modifiedPageMeta = pageMeta.toMutableMeta().apply {
|
||||
"name" put route.toString()
|
||||
}
|
||||
|
||||
htmlBuilder.html {
|
||||
content(this, StaticWebPage(Laminate(modifiedPageMeta, siteMeta)))
|
||||
}
|
||||
|
||||
val newPath = if (route.isEmpty()) {
|
||||
outputPath.resolve("index.html")
|
||||
} else {
|
||||
outputPath.resolve(route.toWebPath() + ".html")
|
||||
}
|
||||
|
||||
newPath.parent.createDirectories()
|
||||
newPath.writeText(htmlBuilder.finalize())
|
||||
}
|
||||
|
||||
override fun route(
|
||||
routeName: Name,
|
||||
dataOverride: DataTree<*>?,
|
||||
routeMeta: Meta,
|
||||
): SiteBuilder = StaticSiteBuilder(
|
||||
snark = snark,
|
||||
data = dataOverride ?: data,
|
||||
siteMeta = Laminate(routeMeta, siteMeta),
|
||||
baseUrl = baseUrl,
|
||||
route = route + routeName,
|
||||
outputPath = outputPath.resolve(routeName.toWebPath())
|
||||
)
|
||||
|
||||
override fun site(
|
||||
routeName: Name,
|
||||
dataOverride: DataTree<*>?,
|
||||
routeMeta: Meta,
|
||||
): SiteBuilder = StaticSiteBuilder(
|
||||
snark = snark,
|
||||
data = dataOverride ?: data,
|
||||
siteMeta = Laminate(routeMeta, siteMeta),
|
||||
baseUrl = if (baseUrl == "") "" else resolveRef(baseUrl, routeName.toWebPath()),
|
||||
route = Name.EMPTY,
|
||||
outputPath = outputPath.resolve(routeName.toWebPath())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a static site using given [SnarkEnvironment] in provided [outputPath].
|
||||
* Use [siteUrl] as a base for all resolved URLs. By default, use [outputPath] absolute path as a base.
|
||||
*
|
||||
*/
|
||||
public fun SnarkHtml.static(
|
||||
data: DataTree<*>,
|
||||
outputPath: Path,
|
||||
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
|
||||
siteMeta: Meta = data.meta,
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
StaticSiteBuilder(this, data, siteMeta, siteUrl, Name.EMPTY, outputPath).block()
|
||||
}
|
@ -1,22 +1,19 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import kotlinx.html.A
|
||||
import kotlinx.html.FlowContent
|
||||
import kotlinx.html.Tag
|
||||
import kotlinx.html.TagConsumer
|
||||
import kotlinx.html.*
|
||||
import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import space.kscience.snark.TextProcessor
|
||||
|
||||
public class WebPageTextProcessor(private val page: WebPage) : TextProcessor {
|
||||
public class WebPageTextProcessor(private val page: PageContext) : TextProcessor {
|
||||
private val regex = """\$\{([\w.]*)(?>\("(.*)"\))?}""".toRegex()
|
||||
|
||||
/**
|
||||
* A basic [TextProcessor] that replaces `${...}` expressions in text. The following expressions are recognised:
|
||||
* * `homeRef` resolves to [homeRef]
|
||||
* * `resolveRef("...")` -> [WebPage.resolveRef]
|
||||
* * `resolvePageRef("...")` -> [WebPage.resolvePageRef]
|
||||
* * `pageMeta.get("...") -> [WebPage.pageMeta] get string method
|
||||
* * `resolveRef("...")` -> [PageContext.resolveRef]
|
||||
* * `resolvePageRef("...")` -> [PageContext.resolvePageRef]
|
||||
* * `pageMeta.get("...") -> [PageContext.pageMeta] get string method
|
||||
* Otherwise return unchanged string
|
||||
*/
|
||||
override fun process(text: CharSequence): String = text.replace(regex) { match ->
|
||||
@ -45,9 +42,8 @@ public class WebPageTextProcessor(private val page: WebPage) : TextProcessor {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class WebPagePostprocessor<out R>(
|
||||
public val page: WebPage,
|
||||
public val page: PageContext,
|
||||
private val consumer: TagConsumer<R>,
|
||||
) : TagConsumer<R> by consumer {
|
||||
|
||||
@ -64,9 +60,20 @@ public class WebPagePostprocessor<out R>(
|
||||
override fun onTagContent(content: CharSequence) {
|
||||
consumer.onTagContent(processor.process(content))
|
||||
}
|
||||
|
||||
override fun onTagContentUnsafe(block: Unsafe.() -> Unit) {
|
||||
val proxy = object :Unsafe{
|
||||
override fun String.unaryPlus() {
|
||||
consumer.onTagContentUnsafe {
|
||||
processor.process(this@unaryPlus).unaryPlus()
|
||||
}
|
||||
}
|
||||
}
|
||||
proxy.block()
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun FlowContent.withSnarkPage(page: WebPage, block: FlowContent.() -> Unit) {
|
||||
public inline fun FlowContent.withSnarkPage(page: PageContext, block: FlowContent.() -> Unit) {
|
||||
val fc = object : FlowContent by this {
|
||||
override val consumer: TagConsumer<*> = WebPagePostprocessor(page, this@withSnarkPage.consumer)
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
package space.kscience.snark.html
|
||||
|
||||
import space.kscience.dataforge.data.DataTreeItem
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
|
||||
public class SnarkHtmlEnvironmentBuilder {
|
||||
public val layouts: HashMap<Name, SiteLayout> = HashMap()
|
||||
|
||||
public fun layout(name: String, body: context(SiteBuilder) (DataTreeItem<*>) -> Unit) {
|
||||
layouts[name.parseAsName()] = object : SiteLayout {
|
||||
context(SiteBuilder) override fun render(item: DataTreeItem<*>) = body(site, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//public fun SnarkEnvironment.html(block: SnarkHtmlEnvironmentBuilder.() -> Unit) {
|
||||
// contract {
|
||||
// callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
// }
|
||||
//
|
||||
// val envBuilder = SnarkHtmlEnvironmentBuilder().apply(block)
|
||||
//
|
||||
// val plugin = object : AbstractPlugin() {
|
||||
// val snark by require(SnarkHtmlPlugin)
|
||||
//
|
||||
// override val tag: PluginTag = PluginTag("@extension[${hashCode()}]")
|
||||
//
|
||||
//
|
||||
// override fun content(target: String): Map<Name, Any> = when (target) {
|
||||
// SiteLayout.TYPE -> envBuilder.layouts
|
||||
// else -> super.content(target)
|
||||
// }
|
||||
// }
|
||||
// registerPlugin(plugin)
|
||||
//}
|
@ -8,11 +8,11 @@ import kotlinx.io.readString
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||
import org.intellij.markdown.html.HtmlGenerator
|
||||
import org.intellij.markdown.parser.MarkdownParser
|
||||
import space.kscience.snark.SnarkIOReader
|
||||
import space.kscience.snark.SnarkReader
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
public object HtmlReader : SnarkIOReader<HtmlFragment> {
|
||||
public object HtmlReader : SnarkReader<HtmlFragment> {
|
||||
override val types: Set<String> = setOf("html")
|
||||
|
||||
override fun readFrom(source: String): HtmlFragment = HtmlFragment {
|
||||
@ -25,7 +25,7 @@ public object HtmlReader : SnarkIOReader<HtmlFragment> {
|
||||
override val type: KType = typeOf<HtmlFragment>()
|
||||
}
|
||||
|
||||
public object MarkdownReader : SnarkIOReader<HtmlFragment> {
|
||||
public object MarkdownReader : SnarkReader<HtmlFragment> {
|
||||
override val type: KType = typeOf<HtmlFragment>()
|
||||
|
||||
override val types: Set<String> = setOf("text/markdown", "md", "markdown")
|
||||
@ -46,7 +46,7 @@ public object MarkdownReader : SnarkIOReader<HtmlFragment> {
|
||||
|
||||
override fun readFrom(source: Source): HtmlFragment = readFrom(source.readString())
|
||||
|
||||
public val snarkReader: SnarkIOReader<HtmlFragment> = SnarkIOReader(this, ContentType.parse("text/markdown"))
|
||||
public val snarkReader: SnarkReader<HtmlFragment> = SnarkReader(this, "text/markdown")
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,156 @@
|
||||
package space.kscience.snark.html.static
|
||||
|
||||
import kotlinx.html.html
|
||||
import kotlinx.html.stream.createHTML
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.buffered
|
||||
import space.kscience.dataforge.data.*
|
||||
import space.kscience.dataforge.io.Binary
|
||||
import space.kscience.dataforge.io.writeBinary
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.isEmpty
|
||||
import space.kscience.dataforge.names.plus
|
||||
import space.kscience.dataforge.workspace.FileData
|
||||
import space.kscience.dataforge.workspace.Workspace
|
||||
import space.kscience.snark.html.*
|
||||
import java.nio.file.Path
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.io.path.*
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of [SiteContext] to render site as a static directory [outputPath]
|
||||
*/
|
||||
internal class StaticSiteContext(
|
||||
override val siteMeta: Meta,
|
||||
private val baseUrl: String,
|
||||
override val route: Name,
|
||||
private val outputPath: Path,
|
||||
) : SiteContext {
|
||||
|
||||
|
||||
|
||||
// @OptIn(ExperimentalPathApi::class)
|
||||
// private suspend fun files(item: DataTreeItem<Any>, routeName: Name) {
|
||||
// //try using direct file rendering
|
||||
// item.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
||||
// val file = Path.of(it)
|
||||
// val targetPath = outputPath.resolve(routeName.toWebPath())
|
||||
// targetPath.parent.createDirectories()
|
||||
// file.copyToRecursively(targetPath, followLinks = false)
|
||||
// //success, don't do anything else
|
||||
// return@files
|
||||
// }
|
||||
//
|
||||
// when (item) {
|
||||
// is DataTreeItem.Leaf -> {
|
||||
// val datum = item.data
|
||||
// if (datum.type != typeOf<Binary>()) error("Can't directly serve file of type ${item.data.type}")
|
||||
// val targetPath = outputPath.resolve(routeName.toWebPath())
|
||||
// val binary = datum.await() as Binary
|
||||
// targetPath.outputStream().asSink().buffered().use {
|
||||
// it.writeBinary(binary)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// is DataTreeItem.Node -> {
|
||||
// item.tree.items.forEach { (token, childItem) ->
|
||||
// files(childItem, routeName + token)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
override suspend fun static(route: Name, data: Data<Binary>) {
|
||||
data.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
||||
val file = Path.of(it)
|
||||
val targetPath = outputPath.resolve(route.toWebPath())
|
||||
targetPath.parent.createDirectories()
|
||||
file.copyToRecursively(targetPath, followLinks = false)
|
||||
//success, don't do anything else
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type != typeOf<Binary>()) error("Can't directly serve file of type ${data.type}")
|
||||
val targetPath = outputPath.resolve(route.toWebPath())
|
||||
val binary = data.await()
|
||||
targetPath.outputStream().asSink().buffered().use {
|
||||
it.writeBinary(binary)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
|
||||
ref
|
||||
} else if (ref.isEmpty()) {
|
||||
baseUrl
|
||||
} else {
|
||||
"${baseUrl.removeSuffix("/")}/$ref"
|
||||
}
|
||||
|
||||
inner class StaticPageContext(override val pageMeta: Meta) : PageContext {
|
||||
|
||||
override fun resolveRef(ref: String): String =
|
||||
this@StaticSiteContext.resolveRef(this@StaticSiteContext.baseUrl, ref)
|
||||
|
||||
override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef(
|
||||
(if (relative) this@StaticSiteContext.route + pageName else pageName).toWebPath() + ".html"
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun page(route: Name, data: DataSet<Any>, pageMeta: Meta, htmlPage: HtmlPage) {
|
||||
|
||||
val htmlBuilder = createHTML()
|
||||
|
||||
val modifiedPageMeta = pageMeta.toMutableMeta().apply {
|
||||
"name" put route.toString()
|
||||
}
|
||||
|
||||
htmlBuilder.html {
|
||||
with(htmlPage) {
|
||||
renderPage(StaticPageContext(Laminate(modifiedPageMeta, siteMeta)), data)
|
||||
}
|
||||
}
|
||||
|
||||
val newPath = if (route.isEmpty()) {
|
||||
outputPath.resolve("index.html")
|
||||
} else {
|
||||
outputPath.resolve(route.toWebPath() + ".html")
|
||||
}
|
||||
|
||||
newPath.parent.createDirectories()
|
||||
newPath.writeText(htmlBuilder.finalize())
|
||||
}
|
||||
|
||||
override suspend fun site(route: Name, data: DataSet<Any>, siteMeta: Meta, htmlSite: HtmlSite) {
|
||||
val subSiteContext = StaticSiteContext(
|
||||
siteMeta = Laminate(siteMeta, this.siteMeta),
|
||||
baseUrl = if (baseUrl == "") "" else resolveRef(baseUrl, route.toWebPath()),
|
||||
route = Name.EMPTY,
|
||||
outputPath = outputPath.resolve(route.toWebPath())
|
||||
)
|
||||
htmlSite.renderSite(subSiteContext, data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a static site using given [SnarkEnvironment] in provided [outputPath].
|
||||
* Use [siteUrl] as a base for all resolved URLs. By default, use [outputPath] absolute path as a base.
|
||||
*
|
||||
*/
|
||||
public fun SnarkHtml.staticSite(
|
||||
data: DataSet<*>,
|
||||
outputPath: Path,
|
||||
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
|
||||
siteMeta: Meta = data.meta,
|
||||
block: SiteContext.() -> Unit,
|
||||
) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
StaticSiteContext(siteMeta, siteUrl, Name.EMPTY, outputPath).block()
|
||||
}
|
@ -10,21 +10,16 @@ import io.ktor.server.html.respondHtml
|
||||
import io.ktor.server.http.content.staticFiles
|
||||
import io.ktor.server.plugins.origin
|
||||
import io.ktor.server.response.respondBytes
|
||||
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 io.ktor.server.routing.*
|
||||
import kotlinx.css.CssBuilder
|
||||
import kotlinx.html.CommonAttributeGroupFacade
|
||||
import kotlinx.html.HTML
|
||||
import kotlinx.html.head
|
||||
import kotlinx.html.style
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.context.error
|
||||
import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.data.DataTree
|
||||
import space.kscience.dataforge.data.DataTreeItem
|
||||
import space.kscience.dataforge.data.await
|
||||
import space.kscience.dataforge.data.getItem
|
||||
import space.kscience.dataforge.data.*
|
||||
import space.kscience.dataforge.io.Binary
|
||||
import space.kscience.dataforge.io.toByteArray
|
||||
import space.kscience.dataforge.meta.*
|
||||
@ -33,10 +28,7 @@ import space.kscience.dataforge.names.cutLast
|
||||
import space.kscience.dataforge.names.endsWith
|
||||
import space.kscience.dataforge.names.plus
|
||||
import space.kscience.dataforge.workspace.FileData
|
||||
import space.kscience.snark.html.SiteBuilder
|
||||
import space.kscience.snark.html.SnarkHtml
|
||||
import space.kscience.snark.html.WebPage
|
||||
import space.kscience.snark.html.toWebPath
|
||||
import space.kscience.snark.html.*
|
||||
import java.nio.file.Path
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
@ -47,17 +39,16 @@ public fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
|
||||
}
|
||||
|
||||
public class KtorSiteBuilder(
|
||||
override val snark: SnarkHtml,
|
||||
override val data: DataTree<*>,
|
||||
override val context: Context,
|
||||
override val siteMeta: Meta,
|
||||
private val baseUrl: String,
|
||||
override val route: Name,
|
||||
private val ktorRoute: Route,
|
||||
) : SiteBuilder {
|
||||
) : SiteContext, ContextAware {
|
||||
|
||||
private fun files(item: DataTreeItem<Any>, routeName: Name) {
|
||||
//try using direct file rendering
|
||||
item.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
||||
|
||||
override suspend fun static(route: Name, data: Data<Binary>) {
|
||||
data.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
||||
val file = try {
|
||||
Path.of(it).toFile()
|
||||
} catch (ex: Exception) {
|
||||
@ -66,62 +57,27 @@ public class KtorSiteBuilder(
|
||||
return@let
|
||||
}
|
||||
|
||||
val fileName = routeName.toWebPath()
|
||||
val fileName = route.toWebPath()
|
||||
ktorRoute.staticFiles(fileName, file)
|
||||
//success, don't do anything else
|
||||
return@files
|
||||
return
|
||||
}
|
||||
when (item) {
|
||||
is DataTreeItem.Leaf -> {
|
||||
val datum = item.data
|
||||
if (datum.type != typeOf<Binary>()) error("Can't directly serve file of type ${item.data.type}")
|
||||
ktorRoute.get(routeName.toWebPath()) {
|
||||
val binary = datum.await() as Binary
|
||||
val extension = item.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: ""
|
||||
val contentType: ContentType = extension
|
||||
.let(ContentType::fromFileExtension)
|
||||
.firstOrNull()
|
||||
?: ContentType.Any
|
||||
call.respondBytes(contentType = contentType) {
|
||||
//TODO optimize using streaming
|
||||
binary.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DataTreeItem.Node -> {
|
||||
item.tree.items.forEach { (token, childItem) ->
|
||||
files(childItem, routeName + token)
|
||||
}
|
||||
if (data.type != typeOf<Binary>()) error("Can't directly serve file of type ${data.type}")
|
||||
ktorRoute.get(route.toWebPath()) {
|
||||
val binary = data.await()
|
||||
val extension = data.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: ""
|
||||
val contentType: ContentType = extension
|
||||
.let(ContentType::fromFileExtension)
|
||||
.firstOrNull()
|
||||
?: ContentType.Any
|
||||
call.respondBytes(contentType = contentType) {
|
||||
//TODO optimize using streaming
|
||||
binary.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun static(dataName: Name, routeName: Name) {
|
||||
val item: DataTreeItem<Any> = data.getItem(dataName) ?: error("Data with name $dataName is not resolved")
|
||||
files(item, routeName)
|
||||
}
|
||||
//
|
||||
// override fun file(file: Path, webPath: String) {
|
||||
// if (file.isDirectory()) {
|
||||
// ktorRoute.static(webPath) {
|
||||
// //TODO check non-standard FS and convert
|
||||
// files(file.toFile())
|
||||
// }
|
||||
// } else if (webPath.isBlank()) {
|
||||
// error("Can't mount file to an empty route")
|
||||
// } else {
|
||||
// ktorRoute.file(webPath, file.toFile())
|
||||
// }
|
||||
// }
|
||||
|
||||
// override fun file(dataName: Name, webPath: String) {
|
||||
// val fileData = data[dataName]
|
||||
// if(fileData is FileData){
|
||||
// ktorRoute.file(webPath)
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
|
||||
ref
|
||||
} else if (ref.isEmpty()) {
|
||||
@ -131,12 +87,10 @@ public class KtorSiteBuilder(
|
||||
}
|
||||
|
||||
|
||||
private inner class KtorWebPage(
|
||||
private inner class KtorPageContext(
|
||||
val pageBaseUrl: String,
|
||||
override val pageMeta: Meta,
|
||||
) : WebPage {
|
||||
override val snark: SnarkHtml get() = this@KtorSiteBuilder.snark
|
||||
override val data: DataTree<*> get() = this@KtorSiteBuilder.data
|
||||
) : PageContext {
|
||||
|
||||
override fun resolveRef(ref: String): String = this@KtorSiteBuilder.resolveRef(pageBaseUrl, ref)
|
||||
|
||||
@ -145,7 +99,7 @@ public class KtorSiteBuilder(
|
||||
relative: Boolean,
|
||||
): String {
|
||||
val fullPageName = if (relative) this@KtorSiteBuilder.route + pageName else pageName
|
||||
return if (fullPageName.endsWith(SiteBuilder.INDEX_PAGE_TOKEN)) {
|
||||
return if (fullPageName.endsWith(SiteContext.INDEX_PAGE_TOKEN)) {
|
||||
resolveRef(fullPageName.cutLast().toWebPath())
|
||||
} else {
|
||||
resolveRef(fullPageName.toWebPath())
|
||||
@ -153,7 +107,7 @@ public class KtorSiteBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun page(route: Name, pageMeta: Meta, content: context(HTML, WebPage) () -> Unit) {
|
||||
override suspend fun page(route: Name, data: DataSet<Any>, pageMeta: Meta, htmlPage: HtmlPage) {
|
||||
ktorRoute.get(route.toWebPath()) {
|
||||
val request = call.request
|
||||
//substitute host for url for backwards calls
|
||||
@ -167,53 +121,33 @@ public class KtorSiteBuilder(
|
||||
"name" put route.toString()
|
||||
"url" put url.buildString()
|
||||
}
|
||||
val pageBuilder = KtorWebPage(url.buildString(), Laminate(modifiedPageMeta, siteMeta))
|
||||
val pageBuilder = KtorPageContext(url.buildString(), Laminate(modifiedPageMeta, siteMeta))
|
||||
|
||||
call.respondHtml {
|
||||
head{}
|
||||
content(this, pageBuilder)
|
||||
head {}
|
||||
with(htmlPage) {
|
||||
renderPage(pageBuilder, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun route(
|
||||
routeName: Name,
|
||||
dataOverride: DataTree<*>?,
|
||||
routeMeta: Meta,
|
||||
): SiteBuilder = KtorSiteBuilder(
|
||||
snark = snark,
|
||||
data = dataOverride ?: data,
|
||||
siteMeta = Laminate(routeMeta, siteMeta),
|
||||
baseUrl = baseUrl,
|
||||
route = this.route + routeName,
|
||||
ktorRoute = ktorRoute.createRouteFromPath(routeName.toWebPath())
|
||||
)
|
||||
override suspend fun site(route: Name, data: DataSet<Any>, siteMeta: Meta, htmlSite: HtmlSite) {
|
||||
val context = KtorSiteBuilder(
|
||||
context,
|
||||
siteMeta = Laminate(siteMeta, this.siteMeta),
|
||||
baseUrl = resolveRef(baseUrl, route.toWebPath()),
|
||||
route = Name.EMPTY,
|
||||
ktorRoute = ktorRoute.createRouteFromPath(route.toWebPath())
|
||||
)
|
||||
|
||||
override fun site(
|
||||
routeName: Name,
|
||||
dataOverride: DataTree<*>?,
|
||||
routeMeta: Meta,
|
||||
): SiteBuilder = KtorSiteBuilder(
|
||||
snark = snark,
|
||||
data = dataOverride ?: data,
|
||||
siteMeta = Laminate(routeMeta, siteMeta),
|
||||
baseUrl = resolveRef(baseUrl, routeName.toWebPath()),
|
||||
route = Name.EMPTY,
|
||||
ktorRoute = ktorRoute.createRouteFromPath(routeName.toWebPath())
|
||||
)
|
||||
htmlSite.renderSite(context, data)
|
||||
}
|
||||
|
||||
//
|
||||
// override fun resourceFile(resourcesPath: String, webPath: String) {
|
||||
// ktorRoute.resource(resourcesPath, resourcesPath)
|
||||
// }
|
||||
|
||||
// override fun resourceDirectory(resourcesPath: String) {
|
||||
// ktorRoute.resources(resourcesPath)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun Route.site(
|
||||
snarkHtmlPlugin: SnarkHtml,
|
||||
context: Context,
|
||||
data: DataTree<*>,
|
||||
baseUrl: String = "",
|
||||
siteMeta: Meta = data.meta,
|
||||
@ -222,20 +156,20 @@ private fun Route.site(
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
block(KtorSiteBuilder(snarkHtmlPlugin, data, siteMeta, baseUrl, route = Name.EMPTY, this@Route))
|
||||
block(KtorSiteBuilder(context, siteMeta, baseUrl, route = Name.EMPTY, this@Route))
|
||||
}
|
||||
|
||||
public fun Application.site(
|
||||
snark: SnarkHtml,
|
||||
context: Context,
|
||||
data: DataTree<*>,
|
||||
baseUrl: String = "",
|
||||
siteMeta: Meta = data.meta,
|
||||
block: SiteBuilder.() -> Unit,
|
||||
block: SiteContext.() -> Unit,
|
||||
) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
routing {
|
||||
site(snark, data, baseUrl, siteMeta, block)
|
||||
site(context, data, baseUrl, siteMeta, block)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user