From 7a2b5c1768a4dfa3ca5e57d3f342405166afd66e Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 30 Apr 2024 19:00:37 +0300 Subject: [PATCH] Fully working document processing --- .../data/{ => loremIpsum}/chapter1.md | 5 +- .../data/{ => loremIpsum}/chapter2.md | 0 .../data/{ => loremIpsum}/chapter3.md | 3 +- .../document/data/loremIpsum/document.yaml | 14 +++ examples/document/src/jvmMain/kotlin/main.kt | 40 ++++-- .../src/jvmMain/kotlin/snarkApplication.kt | 2 + .../space/kscience/snark/ReWrapAction.kt | 25 +--- .../space/kscience/snark/SnarkReader.kt | 22 +++- snark-html/build.gradle.kts | 1 + .../kscience/snark/html/MarkdownReader.kt | 5 +- .../space/kscience/snark/html/ParseAction.kt | 34 +++-- .../space/kscience/snark/html/SnarkHtml.kt | 41 ++++-- .../kscience/snark/html/SnarkHtmlReader.kt | 8 +- .../snark/html/document/DocumentBuilder.kt | 117 ++++++++++++++---- .../snark/html/document/DocumentDescriptor.kt | 28 +++++ .../snark/html/document/DocumentFragment.kt | 41 ++++-- 16 files changed, 291 insertions(+), 95 deletions(-) rename examples/document/data/{ => loremIpsum}/chapter1.md (96%) rename examples/document/data/{ => loremIpsum}/chapter2.md (100%) rename examples/document/data/{ => loremIpsum}/chapter3.md (99%) create mode 100644 examples/document/data/loremIpsum/document.yaml create mode 100644 snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentDescriptor.kt diff --git a/examples/document/data/chapter1.md b/examples/document/data/loremIpsum/chapter1.md similarity index 96% rename from examples/document/data/chapter1.md rename to examples/document/data/loremIpsum/chapter1.md index fb186b3..cb8a306 100644 --- a/examples/document/data/chapter1.md +++ b/examples/document/data/loremIpsum/chapter1.md @@ -1,9 +1,8 @@ --- -type: markdown -order: 1 +contentType: markdown --- -# ${documentName} +Document name: ${documentName} ${documentMeta.metaValue} diff --git a/examples/document/data/chapter2.md b/examples/document/data/loremIpsum/chapter2.md similarity index 100% rename from examples/document/data/chapter2.md rename to examples/document/data/loremIpsum/chapter2.md diff --git a/examples/document/data/chapter3.md b/examples/document/data/loremIpsum/chapter3.md similarity index 99% rename from examples/document/data/chapter3.md rename to examples/document/data/loremIpsum/chapter3.md index b61dafb..2e5eedc 100644 --- a/examples/document/data/chapter3.md +++ b/examples/document/data/loremIpsum/chapter3.md @@ -1,6 +1,5 @@ --- -type: markdown -order: 3 +contentType: markdown --- ## Chapter ${section(1)} diff --git a/examples/document/data/loremIpsum/document.yaml b/examples/document/data/loremIpsum/document.yaml new file mode 100644 index 0000000..a8f3771 --- /dev/null +++ b/examples/document/data/loremIpsum/document.yaml @@ -0,0 +1,14 @@ +route: lorem.ipsum +title: Lorem Ipsum +authors: + - name: Alexander Nozik + affiliation: MIPT +fragments: + - name: chapter1 + type: data + - name: chapter2 + type: data + - name: chapter3 + type: data +documentMeta: + metaValue: Hello world! \ No newline at end of file diff --git a/examples/document/src/jvmMain/kotlin/main.kt b/examples/document/src/jvmMain/kotlin/main.kt index 09cd4bd..4ffb9c3 100644 --- a/examples/document/src/jvmMain/kotlin/main.kt +++ b/examples/document/src/jvmMain/kotlin/main.kt @@ -1,24 +1,42 @@ import io.ktor.server.application.Application import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.asName -import space.kscience.snark.html.document.document -import space.kscience.snark.html.document.fragment +import kotlinx.html.ScriptCrossorigin +import kotlinx.html.link +import kotlinx.html.script +import space.kscience.snark.html.document.allDocuments @Suppress("unused") -fun Application.documents() = snarkApplication { - document("loremIpsum".asName(), Meta { "metaValue" put "Hello world!" }) { - fragment("chapter1") - fragment("chapter2") - fragment("chapter3") - } +fun Application.renderAllDocuments() = snarkApplication { + allDocuments( + headers = { + link { + rel = "stylesheet" + href = "https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" + attributes["integrity"] = "sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" + attributes["crossorigin"] = "anonymous" + } + script { + defer = true + src = "https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" + integrity = "sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" + crossorigin = ScriptCrossorigin.anonymous + } + script { + defer = true + src = "https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" + integrity = "sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" + crossorigin = ScriptCrossorigin.anonymous + attributes["onload"] = "renderMathInElement(document.body);" + } + } + ) } fun main() { embeddedServer(CIO) { - documents() + renderAllDocuments() }.start(true) } \ No newline at end of file diff --git a/examples/document/src/jvmMain/kotlin/snarkApplication.kt b/examples/document/src/jvmMain/kotlin/snarkApplication.kt index 34790eb..dc0f55d 100644 --- a/examples/document/src/jvmMain/kotlin/snarkApplication.kt +++ b/examples/document/src/jvmMain/kotlin/snarkApplication.kt @@ -1,5 +1,6 @@ import io.ktor.server.application.Application import io.ktor.server.application.log +import io.ktor.server.http.content.staticResources import io.ktor.server.routing.routing import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextBuilder @@ -42,6 +43,7 @@ fun Application.snarkApplication(contextBuilder: ContextBuilder.() -> Unit = {}, } routing { + staticResources("/css","css") site(context, siteData, content = site) } } \ No newline at end of file diff --git a/snark-core/src/commonMain/kotlin/space/kscience/snark/ReWrapAction.kt b/snark-core/src/commonMain/kotlin/space/kscience/snark/ReWrapAction.kt index e8fe2b9..df478e2 100644 --- a/snark-core/src/commonMain/kotlin/space/kscience/snark/ReWrapAction.kt +++ b/snark-core/src/commonMain/kotlin/space/kscience/snark/ReWrapAction.kt @@ -15,12 +15,12 @@ import kotlin.reflect.typeOf public class ReWrapAction( type: KType, private val newMeta: MutableMeta.(name: Name) -> Unit = {}, - private val newName: (name: Name, meta: Meta?) -> Name, + private val newName: (name: Name, meta: Meta?, type: KType) -> Name, ) : AbstractAction(type) { override fun DataSink.generate(data: DataTree, meta: Meta) { data.forEach { namedData -> put( - newName(namedData.name, namedData.meta), + newName(namedData.name, namedData.meta, namedData.type), namedData.data.withMeta(namedData.meta.copy { newMeta(namedData.name) }) ) } @@ -28,34 +28,19 @@ public class ReWrapAction( override fun DataSink.update(source: DataTree, meta: Meta, namedData: NamedData) { put( - newName(namedData.name, namedData.meta), + newName(namedData.name, namedData.meta, namedData.type), namedData.withMeta(namedData.meta.copy { newMeta(namedData.name) }) ) } public companion object { - public inline fun removeExtensions( - vararg bypassExtensions: String, - noinline newMeta: MutableMeta.(name: Name) -> Unit = {}, - ): ReWrapAction = ReWrapAction(typeOf(), newMeta = newMeta) { name, _ -> - name.replaceLast { token -> - val extension = token.body.substringAfterLast('.') - if (extension in bypassExtensions) { - NameToken(token.body.removeSuffix(".$extension")) - } else { - token - } - } - } - public inline fun removeIndex(): ReWrapAction = ReWrapAction(typeOf()) { name, _ -> - if (name.endsWith("index")) name.cutLast() else name - } + } } public inline fun ReWrapAction( noinline newMeta: MutableMeta.(name: Name) -> Unit = {}, - noinline newName: (Name, Meta?) -> Name, + noinline newName: (Name, Meta?, type: KType) -> Name, ): ReWrapAction = ReWrapAction(typeOf(), newMeta, newName) diff --git a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkReader.kt b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkReader.kt index 5cf1cf8..6fc7c54 100644 --- a/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkReader.kt +++ b/snark-core/src/commonMain/kotlin/space/kscience/snark/SnarkReader.kt @@ -5,10 +5,13 @@ import space.kscience.dataforge.io.asBinary import space.kscience.dataforge.misc.DfType import space.kscience.snark.SnarkReader.Companion.DEFAULT_PRIORITY import space.kscience.snark.SnarkReader.Companion.DF_TYPE +import kotlin.reflect.KType +import kotlin.reflect.typeOf @DfType(DF_TYPE) public interface SnarkReader : IOReader { - public val types: Set + public val outputType: KType + public val inputContentTypes: Set public val priority: Int get() = DEFAULT_PRIORITY public fun readFrom(source: String): T @@ -23,13 +26,14 @@ public interface SnarkReader : IOReader { * * @param T The type of data to be read by the IOReader. * @property reader The underlying IOReader instance used for reading data. - * @property types The set of supported types that can be read by the SnarkIOReader. + * @property inputContentTypes The set of supported types that can be read by the SnarkIOReader. * @property priority The priority of the SnarkIOReader. Higher priority SnarkIOReader instances will be preferred over lower priority ones. */ private class SnarkReaderWrapper( private val reader: IOReader, - override val types: Set, + override val outputType: KType, + override val inputContentTypes: Set, override val priority: Int = DEFAULT_PRIORITY, ) : IOReader by reader, SnarkReader { @@ -38,6 +42,14 @@ private class SnarkReaderWrapper( public fun SnarkReader( reader: IOReader, - vararg types: String, + outputType: KType, + vararg inputContentTypes: String, priority: Int = DEFAULT_PRIORITY, -): SnarkReader = SnarkReaderWrapper(reader, types.toSet(), priority) \ No newline at end of file +): SnarkReader = SnarkReaderWrapper(reader, outputType, inputContentTypes.toSet(), priority) + + +public inline fun SnarkReader( + reader: IOReader, + vararg inputContentTypes: String, + priority: Int = DEFAULT_PRIORITY, +): SnarkReader = SnarkReader(reader, typeOf(), inputContentTypes = inputContentTypes, priority) \ No newline at end of file diff --git a/snark-html/build.gradle.kts b/snark-html/build.gradle.kts index 1749d91..3f1463d 100644 --- a/snark-html/build.gradle.kts +++ b/snark-html/build.gradle.kts @@ -8,6 +8,7 @@ val ktorVersion = space.kscience.gradle.KScienceVersions.ktorVersion kscience{ jvm() + useSerialization() useContextReceivers() commonMain{ api(projects.snarkCore) diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt index 4660e5c..1963750 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt @@ -71,7 +71,7 @@ public object SnarkFlavorDescriptor : GFMFlavourDescriptor(false) { } public object MarkdownReader : SnarkHtmlReader { - override val types: Set = setOf("text/markdown", "md", "markdown") + override val inputContentTypes: Set = setOf("text/markdown", "md", "markdown") override fun readFrom(source: String): PageFragment = PageFragment { val parsedTree = markdownParser.parse(IElementType("ROOT"), source) @@ -88,7 +88,4 @@ public object MarkdownReader : SnarkHtmlReader { private val markdownParser = MarkdownParser(markdownFlavor) override fun readFrom(source: Source): PageFragment = readFrom(source.readString()) - - public val snarkReader: SnarkReader = SnarkReader(this, "text/markdown") - } \ No newline at end of file diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/ParseAction.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/ParseAction.kt index 6d4abf5..448e0e7 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/ParseAction.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/ParseAction.kt @@ -6,17 +6,35 @@ import space.kscience.dataforge.io.Binary import space.kscience.dataforge.io.toByteArray import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get +import space.kscience.dataforge.misc.DFInternal +import space.kscience.snark.SnarkReader import space.kscience.snark.TextProcessor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KType import kotlin.reflect.typeOf -public class ParseAction(private val snarkHtml: SnarkHtml) : - AbstractAction(typeOf()) { +@OptIn(DFInternal::class) +internal fun Data.transform( + type: KType, + meta: Meta = this.meta, + coroutineContext: CoroutineContext = EmptyCoroutineContext, + block: suspend (T) -> R, +): Data { + val data = Data(type, meta, coroutineContext, listOf(this)) { + block(await()) + } + return data +} - private fun parseOne(data: NamedData): NamedData? = with(snarkHtml) { +public class ParseAction(private val snarkHtml: SnarkHtml) : + AbstractAction(typeOf()) { + + private fun parseOne(data: NamedData): NamedData? = with(snarkHtml) { val contentType = getContentType(data.name, data.meta) - val parser = snark.readers.values.filterIsInstance().filter { parser -> - contentType in parser.types + val parser: SnarkReader? = snark.readers.values.filter { parser -> + contentType in parser.inputContentTypes }.maxByOrNull { it.priority } @@ -24,7 +42,7 @@ public class ParseAction(private val snarkHtml: SnarkHtml) : //ignore data for which parser is not found if (parser != null) { val preprocessor = meta[TextProcessor.TEXT_PREPROCESSOR_KEY]?.let { snark.preprocessor(it) } - data.transform { + data.transform(parser.outputType) { if (preprocessor == null) { parser.readFrom(it) } else { @@ -38,13 +56,13 @@ public class ParseAction(private val snarkHtml: SnarkHtml) : } } - override fun DataSink.generate(data: DataTree, meta: Meta) { + override fun DataSink.generate(data: DataTree, meta: Meta) { data.forEach { parseOne(it)?.let { put(it) } } } - override fun DataSink.update(source: DataTree, meta: Meta, namedData: NamedData) { + override fun DataSink.update(source: DataTree, meta: Meta, namedData: NamedData) { parseOne(namedData)?.let { put(it) } } } \ No newline at end of file diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtml.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtml.kt index 94d3793..533e29c 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtml.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtml.kt @@ -22,8 +22,7 @@ import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.set import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DFExperimental -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.asName +import space.kscience.dataforge.names.* import space.kscience.dataforge.provider.dfType import space.kscience.dataforge.workspace.* import space.kscience.snark.ReWrapAction @@ -33,6 +32,7 @@ import space.kscience.snark.TextProcessor import java.net.URLConnection import kotlin.io.path.Path import kotlin.io.path.extension +import kotlin.reflect.typeOf public fun DataTree.transform(action: Action, meta: Meta = Meta.EMPTY): DataTree = @@ -52,8 +52,8 @@ public class SnarkHtml : WorkspacePlugin() { SnarkReader::class.dfType -> mapOf( "html".asName() to HtmlReader, "markdown".asName() to MarkdownReader, - "json".asName() to SnarkReader(JsonMetaFormat, ContentType.Application.Json.toString()), - "yaml".asName() to SnarkReader(YamlMetaFormat, "text/yaml", "yaml"), + "json".asName() to SnarkReader(JsonMetaFormat, ContentType.Application.Json.toString()), + "yaml".asName() to SnarkReader(YamlMetaFormat, "text/yaml", "yaml"), ) else -> super.content(target) @@ -64,16 +64,33 @@ public class SnarkHtml : WorkspacePlugin() { URLConnection.guessContentTypeFromName(filePath) ?: Path(filePath).extension } - public val prepareHeaderAction: ReWrapAction = ReWrapAction.removeExtensions("html", "md") { name -> - val contentType = getContentType(name, this) - set(CONTENT_TYPE_KEY, contentType) + internal val prepareHeaderAction: ReWrapAction = ReWrapAction( + type = typeOf(), + newMeta = { name -> + val contentType = getContentType(name, this) + set(FILE_NAME_KEY, name.last().toStringUnescaped()) + set(CONTENT_TYPE_KEY, contentType) + } + ) { name, _, type -> + name.replaceLast { token -> + val extension = token.body.substringAfterLast('.') + if (type != typeOf()) { + NameToken(token.body.removeSuffix(".$extension")) + } else { + token + } + } } - public fun parse(name: Name, markup: String, meta: Meta): PageFragment { + public val removeIndexAction: ReWrapAction = ReWrapAction(typeOf()) { name, _, _ -> + if (name.endsWith("index")) name.cutLast() else name + } + + public fun parseMarkup(name: Name, markup: String, meta: Meta): PageFragment { val contentType = getContentType(name, meta) val parser = snark.readers.values.filterIsInstance().filter { parser -> - contentType in parser.types + contentType in parser.inputContentTypes }.maxByOrNull { it.priority } ?: error("Parser for name $name and meta $meta not found") @@ -88,10 +105,8 @@ public class SnarkHtml : WorkspacePlugin() { } } + public val parseAction: Action = ParseAction(this) - public val removeIndexAction: ReWrapAction = ReWrapAction.removeIndex() - - public val parseAction: Action = ParseAction(this) private val allDataNotNull: DataSelector get() = DataSelector { workspace, _ -> workspace.data.filterByType() } @@ -108,6 +123,8 @@ public class SnarkHtml : WorkspacePlugin() { public companion object : PluginFactory { override val tag: PluginTag = PluginTag("snark.html") + public val FILE_NAME_KEY: Name = "contentType".asName() + public val CONTENT_TYPE_KEY: Name = "contentType".asName() override fun build(context: Context, meta: Meta): SnarkHtml = SnarkHtml() diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt index 924ef89..bbd05f9 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt @@ -5,12 +5,16 @@ import kotlinx.html.unsafe import kotlinx.io.Source import kotlinx.io.readString import space.kscience.snark.SnarkReader +import kotlin.reflect.KType +import kotlin.reflect.typeOf -public interface SnarkHtmlReader : SnarkReader +public interface SnarkHtmlReader : SnarkReader{ + override val outputType: KType get() = typeOf() +} public object HtmlReader : SnarkHtmlReader { - override val types: Set = setOf("html") + override val inputContentTypes: Set = setOf("html") override fun readFrom(source: String): PageFragment = PageFragment { div { diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentBuilder.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentBuilder.kt index 5b700fb..6b7fa68 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentBuilder.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentBuilder.kt @@ -1,9 +1,10 @@ package space.kscience.snark.html.document +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.html.body -import kotlinx.html.head -import kotlinx.html.title +import kotlinx.html.* +import space.kscience.dataforge.context.info +import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.request import space.kscience.dataforge.data.* import space.kscience.dataforge.meta.Laminate @@ -11,6 +12,9 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.cutLast +import space.kscience.dataforge.names.endsWith +import space.kscience.dataforge.names.parseAsName import space.kscience.snark.SnarkBuilder import space.kscience.snark.SnarkContext import space.kscience.snark.html.* @@ -26,7 +30,7 @@ public interface DocumentBuilder : SnarkContext { public val documentMeta: Meta - public val documentData: DataTree<*> + public val data: DataTree<*> public suspend fun fragment(fragment: Data<*>, overrideMeta: Meta? = null) @@ -43,10 +47,13 @@ public suspend fun DocumentBuilder.fragment(fragmentName: String) { fragment(site.siteData[fragmentName] ?: error("Can't find data fragment for $fragmentName in site data.")) } -private class PageBasedDocumentBuilder(val page: PageContextWithData) : DocumentBuilder { +private class PageBasedDocumentBuilder( + val page: PageContextWithData, + private val dataRootName: Name, +) : DocumentBuilder { override val documentName: Name get() = page.pageRoute override val documentMeta: Meta get() = page.pageMeta - override val documentData: DataTree<*> get() = page.data + override val data: DataTree<*> = page.data.branch(dataRootName) ?: DataTree.EMPTY val fragments = mutableListOf() @@ -56,24 +63,36 @@ private class PageBasedDocumentBuilder(val page: PageContextWithData) : Document override suspend fun fragment(fragment: DocumentFragment, overrideMeta: Meta?) { when (fragment) { + + is ImageDocumentFragment -> fragment { + figure("snark-figure") { + img(classes = "snark-image") { + src = fragment.path + alt = fragment.meta["alt"].string ?: "" + } + fragment.meta["caption"].string?.let { caption -> + figcaption("snark-figure-caption") { +caption } + } + } + } + + is MarkupDocumentFragment -> { + val snarkHtml = page.context.request(SnarkHtml) + snarkHtml.parseMarkup(Name.EMPTY, fragment.text, fragment.meta) + } + + is DataDocumentFragment -> { + val data = data[fragment.name] + ?: error("Can't find data with name ${fragment.name} for $fragment") + fragment(data) + } + is ListDocumentFragment -> { val meta = Laminate(overrideMeta, fragment.meta) fragment.fragments.forEach { fragment(it, meta) } } - is ImageDocumentFragment -> TODO() - is MarkupDocumentFragment -> { - val snarkHtml = page.context.request(SnarkHtml) - TODO() - } - - is DataDocumentFragment -> { - val data = documentData[fragment.dataName] - ?: error("Can't find data with name ${fragment.dataName} for $fragment") - fragment(data) - } - - is LayoutDocumentFragment -> TODO() + is LayoutDocumentFragment -> TODO("Layouts are not implemented") } } @@ -82,7 +101,7 @@ private class PageBasedDocumentBuilder(val page: PageContextWithData) : Document typeOf() -> fragment(fragment.await() as PageFragment) typeOf() -> fragment( fragment.await() as DocumentFragment, - Laminate(overrideMeta, documentData.meta) + Laminate(overrideMeta, data.meta) ) typeOf() -> fragment( @@ -98,11 +117,13 @@ private class PageBasedDocumentBuilder(val page: PageContextWithData) : Document public fun SiteContextWithData.document( documentName: Name, documentMeta: Meta = Meta.EMPTY, + headers: MetaDataContent.() -> Unit = {}, block: suspend DocumentBuilder.() -> Unit, ): Unit = page(documentName, documentMeta) { - val documentBuilder = runBlocking { PageBasedDocumentBuilder(page).apply { block() } } + val documentBuilder = runBlocking { PageBasedDocumentBuilder(page, documentName).apply { block() } } head { title(documentMeta["title"].string ?: "Snark document") + headers() } body { postprocess(FtlDocumentProcessor(this@document.context, documentBuilder)) { @@ -111,4 +132,58 @@ public fun SiteContextWithData.document( } } } +} + +public fun SiteContextWithData.document( + route: Name, + dataName: Name, + descriptor: DocumentDescriptor, + headers: MetaDataContent.() -> Unit = {}, +): Unit = page(route, descriptor.documentMeta ?: Meta.EMPTY) { + val documentBuilder = runBlocking { + PageBasedDocumentBuilder(page, dataName).apply { + descriptor.fragments.forEach { + fragment(it) + } + } + } + head { + title(descriptor.title ?: "Snark document") + headers() + } + body { + h1("title") { +(descriptor.title ?: dataName.toString()) } + descriptor.authors.forEach { + div("author") { + div("author-name") { +it.name } + it.affiliation?.let { affiliation -> div("author-affiliation") { +affiliation } } + } + } + postprocess(FtlDocumentProcessor(this@document.context, documentBuilder)) { + documentBuilder.fragments.forEach { + fragment(it) + } + } + } +} + +public fun SiteContextWithData.allDocuments( + headers: MetaDataContent.() -> Unit = {}, +) { + siteData.forEach { documentData -> + if (documentData.type == typeOf() && documentData.name.endsWith("document")) { + context.launch { + val descriptor = DocumentDescriptor.read(documentData.data.await() as Meta) + val directory = documentData.name.cutLast() + val route = descriptor.route?.parseAsName(false) ?: directory + context.logger.info { "Loading document $route" } + document( + route = route, + dataName = directory, + descriptor = descriptor, + headers = headers + ) + } + } + } } \ No newline at end of file diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentDescriptor.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentDescriptor.kt new file mode 100644 index 0000000..eb73622 --- /dev/null +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentDescriptor.kt @@ -0,0 +1,28 @@ +package space.kscience.snark.html.document + +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.misc.DFExperimental + + +public class Author : Scheme() { + public var name: String by string { error("Name is required") } + public var affiliation: String? by string() + + public companion object : SchemeSpec(::Author) +} + +public class DocumentDescriptor : Scheme() { + + public var route: String? by string() + + public var title: String? by string() + + public var documentMeta: Meta? by node() + + public var authors: List by listOfScheme(Author) + + @OptIn(DFExperimental::class) + public var fragments: List by meta.listOfSerializable() + + public companion object : SchemeSpec(::DocumentDescriptor) +} \ No newline at end of file diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentFragment.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentFragment.kt index c4b39e6..998c642 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentFragment.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/document/DocumentFragment.kt @@ -1,19 +1,46 @@ package space.kscience.snark.html.document -import kotlinx.io.files.Path +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name -public sealed interface DocumentFragment{ +@Serializable +public sealed interface DocumentFragment { public val meta: Meta } -public class MarkupDocumentFragment(public val text: String, override val meta: Meta) : DocumentFragment +@Serializable +@SerialName("markup") +public class MarkupDocumentFragment( + public val text: String, + override val meta: Meta = Meta.EMPTY, +) : DocumentFragment -public class ImageDocumentFragment(public val image: Path, override val meta: Meta) : DocumentFragment +@Serializable +@SerialName("image") +public class ImageDocumentFragment( + public val path: String, + override val meta: Meta = Meta.EMPTY, +) : DocumentFragment -public class DataDocumentFragment(public val dataName: Name, override val meta: Meta) : DocumentFragment +@Serializable +@SerialName("data") +public class DataDocumentFragment( + public val name: Name, + override val meta: Meta = Meta.EMPTY, +) : DocumentFragment -public class ListDocumentFragment(public val fragments: List, override val meta: Meta) : DocumentFragment +@Serializable +@SerialName("list") +public class ListDocumentFragment( + public val fragments: List, + override val meta: Meta = Meta.EMPTY, +) : DocumentFragment -public class LayoutDocumentFragment(public val fragments: Map, override val meta: Meta) : DocumentFragment +@Serializable +@SerialName("layout") +public class LayoutDocumentFragment( + public val fragments: Map, + override val meta: Meta = Meta.EMPTY, +) : DocumentFragment