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 io.ktor.utils.io.core.readBytes
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.IOReader 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.meta.Meta
import space.kscience.dataforge.misc.Type import space.kscience.dataforge.misc.Type
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf
/** /**
* A parser of binary content including priority flag and file extensions * 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 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 //typealias HtmlFragment = context(PageBuilder, TagConsumer<*>) () -> Unit
public fun interface HtmlFragment { 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 //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) // 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) } 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.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.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.snark.SnarkContext import space.kscience.snark.SnarkContext
import java.nio.file.Path 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 { public interface SiteBuilder : ContextAware, SnarkContext {
/**
* Data used for site construction. The type of the data is not limited
*/
public val data: DataTree<*> 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 override val context: Context get() = snark.context
/**
* Site configuration
*/
public val siteMeta: Meta 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) /**
* 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(Page, HTML) () -> Unit) */
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. * 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( public fun route(
routeName: Name, routeName: Name,
@ -91,10 +107,3 @@ public inline fun SiteBuilder.route(
// withData(mountedData).block() // 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 -> path?.let { resourcePath ->
//If remote path provided, use a single resource //If remote path provided, use a single resource
remotePath?.let { remotePath?.let {
assetResourceFile(it, resourcePath) resourceFile(it, resourcePath)
return@forEach return@forEach
} }
//otherwise use package resources //otherwise use package resources
assetResourceDirectory(resourcePath) resourceDirectory(resourcePath)
} }
} }
rootMeta.getIndexed("file".asName()).forEach { (_, meta) -> rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
val remotePath by meta.string { error("File remote path is not provided") } val remotePath by meta.string { error("File remote path is not provided") }
val path by meta.string { error("File 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) -> rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
val path by meta.string { error("Directory path is not provided") } 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 public typealias DataRenderer = SiteBuilder.(name: Name, data: Data<Any>) -> Unit
/** /**
@ -114,7 +117,9 @@ public fun SiteBuilder.pages(
pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer) pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
} }
/**
* An abstraction to render singular data or a data tree.
*/
@Type(SiteLayout.TYPE) @Type(SiteLayout.TYPE)
public fun interface SiteLayout { 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 { public object DefaultSiteLayout : SiteLayout {
context(SiteBuilder) override fun render(item: DataTreeItem<*>) { context(SiteBuilder) override fun render(item: DataTreeItem<*>) {
pages(item) pages(item)

View File

@ -3,7 +3,9 @@ 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.* 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.YamlMetaFormat
import space.kscience.dataforge.io.yaml.YamlPlugin import space.kscience.dataforge.io.yaml.YamlPlugin
import space.kscience.dataforge.meta.Meta 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.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
import kotlin.reflect.KClass 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 class SnarkHtmlPlugin : AbstractPlugin() {
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() {
private val yaml by require(YamlPlugin) private val yaml by require(YamlPlugin)
public val io: IOPlugin get() = yaml.io public val io: IOPlugin get() = yaml.io
@ -54,8 +40,8 @@ public class SnarkPlugin : AbstractPlugin() {
context.gather(SiteLayout.TYPE, true) context.gather(SiteLayout.TYPE, true)
} }
private val textTransformations: Map<Name, TextTransformation> by lazy { private val textProcessors: Map<Name, TextProcessor> by lazy {
context.gather(TextTransformation.TYPE, true) context.gather(TextProcessor.TYPE, true)
} }
internal fun siteLayout(layoutMeta: Meta): SiteLayout { 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") 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 val transformationName = transformationMeta.string
?: transformationMeta["name"].string ?: error("Transformation name not defined in $transformationMeta") ?: 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") ?: 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"), "jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"),
"gif".asName() to SnarkParser(ImageIOReader, "gif"), "gif".asName() to SnarkParser(ImageIOReader, "gif"),
) )
TextTransformation.TYPE -> mapOf( TextProcessor.TYPE -> mapOf(
"basic".asName() to BasicTextTransformation "basic".asName() to BasicTextProcessor
) )
else -> super.content(target) else -> super.content(target)
} }
public companion object : PluginFactory<SnarkPlugin> { public companion object : PluginFactory<SnarkHtmlPlugin> {
override val tag: PluginTag = PluginTag("snark") 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 { private val byteArrayIOReader = IOReader {
readBytes() 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) @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 fileExtension = meta[FileData.META_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
@ -110,7 +109,7 @@ public fun SnarkPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDir
it.priority it.priority
} ?: run { } ?: run {
logger.warn { "The parser is not found for file $dataPath with meta $meta" } logger.warn { "The parser is not found for file $dataPath with meta $meta" }
SnarkPlugin.byteArraySnarkParser SnarkHtmlPlugin.byteArraySnarkParser
} }
parser.reader(context, meta) 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 = override fun parse(context: Context, meta: Meta, bytes: ByteArray): R =
parseText(bytes.decodeToString(), meta) parseText(bytes.decodeToString(), meta)
public fun transformText(text: String, meta: Meta, page: Page): String = public fun transformText(text: String, meta: Meta, page: WebPage): String =
meta[TextTransformation.TEXT_TRANSFORMATION_KEY]?.let { meta[TextProcessor.TEXT_TRANSFORMATION_KEY]?.let {
with(page) { page.snark.textTransformation(it).transform(text) } with(page) { page.snark.textProcessor(it).process(text) }
} ?: text } ?: text
} }

View File

@ -8,6 +8,7 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.withDefault import space.kscience.dataforge.meta.withDefault
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.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
@ -15,12 +16,15 @@ import kotlin.contracts.contract
import kotlin.io.path.* import kotlin.io.path.*
/**
* An implementation of [SiteBuilder] to render site as a static directory [outputPath]
*/
internal class StaticSiteBuilder( internal class StaticSiteBuilder(
override val snark: SnarkPlugin, override val snark: SnarkHtmlPlugin,
override val data: DataTree<*>, override val data: DataTree<*>,
override val siteMeta: Meta, override val siteMeta: Meta,
private val baseUrl: String, private val baseUrl: String,
private val path: Path, private val outputPath: Path,
) : SiteBuilder { ) : SiteBuilder {
private fun Path.copyRecursively(target: Path) { private fun Path.copyRecursively(target: Path) {
Files.walk(this).forEach { source: Path -> Files.walk(this).forEach { source: Path ->
@ -32,27 +36,28 @@ internal class StaticSiteBuilder(
} }
} }
override fun assetFile(remotePath: String, file: Path) { override fun file(file: Path, remotePath: String) {
val targetPath = path.resolve(remotePath) 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() targetPath.parent.createDirectories()
file.copyTo(targetPath, true) 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) { override fun resourceFile(remotePath: String, resourcesPath: String) {
val targetPath = path.resolve(remotePath) val targetPath = outputPath.resolve(remotePath)
targetPath.parent.createDirectories() targetPath.parent.createDirectories()
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true) javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true)
} }
override fun assetResourceDirectory(resourcesPath: String) { override fun resourceDirectory(resourcesPath: String) {
path.parent.createDirectories() outputPath.parent.createDirectories()
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path) javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(outputPath)
} }
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) { private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
@ -63,10 +68,10 @@ internal class StaticSiteBuilder(
"${baseUrl.removeSuffix("/")}/$ref" "${baseUrl.removeSuffix("/")}/$ref"
} }
inner class StaticPage : Page { inner class StaticWebPage : WebPage {
override val data: DataTree<*> get() = this@StaticSiteBuilder.data override val data: DataTree<*> get() = this@StaticSiteBuilder.data
override val pageMeta: Meta get() = this@StaticSiteBuilder.siteMeta 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) 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() val htmlBuilder = createHTML()
htmlBuilder.html { htmlBuilder.html {
content(StaticPage(), this) content(StaticWebPage(), this)
} }
val newPath = if (route.isEmpty()) { val newPath = if (route.isEmpty()) {
path.resolve("index.html") outputPath.resolve("index.html")
} else { } else {
path.resolve(route.toWebPath() + ".html") outputPath.resolve(route.toWebPath() + ".html")
} }
newPath.parent.createDirectories() newPath.parent.createDirectories()
@ -108,18 +113,23 @@ internal class StaticSiteBuilder(
} else { } else {
baseUrl 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, outputPath: Path,
data: DataTree<*> = DataTree.empty(),
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"), siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
block: SiteBuilder.() -> Unit, block: SiteBuilder.() -> Unit,
) { ) {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) 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 override val context: Context get() = snark.context
@ -32,11 +32,11 @@ public interface Page : ContextAware, SnarkContext {
public fun resolvePageRef(pageName: Name): String 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 * 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.Name
import space.kscience.dataforge.names.cutLast import space.kscience.dataforge.names.cutLast
import space.kscience.dataforge.names.endsWith import space.kscience.dataforge.names.endsWith
import space.kscience.snark.SnarkEnvironment
import space.kscience.snark.html.* import space.kscience.snark.html.*
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
import kotlin.io.path.isDirectory
@PublishedApi @PublishedApi
internal class KtorSiteBuilder( internal class KtorSiteBuilder(
override val snark: SnarkPlugin, override val snark: SnarkHtmlPlugin,
override val data: DataTree<*>, override val data: DataTree<*>,
override val siteMeta: Meta, override val siteMeta: Meta,
private val baseUrl: String, private val baseUrl: String,
private val ktorRoute: Route, private val ktorRoute: Route,
) : SiteBuilder { ) : SiteBuilder {
override fun assetFile(remotePath: String, file: Path) { override fun file(file: Path, remotePath: String) {
ktorRoute.file(remotePath, file.toFile()) if (file.isDirectory()) {
}
override fun assetDirectory(remotePath: String, directory: Path) {
ktorRoute.static(remotePath) { 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, val pageBaseUrl: String,
override val pageMeta: Meta = this@KtorSiteBuilder.siteMeta, override val pageMeta: Meta = this@KtorSiteBuilder.siteMeta,
) : Page { ) : WebPage {
override val snark: SnarkPlugin 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 = 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()) { ktorRoute.get(route.toWebPath()) {
call.respondHtml { call.respondHtml {
val request = call.request val request = call.request
@ -79,7 +84,7 @@ internal class KtorSiteBuilder(
host = request.host() host = request.host()
port = request.port() port = request.port()
} }
val pageBuilder = KtorPage(url.buildString()) val pageBuilder = KtorWebPage(url.buildString())
content(pageBuilder, this) 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) ktorRoute.resource(resourcesPath, resourcesPath)
} }
override fun assetResourceDirectory(resourcesPath: String) { override fun resourceDirectory(resourcesPath: String) {
ktorRoute.resources(resourcesPath) ktorRoute.resources(resourcesPath)
} }
} }
public inline fun Route.snarkSite( context(Route, SnarkEnvironment) public fun siteInRoute(
snark: SnarkPlugin,
data: DataTree<*>,
meta: Meta = data.meta,
block: SiteBuilder.() -> Unit, block: SiteBuilder.() -> Unit,
) { ) {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) callsInPlace(block, InvocationKind.EXACTLY_ONCE)
} }
block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite)) block(KtorSiteBuilder(buildHtmlPlugin(), data, meta, "", this@Route))
} }
public fun Application.snarkSite( context(Application) public fun SnarkEnvironment.site(
snark: SnarkPlugin,
data: DataTree<*> = DataTree.empty(),
meta: Meta = data.meta,
block: SiteBuilder.() -> Unit, block: SiteBuilder.() -> Unit,
) { ) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
routing { routing {
snarkSite(snark, data, meta, block) siteInRoute(block)
} }
} }