Spliting site and the snark project complete
This commit is contained in:
parent
406ac3fb9e
commit
cb60fbcfe2
@ -9,10 +9,10 @@ allprojects {
|
||||
if(name!="snark-gradle-plugin") {
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dataforgeVersion by extra("0.6.0-dev-9")
|
||||
val dataforgeVersion by extra("0.6.0-dev-10")
|
@ -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)
|
@ -4,9 +4,12 @@ import io.ktor.utils.io.core.Input
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.io.IOReader
|
||||
import space.kscience.dataforge.io.asBinary
|
||||
import space.kscience.dataforge.io.readWith
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.misc.Type
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
* A parser of binary content including priority flag and file extensions
|
||||
@ -31,4 +34,22 @@ public interface SnarkParser<out R> {
|
||||
public const val TYPE: String = "snark.parser"
|
||||
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())
|
@ -16,7 +16,7 @@ import space.kscience.snark.SnarkContext
|
||||
//typealias HtmlFragment = context(PageBuilder, TagConsumer<*>) () -> Unit
|
||||
|
||||
public fun interface HtmlFragment {
|
||||
public fun TagConsumer<*>.renderFragment(page: Page)
|
||||
public fun TagConsumer<*>.renderFragment(page: WebPage)
|
||||
//TODO move pageBuilder to a context receiver after KT-52967 is fixed
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ public typealias HtmlData = Data<HtmlFragment>
|
||||
// Data(HtmlFragment(content), meta)
|
||||
|
||||
|
||||
context(Page) public fun FlowContent.htmlData(data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
|
||||
context(WebPage) public fun FlowContent.htmlData(data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
|
||||
with(data.await()) { consumer.renderFragment(page) }
|
||||
}
|
||||
|
||||
|
@ -4,15 +4,12 @@ import kotlinx.html.HTML
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.data.DataTree
|
||||
import space.kscience.dataforge.data.DataTreeItem
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.NameToken
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import space.kscience.snark.SnarkContext
|
||||
import java.nio.file.Path
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
|
||||
/**
|
||||
@ -20,27 +17,46 @@ import kotlin.reflect.typeOf
|
||||
*/
|
||||
public interface SiteBuilder : ContextAware, SnarkContext {
|
||||
|
||||
/**
|
||||
* Data used for site construction. The type of the data is not limited
|
||||
*/
|
||||
public val data: DataTree<*>
|
||||
|
||||
public val snark: SnarkPlugin
|
||||
/**
|
||||
* Snark plugin and context used for layout resolution, preprocessors, etc
|
||||
*/
|
||||
public val snark: SnarkHtmlPlugin
|
||||
|
||||
override val context: Context get() = snark.context
|
||||
|
||||
/**
|
||||
* Site configuration
|
||||
*/
|
||||
public val siteMeta: Meta
|
||||
|
||||
public fun assetFile(remotePath: String, file: Path)
|
||||
/**
|
||||
* Add a static file or directory to this site/route at [remotePath]
|
||||
*/
|
||||
public fun file(file: Path, remotePath: String = file.fileName.toString())
|
||||
|
||||
public fun assetDirectory(remotePath: String, directory: Path)
|
||||
/**
|
||||
* Add a static file (single) from resources
|
||||
*/
|
||||
public fun resourceFile(remotePath: String, resourcesPath: String)
|
||||
|
||||
public fun assetResourceFile(remotePath: String, resourcesPath: String)
|
||||
/**
|
||||
* Add a resource directory to route
|
||||
*/
|
||||
public fun resourceDirectory(resourcesPath: String)
|
||||
|
||||
public fun assetResourceDirectory(resourcesPath: String)
|
||||
|
||||
public fun page(route: Name = Name.EMPTY, content: context(Page, HTML) () -> Unit)
|
||||
/**
|
||||
* Create a single page at given [route]. If route is empty, create an index page at current route.
|
||||
*/
|
||||
public fun page(route: Name = Name.EMPTY, content: context(WebPage, HTML) () -> Unit)
|
||||
|
||||
/**
|
||||
* Create a route with optional data tree override. For example one could use a subtree of the initial tree.
|
||||
* By default, the same data tree is used for route
|
||||
* By default, the same data tree is used for route.
|
||||
*/
|
||||
public fun route(
|
||||
routeName: Name,
|
||||
@ -90,11 +106,4 @@ public inline fun SiteBuilder.route(
|
||||
// route(route) {
|
||||
// withData(mountedData).block()
|
||||
// }
|
||||
//}
|
||||
|
||||
//TODO move to DF
|
||||
public fun DataTree.Companion.empty(meta: Meta = Meta.EMPTY): DataTree<Any> = object : DataTree<Any> {
|
||||
override val items: Map<NameToken, DataTreeItem<Any>> get() = emptyMap()
|
||||
override val dataType: KType get() = typeOf<Any>()
|
||||
override val meta: Meta get() = meta
|
||||
}
|
||||
//}
|
@ -30,27 +30,30 @@ internal fun SiteBuilder.assetsFrom(rootMeta: Meta) {
|
||||
path?.let { resourcePath ->
|
||||
//If remote path provided, use a single resource
|
||||
remotePath?.let {
|
||||
assetResourceFile(it, resourcePath)
|
||||
resourceFile(it, resourcePath)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
//otherwise use package resources
|
||||
assetResourceDirectory(resourcePath)
|
||||
resourceDirectory(resourcePath)
|
||||
}
|
||||
}
|
||||
|
||||
rootMeta.getIndexed("file".asName()).forEach { (_, meta) ->
|
||||
val remotePath by meta.string { error("File remote path is not provided") }
|
||||
val path by meta.string { error("File path is not provided") }
|
||||
assetFile(remotePath, Path.of(path))
|
||||
file(Path.of(path), remotePath)
|
||||
}
|
||||
|
||||
rootMeta.getIndexed("directory".asName()).forEach { (_, meta) ->
|
||||
val path by meta.string { error("Directory path is not provided") }
|
||||
assetDirectory("", Path.of(path))
|
||||
file(Path.of(path), "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render (or don't) given data piece
|
||||
*/
|
||||
public typealias DataRenderer = SiteBuilder.(name: Name, data: Data<Any>) -> Unit
|
||||
|
||||
/**
|
||||
@ -114,7 +117,9 @@ public fun SiteBuilder.pages(
|
||||
pages(dataPath.parseAsName(), remotePath, dataRenderer = dataRenderer)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An abstraction to render singular data or a data tree.
|
||||
*/
|
||||
@Type(SiteLayout.TYPE)
|
||||
public fun interface SiteLayout {
|
||||
|
||||
@ -142,7 +147,9 @@ public fun interface SiteLayout {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The default [SiteLayout]. It renders all [HtmlData] pages as t with simple headers via [SiteLayout.defaultDataRenderer]
|
||||
*/
|
||||
public object DefaultSiteLayout : SiteLayout {
|
||||
context(SiteBuilder) override fun render(item: DataTreeItem<*>) {
|
||||
pages(item)
|
||||
|
@ -3,7 +3,9 @@ package space.kscience.snark.html
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.data.DataTree
|
||||
import space.kscience.dataforge.io.*
|
||||
import space.kscience.dataforge.io.IOPlugin
|
||||
import space.kscience.dataforge.io.IOReader
|
||||
import space.kscience.dataforge.io.JsonMetaFormat
|
||||
import space.kscience.dataforge.io.yaml.YamlMetaFormat
|
||||
import space.kscience.dataforge.io.yaml.YamlPlugin
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
@ -15,32 +17,16 @@ import space.kscience.dataforge.names.asName
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import space.kscience.dataforge.workspace.FileData
|
||||
import space.kscience.dataforge.workspace.readDataDirectory
|
||||
import space.kscience.snark.SnarkEnvironment
|
||||
import space.kscience.snark.SnarkParser
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@PublishedApi
|
||||
internal class SnarkParserWrapper<R : Any>(
|
||||
val reader: IOReader<R>,
|
||||
override val type: KType,
|
||||
override val fileExtensions: Set<String>,
|
||||
) : SnarkParser<R> {
|
||||
override fun parse(context: Context, meta: Meta, bytes: ByteArray): R = bytes.asBinary().readWith(reader)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic parser from reader
|
||||
* A plugin used for rendering a [DataTree] as HTML
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
public inline fun <reified R : Any> SnarkParser(
|
||||
reader: IOReader<R>,
|
||||
vararg fileExtensions: String,
|
||||
): SnarkParser<R> = SnarkParserWrapper(reader, typeOf<R>(), fileExtensions.toSet())
|
||||
|
||||
public class SnarkPlugin : AbstractPlugin() {
|
||||
public class SnarkHtmlPlugin : AbstractPlugin() {
|
||||
private val yaml by require(YamlPlugin)
|
||||
public val io: IOPlugin get() = yaml.io
|
||||
|
||||
@ -54,8 +40,8 @@ public class SnarkPlugin : AbstractPlugin() {
|
||||
context.gather(SiteLayout.TYPE, true)
|
||||
}
|
||||
|
||||
private val textTransformations: Map<Name, TextTransformation> by lazy {
|
||||
context.gather(TextTransformation.TYPE, true)
|
||||
private val textProcessors: Map<Name, TextProcessor> by lazy {
|
||||
context.gather(TextProcessor.TYPE, true)
|
||||
}
|
||||
|
||||
internal fun siteLayout(layoutMeta: Meta): SiteLayout {
|
||||
@ -64,10 +50,10 @@ public class SnarkPlugin : AbstractPlugin() {
|
||||
return siteLayouts[layoutName.parseAsName()] ?: error("Layout with name $layoutName not found in $this")
|
||||
}
|
||||
|
||||
internal fun textTransformation(transformationMeta: Meta): TextTransformation {
|
||||
internal fun textProcessor(transformationMeta: Meta): TextProcessor {
|
||||
val transformationName = transformationMeta.string
|
||||
?: transformationMeta["name"].string ?: error("Transformation name not defined in $transformationMeta")
|
||||
return textTransformations[transformationName.parseAsName()]
|
||||
return textProcessors[transformationName.parseAsName()]
|
||||
?: error("Text transformation with name $transformationName not found in $this")
|
||||
}
|
||||
|
||||
@ -81,17 +67,17 @@ public class SnarkPlugin : AbstractPlugin() {
|
||||
"jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"),
|
||||
"gif".asName() to SnarkParser(ImageIOReader, "gif"),
|
||||
)
|
||||
TextTransformation.TYPE -> mapOf(
|
||||
"basic".asName() to BasicTextTransformation
|
||||
TextProcessor.TYPE -> mapOf(
|
||||
"basic".asName() to BasicTextProcessor
|
||||
)
|
||||
else -> super.content(target)
|
||||
}
|
||||
|
||||
public companion object : PluginFactory<SnarkPlugin> {
|
||||
public companion object : PluginFactory<SnarkHtmlPlugin> {
|
||||
override val tag: PluginTag = PluginTag("snark")
|
||||
override val type: KClass<out SnarkPlugin> = SnarkPlugin::class
|
||||
override val type: KClass<out SnarkHtmlPlugin> = SnarkHtmlPlugin::class
|
||||
|
||||
override fun build(context: Context, meta: Meta): SnarkPlugin = SnarkPlugin()
|
||||
override fun build(context: Context, meta: Meta): SnarkHtmlPlugin = SnarkHtmlPlugin()
|
||||
|
||||
private val byteArrayIOReader = IOReader {
|
||||
readBytes()
|
||||
@ -101,8 +87,21 @@ public class SnarkPlugin : AbstractPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load necessary dependencies and return a [SnarkHtmlPlugin] in a finalized context
|
||||
*/
|
||||
public fun SnarkEnvironment.buildHtmlPlugin(): SnarkHtmlPlugin {
|
||||
val context = parentContext.buildContext("snark".asName()) {
|
||||
plugin(SnarkHtmlPlugin)
|
||||
plugins.forEach {
|
||||
plugin(it)
|
||||
}
|
||||
}
|
||||
return context.fetch(SnarkHtmlPlugin)
|
||||
}
|
||||
|
||||
@OptIn(DFExperimental::class)
|
||||
public fun SnarkPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path) { dataPath, meta ->
|
||||
public fun SnarkHtmlPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDirectory(path) { dataPath, meta ->
|
||||
val fileExtension = meta[FileData.META_FILE_EXTENSION_KEY].string ?: dataPath.extension
|
||||
val parser: SnarkParser<Any> = parsers.values.filter { parser ->
|
||||
fileExtension in parser.fileExtensions
|
||||
@ -110,7 +109,7 @@ public fun SnarkPlugin.readDirectory(path: Path): DataTree<Any> = io.readDataDir
|
||||
it.priority
|
||||
} ?: run {
|
||||
logger.warn { "The parser is not found for file $dataPath with meta $meta" }
|
||||
SnarkPlugin.byteArraySnarkParser
|
||||
SnarkHtmlPlugin.byteArraySnarkParser
|
||||
}
|
||||
|
||||
parser.reader(context, meta)
|
@ -23,9 +23,9 @@ public abstract class SnarkTextParser<R> : SnarkParser<R> {
|
||||
override fun parse(context: Context, meta: Meta, bytes: ByteArray): R =
|
||||
parseText(bytes.decodeToString(), meta)
|
||||
|
||||
public fun transformText(text: String, meta: Meta, page: Page): String =
|
||||
meta[TextTransformation.TEXT_TRANSFORMATION_KEY]?.let {
|
||||
with(page) { page.snark.textTransformation(it).transform(text) }
|
||||
public fun transformText(text: String, meta: Meta, page: WebPage): String =
|
||||
meta[TextProcessor.TEXT_TRANSFORMATION_KEY]?.let {
|
||||
with(page) { page.snark.textProcessor(it).process(text) }
|
||||
} ?: text
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.withDefault
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.isEmpty
|
||||
import space.kscience.snark.SnarkEnvironment
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.contracts.InvocationKind
|
||||
@ -15,12 +16,15 @@ import kotlin.contracts.contract
|
||||
import kotlin.io.path.*
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of [SiteBuilder] to render site as a static directory [outputPath]
|
||||
*/
|
||||
internal class StaticSiteBuilder(
|
||||
override val snark: SnarkPlugin,
|
||||
override val snark: SnarkHtmlPlugin,
|
||||
override val data: DataTree<*>,
|
||||
override val siteMeta: Meta,
|
||||
private val baseUrl: String,
|
||||
private val path: Path,
|
||||
private val outputPath: Path,
|
||||
) : SiteBuilder {
|
||||
private fun Path.copyRecursively(target: Path) {
|
||||
Files.walk(this).forEach { source: Path ->
|
||||
@ -32,27 +36,28 @@ internal class StaticSiteBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun assetFile(remotePath: String, file: Path) {
|
||||
val targetPath = path.resolve(remotePath)
|
||||
targetPath.parent.createDirectories()
|
||||
file.copyTo(targetPath, true)
|
||||
override fun file(file: Path, remotePath: String) {
|
||||
val targetPath = outputPath.resolve(remotePath)
|
||||
if(file.isDirectory()){
|
||||
targetPath.parent.createDirectories()
|
||||
file.copyRecursively(targetPath)
|
||||
} else if(remotePath.isBlank()) {
|
||||
error("Can't mount file to an empty route")
|
||||
} else {
|
||||
targetPath.parent.createDirectories()
|
||||
file.copyTo(targetPath, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun assetDirectory(remotePath: String, directory: Path) {
|
||||
val targetPath = path.resolve(remotePath)
|
||||
targetPath.parent.createDirectories()
|
||||
directory.copyRecursively(targetPath)
|
||||
}
|
||||
|
||||
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
||||
val targetPath = path.resolve(remotePath)
|
||||
override fun resourceFile(remotePath: String, resourcesPath: String) {
|
||||
val targetPath = outputPath.resolve(remotePath)
|
||||
targetPath.parent.createDirectories()
|
||||
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyTo(targetPath, true)
|
||||
}
|
||||
|
||||
override fun assetResourceDirectory(resourcesPath: String) {
|
||||
path.parent.createDirectories()
|
||||
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(path)
|
||||
override fun resourceDirectory(resourcesPath: String) {
|
||||
outputPath.parent.createDirectories()
|
||||
javaClass.getResource(resourcesPath)?.let { Path.of(it.toURI()) }?.copyRecursively(outputPath)
|
||||
}
|
||||
|
||||
private fun resolveRef(baseUrl: String, ref: String) = if (baseUrl.isEmpty()) {
|
||||
@ -63,10 +68,10 @@ internal class StaticSiteBuilder(
|
||||
"${baseUrl.removeSuffix("/")}/$ref"
|
||||
}
|
||||
|
||||
inner class StaticPage : Page {
|
||||
inner class StaticWebPage : WebPage {
|
||||
override val data: DataTree<*> get() = this@StaticSiteBuilder.data
|
||||
override val pageMeta: Meta get() = this@StaticSiteBuilder.siteMeta
|
||||
override val snark: SnarkPlugin get() = this@StaticSiteBuilder.snark
|
||||
override val snark: SnarkHtmlPlugin get() = this@StaticSiteBuilder.snark
|
||||
|
||||
|
||||
override fun resolveRef(ref: String): String = resolveRef(baseUrl, ref)
|
||||
@ -77,17 +82,17 @@ internal class StaticSiteBuilder(
|
||||
}
|
||||
|
||||
|
||||
override fun page(route: Name, content: context(Page, HTML) () -> Unit) {
|
||||
override fun page(route: Name, content: context(WebPage, HTML) () -> Unit) {
|
||||
val htmlBuilder = createHTML()
|
||||
|
||||
htmlBuilder.html {
|
||||
content(StaticPage(), this)
|
||||
content(StaticWebPage(), this)
|
||||
}
|
||||
|
||||
val newPath = if (route.isEmpty()) {
|
||||
path.resolve("index.html")
|
||||
outputPath.resolve("index.html")
|
||||
} else {
|
||||
path.resolve(route.toWebPath() + ".html")
|
||||
outputPath.resolve(route.toWebPath() + ".html")
|
||||
}
|
||||
|
||||
newPath.parent.createDirectories()
|
||||
@ -108,18 +113,23 @@ internal class StaticSiteBuilder(
|
||||
} else {
|
||||
baseUrl
|
||||
},
|
||||
path = path.resolve(routeName.toWebPath())
|
||||
outputPath = outputPath.resolve(routeName.toWebPath())
|
||||
)
|
||||
}
|
||||
|
||||
public fun SnarkPlugin.renderStatic(
|
||||
/**
|
||||
* Create a static site using given [data] in provided [outputPath].
|
||||
* Use [siteUrl] as a base for all resolved URLs. By default, use [outputPath] absolute path as a base.
|
||||
*
|
||||
*/
|
||||
public fun SnarkEnvironment.static(
|
||||
outputPath: Path,
|
||||
data: DataTree<*> = DataTree.empty(),
|
||||
siteUrl: String = outputPath.absolutePathString().replace("\\", "/"),
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
StaticSiteBuilder(this, data, meta, siteUrl, outputPath).block()
|
||||
val plugin = buildHtmlPlugin()
|
||||
StaticSiteBuilder(plugin, data, meta, siteUrl, outputPath).block()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,9 +17,9 @@ context(SnarkContext) public fun Name.toWebPath(): String = tokens.joinToString(
|
||||
}
|
||||
}
|
||||
|
||||
public interface Page : ContextAware, SnarkContext {
|
||||
public interface WebPage : ContextAware, SnarkContext {
|
||||
|
||||
public val snark: SnarkPlugin
|
||||
public val snark: SnarkHtmlPlugin
|
||||
|
||||
override val context: Context get() = snark.context
|
||||
|
||||
@ -32,11 +32,11 @@ public interface Page : ContextAware, SnarkContext {
|
||||
public fun resolvePageRef(pageName: Name): String
|
||||
}
|
||||
|
||||
context(Page) public val page: Page get() = this@Page
|
||||
context(WebPage) public val page: WebPage get() = this@WebPage
|
||||
|
||||
public fun Page.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
|
||||
public fun WebPage.resolvePageRef(pageName: String): String = resolvePageRef(pageName.parseAsName())
|
||||
|
||||
public val Page.homeRef: String get() = resolvePageRef(SiteBuilder.INDEX_PAGE_TOKEN.asName())
|
||||
public val WebPage.homeRef: String get() = resolvePageRef(SiteBuilder.INDEX_PAGE_TOKEN.asName())
|
||||
|
||||
/**
|
||||
* Resolve a Html builder by its full name
|
@ -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)
|
||||
}
|
@ -20,27 +20,32 @@ import space.kscience.dataforge.meta.withDefault
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.cutLast
|
||||
import space.kscience.dataforge.names.endsWith
|
||||
import space.kscience.snark.SnarkEnvironment
|
||||
import space.kscience.snark.html.*
|
||||
import java.nio.file.Path
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
@PublishedApi
|
||||
internal class KtorSiteBuilder(
|
||||
override val snark: SnarkPlugin,
|
||||
override val snark: SnarkHtmlPlugin,
|
||||
override val data: DataTree<*>,
|
||||
override val siteMeta: Meta,
|
||||
private val baseUrl: String,
|
||||
private val ktorRoute: Route,
|
||||
) : SiteBuilder {
|
||||
|
||||
override fun assetFile(remotePath: String, file: Path) {
|
||||
ktorRoute.file(remotePath, file.toFile())
|
||||
}
|
||||
|
||||
override fun assetDirectory(remotePath: String, directory: Path) {
|
||||
ktorRoute.static(remotePath) {
|
||||
files(directory.toFile())
|
||||
override fun file(file: Path, remotePath: String) {
|
||||
if (file.isDirectory()) {
|
||||
ktorRoute.static(remotePath) {
|
||||
//TODO check non-standard FS and convert
|
||||
files(file.toFile())
|
||||
}
|
||||
} else if (remotePath.isBlank()) {
|
||||
error("Can't mount file to an empty route")
|
||||
} else {
|
||||
ktorRoute.file(remotePath, file.toFile())
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,11 +58,11 @@ internal class KtorSiteBuilder(
|
||||
}
|
||||
|
||||
|
||||
inner class KtorPage(
|
||||
inner class KtorWebPage(
|
||||
val pageBaseUrl: String,
|
||||
override val pageMeta: Meta = this@KtorSiteBuilder.siteMeta,
|
||||
) : Page {
|
||||
override val snark: SnarkPlugin get() = this@KtorSiteBuilder.snark
|
||||
) : WebPage {
|
||||
override val snark: SnarkHtmlPlugin get() = this@KtorSiteBuilder.snark
|
||||
override val data: DataTree<*> get() = this@KtorSiteBuilder.data
|
||||
|
||||
override fun resolveRef(ref: String): String = resolveRef(pageBaseUrl, ref)
|
||||
@ -69,7 +74,7 @@ internal class KtorSiteBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun page(route: Name, content: context(Page, HTML)() -> Unit) {
|
||||
override fun page(route: Name, content: context(WebPage, HTML)() -> Unit) {
|
||||
ktorRoute.get(route.toWebPath()) {
|
||||
call.respondHtml {
|
||||
val request = call.request
|
||||
@ -79,7 +84,7 @@ internal class KtorSiteBuilder(
|
||||
host = request.host()
|
||||
port = request.port()
|
||||
}
|
||||
val pageBuilder = KtorPage(url.buildString())
|
||||
val pageBuilder = KtorWebPage(url.buildString())
|
||||
content(pageBuilder, this)
|
||||
}
|
||||
}
|
||||
@ -103,34 +108,31 @@ internal class KtorSiteBuilder(
|
||||
)
|
||||
|
||||
|
||||
override fun assetResourceFile(remotePath: String, resourcesPath: String) {
|
||||
override fun resourceFile(remotePath: String, resourcesPath: String) {
|
||||
ktorRoute.resource(resourcesPath, resourcesPath)
|
||||
}
|
||||
|
||||
override fun assetResourceDirectory(resourcesPath: String) {
|
||||
override fun resourceDirectory(resourcesPath: String) {
|
||||
ktorRoute.resources(resourcesPath)
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun Route.snarkSite(
|
||||
snark: SnarkPlugin,
|
||||
data: DataTree<*>,
|
||||
meta: Meta = data.meta,
|
||||
context(Route, SnarkEnvironment) public fun siteInRoute(
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
block(KtorSiteBuilder(snark, data, meta, "", this@snarkSite))
|
||||
block(KtorSiteBuilder(buildHtmlPlugin(), data, meta, "", this@Route))
|
||||
}
|
||||
|
||||
public fun Application.snarkSite(
|
||||
snark: SnarkPlugin,
|
||||
data: DataTree<*> = DataTree.empty(),
|
||||
meta: Meta = data.meta,
|
||||
context(Application) public fun SnarkEnvironment.site(
|
||||
block: SiteBuilder.() -> Unit,
|
||||
) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
routing {
|
||||
snarkSite(snark, data, meta, block)
|
||||
siteInRoute(block)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user