Hack to fix language switch

This commit is contained in:
Alexander Nozik 2024-02-28 22:06:39 +03:00
parent 35cd0e828a
commit eddeea8758
12 changed files with 234 additions and 203 deletions

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,7 +1,6 @@
rootProject.name = "snark"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
//enableFeaturePreview("VERSION_CATALOGS")
pluginManagement {

View File

@ -0,0 +1,17 @@
package space.kscience.snark
import space.kscience.dataforge.data.Data
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.GenericDataTree
import space.kscience.dataforge.names.NameToken
import kotlin.reflect.KType
public class DataTreeWithDefault<T>(public val tree: DataTree<T>, val default: DataTree<T>) :
DataTree<T> {
override val dataType: KType get() = tree.dataType
override val self: DataTreeWithDefault<T> get() = this
override val data: Data<T>? get() = tree.data ?: default.data
override val items: Map<NameToken, GenericDataTree<T, *>> get() = default.items + tree.items
}

View File

@ -1,12 +0,0 @@
package space.kscience.snark
import space.kscience.dataforge.data.DataSource
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.filterByType
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.startsWith
public inline fun <reified R : Any> DataTree<*>.branch(
branchName: Name,
): DataSource<R> = filterByType<R> { name, _, _ -> name.startsWith(branchName) }

View File

@ -5,6 +5,7 @@ import space.kscience.dataforge.actions.transform
import space.kscience.dataforge.data.*
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.*
import space.kscience.snark.DataTreeWithDefault
import space.kscience.snark.SnarkBuilder
import space.kscience.snark.html.Language.Companion.SITE_LANGUAGE_KEY
import space.kscience.snark.html.Language.Companion.SITE_LANGUAGE_MAP_KEY
@ -16,26 +17,37 @@ public class Language : Scheme() {
/**
* Language key override
*/
public var key: String? by string()
public var key: String by string { error("Language key is not defined") }
/**
* Page name prefix
* Data location
*/
public var prefix: String? by string()
public var dataPath: Name by value<Name>(
reader = { (it?.string ?: key).parseAsName(true) },
writer = { it.toString().asValue() }
)
/**
* An override for data path. By default uses [prefix]
* Page name prefix override
*/
public var dataPath: String? by string()
public var route: Name by value<Name>(
reader = { (it?.string ?: key).parseAsName(true) },
writer = { it.toString().asValue() }
)
/**
* Target page name with a given language key
*/
public var target: Name?
get() = meta["target"].string?.parseAsName(false)
set(value) {
meta["target"] = value?.toString()?.asValue()
}
// /**
// * An override for data path. By default uses [prefix]
// */
// public var dataPath: String? by string()
//
// /**
// * Target page name with a given language key
// */
// public var target: Name?
// get() = meta["target"].string?.parseAsName(false)
// set(value) {
// meta["target"] = value?.toString()?.asValue()
// }
public companion object : SchemeSpec<Language>(::Language) {
@ -48,32 +60,21 @@ public class Language : Scheme() {
public val SITE_LANGUAGE_MAP_KEY: Name = SiteContext.SITE_META_KEY + LANGUAGE_MAP_KEY
public const val DEFAULT_LANGUAGE: String = "en"
// /**
// * Automatically build a language map for a data piece with given [name] based on existence of appropriate data nodes.
// */
// context(PageContextWithData)
// public fun languageMapFor(name: Name): Meta = Meta {
// val currentLanguagePrefix = languages[language]?.get(Language::prefix.name)?.string ?: language
// val fullName = (site.route.removeFirstOrNull(currentLanguagePrefix.asName()) ?: site.route) + name
// languages.forEach { (key, meta) ->
// val languagePrefix: String = meta[Language::prefix.name].string ?: key
// val nameWithLanguage: Name = if (languagePrefix.isBlank()) {
// fullName
// } else {
// languagePrefix.asName() + fullName
// }
// if (data.resolveHtmlOrNull(name) != null) {
// key put meta.asMutableMeta().apply {
// Language::target.name put nameWithLanguage.toString()
// }
// }
// }
// }
}
}
public fun Language(prefix: String): Language = Language { this.prefix = prefix }
public fun Language(
key: String,
route: Name = key.parseAsName(true),
modifier: Language.() -> Unit = {},
): Language = Language {
this.key = key
this.route = route
modifier()
}
internal val Data<*>.language: String?
get() = meta[Language.LANGUAGE_KEY].string?.lowercase()
public val SiteContext.languageMap: Map<String, Language>
get() = siteMeta[SITE_LANGUAGE_MAP_KEY]?.items?.map {
@ -83,61 +84,95 @@ public val SiteContext.languageMap: Map<String, Language>
public val SiteContext.language: String
get() = siteMeta[SITE_LANGUAGE_KEY].string ?: Language.DEFAULT_LANGUAGE
public val SiteContext.languagePrefix: Name
get() = languageMap[language]?.let { it.prefix ?: language }?.parseAsName() ?: Name.EMPTY
/**
* Walk the data tree depth-first
*/
private fun <T, TR : GenericDataTree<T, TR>> TR.walk(
namePrefix: Name = Name.EMPTY,
): Sequence<Pair<Name, TR>> = sequence {
yield(namePrefix to this@walk)
items.forEach { (token, tree) ->
yieldAll(tree.walk(namePrefix + token))
}
}
private class LanguageBranchAction(val prefix: Name) : AbstractAction<Any, Any>(typeOf<Any>()) {
private class LanguageMapAction(val languages: Set<Language>) : AbstractAction<Any, Any>(typeOf<Any>()) {
override fun DataSink<Any>.generate(data: DataTree<Any>, meta: Meta) {
data.forEach { (name, item) ->
val nameWithoutPrefix = name.removeFirstOrNull(prefix)
if (nameWithoutPrefix != null) {
// put language item as is
put(nameWithoutPrefix, item)
} else if (data[name + prefix] == null) {
// put if language item is missing
put(name, item)
val languageMapCache = mutableMapOf<Name, MutableMap<Language, DataTree<Any>>>()
data.walk().forEach { (name, node) ->
val language = node.data?.language?.let { itemLanguage -> languages.find { it.key == itemLanguage } }
if (language == null) {
// put data without a language into all buckets
languageMapCache[name] = languages.associateWithTo(HashMap()) { node }
} else {
// collect data with language markers
val nameWithoutPrefix = if (name.startsWith(language.dataPath)) name.cutFirst() else name
languageMapCache.getOrPut(nameWithoutPrefix) { mutableMapOf() }[language] = node
}
}
languageMapCache.forEach { (nodeName, languageMap) ->
val languageMapMeta = Meta {
languageMap.keys.forEach { language ->
set(language.key, (language.route + nodeName).toWebPath())
}
}
languageMap.forEach { (language, node) ->
val languagePrefix = language.dataPath
val nodeData = node.data
if (nodeData != null) {
put(
languagePrefix + nodeName,
nodeData.withMeta { set(Language.LANGUAGE_MAP_KEY, languageMapMeta) }
)
} else {
wrap(languagePrefix + nodeName, Unit, Meta { set(Language.LANGUAGE_MAP_KEY, languageMapMeta) })
}
}
}
}
override fun DataSink<Any>.update(source: DataTree<Any>, meta: Meta, namedData: NamedData<Any>) {
val name = namedData.name
val nameWithoutPrefix = name.removeFirstOrNull(prefix)
if (nameWithoutPrefix != null) {
// put language item as is
put(nameWithoutPrefix, namedData.data)
} else if (source[name + prefix] == null) {
// put if language item is missing
put(name, namedData.data)
TODO("Not yet implemented")
}
}
/**
* Create a multiple sites for different languages. All sites use the same [content], but rely on different data
*/
@SnarkBuilder
public fun SiteContextWithData.multiLanguageSite(
defaultLanguage: Language,
vararg languages: Language,
content: HtmlSite,
) {
val languageSet = setOf(defaultLanguage, *languages)
val languageMappedData = siteData.filterByType<Any>().transform(
LanguageMapAction(languageSet)
)
languageSet.forEach { language ->
val languageSiteMeta = Meta {
SITE_LANGUAGE_KEY put language.key
SITE_LANGUAGE_MAP_KEY put Meta {
languageSet.forEach {
it.key put it
}
}
}
/**
* Create a multiple sites for different languages. All sites use the same [content], but rely on different data
*
* @param data a common data root for all sites
*/
@SnarkBuilder
public fun SiteContext.multiLanguageSite(
data: DataTree<*>,
languageMap: Map<String, Language>,
content: HtmlSite,
) {
languageMap.forEach { (languageKey, language) ->
val prefix = language.prefix ?: languageKey
val languageSiteMeta = Meta {
SITE_LANGUAGE_KEY put languageKey
SITE_LANGUAGE_MAP_KEY put Meta {
languageMap.forEach {
it.key put it.value
}
}
}
val overlayData = DataTreeWithDefault<Any>(
languageMappedData.branch(language.dataPath)!!,
languageMappedData.branch(defaultLanguage.dataPath)!!
)
site(
prefix.parseAsName(),
data.filterByType<Any>().transform(LanguageBranchAction(Name.parse(language.dataPath ?: prefix))),
language.route,
overlayData,
siteMeta = Laminate(languageSiteMeta, siteMeta),
content
)
@ -156,32 +191,7 @@ public val PageContext.language: String
public val PageContext.languageMap: Map<String, Meta>
get() = pageMeta[Language.LANGUAGE_MAP_KEY]?.items?.mapKeys { it.key.toStringUnescaped() } ?: emptyMap()
public fun PageContext.localisedPageRef(pageName: Name, relative: Boolean = false): String {
val prefix = languageMap[language]?.get(Language::prefix.name)?.string?.parseAsName() ?: Name.EMPTY
return resolvePageRef(prefix + pageName, relative)
public fun PageContext.localisedPageRef(pageName: Name): String {
val prefix = languageMap[language]?.get(Language::dataPath.name)?.string?.parseAsName() ?: Name.EMPTY
return resolvePageRef(prefix + pageName)
}
//
///**
// * Render all pages in a node with given name. Use localization prefix if appropriate data is available.
// */
//public fun SiteContext.localizedPages(
// dataPath: Name,
// remotePath: Name = dataPath,
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
//) {
// val item = resolveData.getItem(languagePrefix + dataPath)
// ?: resolveData.getItem(dataPath)
// ?: error("No data found by name $dataPath")
// route(remotePath) {
// pages(item, dataRenderer)
// }
//}
//
//public fun SiteContext.localizedPages(
// dataPath: String,
// remotePath: Name = dataPath.parseAsName(),
// dataRenderer: DataRenderer = DataRenderer.DEFAULT,
//) {
// localizedPages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
//}

View File

@ -17,4 +17,4 @@ public fun <T> Data<T>.withMeta(newMeta: Meta): Data<T> = if (this is MetaMaskDa
MetaMaskData(this, newMeta)
}
public inline fun <T> Data<T>.mapMeta(block: MutableMeta.() -> Unit): Data<T> = withMeta(meta.copy(block))
public inline fun <T> Data<T>.withMeta(block: MutableMeta.() -> Unit): Data<T> = withMeta(meta.copy(block))

View File

@ -1,13 +1,13 @@
package space.kscience.snark.html
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.appendPathSegments
import space.kscience.dataforge.data.DataTree
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.asName
import space.kscience.dataforge.names.hasIndex
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.*
import space.kscience.snark.SnarkBuilder
import space.kscience.snark.SnarkContext
@ -27,32 +27,49 @@ public interface PageContext : SnarkContext {
public val site: SiteContext
public val host: Url
/**
* A metadata for a page. It should include site metadata
*/
public val pageMeta: Meta
/**
* Resolve absolute url for given [ref]
* A route relative to parent site. Includes [SiteContext.siteRoute].
*/
public val pageRoute: Name
/**
* Resolve absolute url for given relative [ref]
*
*/
public fun resolveRef(ref: String): String
public fun resolveRef(ref: String, targetSite: SiteContext = site): String {
val pageUrl = URLBuilder(host)
.appendPathSegments(targetSite.path, true)
.appendPathSegments(ref)
return pageUrl.buildString().removeSuffix("/")
}
/**
* Resolve absolute url for a page with given [pageName].
*
* @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): String
public fun resolveRelativePageRef(pageName: Name): String = resolvePageRef(pageRoute + pageName)
}
context(PageContext)
public val page: PageContext
get() = this@PageContext
public fun PageContext.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
public fun PageContext.resolvePageRef(pageName: String): String =
resolvePageRef(pageName.parseAsName())
public val PageContext.homeRef: String get() = resolvePageRef(SiteContext.INDEX_PAGE_TOKEN.asName())
public val PageContext.homeRef: String get() = resolvePageRef(Name.EMPTY)
public val PageContext.name: Name? get() = pageMeta["name"].string?.parseAsName()

View File

@ -36,10 +36,6 @@ 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<*>.order: Int?
get() = meta["order"]?.int

View File

@ -12,7 +12,7 @@ import kotlin.reflect.typeOf
public class ParseAction(private val snarkHtml: SnarkHtml) :
AbstractAction<Binary, PageFragment>(typeOf<PageFragment>()) {
private fun parseOne(data: NamedData<Binary>, actionMeta: Meta): NamedData<PageFragment>? = with(snarkHtml) {
private fun parseOne(data: NamedData<Binary>): NamedData<PageFragment>? = with(snarkHtml) {
val contentType = getContentType(data.name, data.meta)
val parser = snark.readers.values.filterIsInstance<SnarkHtmlReader>().filter { parser ->
@ -40,11 +40,11 @@ public class ParseAction(private val snarkHtml: SnarkHtml) :
override fun DataSink<PageFragment>.generate(data: DataTree<Binary>, meta: Meta) {
data.forEach {
parseOne(it, meta)?.let { put(it) }
parseOne(it)?.let { put(it) }
}
}
override fun DataSink<PageFragment>.update(source: DataTree<Binary>, meta: Meta, namedData: NamedData<Binary>) {
parseOne(namedData,meta)?.let { put(it) }
parseOne(namedData)?.let { put(it) }
}
}

View File

@ -15,11 +15,15 @@ import space.kscience.snark.SnarkContext
*/
@SnarkBuilder
public interface SiteContext : SnarkContext {
/**
* A context path segments for this site
*/
public val path: List<String>
/**
* Route name of this [SiteContext] relative to the site root
*/
public val route: Name
public val siteRoute: Name
/**
* Site configuration
@ -49,7 +53,7 @@ public interface SiteContext : SnarkContext {
)
/**
* Create a route block with its own data. Does not change base url
* Create a route block with its own data. Does not change the context path
*/
@SnarkBuilder
public fun route(
@ -60,7 +64,7 @@ public interface SiteContext : SnarkContext {
)
/**
* Creates a sub-site and sets it as site base url
* Creates a sub-site and changes context path to match [name]
* @param route mount site at [rootName]
*/
@SnarkBuilder

View File

@ -1,5 +1,6 @@
package space.kscience.snark.html.static
import io.ktor.http.Url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
@ -10,7 +11,6 @@ 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.snark.html.*
import java.nio.file.Path
@ -23,8 +23,9 @@ import kotlin.reflect.typeOf
*/
internal class StaticSiteContext(
override val siteMeta: Meta,
private val baseUrl: String,
override val route: Name,
private val baseUrl: Url,
override val path: List<String>,
override val siteRoute: Name,
private val outputPath: Path,
) : SiteContext {
@ -82,21 +83,23 @@ internal class StaticSiteContext(
}
}
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
ref
} else if (ref.isEmpty()) {
baseUrl
} else {
"${baseUrl.removeSuffix("/")}/$ref"
}
// private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
// ref
// } else if (ref.isEmpty()) {
// baseUrl
// } else {
// "${baseUrl.removeSuffix("/")}/$ref"
// }
class StaticPageContext(override val site: StaticSiteContext, override val pageMeta: Meta) : PageContext {
class StaticPageContext(
override val site: StaticSiteContext,
override val host: Url,
override val pageRoute: Name,
override val pageMeta: Meta,
) : PageContext {
override fun resolveRef(ref: String): String =
site.resolveRef(site.baseUrl, ref)
override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef(
(if (relative) site.route + pageName else pageName).toWebPath() + ".html"
override fun resolvePageRef(pageName: Name): String = resolveRef(
pageName.toWebPath() + ".html"
)
}
@ -115,7 +118,7 @@ internal class StaticSiteContext(
newPath.parent.createDirectories()
val pageContext = StaticPageContext(this, Laminate(modifiedPageMeta, siteMeta))
val pageContext = StaticPageContext(this, baseUrl, route, Laminate(modifiedPageMeta, siteMeta))
newPath.writeText(HtmlPage.createHtmlString(pageContext, data, content))
}
@ -124,7 +127,8 @@ internal class StaticSiteContext(
StaticSiteContext(
siteMeta = Laminate(siteMeta, this@StaticSiteContext.siteMeta),
baseUrl = baseUrl,
route = route,
path = emptyList(),
siteRoute = route,
outputPath = outputPath.resolve(route.toWebPath())
),
data ?: DataTree.EMPTY
@ -140,8 +144,9 @@ internal class StaticSiteContext(
val siteContextWithData = SiteContextWithData(
StaticSiteContext(
siteMeta = Laminate(siteMeta, this@StaticSiteContext.siteMeta),
baseUrl = if (baseUrl == "") "" else resolveRef(baseUrl, route.toWebPath()),
route = Name.EMPTY,
baseUrl = baseUrl,
path = emptyList(),
siteRoute = route,
outputPath = outputPath.resolve(route.toWebPath())
),
data ?: DataTree.EMPTY
@ -164,12 +169,12 @@ internal class StaticSiteContext(
public suspend fun SnarkHtml.staticSite(
data: DataTree<*>?,
outputPath: Path,
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
siteUrl: Url = Url(outputPath.absolutePathString()),
siteMeta: Meta = data?.meta ?: Meta.EMPTY,
content: HtmlSite,
) {
val siteContextWithData = SiteContextWithData(
StaticSiteContext(siteMeta, siteUrl, Name.EMPTY, outputPath),
StaticSiteContext(siteMeta, siteUrl, emptyList(), Name.EMPTY, outputPath),
data ?: DataTree.EMPTY
)
with(content) {

View File

@ -20,10 +20,7 @@ import space.kscience.dataforge.data.await
import space.kscience.dataforge.data.meta
import space.kscience.dataforge.io.Binary
import space.kscience.dataforge.io.toByteArray
import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.toMutableMeta
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.cutLast
import space.kscience.dataforge.names.endsWith
@ -37,11 +34,11 @@ import kotlin.reflect.typeOf
// style = CssBuilder().block().toString()
//}
public class KtorSiteContext(
internal class KtorSiteContext(
override val context: Context,
override val siteMeta: Meta,
private val baseUrl: String,
override val route: Name,
override val path: List<String>,
override val siteRoute: Name,
private val ktorRoute: Route,
) : SiteContext, ContextAware {
@ -77,32 +74,21 @@ public class KtorSiteContext(
}
}
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
ref
} else if (ref.isEmpty()) {
baseUrl
} else {
"${baseUrl.removeSuffix("/")}/$ref"
}
private class KtorPageContext(
override val site: KtorSiteContext,
val pageBaseUrl: String,
override val host: Url,
override val pageRoute: Name,
override val pageMeta: Meta,
) : PageContext {
override fun resolveRef(ref: String): String = site.resolveRef(pageBaseUrl, ref)
override fun resolvePageRef(
pageName: Name,
relative: Boolean,
): String {
val fullPageName = if (relative) site.route + pageName else pageName
return if (fullPageName.endsWith(SiteContext.INDEX_PAGE_TOKEN)) {
resolveRef(fullPageName.cutLast().toWebPath())
return if (pageName.endsWith(SiteContext.INDEX_PAGE_TOKEN)) {
resolveRef(pageName.cutLast().toWebPath())
} else {
resolveRef(fullPageName.toWebPath())
resolveRef(pageName.toWebPath())
}
}
}
@ -111,19 +97,28 @@ public class KtorSiteContext(
ktorRoute.get(route.toWebPath()) {
val request = call.request
//substitute host for url for backwards calls
val url = URLBuilder(baseUrl).apply {
// val url = URLBuilder(baseUrl).apply {
// protocol = URLProtocol.createOrDefault(request.origin.scheme)
// host = request.origin.serverHost
// port = request.origin.serverPort
// }
val hostUrl = URLBuilder().apply {
protocol = URLProtocol.createOrDefault(request.origin.scheme)
host = request.origin.serverHost
port = request.origin.serverPort
}
val modifiedPageMeta = pageMeta.toMutableMeta().apply {
"name" put route.toString()
"url" put url.buildString()
val modifiedPageMeta = pageMeta.copy {
"host" put hostUrl.buildString()
"path" put path.map { it.asValue() }.asValue()
"route" put (siteRoute + route).toString()
}
val pageContext = KtorPageContext(
site = this@KtorSiteContext,
pageBaseUrl = url.buildString(),
host = hostUrl.build(),
pageRoute = siteRoute + route,
pageMeta = Laminate(modifiedPageMeta, siteMeta)
)
//render page in suspend environment
@ -138,8 +133,8 @@ public class KtorSiteContext(
KtorSiteContext(
context,
siteMeta = Laminate(siteMeta, this@KtorSiteContext.siteMeta),
baseUrl = baseUrl,
route = route,
path = path,
siteRoute = route,
ktorRoute = ktorRoute.createRouteFromPath(route.toWebPath())
),
data ?: DataTree.EMPTY
@ -156,8 +151,8 @@ public class KtorSiteContext(
KtorSiteContext(
context,
siteMeta = Laminate(siteMeta, this@KtorSiteContext.siteMeta),
baseUrl = resolveRef(baseUrl, route.toWebPath()),
route = Name.EMPTY,
path = path + route.tokens.map { it.toStringUnescaped() },
siteRoute = Name.EMPTY,
ktorRoute = ktorRoute.createRouteFromPath(route.toWebPath())
),
data ?: DataTree.EMPTY
@ -174,12 +169,12 @@ public class KtorSiteContext(
public fun Route.site(
context: Context,
data: DataTree<*>?,
baseUrl: String = "",
path: List<String> = emptyList(),
siteMeta: Meta = data?.meta ?: Meta.EMPTY,
content: HtmlSite,
) {
val siteContext = SiteContextWithData(
KtorSiteContext(context, siteMeta, baseUrl, route = Name.EMPTY, this@Route),
KtorSiteContext(context, siteMeta, path = path, siteRoute = Name.EMPTY, this@Route),
data ?: DataTree.EMPTY
)
with(content) {