No compile-time errors

This commit is contained in:
Alexander Nozik 2024-01-01 21:30:17 +03:00
parent 738f41265f
commit c0f869f6e3
14 changed files with 211 additions and 168 deletions

View File

@ -14,7 +14,7 @@ allprojects {
}
}
val dataforgeVersion by extra("0.7.0")
val dataforgeVersion by extra("0.7.1")
ksciencePublish {
pom("https://github.com/SciProgCentre/snark") {

View File

@ -2,11 +2,11 @@ package space.kscience.snark
import space.kscience.dataforge.io.IOReader
import space.kscience.dataforge.io.asBinary
import space.kscience.dataforge.misc.DfId
import space.kscience.dataforge.misc.DfType
import space.kscience.snark.SnarkReader.Companion.DEFAULT_PRIORITY
import space.kscience.snark.SnarkReader.Companion.DF_TYPE
@DfId(DF_TYPE)
@DfType(DF_TYPE)
public interface SnarkReader<out T> : IOReader<T> {
public val types: Set<String>
public val priority: Int get() = DEFAULT_PRIORITY
@ -39,5 +39,5 @@ private class SnarkReaderWrapper<out T>(
public fun <T : Any> SnarkReader(
reader: IOReader<T>,
vararg types: String,
priority: Int = DEFAULT_PRIORITY
priority: Int = DEFAULT_PRIORITY,
): SnarkReader<T> = SnarkReaderWrapper(reader, types.toSet(), priority)

View File

@ -1,12 +1,12 @@
package space.kscience.snark
import space.kscience.dataforge.misc.DfId
import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.NameToken
/**
* An object that conducts page-based text transformation. Like using link replacement or templating.
*/
@DfId(TextProcessor.DF_TYPE)
@DfType(TextProcessor.DF_TYPE)
public fun interface TextProcessor {
public fun process(text: CharSequence): String

View File

@ -0,0 +1,16 @@
package space.kscience.snark
import space.kscience.dataforge.data.DataSet
import space.kscience.dataforge.data.filterByType
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.startsWith
import kotlin.reflect.typeOf
public inline fun <reified R : Any> DataSet<*>.branch(
branchName: Name,
): DataSet<R> = filterByType(typeOf<R>()) { name, _ -> name.startsWith(branchName) }
public inline fun <reified R : Any> DataSet<*>.branch(
branchName: String,
): DataSet<R> = filterByType(typeOf<R>()) { name, _ -> name.startsWith(branchName.parseAsName()) }

View File

@ -0,0 +1,81 @@
package space.kscience.snark.html
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.html.FlowContent
import space.kscience.dataforge.data.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.names.startsWith
import space.kscience.snark.SnarkContext
public fun interface DataFragment {
public suspend fun FlowContent.renderFragment(page: PageContext, data: DataSet<*>)
}
context(PageContext)
public fun FlowContent.htmlData(data: DataSet<*>, fragment: Data<DataFragment>): Unit = runBlocking(Dispatchers.IO) {
with(fragment.await()) { renderFragment(page, data) }
}
context(SnarkContext)
public val Data<*>.id: String
get() = meta["id"]?.string ?: "block[${hashCode()}]"
context(SnarkContext)
public val Data<*>.language: String?
get() = meta["language"].string?.lowercase()
context(SnarkContext)
public val Data<*>.order: Int?
get() = meta["order"]?.int
context(SnarkContext)
public val Data<*>.published: Boolean
get() = meta["published"].string != "false"
/**
* Resolve a Html builder by its full name
*/
context(SnarkContext)
public fun DataSet<*>.resolveHtmlOrNull(name: Name): Data<DataFragment>? {
val resolved = (getByType<DataFragment>(name) ?: getByType<DataFragment>(name + SiteContext.INDEX_PAGE_TOKEN))
return resolved?.takeIf {
it.published //TODO add language confirmation
}
}
context(SnarkContext)
public fun DataSet<*>.resolveHtmlOrNull(name: String): Data<DataFragment>? = resolveHtmlOrNull(name.parseAsName())
context(SnarkContext)
public fun DataSet<*>.resolveHtml(name: String): Data<DataFragment> = resolveHtmlOrNull(name)
?: error("Html fragment with name $name is not resolved")
/**
* Find all Html blocks using given name/meta filter
*/
context(SnarkContext)
public fun DataSet<*>.resolveAllHtml(
predicate: (name: Name, meta: Meta) -> Boolean,
): Map<Name, Data<DataFragment>> = filterByType<DataFragment> { name, meta ->
predicate(name, meta)
&& meta["published"].string != "false"
//TODO add language confirmation
}.asSequence().associate { it.name to it.data }
context(SnarkContext)
public fun DataSet<*>.findHtmlByContentType(
contentType: String,
baseName: Name = Name.EMPTY,
): Map<Name, Data<DataFragment>> = resolveAllHtml { name, meta ->
name.startsWith(baseName) && meta["content_type"].string == contentType
}

View File

@ -1,44 +0,0 @@
package space.kscience.snark.html
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer
import space.kscience.dataforge.data.Data
import space.kscience.dataforge.data.await
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import space.kscience.snark.SnarkContext
//TODO replace by VisionForge type
public fun interface HtmlFragment {
public fun TagConsumer<*>.renderFragment()
}
public typealias HtmlData = Data<HtmlFragment>
public fun FlowContent.htmlData(page: PageContext, data: HtmlData): Unit = runBlocking(Dispatchers.IO) {
withSnarkPage(page) {
with(data.await()) { consumer.renderFragment() }
}
}
context(SnarkContext)
public val Data<*>.id: String
get() = meta["id"]?.string ?: "block[${hashCode()}]"
context(SnarkContext)
public val Data<*>.language: String?
get() = meta["language"].string?.lowercase()
context(SnarkContext)
public val Data<*>.order: Int?
get() = meta["order"]?.int
context(SnarkContext)
public val Data<*>.published: Boolean
get() = meta["published"].string != "false"

View File

@ -1,15 +1,29 @@
package space.kscience.snark.html
import kotlinx.html.HTML
import kotlinx.html.stream.createHTML
import kotlinx.html.visitTagAndFinalize
import space.kscience.dataforge.data.DataSet
import space.kscience.dataforge.data.DataSetBuilder
import space.kscience.dataforge.data.node
import space.kscience.dataforge.data.static
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
public fun interface HtmlPage {
public fun HTML.renderPage(pageContext: PageContext, pageData: DataSet<*>)
public suspend fun HTML.renderPage(page: PageContext, data: DataSet<*>)
public companion object{
public suspend fun createHtmlString(pageContext: PageContext, page: HtmlPage, data: DataSet<*>): String{
return createHTML().run {
HTML(kotlinx.html.emptyMap, this, null).visitTagAndFinalize(this) {
with(page) {
renderPage(pageContext, data)
}
}
}
}
}
}
@ -22,6 +36,7 @@ public fun DataSetBuilder<Any>.page(name: Name, pageMeta: Meta = Meta.EMPTY, blo
// if (data.type == typeOf<HtmlData>()) {
// val languageMeta: Meta = Language.forName(name)
//

View File

@ -11,7 +11,7 @@ import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
public fun interface HtmlSite {
public fun renderSite(siteContext: SiteContext, siteData: DataSet<Any>)
public suspend fun SiteContext.renderSite(data: DataSet<Any>)
}
public fun DataSetBuilder<Any>.site(

View File

@ -75,9 +75,9 @@ public val SiteContext.languagePrefix: Name
get() = languages[language]?.let { it[Language::prefix.name].string ?: language }?.parseAsName() ?: Name.EMPTY
@SnarkBuilder
public suspend fun SiteContext.multiLanguageSite(data: DataSet<Any>, languageMap: Map<String, Meta>, site: HtmlSite) {
languageMap.forEach { (languageKey, languageMeta) ->
val prefix = languageMeta[Language::prefix.name].string ?: languageKey
public suspend fun SiteContext.multiLanguageSite(data: DataSet<Any>, languageMap: Map<String, Language>, site: HtmlSite) {
languageMap.forEach { (languageKey, language) ->
val prefix = language.prefix ?: languageKey
val languageSiteMeta = Meta {
SITE_LANGUAGE_KEY put languageKey
SITE_LANGUAGES_KEY put Meta {

View File

@ -25,6 +25,8 @@ public fun Name.toWebPath(): String = tokens.joinToString(separator = "/") {
@SnarkBuilder
public interface PageContext : SnarkContext {
public val site: SiteContext
/**
* A metadata for a page. It should include site metadata
*/
@ -53,42 +55,3 @@ public fun PageContext.resolvePageRef(pageName: String): String = resolvePageRef
public val PageContext.homeRef: String get() = resolvePageRef(SiteContext.INDEX_PAGE_TOKEN.asName())
public val PageContext.name: Name? get() = pageMeta["name"].string?.parseAsName()
/**
* Resolve a Html builder by its full name
*/
context(SnarkContext)
public fun DataTree<*>.resolveHtmlOrNull(name: Name): HtmlData? {
val resolved = (getByType<HtmlFragment>(name) ?: getByType<HtmlFragment>(name + SiteContext.INDEX_PAGE_TOKEN))
return resolved?.takeIf {
it.published //TODO add language confirmation
}
}
context(SnarkContext)
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
*/
context(SnarkContext)
public fun DataTree<*>.resolveAllHtml(predicate: (name: Name, meta: Meta) -> Boolean): Map<Name, HtmlData> =
filterByType<HtmlFragment> { name, meta ->
predicate(name, meta)
&& meta["published"].string != "false"
//TODO add language confirmation
}.asSequence().associate { it.name to it.data }
context(SnarkContext)
public fun DataTree<*>.findByContentType(
contentType: String,
baseName: Name = Name.EMPTY,
): Map<Name, Data<HtmlFragment>> = resolveAllHtml { name, meta ->
name.startsWith(baseName) && meta["content_type"].string == contentType
}

View File

@ -6,12 +6,11 @@ import space.kscience.dataforge.io.Binary
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.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.workspace.Workspace
import space.kscience.dataforge.names.*
import space.kscience.snark.SnarkBuilder
import space.kscience.snark.SnarkContext
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf
/**
@ -74,6 +73,24 @@ public interface SiteContext : SnarkContext {
}
}
public suspend fun SiteContext.static(dataSet: DataSet<Binary>, prefix: Name = Name.EMPTY) {
dataSet.forEach { (name, data) ->
static(prefix + name, data)
}
}
public suspend fun SiteContext.static(dataSet: DataSet<*>, branch: String, prefix: String = branch) {
val branchName = branch.parseAsName()
val prefixName = prefix.parseAsName()
val binaryType = typeOf<Binary>()
dataSet.forEach { (name, data) ->
@Suppress("UNCHECKED_CAST")
if (name.startsWith(branchName) && data.type.isSubtypeOf(binaryType)) {
static(prefixName + name, data as Data<Binary>)
}
}
}
@SnarkBuilder
public suspend fun SiteContext.page(
route: Name,

View File

@ -1,6 +1,5 @@
package space.kscience.snark.html
import io.ktor.http.ContentType
import kotlinx.html.div
import kotlinx.html.unsafe
import kotlinx.io.Source
@ -12,25 +11,25 @@ import space.kscience.snark.SnarkReader
import kotlin.reflect.KType
import kotlin.reflect.typeOf
public object HtmlReader : SnarkReader<HtmlFragment> {
public object HtmlReader : SnarkReader<DataFragment> {
override val types: Set<String> = setOf("html")
override fun readFrom(source: String): HtmlFragment = HtmlFragment {
override fun readFrom(source: String): DataFragment = DataFragment { _, _ ->
div {
unsafe { +source }
}
}
override fun readFrom(source: Source): HtmlFragment = readFrom(source.readString())
override val type: KType = typeOf<HtmlFragment>()
override fun readFrom(source: Source): DataFragment = readFrom(source.readString())
override val type: KType = typeOf<DataFragment>()
}
public object MarkdownReader : SnarkReader<HtmlFragment> {
override val type: KType = typeOf<HtmlFragment>()
public object MarkdownReader : SnarkReader<DataFragment> {
override val type: KType = typeOf<DataFragment>()
override val types: Set<String> = setOf("text/markdown", "md", "markdown")
override fun readFrom(source: String): HtmlFragment = HtmlFragment {
override fun readFrom(source: String): DataFragment = DataFragment { _, _ ->
val parsedTree = markdownParser.buildMarkdownTreeFromString(source)
val htmlString = HtmlGenerator(source, parsedTree, markdownFlavor).generateHtml()
@ -44,9 +43,9 @@ public object MarkdownReader : SnarkReader<HtmlFragment> {
private val markdownFlavor = CommonMarkFlavourDescriptor()
private val markdownParser = MarkdownParser(markdownFlavor)
override fun readFrom(source: Source): HtmlFragment = readFrom(source.readString())
override fun readFrom(source: Source): DataFragment = readFrom(source.readString())
public val snarkReader: SnarkReader<HtmlFragment> = SnarkReader(this, "text/markdown")
public val snarkReader: SnarkReader<DataFragment> = SnarkReader(this, "text/markdown")
}

View File

@ -12,7 +12,6 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.workspace.FileData
import space.kscience.dataforge.workspace.Workspace
import space.kscience.snark.html.*
import java.nio.file.Path
import kotlin.contracts.InvocationKind
@ -32,7 +31,6 @@ internal class StaticSiteContext(
) : SiteContext {
// @OptIn(ExperimentalPathApi::class)
// private suspend fun files(item: DataTreeItem<Any>, routeName: Name) {
// //try using direct file rendering
@ -91,30 +89,23 @@ internal class StaticSiteContext(
"${baseUrl.removeSuffix("/")}/$ref"
}
inner class StaticPageContext(override val pageMeta: Meta) : PageContext {
class StaticPageContext(override val site: StaticSiteContext, override val pageMeta: Meta) : PageContext {
override fun resolveRef(ref: String): String =
this@StaticSiteContext.resolveRef(this@StaticSiteContext.baseUrl, ref)
site.resolveRef(site.baseUrl, ref)
override fun resolvePageRef(pageName: Name, relative: Boolean): String = resolveRef(
(if (relative) this@StaticSiteContext.route + pageName else pageName).toWebPath() + ".html"
(if (relative) site.route + pageName else pageName).toWebPath() + ".html"
)
}
override suspend fun page(route: Name, data: DataSet<Any>, pageMeta: Meta, htmlPage: HtmlPage) {
val htmlBuilder = createHTML()
val modifiedPageMeta = pageMeta.toMutableMeta().apply {
"name" put route.toString()
}
htmlBuilder.html {
with(htmlPage) {
renderPage(StaticPageContext(Laminate(modifiedPageMeta, siteMeta)), data)
}
}
val newPath = if (route.isEmpty()) {
outputPath.resolve("index.html")
} else {
@ -122,17 +113,20 @@ internal class StaticSiteContext(
}
newPath.parent.createDirectories()
newPath.writeText(htmlBuilder.finalize())
val pageContext = StaticPageContext(this, Laminate(modifiedPageMeta, siteMeta))
newPath.writeText(HtmlPage.createHtmlString(pageContext,htmlPage, data))
}
override suspend fun site(route: Name, data: DataSet<Any>, siteMeta: Meta, htmlSite: HtmlSite) {
val subSiteContext = StaticSiteContext(
siteMeta = Laminate(siteMeta, this.siteMeta),
with(htmlSite) {
StaticSiteContext(
siteMeta = Laminate(siteMeta, this@StaticSiteContext.siteMeta),
baseUrl = if (baseUrl == "") "" else resolveRef(baseUrl, route.toWebPath()),
route = Name.EMPTY,
outputPath = outputPath.resolve(route.toWebPath())
)
htmlSite.renderSite(subSiteContext, data)
).renderSite(data)
}
}
}

View File

@ -1,28 +1,31 @@
package space.kscience.snark.ktor
import io.ktor.http.ContentType
import io.ktor.http.URLBuilder
import io.ktor.http.URLProtocol
import io.ktor.http.fromFileExtension
import io.ktor.http.*
import io.ktor.http.content.TextContent
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.staticFiles
import io.ktor.server.plugins.origin
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.*
import kotlinx.css.CssBuilder
import kotlinx.html.CommonAttributeGroupFacade
import kotlinx.html.head
import kotlinx.html.style
import io.ktor.server.routing.Route
import io.ktor.server.routing.createRouteFromPath
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.data.*
import space.kscience.dataforge.data.Data
import space.kscience.dataforge.data.DataSet
import space.kscience.dataforge.data.DataTree
import space.kscience.dataforge.data.await
import space.kscience.dataforge.io.Binary
import space.kscience.dataforge.io.toByteArray
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.toMutableMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.cutLast
import space.kscience.dataforge.names.endsWith
@ -34,11 +37,11 @@ import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.reflect.typeOf
public fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
style = CssBuilder().block().toString()
}
//public fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
// style = CssBuilder().block().toString()
//}
public class KtorSiteBuilder(
public class KtorSiteContext(
override val context: Context,
override val siteMeta: Meta,
private val baseUrl: String,
@ -87,18 +90,19 @@ public class KtorSiteBuilder(
}
private inner class KtorPageContext(
private class KtorPageContext(
override val site: KtorSiteContext,
val pageBaseUrl: String,
override val pageMeta: Meta,
) : PageContext {
override fun resolveRef(ref: String): String = this@KtorSiteBuilder.resolveRef(pageBaseUrl, ref)
override fun resolveRef(ref: String): String = site.resolveRef(pageBaseUrl, ref)
override fun resolvePageRef(
pageName: Name,
relative: Boolean,
): String {
val fullPageName = if (relative) this@KtorSiteBuilder.route + pageName else pageName
val fullPageName = if (relative) site.route + pageName else pageName
return if (fullPageName.endsWith(SiteContext.INDEX_PAGE_TOKEN)) {
resolveRef(fullPageName.cutLast().toWebPath())
} else {
@ -121,27 +125,25 @@ public class KtorSiteBuilder(
"name" put route.toString()
"url" put url.buildString()
}
val pageBuilder = KtorPageContext(url.buildString(), Laminate(modifiedPageMeta, siteMeta))
val pageContext =
KtorPageContext(this@KtorSiteContext, url.buildString(), Laminate(modifiedPageMeta, siteMeta))
//render page in suspend environment
val html = HtmlPage.createHtmlString(pageContext, htmlPage, data)
call.respondHtml {
head {}
with(htmlPage) {
renderPage(pageBuilder, data)
}
}
call.respond(TextContent(html, ContentType.Text.Html.withCharset(Charsets.UTF_8), HttpStatusCode.OK))
}
}
override suspend fun site(route: Name, data: DataSet<Any>, siteMeta: Meta, htmlSite: HtmlSite) {
val context = KtorSiteBuilder(
with(htmlSite) {
KtorSiteContext(
context,
siteMeta = Laminate(siteMeta, this.siteMeta),
siteMeta = Laminate(siteMeta, this@KtorSiteContext.siteMeta),
baseUrl = resolveRef(baseUrl, route.toWebPath()),
route = Name.EMPTY,
ktorRoute = ktorRoute.createRouteFromPath(route.toWebPath())
)
htmlSite.renderSite(context, data)
).renderSite(data)
}
}
}
@ -151,12 +153,12 @@ private fun Route.site(
data: DataTree<*>,
baseUrl: String = "",
siteMeta: Meta = data.meta,
block: KtorSiteBuilder.() -> Unit,
block: KtorSiteContext.() -> Unit,
) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(KtorSiteBuilder(context, siteMeta, baseUrl, route = Name.EMPTY, this@Route))
block(KtorSiteContext(context, siteMeta, baseUrl, route = Name.EMPTY, this@Route))
}
public fun Application.site(