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)
|
public val io: IOPlugin by require(IOPlugin)
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
public val readers: Map<Name, SnarkIOReader<Any>> by lazy {
|
public val readers: Map<Name, SnarkReader<Any>> by lazy {
|
||||||
context.gather<SnarkIOReader<Any>>(SnarkIOReader.DF_TYPE, inherit = true)
|
context.gather<SnarkReader<Any>>(SnarkReader.DF_TYPE, inherit = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,7 +50,7 @@ public class Snark : WorkspacePlugin() {
|
|||||||
readByteArray()
|
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.IOReader
|
||||||
import space.kscience.dataforge.io.asBinary
|
import space.kscience.dataforge.io.asBinary
|
||||||
import space.kscience.dataforge.misc.DfId
|
import space.kscience.dataforge.misc.DfId
|
||||||
import space.kscience.snark.SnarkIOReader.Companion.DEFAULT_PRIORITY
|
import space.kscience.snark.SnarkReader.Companion.DEFAULT_PRIORITY
|
||||||
import space.kscience.snark.SnarkIOReader.Companion.DF_TYPE
|
import space.kscience.snark.SnarkReader.Companion.DF_TYPE
|
||||||
|
|
||||||
@DfId(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 types: Set<String>
|
||||||
public val priority: Int get() = DEFAULT_PRIORITY
|
public val priority: Int get() = DEFAULT_PRIORITY
|
||||||
public fun readFrom(source: String): T
|
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.
|
* @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>,
|
private val reader: IOReader<T>,
|
||||||
override val types: Set<String>,
|
override val types: Set<String>,
|
||||||
override val priority: Int = DEFAULT_PRIORITY,
|
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())
|
override fun readFrom(source: String): T = readFrom(source.encodeToByteArray().asBinary())
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> SnarkIOReader(
|
public fun <T : Any> SnarkReader(
|
||||||
reader: IOReader<T>,
|
reader: IOReader<T>,
|
||||||
vararg types: String,
|
vararg types: String,
|
||||||
priority: Int = DEFAULT_PRIORITY
|
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>
|
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) {
|
withSnarkPage(page) {
|
||||||
with(data.await()) { consumer.renderFragment() }
|
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
|
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.meta.*
|
||||||
import space.kscience.dataforge.names.*
|
import space.kscience.dataforge.names.*
|
||||||
import space.kscience.snark.SnarkBuilder
|
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
|
import space.kscience.snark.html.Language.Companion.SITE_LANGUAGE_KEY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class Language : Scheme() {
|
public class Language : Scheme() {
|
||||||
/**
|
/**
|
||||||
* Language key override
|
* Language key override
|
||||||
@ -35,49 +35,50 @@ public class Language : Scheme() {
|
|||||||
|
|
||||||
public val LANGUAGES_KEY: Name = "languages".asName()
|
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"
|
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.
|
// * Automatically build a language map for a data piece with given [name] based on existence of appropriate data nodes.
|
||||||
*/
|
// */
|
||||||
context(SiteBuilder)
|
// context(SiteContext)
|
||||||
public fun forName(name: Name): Meta = Meta {
|
// public fun forName(name: Name): Meta = Meta {
|
||||||
val currentLanguagePrefix = languages[language]?.get(Language::prefix.name)?.string ?: language
|
// val currentLanguagePrefix = languages[language]?.get(Language::prefix.name)?.string ?: language
|
||||||
val fullName = (route.removeFirstOrNull(currentLanguagePrefix.asName()) ?: route) + name
|
// val fullName = (route.removeFirstOrNull(currentLanguagePrefix.asName()) ?: route) + name
|
||||||
languages.forEach { (key, meta) ->
|
// languages.forEach { (key, meta) ->
|
||||||
val languagePrefix: String = meta[Language::prefix.name].string ?: key
|
// val languagePrefix: String = meta[Language::prefix.name].string ?: key
|
||||||
val nameWithLanguage: Name = if (languagePrefix.isBlank()) {
|
// val nameWithLanguage: Name = if (languagePrefix.isBlank()) {
|
||||||
fullName
|
// fullName
|
||||||
} else {
|
// } else {
|
||||||
languagePrefix.asName() + fullName
|
// languagePrefix.asName() + fullName
|
||||||
}
|
// }
|
||||||
if (data.getItem(name) != null) {
|
// if (resolveData.getItem(name) != null) {
|
||||||
key put meta.asMutableMeta().apply {
|
// key put meta.asMutableMeta().apply {
|
||||||
Language::target.name put nameWithLanguage.toString()
|
// 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()
|
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
|
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
|
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) ->
|
languageMap.forEach { (languageKey, languageMeta) ->
|
||||||
val prefix = languageMeta[Language::prefix.name].string ?: languageKey
|
val prefix = languageMeta[Language::prefix.name].string ?: languageKey
|
||||||
val routeMeta = Meta {
|
val languageSiteMeta = Meta {
|
||||||
SITE_LANGUAGE_KEY put languageKey
|
SITE_LANGUAGE_KEY put languageKey
|
||||||
SITE_LANGUAGES_KEY put Meta {
|
SITE_LANGUAGES_KEY put Meta {
|
||||||
languageMap.forEach {
|
languageMap.forEach {
|
||||||
@ -85,65 +86,48 @@ public fun SiteBuilder.withLanguages(languageMap: Map<String, Meta>, block: Site
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
route(prefix, routeMeta = routeMeta) {
|
site(prefix.parseAsName(), data.branch(prefix), siteMeta = Laminate(languageSiteMeta, siteMeta), site)
|
||||||
block(languageKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
* 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
|
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
|
* 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()
|
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
|
val prefix = languages[language]?.get(Language::prefix.name)?.string?.parseAsName() ?: Name.EMPTY
|
||||||
return resolvePageRef(prefix + pageName, relative)
|
return resolvePageRef(prefix + pageName, relative)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Render all pages in a node with given name. Use localization prefix if appropriate data is available.
|
// * Render all pages in a node with given name. Use localization prefix if appropriate data is available.
|
||||||
*/
|
// */
|
||||||
public fun SiteBuilder.localizedPages(
|
//public fun SiteContext.localizedPages(
|
||||||
dataPath: Name,
|
// dataPath: Name,
|
||||||
remotePath: Name = dataPath,
|
// remotePath: Name = dataPath,
|
||||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||||
) {
|
//) {
|
||||||
val item = data.getItem(languagePrefix + dataPath)
|
// val item = resolveData.getItem(languagePrefix + dataPath)
|
||||||
?: data.getItem(dataPath)
|
// ?: resolveData.getItem(dataPath)
|
||||||
?: error("No data found by name $dataPath")
|
// ?: error("No data found by name $dataPath")
|
||||||
route(remotePath) {
|
// route(remotePath) {
|
||||||
pages(item, dataRenderer)
|
// pages(item, dataRenderer)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
public fun SiteBuilder.localizedPages(
|
//public fun SiteContext.localizedPages(
|
||||||
dataPath: String,
|
// dataPath: String,
|
||||||
remotePath: Name = dataPath.parseAsName(),
|
// remotePath: Name = dataPath.parseAsName(),
|
||||||
dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
|
||||||
) {
|
//) {
|
||||||
localizedPages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
// localizedPages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||||
}
|
//}
|
@ -1,5 +1,6 @@
|
|||||||
package space.kscience.snark.html
|
package space.kscience.snark.html
|
||||||
|
|
||||||
|
import kotlinx.html.HTML
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.data.*
|
import space.kscience.dataforge.data.*
|
||||||
@ -10,7 +11,6 @@ import space.kscience.dataforge.names.*
|
|||||||
import space.kscience.snark.SnarkBuilder
|
import space.kscience.snark.SnarkBuilder
|
||||||
import space.kscience.snark.SnarkContext
|
import space.kscience.snark.SnarkContext
|
||||||
|
|
||||||
context(SnarkContext)
|
|
||||||
public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
|
public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
|
||||||
if (it.hasIndex()) {
|
if (it.hasIndex()) {
|
||||||
"${it.body}[${it.index}]"
|
"${it.body}[${it.index}]"
|
||||||
@ -23,13 +23,7 @@ public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
|
|||||||
* A context for building a single page
|
* A context for building a single page
|
||||||
*/
|
*/
|
||||||
@SnarkBuilder
|
@SnarkBuilder
|
||||||
public interface WebPage : ContextAware, SnarkContext {
|
public interface PageContext : SnarkContext {
|
||||||
|
|
||||||
public val snark: SnarkHtml
|
|
||||||
|
|
||||||
override val context: Context get() = snark.context
|
|
||||||
|
|
||||||
public val data: DataTree<*>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A metadata for a page. It should include site metadata
|
* 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].
|
* 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
|
public fun resolvePageRef(pageName: Name, relative: Boolean = false): String
|
||||||
}
|
}
|
||||||
|
|
||||||
context(WebPage)
|
context(PageContext)
|
||||||
public val page: WebPage
|
public val page: PageContext
|
||||||
get() = this@WebPage
|
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
|
* Resolve a Html builder by its full name
|
||||||
*/
|
*/
|
||||||
context(SnarkContext)
|
context(SnarkContext)
|
||||||
public fun DataTree<*>.resolveHtmlOrNull(name: Name): HtmlData? {
|
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 {
|
return resolved?.takeIf {
|
||||||
it.published //TODO add language confirmation
|
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 kotlinx.io.readByteArray
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.data.*
|
import space.kscience.dataforge.data.*
|
||||||
import space.kscience.dataforge.io.Binary
|
|
||||||
import space.kscience.dataforge.io.IOPlugin
|
import space.kscience.dataforge.io.IOPlugin
|
||||||
import space.kscience.dataforge.io.IOReader
|
import space.kscience.dataforge.io.IOReader
|
||||||
import space.kscience.dataforge.io.JsonMetaFormat
|
import space.kscience.dataforge.io.JsonMetaFormat
|
||||||
@ -21,18 +20,12 @@ import space.kscience.dataforge.provider.dfId
|
|||||||
import space.kscience.dataforge.workspace.*
|
import space.kscience.dataforge.workspace.*
|
||||||
import space.kscience.snark.ImageIOReader
|
import space.kscience.snark.ImageIOReader
|
||||||
import space.kscience.snark.Snark
|
import space.kscience.snark.Snark
|
||||||
import space.kscience.snark.SnarkIOReader
|
import space.kscience.snark.SnarkReader
|
||||||
import space.kscience.snark.TextProcessor
|
import space.kscience.snark.TextProcessor
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.extension
|
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
|
* A plugin used for rendering a [DataTree] as HTML
|
||||||
@ -44,33 +37,17 @@ public class SnarkHtml : WorkspacePlugin() {
|
|||||||
|
|
||||||
override val tag: PluginTag get() = Companion.tag
|
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) {
|
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||||
SnarkIOReader::class.dfId -> mapOf(
|
SnarkReader::class.dfId -> mapOf(
|
||||||
"html".asName() to HtmlReader,
|
"html".asName() to HtmlReader,
|
||||||
"markdown".asName() to MarkdownReader,
|
"markdown".asName() to MarkdownReader,
|
||||||
"json".asName() to SnarkIOReader(JsonMetaFormat, ContentType.Application.Json),
|
"json".asName() to SnarkReader(JsonMetaFormat, ContentType.Application.Json.toString()),
|
||||||
"yaml".asName() to SnarkIOReader(YamlMetaFormat, "text/yaml", "yaml"),
|
"yaml".asName() to SnarkReader(YamlMetaFormat, "text/yaml", "yaml"),
|
||||||
"png".asName() to SnarkIOReader(ImageIOReader, ContentType.Image.PNG),
|
"png".asName() to SnarkReader(ImageIOReader, ContentType.Image.PNG.toString()),
|
||||||
"jpg".asName() to SnarkIOReader(ImageIOReader, ContentType.Image.JPEG),
|
"jpg".asName() to SnarkReader(ImageIOReader, ContentType.Image.JPEG.toString()),
|
||||||
"gif".asName() to SnarkIOReader(ImageIOReader, ContentType.Image.GIF),
|
"gif".asName() to SnarkReader(ImageIOReader, ContentType.Image.GIF.toString()),
|
||||||
"svg".asName() to SnarkIOReader(IOReader.binary, ContentType.Image.SVG, ContentType.parse("svg")),
|
"svg".asName() to SnarkReader(IOReader.binary, ContentType.Image.SVG.toString(), "svg"),
|
||||||
"raw".asName() to SnarkIOReader(
|
"raw".asName() to SnarkReader(
|
||||||
IOReader.binary,
|
IOReader.binary,
|
||||||
"css",
|
"css",
|
||||||
"js",
|
"js",
|
||||||
@ -86,13 +63,6 @@ public class SnarkHtml : WorkspacePlugin() {
|
|||||||
else -> super.content(target)
|
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> {
|
public val preprocess: TaskReference<String> by task<String> {
|
||||||
pipeFrom<String, String>(dataByType<String>()) { text, _, meta ->
|
pipeFrom<String, String>(dataByType<String>()) { text, _, meta ->
|
||||||
meta[TextProcessor.TEXT_TRANSFORMATION_KEY]?.let {
|
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> {
|
// public val textTransformationAction: Action<String, String> = Action.map<String, String> {
|
||||||
// val transformations = actionMeta.getIndexed("transformation").entries.sortedBy {
|
// val transformations = actionMeta.getIndexed("transformation").entries.sortedBy {
|
||||||
// it.key?.toIntOrNull() ?: 0
|
// it.key?.toIntOrNull() ?: 0
|
||||||
@ -144,7 +118,7 @@ public class SnarkHtml : WorkspacePlugin() {
|
|||||||
readByteArray()
|
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
|
package space.kscience.snark.html
|
||||||
|
|
||||||
import kotlinx.html.A
|
import kotlinx.html.*
|
||||||
import kotlinx.html.FlowContent
|
|
||||||
import kotlinx.html.Tag
|
|
||||||
import kotlinx.html.TagConsumer
|
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
import space.kscience.snark.TextProcessor
|
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()
|
private val regex = """\$\{([\w.]*)(?>\("(.*)"\))?}""".toRegex()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic [TextProcessor] that replaces `${...}` expressions in text. The following expressions are recognised:
|
* A basic [TextProcessor] that replaces `${...}` expressions in text. The following expressions are recognised:
|
||||||
* * `homeRef` resolves to [homeRef]
|
* * `homeRef` resolves to [homeRef]
|
||||||
* * `resolveRef("...")` -> [WebPage.resolveRef]
|
* * `resolveRef("...")` -> [PageContext.resolveRef]
|
||||||
* * `resolvePageRef("...")` -> [WebPage.resolvePageRef]
|
* * `resolvePageRef("...")` -> [PageContext.resolvePageRef]
|
||||||
* * `pageMeta.get("...") -> [WebPage.pageMeta] get string method
|
* * `pageMeta.get("...") -> [PageContext.pageMeta] get string method
|
||||||
* Otherwise return unchanged string
|
* Otherwise return unchanged string
|
||||||
*/
|
*/
|
||||||
override fun process(text: CharSequence): String = text.replace(regex) { match ->
|
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 class WebPagePostprocessor<out R>(
|
||||||
public val page: WebPage,
|
public val page: PageContext,
|
||||||
private val consumer: TagConsumer<R>,
|
private val consumer: TagConsumer<R>,
|
||||||
) : TagConsumer<R> by consumer {
|
) : TagConsumer<R> by consumer {
|
||||||
|
|
||||||
@ -64,9 +60,20 @@ public class WebPagePostprocessor<out R>(
|
|||||||
override fun onTagContent(content: CharSequence) {
|
override fun onTagContent(content: CharSequence) {
|
||||||
consumer.onTagContent(processor.process(content))
|
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 {
|
val fc = object : FlowContent by this {
|
||||||
override val consumer: TagConsumer<*> = WebPagePostprocessor(page, this@withSnarkPage.consumer)
|
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.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||||
import org.intellij.markdown.html.HtmlGenerator
|
import org.intellij.markdown.html.HtmlGenerator
|
||||||
import org.intellij.markdown.parser.MarkdownParser
|
import org.intellij.markdown.parser.MarkdownParser
|
||||||
import space.kscience.snark.SnarkIOReader
|
import space.kscience.snark.SnarkReader
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
public object HtmlReader : SnarkIOReader<HtmlFragment> {
|
public object HtmlReader : SnarkReader<HtmlFragment> {
|
||||||
override val types: Set<String> = setOf("html")
|
override val types: Set<String> = setOf("html")
|
||||||
|
|
||||||
override fun readFrom(source: String): HtmlFragment = HtmlFragment {
|
override fun readFrom(source: String): HtmlFragment = HtmlFragment {
|
||||||
@ -25,7 +25,7 @@ public object HtmlReader : SnarkIOReader<HtmlFragment> {
|
|||||||
override val type: KType = typeOf<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 type: KType = typeOf<HtmlFragment>()
|
||||||
|
|
||||||
override val types: Set<String> = setOf("text/markdown", "md", "markdown")
|
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())
|
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.http.content.staticFiles
|
||||||
import io.ktor.server.plugins.origin
|
import io.ktor.server.plugins.origin
|
||||||
import io.ktor.server.response.respondBytes
|
import io.ktor.server.response.respondBytes
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.routing.createRouteFromPath
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
import kotlinx.css.CssBuilder
|
import kotlinx.css.CssBuilder
|
||||||
import kotlinx.html.CommonAttributeGroupFacade
|
import kotlinx.html.CommonAttributeGroupFacade
|
||||||
import kotlinx.html.HTML
|
|
||||||
import kotlinx.html.head
|
import kotlinx.html.head
|
||||||
import kotlinx.html.style
|
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.error
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.data.DataTree
|
import space.kscience.dataforge.data.*
|
||||||
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.Binary
|
||||||
import space.kscience.dataforge.io.toByteArray
|
import space.kscience.dataforge.io.toByteArray
|
||||||
import space.kscience.dataforge.meta.*
|
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.endsWith
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
import space.kscience.dataforge.workspace.FileData
|
import space.kscience.dataforge.workspace.FileData
|
||||||
import space.kscience.snark.html.SiteBuilder
|
import space.kscience.snark.html.*
|
||||||
import space.kscience.snark.html.SnarkHtml
|
|
||||||
import space.kscience.snark.html.WebPage
|
|
||||||
import space.kscience.snark.html.toWebPath
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
@ -47,17 +39,16 @@ public fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class KtorSiteBuilder(
|
public class KtorSiteBuilder(
|
||||||
override val snark: SnarkHtml,
|
override val context: Context,
|
||||||
override val data: DataTree<*>,
|
|
||||||
override val siteMeta: Meta,
|
override val siteMeta: Meta,
|
||||||
private val baseUrl: String,
|
private val baseUrl: String,
|
||||||
override val route: Name,
|
override val route: Name,
|
||||||
private val ktorRoute: Route,
|
private val ktorRoute: Route,
|
||||||
) : SiteBuilder {
|
) : SiteContext, ContextAware {
|
||||||
|
|
||||||
private fun files(item: DataTreeItem<Any>, routeName: Name) {
|
|
||||||
//try using direct file rendering
|
override suspend fun static(route: Name, data: Data<Binary>) {
|
||||||
item.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
data.meta[FileData.FILE_PATH_KEY]?.string?.let {
|
||||||
val file = try {
|
val file = try {
|
||||||
Path.of(it).toFile()
|
Path.of(it).toFile()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@ -66,18 +57,16 @@ public class KtorSiteBuilder(
|
|||||||
return@let
|
return@let
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileName = routeName.toWebPath()
|
val fileName = route.toWebPath()
|
||||||
ktorRoute.staticFiles(fileName, file)
|
ktorRoute.staticFiles(fileName, file)
|
||||||
//success, don't do anything else
|
//success, don't do anything else
|
||||||
return@files
|
return
|
||||||
}
|
}
|
||||||
when (item) {
|
|
||||||
is DataTreeItem.Leaf -> {
|
if (data.type != typeOf<Binary>()) error("Can't directly serve file of type ${data.type}")
|
||||||
val datum = item.data
|
ktorRoute.get(route.toWebPath()) {
|
||||||
if (datum.type != typeOf<Binary>()) error("Can't directly serve file of type ${item.data.type}")
|
val binary = data.await()
|
||||||
ktorRoute.get(routeName.toWebPath()) {
|
val extension = data.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: ""
|
||||||
val binary = datum.await() as Binary
|
|
||||||
val extension = item.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: ""
|
|
||||||
val contentType: ContentType = extension
|
val contentType: ContentType = extension
|
||||||
.let(ContentType::fromFileExtension)
|
.let(ContentType::fromFileExtension)
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
@ -89,39 +78,6 @@ public class KtorSiteBuilder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
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()) {
|
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
|
||||||
ref
|
ref
|
||||||
} else if (ref.isEmpty()) {
|
} else if (ref.isEmpty()) {
|
||||||
@ -131,12 +87,10 @@ public class KtorSiteBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private inner class KtorWebPage(
|
private inner class KtorPageContext(
|
||||||
val pageBaseUrl: String,
|
val pageBaseUrl: String,
|
||||||
override val pageMeta: Meta,
|
override val pageMeta: Meta,
|
||||||
) : WebPage {
|
) : PageContext {
|
||||||
override val snark: SnarkHtml get() = this@KtorSiteBuilder.snark
|
|
||||||
override val data: DataTree<*> get() = this@KtorSiteBuilder.data
|
|
||||||
|
|
||||||
override fun resolveRef(ref: String): String = this@KtorSiteBuilder.resolveRef(pageBaseUrl, ref)
|
override fun resolveRef(ref: String): String = this@KtorSiteBuilder.resolveRef(pageBaseUrl, ref)
|
||||||
|
|
||||||
@ -145,7 +99,7 @@ public class KtorSiteBuilder(
|
|||||||
relative: Boolean,
|
relative: Boolean,
|
||||||
): String {
|
): String {
|
||||||
val fullPageName = if (relative) this@KtorSiteBuilder.route + pageName else pageName
|
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())
|
resolveRef(fullPageName.cutLast().toWebPath())
|
||||||
} else {
|
} else {
|
||||||
resolveRef(fullPageName.toWebPath())
|
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()) {
|
ktorRoute.get(route.toWebPath()) {
|
||||||
val request = call.request
|
val request = call.request
|
||||||
//substitute host for url for backwards calls
|
//substitute host for url for backwards calls
|
||||||
@ -167,53 +121,33 @@ public class KtorSiteBuilder(
|
|||||||
"name" put route.toString()
|
"name" put route.toString()
|
||||||
"url" put url.buildString()
|
"url" put url.buildString()
|
||||||
}
|
}
|
||||||
val pageBuilder = KtorWebPage(url.buildString(), Laminate(modifiedPageMeta, siteMeta))
|
val pageBuilder = KtorPageContext(url.buildString(), Laminate(modifiedPageMeta, siteMeta))
|
||||||
|
|
||||||
call.respondHtml {
|
call.respondHtml {
|
||||||
head{}
|
head {}
|
||||||
content(this, pageBuilder)
|
with(htmlPage) {
|
||||||
|
renderPage(pageBuilder, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun route(
|
override suspend fun site(route: Name, data: DataSet<Any>, siteMeta: Meta, htmlSite: HtmlSite) {
|
||||||
routeName: Name,
|
val context = KtorSiteBuilder(
|
||||||
dataOverride: DataTree<*>?,
|
context,
|
||||||
routeMeta: Meta,
|
siteMeta = Laminate(siteMeta, this.siteMeta),
|
||||||
): SiteBuilder = KtorSiteBuilder(
|
baseUrl = resolveRef(baseUrl, route.toWebPath()),
|
||||||
snark = snark,
|
|
||||||
data = dataOverride ?: data,
|
|
||||||
siteMeta = Laminate(routeMeta, siteMeta),
|
|
||||||
baseUrl = baseUrl,
|
|
||||||
route = this.route + routeName,
|
|
||||||
ktorRoute = ktorRoute.createRouteFromPath(routeName.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,
|
route = Name.EMPTY,
|
||||||
ktorRoute = ktorRoute.createRouteFromPath(routeName.toWebPath())
|
ktorRoute = ktorRoute.createRouteFromPath(route.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(
|
private fun Route.site(
|
||||||
snarkHtmlPlugin: SnarkHtml,
|
context: Context,
|
||||||
data: DataTree<*>,
|
data: DataTree<*>,
|
||||||
baseUrl: String = "",
|
baseUrl: String = "",
|
||||||
siteMeta: Meta = data.meta,
|
siteMeta: Meta = data.meta,
|
||||||
@ -222,20 +156,20 @@ private fun Route.site(
|
|||||||
contract {
|
contract {
|
||||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
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(
|
public fun Application.site(
|
||||||
snark: SnarkHtml,
|
context: Context,
|
||||||
data: DataTree<*>,
|
data: DataTree<*>,
|
||||||
baseUrl: String = "",
|
baseUrl: String = "",
|
||||||
siteMeta: Meta = data.meta,
|
siteMeta: Meta = data.meta,
|
||||||
block: SiteBuilder.() -> Unit,
|
block: SiteContext.() -> Unit,
|
||||||
) {
|
) {
|
||||||
contract {
|
contract {
|
||||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
site(snark, data, baseUrl, siteMeta, block)
|
site(context, data, baseUrl, siteMeta, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user