Spliting site and the snark project complete

This commit is contained in:
Alexander Nozik 2022-07-03 16:29:00 +03:00
parent 406ac3fb9e
commit cb60fbcfe2
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
14 changed files with 307 additions and 159 deletions

View File

@ -15,4 +15,4 @@ allprojects {
}
}
val dataforgeVersion by extra("0.6.0-dev-9")
val dataforgeVersion by extra("0.6.0-dev-10")

View File

@ -0,0 +1,40 @@
package space.kscience.snark
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.Plugin
import space.kscience.dataforge.data.DataSourceBuilder
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.DataTreeBuilder
import space.kscience.dataforge.meta.MutableMeta
import kotlin.reflect.typeOf
public class SnarkEnvironment(public val parentContext: Context) {
private var _data: DataTree<*>? = null
public val data: DataTree<Any> get() = _data ?: DataTree.empty()
public fun data(builder: DataSourceBuilder<Any>.() -> Unit) {
_data = DataTreeBuilder<Any>(typeOf<Any>(), parentContext.coroutineContext).apply(builder)
//TODO use node meta
}
public val meta: MutableMeta = MutableMeta()
public fun meta(block: MutableMeta.() -> Unit) {
meta.apply(block)
}
private val _plugins = HashSet<Plugin>()
public val plugins: Set<Plugin> get() = _plugins
public fun registerPlugin(plugin: Plugin) {
_plugins.add(plugin)
}
public companion object{
public val default: SnarkEnvironment = SnarkEnvironment(Global)
}
}
public fun SnarkEnvironment(parentContext: Context = Global, block: SnarkEnvironment.() -> Unit): SnarkEnvironment =
SnarkEnvironment(parentContext).apply(block)

View File

@ -4,9 +4,12 @@ import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.readBytes
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.IOReader
import space.kscience.dataforge.io.asBinary
import space.kscience.dataforge.io.readWith
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.Type
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* A parser of binary content including priority flag and file extensions
@ -32,3 +35,21 @@ public interface SnarkParser<out R> {
public const val DEFAULT_PRIORITY: Int = 10
}
}
@PublishedApi
internal class SnarkParserWrapper<R : Any>(
val reader: IOReader<R>,
override val type: KType,
override val fileExtensions: Set<String>,
) : SnarkParser<R> {
override fun parse(context: Context, meta: Meta, bytes: ByteArray): R = bytes.asBinary().readWith(reader)
}
/**
* Create a generic parser from reader
*/
@Suppress("FunctionName")
public inline fun <reified R : Any> SnarkParser(
reader: IOReader<R>,
vararg fileExtensions: String,
): SnarkParser<R> = SnarkParserWrapper(reader, typeOf<R>(), fileExtensions.toSet())

View File

@ -16,7 +16,7 @@ import space.kscience.snark.SnarkContext
//typealias HtmlFragment = context(PageBuilder, TagConsumer<*>) () -> Unit
public fun interface HtmlFragment {
public fun TagConsumer<*>.renderFragment(page: Page)
public fun TagConsumer<*>.renderFragment(page: WebPage)
//TODO move pageBuilder to a context receiver after KT-52967 is fixed
}
@ -26,7 +26,7 @@ public typealias HtmlData = Data<HtmlFragment>
// Data(HtmlFragment(content), meta)
context(Page) 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) }
}

View File

@ -4,15 +4,12 @@ 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.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.parseAsName
import space.kscience.snark.SnarkContext
import java.nio.file.Path
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
@ -20,27 +17,46 @@ import kotlin.reflect.typeOf
*/
public interface SiteBuilder : ContextAware, SnarkContext {
/**
* Data used for site construction. The type of the data is not limited
*/
public val data: DataTree<*>
public val snark: SnarkPlugin
/**
* Snark plugin and context used for layout resolution, preprocessors, etc
*/
public val snark: SnarkHtmlPlugin
override val context: Context get() = snark.context
/**
* Site configuration
*/
public val siteMeta: Meta
public fun assetFile(remotePath: String, file: Path)
/**
* Add a static file or directory to this site/route at [remotePath]
*/
public fun file(file: Path, remotePath: String = file.fileName.toString())
public fun assetDirectory(remotePath: String, directory: Path)
/**
* Add a static file (single) from resources
*/
public fun resourceFile(remotePath: String, resourcesPath: String)
public fun assetResourceFile(remotePath: String, resourcesPath: String)
/**
* Add a resource directory to route
*/
public fun resourceDirectory(resourcesPath: String)
public fun assetResourceDirectory(resourcesPath: String)
public fun page(route: Name = Name.EMPTY, content: context(Page, HTML) () -> Unit)
/**
* Create a single page at given [route]. If route is empty, create an index page at current route.
*/
public fun page(route: Name = Name.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.
* By default, the same data tree is used for route
* By default, the same data tree is used for route.
*/
public fun route(
routeName: Name,
@ -91,10 +107,3 @@ public inline fun SiteBuilder.route(
// withData(mountedData).block()
// }
//}
//TODO move to DF
public fun DataTree.Companion.empty(meta: Meta = Meta.EMPTY): DataTree<Any> = object : DataTree<Any> {
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
override val dataType: KType get() = typeOf<Any>()
override val meta: Meta get() = meta
}

View File

@ -30,27 +30,30 @@ internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
path?.let { resourcePath ->
//If remote path provided, use a single resource
remotePath?.let {
assetResourceFile(it, resourcePath)
resourceFile(it, resourcePath)
return@forEach
}
//otherwise use package resources
assetResourceDirectory(resourcePath)
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") }
assetFile(remotePath, Path.of(path))
file(Path.of(path), remotePath)
}
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
val path by meta.string { error("Directory path is not provided") }
assetDirectory("", Path.of(path))
file(Path.of(path), "")
}
}
/**
* Render (or don't) given data piece
*/
public typealias DataRenderer = SiteBuilder.(name: Name, data: Data<Any>) -> Unit
/**
@ -114,7 +117,9 @@ public fun SiteBuilder.pages(
pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
}
/**
* An abstraction to render singular data or a data tree.
*/
@Type(SiteLayout.TYPE)
public fun interface SiteLayout {
@ -142,7 +147,9 @@ public fun interface SiteLayout {
}
}
/**
* The default [SiteLayout]. It renders all [HtmlData] pages as t with simple headers via [SiteLayout.defaultDataRenderer]
*/
public object DefaultSiteLayout : SiteLayout {
context(SiteBuilder) override fun render(item: DataTreeItem<*>) {
pages(item)

View File

@ -3,7 +3,9 @@ package space.kscience.snark.html
import io.ktor.utils.io.core.readBytes
import space.kscience.dataforge.context.*
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.io.*
import space.kscience.dataforge.io.IOPlugin
import space.kscience.dataforge.io.IOReader
import space.kscience.dataforge.io.JsonMetaFormat
import space.kscience.dataforge.io.yaml.YamlMetaFormat
import space.kscience.dataforge.io.yaml.YamlPlugin
import space.kscience.dataforge.meta.Meta
@ -15,32 +17,16 @@ import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.workspace.FileData
import space.kscience.dataforge.workspace.readDataDirectory
import space.kscience.snark.SnarkEnvironment
import space.kscience.snark.SnarkParser
import java.nio.file.Path
import kotlin.io.path.extension
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.typeOf
@PublishedApi
internal class SnarkParserWrapper<R : Any>(
val reader: IOReader<R>,
override val type: KType,
override val fileExtensions: Set<String>,
) : SnarkParser<R> {
override fun parse(context: Context, meta: Meta, bytes: ByteArray): R = bytes.asBinary().readWith(reader)
}
/**
* Create a generic parser from reader
* A plugin used for rendering a [DataTree] as HTML
*/
@Suppress("FunctionName")
public inline fun <reified R : Any> SnarkParser(
reader: IOReader<R>,
vararg fileExtensions: String,
): SnarkParser<R> = SnarkParserWrapper(reader, typeOf<R>(), fileExtensions.toSet())
public class SnarkPlugin : AbstractPlugin() {
public class SnarkHtmlPlugin : AbstractPlugin() {
private val yaml by require(YamlPlugin)
public val io: IOPlugin get() = yaml.io
@ -54,8 +40,8 @@ public class SnarkPlugin : AbstractPlugin() {
context.gather(SiteLayout.TYPE, true)
}
private val textTransformations: Map<Name, TextTransformation> by lazy {
context.gather(TextTransformation.TYPE, true)
private val textProcessors: Map<Name, TextProcessor> by lazy {
context.gather(TextProcessor.TYPE, true)
}
internal fun siteLayout(layoutMeta: Meta): SiteLayout {
@ -64,10 +50,10 @@ public class SnarkPlugin : AbstractPlugin() {
return siteLayouts[layoutName.parseAsName()] ?: error("Layout with name $layoutName not found in $this")
}
internal fun textTransformation(transformationMeta: Meta): TextTransformation {
internal fun textProcessor(transformationMeta: Meta): TextProcessor {
val transformationName = transformationMeta.string
?: transformationMeta["name"].string ?: error("Transformation name not defined in $transformationMeta")
return textTransformations[transformationName.parseAsName()]
return textProcessors[transformationName.parseAsName()]
?: error("Text transformation with name $transformationName not found in $this")
}
@ -81,17 +67,17 @@ public class SnarkPlugin : AbstractPlugin() {
"jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"),
"gif".asName() to SnarkParser(ImageIOReader, "gif"),
)
TextTransformation.TYPE -> mapOf(
"basic".asName() to BasicTextTransformation
TextProcessor.TYPE -> mapOf(
"basic".asName() to BasicTextProcessor
)
else -> super.content(target)
}
public companion object : PluginFactory<SnarkPlugin> {
public companion object : PluginFactory<SnarkHtmlPlugin> {
override val tag: PluginTag = PluginTag("snark")
override val type: KClass<out SnarkPlugin> = SnarkPlugin::class
override val type: KClass<out SnarkHtmlPlugin> = SnarkHtmlPlugin::class
override fun build(context: Context, meta: Meta): SnarkPlugin = SnarkPlugin()
override fun build(context: Context, meta: Meta): SnarkHtmlPlugin = SnarkHtmlPlugin()
private val byteArrayIOReader = IOReader {
readBytes()
@ -101,8 +87,21 @@ public class SnarkPlugin : AbstractPlugin() {
}
}
/**
* Load necessary dependencies and return a [SnarkHtmlPlugin] in a finalized context
*/
public fun SnarkEnvironment.buildHtmlPlugin(): SnarkHtmlPlugin {
val context = parentContext.buildContext("snark".asName()) {
plugin(SnarkHtmlPlugin)
plugins.forEach {
plugin(it)
}
}
return context.fetch(SnarkHtmlPlugin)
}
@OptIn(DFExperimental::class)
public fun SnarkPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path) { dataPath, meta ->
public fun SnarkHtmlPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path) { dataPath, meta ->
val fileExtension = meta[FileData.META_FILE_EXTENSION_KEY].string ?: dataPath.extension
val parser: SnarkParser<Any> = parsers.values.filter { parser ->
fileExtension in parser.fileExtensions
@ -110,7 +109,7 @@ public fun SnarkPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDir
it.priority
} ?: run {
logger.warn { "The parser is not found for file $dataPath with meta $meta" }
SnarkPlugin.byteArraySnarkParser
SnarkHtmlPlugin.byteArraySnarkParser
}
parser.reader(context, meta)

View File

@ -23,9 +23,9 @@ public abstract class SnarkTextParser<R> : SnarkParser<R> {
override fun parse(context: Context, meta: Meta, bytes: ByteArray): R =
parseText(bytes.decodeToString(), meta)
public fun transformText(text: String, meta: Meta, page: Page): String =
meta[TextTransformation.TEXT_TRANSFORMATION_KEY]?.let {
with(page) { page.snark.textTransformation(it).transform(text) }
public fun transformText(text: String, meta: Meta, page: WebPage): String =
meta[TextProcessor.TEXT_TRANSFORMATION_KEY]?.let {
with(page) { page.snark.textProcessor(it).process(text) }
} ?: text
}

View File

@ -8,6 +8,7 @@ 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.snark.SnarkEnvironment
import java.nio.file.Files
import java.nio.file.Path
import kotlin.contracts.InvocationKind
@ -15,12 +16,15 @@ import kotlin.contracts.contract
import kotlin.io.path.*
/**
* An implementation of [SiteBuilder] to render site as a static directory [outputPath]
*/
internal class StaticSiteBuilder(
override val snark: SnarkPlugin,
override val snark: SnarkHtmlPlugin,
override val data: DataTree<*>,
override val siteMeta: Meta,
private val baseUrl: String,
private val path: Path,
private val outputPath: Path,
) : SiteBuilder {
private fun Path.copyRecursively(target: Path) {
Files.walk(this).forEach { source: Path ->
@ -32,27 +36,28 @@ internal class StaticSiteBuilder(
}
}
override fun assetFile(remotePath: String, file: Path) {
val targetPath = path.resolve(remotePath)
override fun file(file: Path, remotePath: String) {
val targetPath = outputPath.resolve(remotePath)
if(file.isDirectory()){
targetPath.parent.createDirectories()
file.copyRecursively(targetPath)
} else if(remotePath.isBlank()) {
error("Can't mount file to an empty route")
} else {
targetPath.parent.createDirectories()
file.copyTo(targetPath, true)
}
override fun assetDirectory(remotePath: String, directory: Path) {
val targetPath = path.resolve(remotePath)
targetPath.parent.createDirectories()
directory.copyRecursively(targetPath)
}
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
val targetPath = path.resolve(remotePath)
override fun resourceFile(remotePath: String, resourcesPath: String) {
val targetPath = outputPath.resolve(remotePath)
targetPath.parent.createDirectories()
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true)
}
override fun assetResourceDirectory(resourcesPath: String) {
path.parent.createDirectories()
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path)
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()) {
@ -63,10 +68,10 @@ internal class StaticSiteBuilder(
"${baseUrl.removeSuffix("/")}/$ref"
}
inner class StaticPage : Page {
inner class StaticWebPage : WebPage {
override val data: DataTree<*> get() = this@StaticSiteBuilder.data
override val pageMeta: Meta get() = this@StaticSiteBuilder.siteMeta
override val snark: SnarkPlugin get() = this@StaticSiteBuilder.snark
override val snark: SnarkHtmlPlugin get() = this@StaticSiteBuilder.snark
override fun resolveRef(ref: String): String = resolveRef(baseUrl, ref)
@ -77,17 +82,17 @@ internal class StaticSiteBuilder(
}
override fun page(route: Name, content: context(Page, HTML) () -> Unit) {
override fun page(route: Name, content: context(WebPage, HTML) () -> Unit) {
val htmlBuilder = createHTML()
htmlBuilder.html {
content(StaticPage(), this)
content(StaticWebPage(), this)
}
val newPath = if (route.isEmpty()) {
path.resolve("index.html")
outputPath.resolve("index.html")
} else {
path.resolve(route.toWebPath() + ".html")
outputPath.resolve(route.toWebPath() + ".html")
}
newPath.parent.createDirectories()
@ -108,18 +113,23 @@ internal class StaticSiteBuilder(
} else {
baseUrl
},
path = path.resolve(routeName.toWebPath())
outputPath = outputPath.resolve(routeName.toWebPath())
)
}
public fun SnarkPlugin.renderStatic(
/**
* Create a static site using given [data] in provided [outputPath].
* Use [siteUrl] as a base for all resolved URLs. By default, use [outputPath] absolute path as a base.
*
*/
public fun SnarkEnvironment.static(
outputPath: Path,
data: DataTree<*> = DataTree.empty(),
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
block: SiteBuilder.() -> Unit,
) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
StaticSiteBuilder(this, data, meta, siteUrl, outputPath).block()
val plugin = buildHtmlPlugin()
StaticSiteBuilder(plugin, data, meta, siteUrl, outputPath).block()
}

View File

@ -0,0 +1,56 @@
package space.kscience.snark.html
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.parseAsName
/**
* An object that conducts page-based text transformation. Like using link replacement or templating.
*/
@Type(TextProcessor.TYPE)
public fun interface TextProcessor {
context(WebPage) public fun process(text: String): String
public companion object {
public const val TYPE: String = "snark.textTransformation"
public val TEXT_TRANSFORMATION_KEY: NameToken = NameToken("transformation")
}
}
/**
* A basic [TextProcessor] that replaces `${...}` expressions in text. The following expressions are recognised:
* * `homeRef` resolves to [homeRef]
* * `resolveRef("...")` -> [WebPage.resolveRef]
* * `resolvePageRef("...")` -> [WebPage.resolvePageRef]
* * `pageMeta.get("...") -> [WebPage.pageMeta] get string method
* Otherwise return unchanged string
*/
public object BasicTextProcessor : TextProcessor {
private val regex = """\$\{([\w.]*)(?>\("([\w.]*)"\))?}""".toRegex()
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)
}
"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

@ -1,38 +0,0 @@
package space.kscience.snark.html
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.NameToken
@Type(TextTransformation.TYPE)
public fun interface TextTransformation {
context(Page) public fun transform(text: String): String
public companion object {
public const val TYPE: String = "snark.textTransformation"
public val TEXT_TRANSFORMATION_KEY: NameToken = NameToken("transformation")
}
}
public object BasicTextTransformation : TextTransformation {
private val regex = "\\\$\\{(\\w*)(?>\\(\"(.*)\"\\))?\\}".toRegex()
context(Page) override fun transform(text: String): String {
return 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)
}
else -> match.value
}
}
}
}

View File

@ -17,9 +17,9 @@ context(SnarkContext) public fun Name.toWebPath(): String = tokens.joinToString(
}
}
public interface Page : ContextAware, SnarkContext {
public interface WebPage : ContextAware, SnarkContext {
public val snark: SnarkPlugin
public val snark: SnarkHtmlPlugin
override val context: Context get() = snark.context
@ -32,11 +32,11 @@ public interface Page : ContextAware, SnarkContext {
public fun resolvePageRef(pageName: Name): String
}
context(Page) public val page: Page get() = this@Page
context(WebPage) public val page: WebPage get() = this@WebPage
public fun Page.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
public fun WebPage.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
public val Page.homeRef: String get() = resolvePageRef(SiteBuilder.INDEX_PAGE_TOKEN.asName())
public val WebPage.homeRef: String get() = resolvePageRef(SiteBuilder.INDEX_PAGE_TOKEN.asName())
/**
* Resolve a Html builder by its full name

View File

@ -0,0 +1,42 @@
package space.kscience.snark.html
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.data.DataTreeItem
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.snark.SnarkEnvironment
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
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(siteBuilder, 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)
}

View File

@ -20,27 +20,32 @@ 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.snark.SnarkEnvironment
import space.kscience.snark.html.*
import java.nio.file.Path
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.io.path.isDirectory
@PublishedApi
internal class KtorSiteBuilder(
override val snark: SnarkPlugin,
override val snark: SnarkHtmlPlugin,
override val data: DataTree<*>,
override val siteMeta: Meta,
private val baseUrl: String,
private val ktorRoute: Route,
) : SiteBuilder {
override fun assetFile(remotePath: String, file: Path) {
ktorRoute.file(remotePath, file.toFile())
}
override fun assetDirectory(remotePath: String, directory: Path) {
override fun file(file: Path, remotePath: String) {
if (file.isDirectory()) {
ktorRoute.static(remotePath) {
files(directory.toFile())
//TODO check non-standard FS and convert
files(file.toFile())
}
} else if (remotePath.isBlank()) {
error("Can't mount file to an empty route")
} else {
ktorRoute.file(remotePath, file.toFile())
}
}
@ -53,11 +58,11 @@ internal class KtorSiteBuilder(
}
inner class KtorPage(
inner class KtorWebPage(
val pageBaseUrl: String,
override val pageMeta: Meta = this@KtorSiteBuilder.siteMeta,
) : Page {
override val snark: SnarkPlugin get() = this@KtorSiteBuilder.snark
) : 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)
@ -69,7 +74,7 @@ internal class KtorSiteBuilder(
}
}
override fun page(route: Name, content: context(Page, HTML)() -> Unit) {
override fun page(route: Name, content: context(WebPage, HTML)() -> Unit) {
ktorRoute.get(route.toWebPath()) {
call.respondHtml {
val request = call.request
@ -79,7 +84,7 @@ internal class KtorSiteBuilder(
host = request.host()
port = request.port()
}
val pageBuilder = KtorPage(url.buildString())
val pageBuilder = KtorWebPage(url.buildString())
content(pageBuilder, this)
}
}
@ -103,34 +108,31 @@ internal class KtorSiteBuilder(
)
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
override fun resourceFile(remotePath: String, resourcesPath: String) {
ktorRoute.resource(resourcesPath, resourcesPath)
}
override fun assetResourceDirectory(resourcesPath: String) {
override fun resourceDirectory(resourcesPath: String) {
ktorRoute.resources(resourcesPath)
}
}
public inline fun Route.snarkSite(
snark: SnarkPlugin,
data: DataTree<*>,
meta: Meta = data.meta,
context(Route, SnarkEnvironment) public fun siteInRoute(
block: SiteBuilder.() -> Unit,
) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite))
block(KtorSiteBuilder(buildHtmlPlugin(), data, meta, "", this@Route))
}
public fun Application.snarkSite(
snark: SnarkPlugin,
data: DataTree<*> = DataTree.empty(),
meta: Meta = data.meta,
context(Application) public fun SnarkEnvironment.site(
block: SiteBuilder.() -> Unit,
) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
routing {
snarkSite(snark, data, meta, block)
siteInRoute(block)
}
}