Work on language support

This commit is contained in:
Alexander Nozik 2023-01-08 15:58:27 +03:00
parent 62da832db1
commit f43b23c84f
Signed by: altavir
GPG Key ID: B10A55DCC7B9AEBB
10 changed files with 412 additions and 193 deletions

View File

@ -1,3 +1,3 @@
kotlin.code.style=official
toolsVersion=0.13.1-kotlin-1.7.20
toolsVersion=0.13.3-kotlin-1.7.20

View File

@ -0,0 +1,83 @@
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.data.getItem
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.appendLeft
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 {
// context (SiteBuilder)
// public fun buildPageMeta(name: Name, data: Data<Any>): Laminate {
// val languages = languages.mapKeys { it.value["key"]?.string ?: it.key }
//
// // detect current language by prefix if it is not defined explicitly
// val currentLanguage: String = data.meta["language"]?.string
// ?: languages.keys.firstOrNull() { key -> name.startsWith(key.parseAsName()) } ?: defaultLanguage
//
// //
// val languageMap = Meta {
// languages.forEach { (key, meta) ->
// val languagePrefix: String = meta.string ?: meta["name"]?.string ?: return@forEach
// val targetName = name.removeHeadOrNull("")
// val targetData = this@SiteBuilder.data[targetName.parseAsName()]
// if (targetData != null) key put targetName
// }
// }
// val languageMeta = Meta {
// "language" put currentLanguage
// if (!languageMap.isEmpty()) {
// "languageMap" put languageMap
// }
// }
// return Laminate(data.meta, languageMeta, siteMeta)
// }
/**
* Automatically build a language map for a data piece with given [name] based on existence of appropriate data nodes.
*/
context(SiteBuilder)
public fun buildLanguageMeta(name: Name): Meta = Meta {
languages.forEach { (key, meta) ->
val languagePrefix = meta["prefix"].string ?: key
val nameWithLanguage: Name = if (languagePrefix.isBlank()) name else name.appendLeft(languagePrefix)
if (data.getItem(name) != null) {
key put meta.asMutableMeta().apply {
"target" put nameWithLanguage.toString()
}
}
}
}
public val DEFAULT: DataRenderer = object : DataRenderer {
context(SiteBuilder)
override fun invoke(name: Name, data: Data<Any>) {
if (data.type == typeOf<HtmlData>()) {
page(name, data.meta) {
head {
title = data.meta["title"].string ?: "Untitled page"
}
body {
@Suppress("UNCHECKED_CAST")
htmlData(data as HtmlData)
}
}
}
}
}
}
}

View File

@ -26,13 +26,23 @@ public typealias HtmlData = Data<HtmlFragment>
// Data(HtmlFragment(content), meta)
context(WebPage) public fun FlowContent.htmlData(data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
context(WebPage)
public fun FlowContent.htmlData(data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
with(data.await()) { consumer.renderFragment(page) }
}
context(SnarkContext) public val Data<*>.id: String get() = meta["id"]?.string ?: "block[${hashCode()}]"
context(SnarkContext) public val Data<*>.language: String? get() = meta["language"].string?.lowercase()
context(SnarkContext)
public val Data<*>.id: String
get() = meta["id"]?.string ?: "block[${hashCode()}]"
context(SnarkContext) public val Data<*>.order: Int? get() = meta["order"]?.int
context(SnarkContext)
public val Data<*>.language: String?
get() = meta["language"].string?.lowercase()
context(SnarkContext) public val Data<*>.published: Boolean get() = meta["published"].string != "false"
context(SnarkContext)
public val Data<*>.order: Int?
get() = meta["order"]?.int
context(SnarkContext)
public val Data<*>.published: Boolean
get() = meta["published"].string != "false"

View File

@ -4,11 +4,18 @@ 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.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.SnarkContext
import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY
import java.nio.file.Path
@ -17,6 +24,11 @@ import java.nio.file.Path
*/
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
*/
@ -51,8 +63,10 @@ public interface SiteBuilder : ContextAware, SnarkContext {
/**
* 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]
*/
public fun page(route: Name = Name.EMPTY, content: context(WebPage, HTML) () -> Unit)
public fun page(route: Name = Name.EMPTY, pageMeta: Meta = Meta.EMPTY, content: context(WebPage, HTML) () -> Unit)
/**
* Create a route with optional data tree override. For example one could use a subtree of the initial tree.
@ -61,36 +75,63 @@ public interface SiteBuilder : ContextAware, SnarkContext {
public fun route(
routeName: Name,
dataOverride: DataTree<*>? = null,
metaOverride: Meta? = null,
setAsRoot: Boolean = false,
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 INDEX_PAGE_TOKEN: NameToken = NameToken("index")
public val UP_PAGE_TOKEN: NameToken = NameToken("..")
}
}
context(SiteBuilder) public val siteBuilder: SiteBuilder get() = this@SiteBuilder
context(SiteBuilder)
public val siteBuilder: SiteBuilder
get() = this@SiteBuilder
public inline fun SiteBuilder.route(
route: Name,
dataOverride: DataTree<*>? = null,
metaOverride: Meta? = null,
setAsRoot: Boolean = false,
routeMeta: Meta = Meta.EMPTY,
block: SiteBuilder.() -> Unit,
) {
route(route, dataOverride, metaOverride, setAsRoot).apply(block)
route(route, dataOverride, routeMeta).apply(block)
}
public inline fun SiteBuilder.route(
route: String,
dataOverride: DataTree<*>? = null,
metaOverride: Meta? = null,
setAsRoot: Boolean = false,
routeMeta: Meta = Meta.EMPTY,
block: SiteBuilder.() -> Unit,
) {
route(route.parseAsName(), dataOverride, metaOverride, setAsRoot).apply(block)
route(route.parseAsName(), dataOverride, routeMeta).apply(block)
}
public inline fun SiteBuilder.site(
route: Name,
dataOverride: DataTree<*>? = null,
routeMeta: Meta = Meta.EMPTY,
block: SiteBuilder.() -> Unit,
) {
site(route, dataOverride, routeMeta).apply(block)
}
public inline fun SiteBuilder.site(
route: String,
dataOverride: DataTree<*>? = null,
routeMeta: Meta = Meta.EMPTY,
block: SiteBuilder.() -> Unit,
) {
site(route.parseAsName(), dataOverride, routeMeta).apply(block)
}
@ -107,3 +148,98 @@ public inline fun SiteBuilder.route(
// withData(mountedData).block()
// }
//}
internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
rootMeta.getIndexed("resource".asName()).forEach { (_, meta) ->
val path by meta.string()
val remotePath by meta.string()
path?.let { resourcePath ->
//If remote path provided, use a single resource
remotePath?.let {
resourceFile(it, resourcePath)
return@forEach
}
//otherwise use package resources
resourceDirectory(resourcePath)
}
}
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
val remotePath by meta.string { error("File remote path is not provided") }
val path by meta.string { error("File path is not provided") }
file(Path.of(path), remotePath)
}
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
val path by meta.string { error("Directory path is not provided") }
file(Path.of(path), "")
}
}
/**
* 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)
}

View File

@ -1,121 +1,8 @@
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.data.DataTreeItem
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.misc.Type
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.html.SiteLayout.Companion.ASSETS_KEY
import space.kscience.snark.html.SiteLayout.Companion.INDEX_PAGE_TOKEN
import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY
import java.nio.file.Path
import kotlin.reflect.typeOf
internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
rootMeta.getIndexed("resource".asName()).forEach { (_, meta) ->
val path by meta.string()
val remotePath by meta.string()
path?.let { resourcePath ->
//If remote path provided, use a single resource
remotePath?.let {
resourceFile(it, resourcePath)
return@forEach
}
//otherwise use package resources
resourceDirectory(resourcePath)
}
}
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
val remotePath by meta.string { error("File remote path is not provided") }
val path by meta.string { error("File path is not provided") }
file(Path.of(path), remotePath)
}
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
val path by meta.string { error("Directory path is not provided") }
file(Path.of(path), "")
}
}
/**
* Render (or don't) given data piece
*/
public typealias DataRenderer = SiteBuilder.(name: Name, data: Data<Any>) -> Unit
/**
* 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 = SiteLayout.defaultDataRenderer,
) {
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 == INDEX_PAGE_TOKEN) {
pages(item, dataRenderer)
} else if (item is DataTreeItem.Leaf) {
dataRenderer(this, token.asName(), item.data)
} else {
route(token.asName()) {
pages(item, dataRenderer)
}
}
}
}
is DataTreeItem.Leaf -> {
dataRenderer.invoke(this, Name.EMPTY, data.data)
}
}
data.meta[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 = SiteLayout.defaultDataRenderer,
) {
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 = SiteLayout.defaultDataRenderer,
) {
pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
}
/**
* An abstraction to render singular data or a data tree.
@ -123,32 +10,20 @@ public fun SiteBuilder.pages(
@Type(SiteLayout.TYPE)
public fun interface SiteLayout {
context(SiteBuilder) public fun render(item: DataTreeItem<*>)
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")
}
}
public val defaultDataRenderer: SiteBuilder.(name: Name, data: Data<*>) -> Unit = { name: Name, data: Data<*> ->
if (data.type == typeOf<HtmlData>()) {
page(name) {
head {
title = data.meta["title"].string ?: "Untitled page"
}
body {
@Suppress("UNCHECKED_CAST")
htmlData(data as HtmlData)
}
}
}
}
}
}
/**
* The default [SiteLayout]. It renders all [HtmlData] pages as t with simple headers via [SiteLayout.defaultDataRenderer]
* 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<*>) {

View File

@ -4,10 +4,11 @@ import kotlinx.html.HTML
import kotlinx.html.html
import kotlinx.html.stream.createHTML
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.withDefault
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.plus
import space.kscience.snark.SnarkEnvironment
import java.nio.file.Files
import java.nio.file.Path
@ -24,6 +25,7 @@ internal class StaticSiteBuilder(
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) {
@ -68,25 +70,25 @@ internal class StaticSiteBuilder(
"${baseUrl.removeSuffix("/")}/$ref"
}
inner class StaticWebPage : WebPage {
inner class StaticWebPage(override val pageMeta: Meta) : WebPage {
override val data: DataTree<*> get() = this@StaticSiteBuilder.data
override val pageMeta: Meta get() = this@StaticSiteBuilder.siteMeta
override val snark: SnarkHtmlPlugin get() = this@StaticSiteBuilder.snark
override fun resolveRef(ref: String): String = resolveRef(baseUrl, ref)
override fun resolvePageRef(pageName: Name): String = resolveRef(
pageName.toWebPath() + ".html"
override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef(
(if (relative) route + pageName else pageName).toWebPath() + ".html"
)
}
override fun page(route: Name, content: context(WebPage, HTML) () -> Unit) {
override fun page(route: Name, pageMeta: Meta, content: context(WebPage, HTML) () -> Unit) {
val htmlBuilder = createHTML()
htmlBuilder.html {
content(StaticWebPage(), this)
content(StaticWebPage(pageMeta), this)
}
val newPath = if (route.isEmpty()) {
@ -102,23 +104,32 @@ internal class StaticSiteBuilder(
override fun route(
routeName: Name,
dataOverride: DataTree<*>?,
metaOverride: Meta?,
setAsRoot: Boolean,
routeMeta: Meta,
): SiteBuilder = StaticSiteBuilder(
snark = snark,
data = dataOverride ?: data,
siteMeta = metaOverride?.withDefault(siteMeta) ?: siteMeta,
baseUrl = if (setAsRoot) {
resolveRef(baseUrl, routeName.toWebPath())
} else {
baseUrl
},
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 = resolveRef(baseUrl, routeName.toWebPath()),
route = Name.EMPTY,
outputPath = outputPath.resolve(routeName.toWebPath())
)
}
/**
* Create a static site using given [data] in provided [outputPath].
* 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.
*
*/
@ -131,5 +142,5 @@ public fun SnarkEnvironment.static(
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val plugin = buildHtmlPlugin()
StaticSiteBuilder(plugin, data, meta, siteUrl, outputPath).block()
StaticSiteBuilder(plugin, data, meta, siteUrl, Name.EMPTY, outputPath).block()
}

View File

@ -11,7 +11,8 @@ import space.kscience.dataforge.names.parseAsName
*/
@Type(TextProcessor.TYPE)
public fun interface TextProcessor {
context(WebPage) public fun process(text: String): String
context(WebPage)
public fun process(text: String): String
public companion object {
public const val TYPE: String = "snark.textTransformation"
@ -31,23 +32,27 @@ public object BasicTextProcessor : TextProcessor {
private val regex = """\$\{([\w.]*)(?>\("(.*)"\))?}""".toRegex()
context(WebPage) override fun process(text: String): String = text.replace(regex) { match ->
context(WebPage)
override fun process(text: String): String = text.replace(regex) { match ->
when (match.groups[1]!!.value) {
"homeRef" -> homeRef
"resolveRef" -> {
val refString = match.groups[2]?.value ?: error("resolveRef requires a string (quoted) argument")
resolveRef(refString)
}
"resolvePageRef" -> {
val refString = match.groups[2]?.value
?: error("resolvePageRef requires a string (quoted) argument")
resolvePageRef(refString)
localisedPageRef(refString.parseAsName())
}
"pageMeta.get" -> {
val nameString = match.groups[2]?.value
?: error("resolvePageRef requires a string (quoted) argument")
pageMeta[nameString.parseAsName()].string ?: "@null"
}
else -> match.value
}
}

View File

@ -9,7 +9,8 @@ import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.*
import space.kscience.snark.SnarkContext
context(SnarkContext) public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
context(SnarkContext)
public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
if (it.hasIndex()) {
"${it.body}[${it.index}]"
} else {
@ -17,6 +18,9 @@ context(SnarkContext) public fun Name.toWebPath(): String = tokens.joinToString(
}
}
/**
* A context for building a single page
*/
public interface WebPage : ContextAware, SnarkContext {
public val snark: SnarkHtmlPlugin
@ -25,14 +29,28 @@ public interface WebPage : ContextAware, SnarkContext {
public val data: DataTree<*>
/**
* A metadata for a page. It should include site metadata
*/
public val pageMeta: Meta
/**
* Resolve absolute url for given [ref]
*
*/
public fun resolveRef(ref: String): String
public fun resolvePageRef(pageName: Name): String
/**
* Resolve absolute url for a page with given [pageName].
*
* @param relative if true, add [SiteBuilder] 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(WebPage)
public val page: WebPage
get() = this@WebPage
public fun WebPage.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
@ -41,7 +59,8 @@ public val WebPage.homeRef: String get() = resolvePageRef(SiteBuilder.INDEX_PAGE
/**
* Resolve a Html builder by its full name
*/
context(SnarkContext) public fun DataTree<*>.resolveHtml(name: Name): HtmlData? {
context(SnarkContext)
public fun DataTree<*>.resolveHtml(name: Name): HtmlData? {
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + SiteBuilder.INDEX_PAGE_TOKEN))
return resolved?.takeIf {
@ -52,7 +71,8 @@ context(SnarkContext) public fun DataTree<*>.resolveHtml(name: Name): HtmlData?
/**
* Find all Html blocks using given name/meta filter
*/
context(SnarkContext) public fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
context(SnarkContext)
public fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
filterByType<HtmlFragment> { name, meta ->
predicate(name, meta)
&& meta["published"].string != "false"
@ -60,7 +80,8 @@ context(SnarkContext) public fun DataTree<*>.resolveAllHtml(predicate: (name: Na
}.asSequence().associate { it.name to it.data }
context(SnarkContext) public fun DataTree<*>.findByContentType(
context(SnarkContext)
public fun DataTree<*>.findByContentType(
contentType: String,
baseName: Name = Name.EMPTY,
): Map<Name, Data<HtmlFragment>> = resolveAllHtml { name, meta ->

View File

@ -0,0 +1,59 @@
package space.kscience.snark.html
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus
public val SiteBuilder.languages: Map<String, Meta>
get() = siteMeta["site.languages"]?.items?.mapKeys { it.key.toStringUnescaped() } ?: emptyMap()
public val SiteBuilder.language: String
get() = siteMeta["site.language"].string ?: "en"
public fun SiteBuilder.withLanguages(languageMap: Map<String, Meta>, block: SiteBuilder.(language: String) -> Unit) {
languageMap.forEach { (languageKey, languageMeta) ->
val prefix = languageMeta["prefix"].string ?: languageKey
val routeMeta = Meta {
"site.language" put languageKey
"site.languages" put Meta {
languageMap.forEach {
it.key put it.value
}
}
}
route(prefix, routeMeta = routeMeta) {
block(languageKey)
}
}
}
public fun SiteBuilder.withLanguages(
vararg language: Pair<String, String>,
block: SiteBuilder.(language: String) -> Unit,
) {
val languageMap = language.associate {
it.first to Meta {
"prefix" put it.second
}
}
withLanguages(languageMap, block)
}
/**
* The language key of this page
*/
public val WebPage.language: String get() = pageMeta["language"]?.string ?: pageMeta["site.language"]?.string ?: "en"
/**
* Mapping of language keys to other language versions of this page
*/
public val WebPage.languages: Map<String, Meta>
get() = pageMeta["languages"]?.items?.mapKeys { it.key.toStringUnescaped() } ?: emptyMap()
public fun WebPage.localisedPageRef(pageName: Name, relative: Boolean = false): String {
val prefix = languages[language]?.get("prefix")?.string?.parseAsName() ?: Name.EMPTY
return resolvePageRef(prefix + pageName, relative)
}

View File

@ -16,11 +16,12 @@ import kotlinx.html.CommonAttributeGroupFacade
import kotlinx.html.HTML
import kotlinx.html.style
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.withDefault
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.cutLast
import space.kscience.dataforge.names.endsWith
import space.kscience.dataforge.names.plus
import space.kscience.snark.SnarkEnvironment
import space.kscience.snark.html.*
import java.nio.file.Path
@ -37,6 +38,7 @@ public class KtorSiteBuilder(
override val data: DataTree<*>,
override val siteMeta: Meta,
private val baseUrl: String,
override val route: Name,
private val ktorRoute: Route,
) : SiteBuilder {
@ -64,21 +66,27 @@ public class KtorSiteBuilder(
private inner class KtorWebPage(
val pageBaseUrl: String,
override val pageMeta: Meta = this@KtorSiteBuilder.siteMeta,
override val pageMeta: Meta,
) : WebPage {
override val snark: SnarkHtmlPlugin get() = this@KtorSiteBuilder.snark
override val data: DataTree<*> get() = this@KtorSiteBuilder.data
override fun resolveRef(ref: String): String = resolveRef(pageBaseUrl, ref)
override fun resolvePageRef(pageName: Name): String = if (pageName.endsWith(SiteBuilder.INDEX_PAGE_TOKEN)) {
resolveRef(pageName.cutLast().toWebPath())
override fun resolvePageRef(
pageName: Name,
relative: Boolean,
): String {
val fullPageName = if(relative) route + pageName else pageName
return if (fullPageName.endsWith(SiteBuilder.INDEX_PAGE_TOKEN)) {
resolveRef(fullPageName.cutLast().toWebPath())
} else {
resolveRef(pageName.toWebPath())
resolveRef(fullPageName.toWebPath())
}
}
}
override fun page(route: Name, content: context(WebPage, HTML)() -> Unit) {
override fun page(route: Name, pageMeta: Meta, content: context(WebPage, HTML)() -> Unit) {
ktorRoute.get(route.toWebPath()) {
call.respondHtml {
val request = call.request
@ -89,7 +97,7 @@ public class KtorSiteBuilder(
port = request.origin.port
}
val pageBuilder = KtorWebPage(url.buildString())
val pageBuilder = KtorWebPage(url.buildString(), Laminate(pageMeta, siteMeta))
content(pageBuilder, this)
}
}
@ -98,17 +106,26 @@ public class KtorSiteBuilder(
override fun route(
routeName: Name,
dataOverride: DataTree<*>?,
metaOverride: Meta?,
setAsRoot: Boolean,
routeMeta: Meta,
): SiteBuilder = KtorSiteBuilder(
snark = snark,
data = dataOverride ?: data,
siteMeta = metaOverride?.withDefault(siteMeta) ?: siteMeta,
baseUrl = if (setAsRoot) {
resolveRef(baseUrl, routeName.toWebPath())
} else {
baseUrl
},
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,
ktorRoute = ktorRoute.createRouteFromPath(routeName.toWebPath())
)
@ -122,17 +139,19 @@ public class KtorSiteBuilder(
}
}
context(Route, SnarkEnvironment) private fun siteInRoute(
context(Route, SnarkEnvironment)
private fun siteInRoute(
baseUrl: String = "",
block: KtorSiteBuilder.() -> Unit,
) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(KtorSiteBuilder(buildHtmlPlugin(), data, meta, baseUrl, this@Route))
block(KtorSiteBuilder(buildHtmlPlugin(), data, meta, baseUrl, route = Name.EMPTY, this@Route))
}
context(Application) public fun SnarkEnvironment.site(
context(Application)
public fun SnarkEnvironment.site(
baseUrl: String = "",
block: KtorSiteBuilder.() -> Unit,
) {