Implemented binary propagation

This commit is contained in:
Alexander Nozik 2023-03-27 10:23:27 +03:00
parent 941da6fab7
commit e6bee125d3
17 changed files with 190 additions and 187 deletions

View File

@ -12,7 +12,7 @@ allprojects {
} }
} }
val dataforgeVersion by extra("0.6.1-dev-4") val dataforgeVersion by extra("0.6.1-dev-6")
ksciencePublish { ksciencePublish {
github("SciProgCentre", "snark") github("SciProgCentre", "snark")

View File

@ -1,3 +1,3 @@
kotlin.code.style=official kotlin.code.style=official
toolsVersion=0.14.2-kotlin-1.8.10 toolsVersion=0.14.5-kotlin-1.8.20-RC

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package space.kscience.snark
@DslMarker
public annotation class SnarkBuilder

View File

@ -3,4 +3,5 @@ package space.kscience.snark
/** /**
* A marker interface for Snark Page and Site builders * A marker interface for Snark Page and Site builders
*/ */
@SnarkBuilder
public interface SnarkContext public interface SnarkContext

View File

@ -1,40 +0,0 @@
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

@ -0,0 +1,15 @@
package space.kscience.snark.html
import io.ktor.util.asStream
import io.ktor.utils.io.core.Input
import space.kscience.dataforge.io.IOReader
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import kotlin.reflect.KType
import kotlin.reflect.typeOf
internal object ImageIOReader : IOReader<BufferedImage> {
override val type: KType get() = typeOf<BufferedImage>()
override fun readObject(input: Input): BufferedImage = ImageIO.read(input.asStream())
}

View File

@ -3,6 +3,7 @@ package space.kscience.snark.html
import space.kscience.dataforge.data.getItem import space.kscience.dataforge.data.getItem
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.html.Language.Companion.SITE_LANGUAGES_KEY 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
@ -46,7 +47,7 @@ public class Language : Scheme() {
context(SiteBuilder) context(SiteBuilder)
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.removeHeadOrNull(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()) {
@ -90,6 +91,7 @@ public fun SiteBuilder.withLanguages(languageMap: Map<String, Meta>, block: Site
} }
} }
@SnarkBuilder
public fun SiteBuilder.withLanguages( public fun SiteBuilder.withLanguages(
vararg language: Pair<String, String>, vararg language: Pair<String, String>,
block: SiteBuilder.(language: String) -> Unit, block: SiteBuilder.(language: String) -> Unit,

View File

@ -5,6 +5,7 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.data.DataTree import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.DataTreeItem import space.kscience.dataforge.data.DataTreeItem
import space.kscience.dataforge.data.branch
import space.kscience.dataforge.data.getItem import space.kscience.dataforge.data.getItem
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -14,6 +15,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.snark.SnarkBuilder
import space.kscience.snark.SnarkContext import space.kscience.snark.SnarkContext
import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY
@ -21,6 +23,7 @@ import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY
/** /**
* An abstraction, which is used to render sites to the different rendering engines * An abstraction, which is used to render sites to the different rendering engines
*/ */
@SnarkBuilder
public interface SiteBuilder : ContextAware, SnarkContext { public interface SiteBuilder : ContextAware, SnarkContext {
/** /**
@ -48,29 +51,16 @@ public interface SiteBuilder : ContextAware, SnarkContext {
/** /**
* Serve a static data as a file from [data] with given [dataName] at given [routeName]. * Serve a static data as a file from [data] with given [dataName] at given [routeName].
*/ */
public fun file(dataName: Name, routeName: Name = dataName) public fun static(dataName: Name, routeName: Name = dataName)
//
// /**
// * Add a static file or directory to this site/route at [webPath]
// */
// public fun file(file: Path, webPath: String = file.fileName.toString())
//
// /**
// * Add a static file (single) from resources
// */
// public fun resourceFile(resourcesPath: String, webPath: String = resourcesPath)
//
// /**
// * Add a resource directory to route
// */
// public fun resourceDirectory(resourcesPath: String)
/** /**
* Create a single page at given [route]. If route is empty, create an index page at current route. * 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] * @param pageMeta additional page meta. [WebPage] will use both it and [siteMeta]
*/ */
public fun page(route: Name = Name.EMPTY, pageMeta: Meta = Meta.EMPTY, content: context(WebPage, HTML) () -> Unit) @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. * Create a route with optional data tree override. For example one could use a subtree of the initial tree.
@ -100,9 +90,10 @@ public interface SiteBuilder : ContextAware, SnarkContext {
} }
context(SiteBuilder) context(SiteBuilder)
public val siteBuilder: SiteBuilder public val site: SiteBuilder
get() = this@SiteBuilder get() = this@SiteBuilder
@SnarkBuilder
public inline fun SiteBuilder.route( public inline fun SiteBuilder.route(
route: Name, route: Name,
dataOverride: DataTree<*>? = null, dataOverride: DataTree<*>? = null,
@ -112,6 +103,7 @@ public inline fun SiteBuilder.route(
route(route, dataOverride, routeMeta).apply(block) route(route, dataOverride, routeMeta).apply(block)
} }
@SnarkBuilder
public inline fun SiteBuilder.route( public inline fun SiteBuilder.route(
route: String, route: String,
dataOverride: DataTree<*>? = null, dataOverride: DataTree<*>? = null,
@ -121,6 +113,7 @@ public inline fun SiteBuilder.route(
route(route.parseAsName(), dataOverride, routeMeta).apply(block) route(route.parseAsName(), dataOverride, routeMeta).apply(block)
} }
@SnarkBuilder
public inline fun SiteBuilder.site( public inline fun SiteBuilder.site(
route: Name, route: Name,
dataOverride: DataTree<*>? = null, dataOverride: DataTree<*>? = null,
@ -130,6 +123,7 @@ public inline fun SiteBuilder.site(
site(route, dataOverride, routeMeta).apply(block) site(route, dataOverride, routeMeta).apply(block)
} }
@SnarkBuilder
public inline fun SiteBuilder.site( public inline fun SiteBuilder.site(
route: String, route: String,
dataOverride: DataTree<*>? = null, dataOverride: DataTree<*>? = null,
@ -139,6 +133,26 @@ public inline fun SiteBuilder.site(
site(route.parseAsName(), dataOverride, routeMeta).apply(block) 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 // * Create a stand-alone site at a given node
@ -154,14 +168,19 @@ public inline fun SiteBuilder.site(
// } // }
//} //}
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) { internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
rootMeta.getIndexed("file".asName()).forEach { (_, meta) -> rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
val webName: String? by meta.string() val webName: String? by meta.string()
val name by meta.string { error("File path is not provided") } val name by meta.string { error("File path is not provided") }
val fileName = name.parseAsName() val fileName = name.parseAsName()
file(fileName, webName?.parseAsName() ?: fileName) static(fileName, webName?.parseAsName() ?: fileName)
} }
} }

View File

@ -3,6 +3,7 @@ package space.kscience.snark.html
import io.ktor.utils.io.core.readBytes import io.ktor.utils.io.core.readBytes
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.data.DataTree import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.io.IOFormat
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
@ -17,7 +18,6 @@ import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.workspace.FileData import space.kscience.dataforge.workspace.FileData
import space.kscience.dataforge.workspace.readDataDirectory import space.kscience.dataforge.workspace.readDataDirectory
import space.kscience.snark.SnarkEnvironment
import space.kscience.snark.SnarkParser import space.kscience.snark.SnarkParser
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.extension import kotlin.io.path.extension
@ -66,16 +66,19 @@ public class SnarkHtmlPlugin : AbstractPlugin() {
"png".asName() to SnarkParser(ImageIOReader, "png"), "png".asName() to SnarkParser(ImageIOReader, "png"),
"jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"), "jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"),
"gif".asName() to SnarkParser(ImageIOReader, "gif"), "gif".asName() to SnarkParser(ImageIOReader, "gif"),
"svg".asName() to SnarkParser(IOReader.binary, "svg"),
"raw".asName() to SnarkParser(IOReader.binary, "css", "js", "scss", "woff", "woff2", "ttf", "eot")
) )
TextProcessor.TYPE -> mapOf( TextProcessor.TYPE -> mapOf(
"basic".asName() to BasicTextProcessor "basic".asName() to BasicTextProcessor
) )
else -> super.content(target) else -> super.content(target)
} }
public companion object : PluginFactory<SnarkHtmlPlugin> { public companion object : PluginFactory<SnarkHtmlPlugin> {
override val tag: PluginTag = PluginTag("snark") override val tag: PluginTag = PluginTag("snark")
override val type: KClass<out SnarkHtmlPlugin> = SnarkHtmlPlugin::class
override fun build(context: Context, meta: Meta): SnarkHtmlPlugin = SnarkHtmlPlugin() override fun build(context: Context, meta: Meta): SnarkHtmlPlugin = SnarkHtmlPlugin()
@ -87,30 +90,28 @@ public class SnarkHtmlPlugin : 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.request(SnarkHtmlPlugin)
}
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public fun SnarkHtmlPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path) { dataPath, meta -> public fun SnarkHtmlPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path, setOf("md","html")) { dataPath, meta ->
val fileExtension = meta[FileData.FILE_EXTENSION_KEY].string ?: dataPath.extension val fileExtension = meta[FileData.FILE_EXTENSION_KEY].string ?: dataPath.extension
val parser: SnarkParser<Any> = parsers.values.filter { parser -> val parser: SnarkParser<Any> = parsers.values.filter { parser ->
fileExtension in parser.fileExtensions fileExtension in parser.fileExtensions
}.maxByOrNull { }.maxByOrNull {
it.priority it.priority
} ?: run { } ?: run {
logger.warn { "The parser is not found for file $dataPath with meta $meta" } logger.debug { "The parser is not found for file $dataPath with meta $meta" }
SnarkHtmlPlugin.byteArraySnarkParser SnarkHtmlPlugin.byteArraySnarkParser
} }
parser.asReader(context, meta) parser.asReader(context, meta)
} }
public fun SnarkHtmlPlugin.readResourceDirectory(
resource: String = "",
classLoader: ClassLoader = SnarkHtmlPlugin::class.java.classLoader,
): DataTree<Any> = readDirectory(
Path.of(
classLoader.getResource(resource)?.toURI() ?: error(
"Resource with name $resource is not resolved"
)
)
)

View File

@ -1,19 +1,14 @@
package space.kscience.snark.html package space.kscience.snark.html
import io.ktor.util.asStream
import io.ktor.utils.io.core.Input
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.unsafe import kotlinx.html.unsafe
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.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.IOReader
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.snark.SnarkParser import space.kscience.snark.SnarkParser
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -61,8 +56,3 @@ internal object SnarkMarkdownParser : SnarkTextParser<HtmlFragment>() {
} }
} }
internal object ImageIOReader : IOReader<BufferedImage> {
override val type: KType get() = typeOf<BufferedImage>()
override fun readObject(input: Input): BufferedImage = ImageIO.read(input.asStream())
}

View File

@ -10,7 +10,6 @@ import space.kscience.dataforge.meta.toMutableMeta
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.plus
import space.kscience.snark.SnarkEnvironment
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
@ -30,7 +29,7 @@ internal class StaticSiteBuilder(
private val outputPath: Path, private val outputPath: Path,
) : SiteBuilder { ) : SiteBuilder {
override fun file(dataName: Name, routeName: Name) { override fun static(dataName: Name, routeName: Name) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -82,15 +81,16 @@ internal class StaticSiteBuilder(
override val snark: SnarkHtmlPlugin get() = this@StaticSiteBuilder.snark override val snark: SnarkHtmlPlugin get() = this@StaticSiteBuilder.snark
override fun resolveRef(ref: String): String = resolveRef(baseUrl, ref) override fun resolveRef(ref: String): String =
this@StaticSiteBuilder.resolveRef(this@StaticSiteBuilder.baseUrl, ref)
override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef( override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef(
(if (relative) route + pageName else pageName).toWebPath() + ".html" (if (relative) this@StaticSiteBuilder.route + pageName else pageName).toWebPath() + ".html"
) )
} }
override fun page(route: Name, pageMeta: Meta, content: context(WebPage, HTML) () -> Unit) { override fun page(route: Name, pageMeta: Meta, content: context(HTML) WebPage.() -> Unit) {
val htmlBuilder = createHTML() val htmlBuilder = createHTML()
val modifiedPageMeta = pageMeta.toMutableMeta().apply { val modifiedPageMeta = pageMeta.toMutableMeta().apply {
@ -98,7 +98,7 @@ internal class StaticSiteBuilder(
} }
htmlBuilder.html { htmlBuilder.html {
content(StaticWebPage(Laminate(modifiedPageMeta, siteMeta)), this) content(this, StaticWebPage(Laminate(modifiedPageMeta, siteMeta)))
} }
val newPath = if (route.isEmpty()) { val newPath = if (route.isEmpty()) {
@ -132,7 +132,7 @@ internal class StaticSiteBuilder(
snark = snark, snark = snark,
data = dataOverride ?: data, data = dataOverride ?: data,
siteMeta = Laminate(routeMeta, siteMeta), siteMeta = Laminate(routeMeta, siteMeta),
baseUrl = if(baseUrl == "") "" else resolveRef(baseUrl, routeName.toWebPath()), baseUrl = if (baseUrl == "") "" else resolveRef(baseUrl, routeName.toWebPath()),
route = Name.EMPTY, route = Name.EMPTY,
outputPath = outputPath.resolve(routeName.toWebPath()) outputPath = outputPath.resolve(routeName.toWebPath())
) )
@ -143,14 +143,15 @@ internal class StaticSiteBuilder(
* Use [siteUrl] as a base for all resolved URLs. By default, use [outputPath] absolute path as a base. * Use [siteUrl] as a base for all resolved URLs. By default, use [outputPath] absolute path as a base.
* *
*/ */
public fun SnarkEnvironment.static( public fun SnarkHtmlPlugin.static(
data: DataTree<*>,
outputPath: Path, outputPath: Path,
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"), siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
siteMeta: Meta = data.meta,
block: SiteBuilder.() -> Unit, block: SiteBuilder.() -> Unit,
) { ) {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) callsInPlace(block, InvocationKind.EXACTLY_ONCE)
} }
val plugin = buildHtmlPlugin() StaticSiteBuilder(this, data, siteMeta, siteUrl, Name.EMPTY, outputPath).block()
StaticSiteBuilder(plugin, data, meta, siteUrl, Name.EMPTY, outputPath).block()
} }

View File

@ -7,6 +7,7 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.* import space.kscience.dataforge.names.*
import space.kscience.snark.SnarkBuilder
import space.kscience.snark.SnarkContext import space.kscience.snark.SnarkContext
context(SnarkContext) context(SnarkContext)
@ -21,6 +22,7 @@ public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
/** /**
* A context for building a single page * A context for building a single page
*/ */
@SnarkBuilder
public interface WebPage : ContextAware, SnarkContext { public interface WebPage : ContextAware, SnarkContext {
public val snark: SnarkHtmlPlugin public val snark: SnarkHtmlPlugin
@ -62,7 +64,7 @@ public val WebPage.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<*>.resolveHtml(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 + SiteBuilder.INDEX_PAGE_TOKEN))
return resolved?.takeIf { return resolved?.takeIf {
@ -71,7 +73,11 @@ public fun DataTree<*>.resolveHtml(name: Name): HtmlData? {
} }
context(SnarkContext) context(SnarkContext)
public fun DataTree<*>.resolveHtml(name: String): HtmlData? = resolveHtml(name.parseAsName()) public fun DataTree<*>.resolveHtmlOrNull(name: String): HtmlData? = resolveHtmlOrNull(name.parseAsName())
context(SnarkContext)
public fun DataTree<*>.resolveHtml(name: String): HtmlData = resolveHtmlOrNull(name)
?: error("Html fragment with name $name is not resolved")
/** /**
* Find all Html blocks using given name/meta filter * Find all Html blocks using given name/meta filter

View File

@ -1,42 +1,37 @@
package space.kscience.snark.html 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.data.DataTreeItem
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.snark.SnarkEnvironment
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
public class SnarkHtmlEnvironmentBuilder { public class SnarkHtmlEnvironmentBuilder {
public val layouts: HashMap<Name, SiteLayout> = HashMap() public val layouts: HashMap<Name, SiteLayout> = HashMap()
public fun layout(name: String, body: context(SiteBuilder) (DataTreeItem<*>) -> Unit) { public fun layout(name: String, body: context(SiteBuilder) (DataTreeItem<*>) -> Unit) {
layouts[name.parseAsName()] = object : SiteLayout { layouts[name.parseAsName()] = object : SiteLayout {
context(SiteBuilder) override fun render(item: DataTreeItem<*>) = body(siteBuilder, item) context(SiteBuilder) override fun render(item: DataTreeItem<*>) = body(site, item)
} }
} }
} }
public fun SnarkEnvironment.html(block: SnarkHtmlEnvironmentBuilder.() -> Unit) { //public fun SnarkEnvironment.html(block: SnarkHtmlEnvironmentBuilder.() -> Unit) {
contract { // contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) // callsInPlace(block, InvocationKind.EXACTLY_ONCE)
} // }
//
val envBuilder = SnarkHtmlEnvironmentBuilder().apply(block) // val envBuilder = SnarkHtmlEnvironmentBuilder().apply(block)
//
val plugin = object : AbstractPlugin() { // val plugin = object : AbstractPlugin() {
val snark by require(SnarkHtmlPlugin) // val snark by require(SnarkHtmlPlugin)
//
override val tag: PluginTag = PluginTag("@extension[${hashCode()}]") // override val tag: PluginTag = PluginTag("@extension[${hashCode()}]")
//
//
override fun content(target: String): Map<Name, Any> = when (target) { // override fun content(target: String): Map<Name, Any> = when (target) {
SiteLayout.TYPE -> envBuilder.layouts // SiteLayout.TYPE -> envBuilder.layouts
else -> super.content(target) // else -> super.content(target)
} // }
} // }
registerPlugin(plugin) // registerPlugin(plugin)
} //}

View File

@ -20,6 +20,8 @@ import kotlinx.css.CssBuilder
import kotlinx.html.CommonAttributeGroupFacade import kotlinx.html.CommonAttributeGroupFacade
import kotlinx.html.HTML import kotlinx.html.HTML
import kotlinx.html.style import kotlinx.html.style
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.data.DataTree import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.DataTreeItem import space.kscience.dataforge.data.DataTreeItem
import space.kscience.dataforge.data.await import space.kscience.dataforge.data.await
@ -32,8 +34,10 @@ 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.SnarkEnvironment import space.kscience.snark.html.SiteBuilder
import space.kscience.snark.html.* import space.kscience.snark.html.SnarkHtmlPlugin
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
@ -52,34 +56,35 @@ public class KtorSiteBuilder(
private val ktorRoute: Route, private val ktorRoute: Route,
) : SiteBuilder { ) : SiteBuilder {
private fun file(item: DataTreeItem<Any>, routeName: Name) { private fun files(item: DataTreeItem<Any>, routeName: Name) {
val extension = item.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: ""
//try using direct file rendering //try using direct file rendering
item.meta[FileData.FILE_PATH_KEY]?.string?.let { item.meta[FileData.FILE_PATH_KEY]?.string?.let {
try { val file = try {
val file = Path.of(it).toFile() Path.of(it).toFile()
if (file.isDirectory) {
ktorRoute.static(routeName.toWebPath()) {
files(file)
}
} else {
val fileName = routeName.toWebPath() + extension //TODO add extension
ktorRoute.file(fileName, file)
}
//success, don't do anything else
return@file
} catch (ex: Exception) { } catch (ex: Exception) {
//failure, //failure,
logger.error { "File $it could not be converted to java.io.File"}
return@let return@let
} }
if (file.isDirectory) {
ktorRoute.static(routeName.toWebPath()) {
files(file)
}
} else {
val fileName = routeName.toWebPath()
ktorRoute.file(fileName, file)
}
//success, don't do anything else
return@files
} }
when (item) { when (item) {
is DataTreeItem.Leaf -> { is DataTreeItem.Leaf -> {
val datum = item.data val datum = item.data
if (datum.type != typeOf<Binary>()) error("Can't directly serve file of type ${item.data.type}") if (datum.type != typeOf<Binary>()) error("Can't directly serve file of type ${item.data.type}")
ktorRoute.get(routeName.toWebPath() + extension) { ktorRoute.get(routeName.toWebPath()) {
val binary = datum.await() as Binary 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()
@ -93,15 +98,15 @@ public class KtorSiteBuilder(
is DataTreeItem.Node -> { is DataTreeItem.Node -> {
item.tree.items.forEach { (token, childItem) -> item.tree.items.forEach { (token, childItem) ->
file(childItem, routeName + token) files(childItem, routeName + token)
} }
} }
} }
} }
override fun file(dataName: Name, routeName: Name) { override fun static(dataName: Name, routeName: Name) {
val item: DataTreeItem<Any> = data.getItem(dataName) ?: error("Data with name is not resolved") val item: DataTreeItem<Any> = data.getItem(dataName) ?: error("Data with name $dataName is not resolved")
file(item, routeName) files(item, routeName)
} }
// //
// override fun file(file: Path, webPath: String) { // override fun file(file: Path, webPath: String) {
@ -140,13 +145,13 @@ public class KtorSiteBuilder(
override val snark: SnarkHtmlPlugin get() = this@KtorSiteBuilder.snark override val snark: SnarkHtmlPlugin get() = this@KtorSiteBuilder.snark
override val data: DataTree<*> get() = this@KtorSiteBuilder.data override val data: DataTree<*> get() = this@KtorSiteBuilder.data
override fun resolveRef(ref: String): String = resolveRef(pageBaseUrl, ref) override fun resolveRef(ref: String): String = this@KtorSiteBuilder.resolveRef(pageBaseUrl, ref)
override fun resolvePageRef( override fun resolvePageRef(
pageName: Name, pageName: Name,
relative: Boolean, relative: Boolean,
): String { ): String {
val fullPageName = if (relative) 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(SiteBuilder.INDEX_PAGE_TOKEN)) {
resolveRef(fullPageName.cutLast().toWebPath()) resolveRef(fullPageName.cutLast().toWebPath())
} else { } else {
@ -155,7 +160,7 @@ public class KtorSiteBuilder(
} }
} }
override fun page(route: Name, pageMeta: Meta, content: context(WebPage, HTML)() -> Unit) { override fun page(route: Name, pageMeta: Meta, content: context(HTML, WebPage) () -> Unit) {
ktorRoute.get(route.toWebPath()) { ktorRoute.get(route.toWebPath()) {
call.respondHtml { call.respondHtml {
val request = call.request val request = call.request
@ -172,7 +177,7 @@ public class KtorSiteBuilder(
} }
val pageBuilder = KtorWebPage(url.buildString(), Laminate(modifiedPageMeta, siteMeta)) val pageBuilder = KtorWebPage(url.buildString(), Laminate(modifiedPageMeta, siteMeta))
content(pageBuilder, this) content(this, pageBuilder)
} }
} }
} }
@ -213,26 +218,30 @@ public class KtorSiteBuilder(
// } // }
} }
context(Route, SnarkEnvironment) private fun Route.site(
private fun siteInRoute( snarkHtmlPlugin: SnarkHtmlPlugin,
data: DataTree<*>,
baseUrl: String = "", baseUrl: String = "",
siteMeta: Meta = data.meta,
block: KtorSiteBuilder.() -> Unit, block: KtorSiteBuilder.() -> Unit,
) { ) {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) callsInPlace(block, InvocationKind.EXACTLY_ONCE)
} }
block(KtorSiteBuilder(buildHtmlPlugin(), data, meta, baseUrl, route = Name.EMPTY, this@Route)) block(KtorSiteBuilder(snarkHtmlPlugin, data, siteMeta, baseUrl, route = Name.EMPTY, this@Route))
} }
context(Application) public fun Application.site(
public fun SnarkEnvironment.site( snark: SnarkHtmlPlugin,
data: DataTree<*>,
baseUrl: String = "", baseUrl: String = "",
block: KtorSiteBuilder.() -> Unit, siteMeta: Meta = data.meta,
block: SiteBuilder.() -> Unit,
) { ) {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) callsInPlace(block, InvocationKind.EXACTLY_ONCE)
} }
routing { routing {
siteInRoute(baseUrl, block) site(snark, data, baseUrl, siteMeta, block)
} }
} }

View File

@ -12,30 +12,30 @@ import java.time.LocalDateTime
import kotlin.io.path.* import kotlin.io.path.*
public fun KtorSiteBuilder.extractResources(uri: URI, targetPath: Path): Path { //public fun KtorSiteBuilder.extractResources(uri: URI, targetPath: Path): Path {
if (Files.isDirectory(targetPath)) { // if (Files.isDirectory(targetPath)) {
logger.info { "Using existing data directory at $targetPath." } // logger.info { "Using existing data directory at $targetPath." }
} else { // } else {
logger.info { "Copying data from $uri into $targetPath." } // logger.info { "Copying data from $uri into $targetPath." }
targetPath.createDirectories() // targetPath.createDirectories()
//Copy everything into a temporary directory // //Copy everything into a temporary directory
FileSystems.newFileSystem(uri, emptyMap<String, Any>()).use { fs -> // FileSystems.newFileSystem(uri, emptyMap<String, Any>()).use { fs ->
val rootPath: Path = fs.provider().getPath(uri) // val rootPath: Path = fs.provider().getPath(uri)
Files.walk(rootPath).forEach { source: Path -> // Files.walk(rootPath).forEach { source: Path ->
if (source.isRegularFile()) { // if (source.isRegularFile()) {
val relative = source.relativeTo(rootPath).toString() // val relative = source.relativeTo(rootPath).toString()
val destination: Path = targetPath.resolve(relative) // val destination: Path = targetPath.resolve(relative)
destination.parent.createDirectories() // destination.parent.createDirectories()
Files.copy(source, destination) // Files.copy(source, destination)
} // }
} // }
} // }
} // }
return targetPath // return targetPath
} //}
//
public fun KtorSiteBuilder.extractResources(resource: String, targetPath: Path): Path = //public fun KtorSiteBuilder.extractResources(resource: String, targetPath: Path): Path =
extractResources(javaClass.getResource(resource)!!.toURI(), targetPath) // extractResources(javaClass.getResource(resource)!!.toURI(), targetPath)
private const val DEPLOY_DATE_FILE = "deployDate" private const val DEPLOY_DATE_FILE = "deployDate"
private const val BUILD_DATE_FILE = "/buildDate" private const val BUILD_DATE_FILE = "/buildDate"