diff --git a/build.gradle.kts b/build.gradle.kts index 425d02f..c3e655d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ allprojects { } } -val dataforgeVersion by extra("0.6.1-dev-4") +val dataforgeVersion by extra("0.6.1-dev-6") ksciencePublish { github("SciProgCentre", "snark") diff --git a/gradle.properties b/gradle.properties index bdedf81..cc14d8b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ kotlin.code.style=official -toolsVersion=0.14.2-kotlin-1.8.10 \ No newline at end of file +toolsVersion=0.14.5-kotlin-1.8.20-RC \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb70..e1bef7e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME 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 zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 5c36ba3..87d8990 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,7 @@ rootProject.name = "snark" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -enableFeaturePreview("VERSION_CATALOGS") +//enableFeaturePreview("VERSION_CATALOGS") pluginManagement { diff --git a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkBuilder.kt b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkBuilder.kt new file mode 100644 index 0000000..bb0844e --- /dev/null +++ b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkBuilder.kt @@ -0,0 +1,4 @@ +package space.kscience.snark + +@DslMarker +public annotation class SnarkBuilder diff --git a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkContext.kt b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkContext.kt index 374d2cc..2f637cd 100644 --- a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkContext.kt +++ b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkContext.kt @@ -3,4 +3,5 @@ package space.kscience.snark /** * A marker interface for Snark Page and Site builders */ +@SnarkBuilder public interface SnarkContext \ No newline at end of file diff --git a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkEnvironment.kt b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkEnvironment.kt deleted file mode 100644 index 2f155bc..0000000 --- a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkEnvironment.kt +++ /dev/null @@ -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 get() = _data ?: DataTree.empty() - - public fun data(builder: DataSourceBuilder.() -> Unit) { - _data = DataTreeBuilder(typeOf(), 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() - public val plugins: Set 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) diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/ImageIOReader.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/ImageIOReader.kt new file mode 100644 index 0000000..84d52e5 --- /dev/null +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/ImageIOReader.kt @@ -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 { + override val type: KType get() = typeOf() + + override fun readObject(input: Input): BufferedImage = ImageIO.read(input.asStream()) +} \ No newline at end of file diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/Language.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/Language.kt index 4d2ef15..3dd1893 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/Language.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/Language.kt @@ -3,6 +3,7 @@ package space.kscience.snark.html import space.kscience.dataforge.data.getItem import space.kscience.dataforge.meta.* 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_LANGUAGE_KEY @@ -46,7 +47,7 @@ public class Language : Scheme() { context(SiteBuilder) public fun forName(name: Name): Meta = Meta { 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) -> val languagePrefix: String = meta[Language::prefix.name].string ?: key val nameWithLanguage: Name = if (languagePrefix.isBlank()) { @@ -90,6 +91,7 @@ public fun SiteBuilder.withLanguages(languageMap: Map, block: Site } } +@SnarkBuilder public fun SiteBuilder.withLanguages( vararg language: Pair, block: SiteBuilder.(language: String) -> Unit, diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/SiteBuilder.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/SiteBuilder.kt index 677bf92..89ba1bd 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/SiteBuilder.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/SiteBuilder.kt @@ -5,6 +5,7 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.data.DataTree import space.kscience.dataforge.data.DataTreeItem +import space.kscience.dataforge.data.branch import space.kscience.dataforge.data.getItem import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get @@ -14,6 +15,7 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.parseAsName +import space.kscience.snark.SnarkBuilder import space.kscience.snark.SnarkContext import space.kscience.snark.html.SiteLayout.Companion.LAYOUT_KEY @@ -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 */ +@SnarkBuilder 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]. */ - public fun file(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) + public fun static(dataName: Name, routeName: Name = dataName) + /** * Create a single page at given [route]. If route is empty, create an index page at current route. * * @param pageMeta additional page meta. [WebPage] will use both it and [siteMeta] */ - 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. @@ -100,9 +90,10 @@ public interface SiteBuilder : ContextAware, SnarkContext { } context(SiteBuilder) -public val siteBuilder: SiteBuilder +public val site: SiteBuilder get() = this@SiteBuilder +@SnarkBuilder public inline fun SiteBuilder.route( route: Name, dataOverride: DataTree<*>? = null, @@ -112,6 +103,7 @@ public inline fun SiteBuilder.route( route(route, dataOverride, routeMeta).apply(block) } +@SnarkBuilder public inline fun SiteBuilder.route( route: String, dataOverride: DataTree<*>? = null, @@ -121,6 +113,7 @@ public inline fun SiteBuilder.route( route(route.parseAsName(), dataOverride, routeMeta).apply(block) } +@SnarkBuilder public inline fun SiteBuilder.site( route: Name, dataOverride: DataTree<*>? = null, @@ -130,6 +123,7 @@ public inline fun SiteBuilder.site( site(route, dataOverride, routeMeta).apply(block) } +@SnarkBuilder public inline fun SiteBuilder.site( route: String, dataOverride: DataTree<*>? = null, @@ -139,6 +133,26 @@ public inline fun SiteBuilder.site( 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 @@ -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) { rootMeta.getIndexed("file".asName()).forEach { (_, meta) -> val webName: String? by meta.string() val name by meta.string { error("File path is not provided") } val fileName = name.parseAsName() - file(fileName, webName?.parseAsName() ?: fileName) + static(fileName, webName?.parseAsName() ?: fileName) } } diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkHtmlPlugin.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkHtmlPlugin.kt index a287c6e..ec5c23e 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkHtmlPlugin.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkHtmlPlugin.kt @@ -3,6 +3,7 @@ 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.IOFormat import space.kscience.dataforge.io.IOPlugin import space.kscience.dataforge.io.IOReader 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.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 @@ -66,16 +66,19 @@ public class SnarkHtmlPlugin : AbstractPlugin() { "png".asName() to SnarkParser(ImageIOReader, "png"), "jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"), "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( "basic".asName() to BasicTextProcessor ) + else -> super.content(target) } public companion object : PluginFactory { override val tag: PluginTag = PluginTag("snark") - override val type: KClass = SnarkHtmlPlugin::class 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) -public fun SnarkHtmlPlugin.readDirectory(path: Path): DataTree = io.readDataDirectory(path) { dataPath, meta -> +public fun SnarkHtmlPlugin.readDirectory(path: Path): DataTree = io.readDataDirectory(path, setOf("md","html")) { dataPath, meta -> val fileExtension = meta[FileData.FILE_EXTENSION_KEY].string ?: dataPath.extension val parser: SnarkParser = parsers.values.filter { parser -> fileExtension in parser.fileExtensions }.maxByOrNull { it.priority } ?: 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 } parser.asReader(context, meta) -} \ No newline at end of file +} + +public fun SnarkHtmlPlugin.readResourceDirectory( + resource: String = "", + classLoader: ClassLoader = SnarkHtmlPlugin::class.java.classLoader, +): DataTree = readDirectory( + Path.of( + classLoader.getResource(resource)?.toURI() ?: error( + "Resource with name $resource is not resolved" + ) + ) +) \ No newline at end of file diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkTextParser.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkTextParser.kt index f03dc41..0d872d4 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkTextParser.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/SnarkTextParser.kt @@ -1,19 +1,14 @@ package space.kscience.snark.html -import io.ktor.util.asStream -import io.ktor.utils.io.core.Input import kotlinx.html.div import kotlinx.html.unsafe import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser import space.kscience.dataforge.context.Context -import space.kscience.dataforge.io.IOReader import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.snark.SnarkParser -import java.awt.image.BufferedImage -import javax.imageio.ImageIO import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -61,8 +56,3 @@ internal object SnarkMarkdownParser : SnarkTextParser() { } } -internal object ImageIOReader : IOReader { - override val type: KType get() = typeOf() - - override fun readObject(input: Input): BufferedImage = ImageIO.read(input.asStream()) -} diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/StaticSiteBuilder.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/StaticSiteBuilder.kt index fa7c04f..b1dbdbe 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/StaticSiteBuilder.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/StaticSiteBuilder.kt @@ -10,7 +10,6 @@ import space.kscience.dataforge.meta.toMutableMeta 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 import kotlin.contracts.InvocationKind @@ -30,7 +29,7 @@ internal class StaticSiteBuilder( private val outputPath: Path, ) : SiteBuilder { - override fun file(dataName: Name, routeName: Name) { + override fun static(dataName: Name, routeName: Name) { TODO("Not yet implemented") } @@ -82,15 +81,16 @@ internal class StaticSiteBuilder( 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( - (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 modifiedPageMeta = pageMeta.toMutableMeta().apply { @@ -98,7 +98,7 @@ internal class StaticSiteBuilder( } htmlBuilder.html { - content(StaticWebPage(Laminate(modifiedPageMeta, siteMeta)), this) + content(this, StaticWebPage(Laminate(modifiedPageMeta, siteMeta))) } val newPath = if (route.isEmpty()) { @@ -132,7 +132,7 @@ internal class StaticSiteBuilder( snark = snark, data = dataOverride ?: data, siteMeta = Laminate(routeMeta, siteMeta), - baseUrl = if(baseUrl == "") "" else resolveRef(baseUrl, routeName.toWebPath()), + baseUrl = if (baseUrl == "") "" else resolveRef(baseUrl, routeName.toWebPath()), route = Name.EMPTY, 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. * */ -public fun SnarkEnvironment.static( +public fun SnarkHtmlPlugin.static( + data: DataTree<*>, outputPath: Path, siteUrl: String = outputPath.absolutePathString().replace("\\", "/"), + siteMeta: Meta = data.meta, block: SiteBuilder.() -> Unit, ) { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - val plugin = buildHtmlPlugin() - StaticSiteBuilder(plugin, data, meta, siteUrl, Name.EMPTY, outputPath).block() + StaticSiteBuilder(this, data, siteMeta, siteUrl, Name.EMPTY, outputPath).block() } \ No newline at end of file diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/WebPage.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/WebPage.kt index 2e39158..77b2fef 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/WebPage.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/WebPage.kt @@ -7,6 +7,7 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string import space.kscience.dataforge.names.* +import space.kscience.snark.SnarkBuilder import space.kscience.snark.SnarkContext context(SnarkContext) @@ -21,6 +22,7 @@ public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") { /** * A context for building a single page */ +@SnarkBuilder public interface WebPage : ContextAware, SnarkContext { 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 */ context(SnarkContext) -public fun DataTree<*>.resolveHtml(name: Name): HtmlData? { +public fun DataTree<*>.resolveHtmlOrNull(name: Name): HtmlData? { val resolved = (getByType(name) ?: getByType(name + SiteBuilder.INDEX_PAGE_TOKEN)) return resolved?.takeIf { @@ -71,7 +73,11 @@ public fun DataTree<*>.resolveHtml(name: Name): HtmlData? { } 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 diff --git a/snark-html/src/main/kotlin/space/kscience/snark/html/htmlEnvironment.kt b/snark-html/src/main/kotlin/space/kscience/snark/html/htmlEnvironment.kt index 0989398..0772f20 100644 --- a/snark-html/src/main/kotlin/space/kscience/snark/html/htmlEnvironment.kt +++ b/snark-html/src/main/kotlin/space/kscience/snark/html/htmlEnvironment.kt @@ -1,42 +1,37 @@ 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 = 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) + context(SiteBuilder) override fun render(item: DataTreeItem<*>) = body(site, item) } } } -public fun SnarkEnvironment.html(block: SnarkHtmlEnvironmentBuilder.() -> Unit) { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - - val envBuilder = SnarkHtmlEnvironmentBuilder().apply(block) - - val plugin = object : AbstractPlugin() { - val snark by require(SnarkHtmlPlugin) - - override val tag: PluginTag = PluginTag("@extension[${hashCode()}]") - - - override fun content(target: String): Map = when (target) { - SiteLayout.TYPE -> envBuilder.layouts - else -> super.content(target) - } - } - registerPlugin(plugin) -} \ No newline at end of file +//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 = when (target) { +// SiteLayout.TYPE -> envBuilder.layouts +// else -> super.content(target) +// } +// } +// registerPlugin(plugin) +//} \ No newline at end of file diff --git a/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/KtorSiteBuilder.kt b/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/KtorSiteBuilder.kt index 56548d7..e69f070 100644 --- a/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/KtorSiteBuilder.kt +++ b/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/KtorSiteBuilder.kt @@ -20,6 +20,8 @@ import kotlinx.css.CssBuilder import kotlinx.html.CommonAttributeGroupFacade import kotlinx.html.HTML 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.DataTreeItem 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.plus import space.kscience.dataforge.workspace.FileData -import space.kscience.snark.SnarkEnvironment -import space.kscience.snark.html.* +import space.kscience.snark.html.SiteBuilder +import space.kscience.snark.html.SnarkHtmlPlugin +import space.kscience.snark.html.WebPage +import space.kscience.snark.html.toWebPath import java.nio.file.Path import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -52,34 +56,35 @@ public class KtorSiteBuilder( private val ktorRoute: Route, ) : SiteBuilder { - private fun file(item: DataTreeItem, routeName: Name) { - val extension = item.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: "" - + private fun files(item: DataTreeItem, routeName: Name) { //try using direct file rendering item.meta[FileData.FILE_PATH_KEY]?.string?.let { - try { - val file = 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 + val file = try { + Path.of(it).toFile() } catch (ex: Exception) { //failure, + logger.error { "File $it could not be converted to java.io.File"} 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) { is DataTreeItem.Leaf -> { val datum = item.data if (datum.type != typeOf()) 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 extension = item.meta[FileData.FILE_EXTENSION_KEY]?.string?.let { ".$it" } ?: "" val contentType: ContentType = extension .let(ContentType::fromFileExtension) .firstOrNull() @@ -93,15 +98,15 @@ public class KtorSiteBuilder( is DataTreeItem.Node -> { item.tree.items.forEach { (token, childItem) -> - file(childItem, routeName + token) + files(childItem, routeName + token) } } } } - override fun file(dataName: Name, routeName: Name) { - val item: DataTreeItem = data.getItem(dataName) ?: error("Data with name is not resolved") - file(item, routeName) + override fun static(dataName: Name, routeName: Name) { + val item: DataTreeItem = data.getItem(dataName) ?: error("Data with name $dataName is not resolved") + files(item, routeName) } // // override fun file(file: Path, webPath: String) { @@ -140,13 +145,13 @@ public class KtorSiteBuilder( 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 resolveRef(ref: String): String = this@KtorSiteBuilder.resolveRef(pageBaseUrl, ref) override fun resolvePageRef( pageName: Name, relative: Boolean, ): 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)) { resolveRef(fullPageName.cutLast().toWebPath()) } 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()) { call.respondHtml { val request = call.request @@ -172,7 +177,7 @@ public class KtorSiteBuilder( } 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 siteInRoute( +private fun Route.site( + snarkHtmlPlugin: SnarkHtmlPlugin, + data: DataTree<*>, baseUrl: String = "", + siteMeta: Meta = data.meta, block: KtorSiteBuilder.() -> Unit, ) { contract { 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 SnarkEnvironment.site( +public fun Application.site( + snark: SnarkHtmlPlugin, + data: DataTree<*>, baseUrl: String = "", - block: KtorSiteBuilder.() -> Unit, + siteMeta: Meta = data.meta, + block: SiteBuilder.() -> Unit, ) { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } routing { - siteInRoute(baseUrl, block) + site(snark, data, baseUrl, siteMeta, block) } } diff --git a/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/extractData.kt b/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/extractData.kt index fc47faf..d3cdb57 100644 --- a/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/extractData.kt +++ b/snark-ktor/src/main/kotlin/space/kscience/snark/ktor/extractData.kt @@ -12,30 +12,30 @@ import java.time.LocalDateTime import kotlin.io.path.* -public fun KtorSiteBuilder.extractResources(uri: URI, targetPath: Path): Path { - if (Files.isDirectory(targetPath)) { - logger.info { "Using existing data directory at $targetPath." } - } else { - logger.info { "Copying data from $uri into $targetPath." } - targetPath.createDirectories() - //Copy everything into a temporary directory - FileSystems.newFileSystem(uri, emptyMap()).use { fs -> - val rootPath: Path = fs.provider().getPath(uri) - Files.walk(rootPath).forEach { source: Path -> - if (source.isRegularFile()) { - val relative = source.relativeTo(rootPath).toString() - val destination: Path = targetPath.resolve(relative) - destination.parent.createDirectories() - Files.copy(source, destination) - } - } - } - } - return targetPath -} - -public fun KtorSiteBuilder.extractResources(resource: String, targetPath: Path): Path = - extractResources(javaClass.getResource(resource)!!.toURI(), targetPath) +//public fun KtorSiteBuilder.extractResources(uri: URI, targetPath: Path): Path { +// if (Files.isDirectory(targetPath)) { +// logger.info { "Using existing data directory at $targetPath." } +// } else { +// logger.info { "Copying data from $uri into $targetPath." } +// targetPath.createDirectories() +// //Copy everything into a temporary directory +// FileSystems.newFileSystem(uri, emptyMap()).use { fs -> +// val rootPath: Path = fs.provider().getPath(uri) +// Files.walk(rootPath).forEach { source: Path -> +// if (source.isRegularFile()) { +// val relative = source.relativeTo(rootPath).toString() +// val destination: Path = targetPath.resolve(relative) +// destination.parent.createDirectories() +// Files.copy(source, destination) +// } +// } +// } +// } +// return targetPath +//} +// +//public fun KtorSiteBuilder.extractResources(resource: String, targetPath: Path): Path = +// extractResources(javaClass.getResource(resource)!!.toURI(), targetPath) private const val DEPLOY_DATE_FILE = "deployDate" private const val BUILD_DATE_FILE = "/buildDate"