diff --git a/build.gradle.kts b/build.gradle.kts index 7be6270f..b5e3fd7e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ val fxVersion by extra("11") allprojects { group = "space.kscience" - version = "0.3.0-dev-3" + version = "0.3.0-dev-4" } subprojects { diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt index 4b18291b..50e51859 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt @@ -1,6 +1,6 @@ package ru.mipt.npm.muon.monitor -import kotlinx.browser.document +import org.w3c.dom.Document import react.dom.client.createRoot import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.fetch @@ -13,7 +13,7 @@ import space.kscience.visionforge.startApplication private class MMDemoApp : Application { - override fun start(state: Map) { + override fun start(document: Document, state: Map) { val context = Context("MM-demo") { plugin(ThreePlugin) diff --git a/demo/playground/notebooks/demo3D.ipynb b/demo/playground/notebooks/demo3D.ipynb new file mode 100644 index 00000000..1efcd3c5 --- /dev/null +++ b/demo/playground/notebooks/demo3D.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "pycharm": { + "is_executing": true + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "@file:DependsOn(\"../build/libs/playground-0.3.0-dev-4-all.jar\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Starting VisionForge server on http://localhost:7777

\n" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vf.startServer()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "\n" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import kotlinx.coroutines.*\n", + "import kotlin.random.Random\n", + "\n", + "Plotly.plot{\n", + " scatter{\n", + " x(1,2,3)\n", + " y(1,2,3)\n", + " if(vf.isServerRunning()){\n", + " vf.launch{\n", + " while(isActive){\n", + " delay(500)\n", + " y(Random.nextDouble(), Random.nextDouble(), Random.nextDouble())\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "vf.stopServer()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "codemirror_mode": "text/x-kotlin", + "file_extension": ".kt", + "mimetype": "text/x-kotlin", + "name": "kotlin", + "nbconvert_exporter": "", + "pygments_lexer": "kotlin", + "version": "1.8.0-dev-3517" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demo/playground/src/jsMain/kotlin/playgroundMain.kt b/demo/playground/src/jsMain/kotlin/playgroundMain.kt index 3ad34867..8e259d48 100644 --- a/demo/playground/src/jsMain/kotlin/playgroundMain.kt +++ b/demo/playground/src/jsMain/kotlin/playgroundMain.kt @@ -1,4 +1,5 @@ import space.kscience.dataforge.misc.DFExperimental +import space.kscience.visionforge.jupyter.VFNotebookPlugin import space.kscience.visionforge.markup.MarkupPlugin import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.ring.ThreeWithControlsPlugin @@ -11,4 +12,5 @@ fun main() = runVisionClient { plugin(PlotlyPlugin) plugin(MarkupPlugin) plugin(TableVisionJsPlugin) + plugin(VFNotebookPlugin) } \ No newline at end of file diff --git a/demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt b/demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt index 651b580d..212d906e 100644 --- a/demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt +++ b/demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt @@ -6,13 +6,13 @@ import space.kscience.dataforge.misc.DFExperimental import space.kscience.gdml.Gdml import space.kscience.plotly.Plot import space.kscience.visionforge.gdml.toVision -import space.kscience.visionforge.jupyter.JupyterPluginBase +import space.kscience.visionforge.jupyter.VFIntegrationBase import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.asVision import space.kscience.visionforge.solid.Solids @DFExperimental -internal class VisionForgePlayGroundForJupyter : JupyterPluginBase( +internal class VisionForgePlayGroundForJupyter : VFIntegrationBase( Context("VisionForge") { plugin(Solids) plugin(PlotlyPlugin) diff --git a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt index 9c645d81..23699443 100644 --- a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt +++ b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt @@ -11,7 +11,6 @@ import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.Name import space.kscience.visionforge.Colors import space.kscience.visionforge.html.VisionPage -import space.kscience.visionforge.server.DataServeMode import space.kscience.visionforge.server.close import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.serve @@ -37,7 +36,6 @@ fun main() { } val server = satContext.visionManager.serve { - dataMode = DataServeMode.UPDATE page(VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) { div("flex-column") { h1 { +"Satellite detector demo" } diff --git a/demo/solid-showcase/src/jsMain/kotlin/space/kscience/visionforge/solid/demo/ThreeDemoApp.kt b/demo/solid-showcase/src/jsMain/kotlin/space/kscience/visionforge/solid/demo/ThreeDemoApp.kt index eb27f4d6..5bdbcefe 100644 --- a/demo/solid-showcase/src/jsMain/kotlin/space/kscience/visionforge/solid/demo/ThreeDemoApp.kt +++ b/demo/solid-showcase/src/jsMain/kotlin/space/kscience/visionforge/solid/demo/ThreeDemoApp.kt @@ -1,9 +1,9 @@ package space.kscience.visionforge.solid.demo -import kotlinx.browser.document import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.w3c.dom.Document import space.kscience.visionforge.Application import space.kscience.visionforge.solid.x import space.kscience.visionforge.solid.y @@ -12,7 +12,7 @@ import kotlin.random.Random private class ThreeDemoApp : Application { - override fun start(state: Map) { + override fun start(document: Document, state: Map) { val element = document.getElementById("demo") ?: error("Element with id 'demo' not found on page") diff --git a/jupyter/src/jsMain/kotlin/VFNotebookPlugin.kt b/jupyter/src/jsMain/kotlin/VFNotebookPlugin.kt new file mode 100644 index 00000000..c13586ba --- /dev/null +++ b/jupyter/src/jsMain/kotlin/VFNotebookPlugin.kt @@ -0,0 +1,51 @@ +package space.kscience.visionforge.jupyter + +import kotlinx.browser.window +import org.w3c.dom.Element +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta +import space.kscience.visionforge.VisionClient +import space.kscience.visionforge.renderAllVisions +import space.kscience.visionforge.renderAllVisionsById +import space.kscience.visionforge.renderAllVisionsIn +import kotlin.reflect.KClass + +@JsExport +public class VFNotebookPlugin : AbstractPlugin() { + private val client by require(VisionClient) + + public fun renderAllVisionsIn(element: Element) { + client.renderAllVisionsIn(element) + } + + public fun renderAllVisionsById(id: String) { + client.renderAllVisionsById(id) + } + + public fun renderAllVisions() { + client.renderAllVisions() + } + + + init { + //register VisionForge in the browser window + window.asDynamic().vf = this + window.asDynamic().VisionForge = this + } + + @Suppress("NON_EXPORTABLE_TYPE") + override val tag: PluginTag get() = Companion.tag + + @Suppress("NON_EXPORTABLE_TYPE") + public companion object : PluginFactory { + override fun build(context: Context, meta: Meta): VFNotebookPlugin = VFNotebookPlugin() + + override val tag: PluginTag = PluginTag(name = "vision.notebook", group = PluginTag.DATAFORGE_GROUP) + + override val type: KClass = VFNotebookPlugin::class + } + +} \ No newline at end of file diff --git a/jupyter/src/jvmMain/kotlin/VisionForgeForNotebook.kt b/jupyter/src/jvmMain/kotlin/VFForNotebook.kt similarity index 70% rename from jupyter/src/jvmMain/kotlin/VisionForgeForNotebook.kt rename to jupyter/src/jvmMain/kotlin/VFForNotebook.kt index a91733ce..4c7b4e29 100644 --- a/jupyter/src/jvmMain/kotlin/VisionForgeForNotebook.kt +++ b/jupyter/src/jvmMain/kotlin/VFForNotebook.kt @@ -1,11 +1,9 @@ package space.kscience.visionforge.jupyter import io.ktor.server.engine.ApplicationEngine -import kotlinx.html.FORM -import kotlinx.html.TagConsumer -import kotlinx.html.p +import kotlinx.coroutines.CoroutineScope +import kotlinx.html.* import kotlinx.html.stream.createHTML -import kotlinx.html.style import org.jetbrains.kotlinx.jupyter.api.HTML import org.jetbrains.kotlinx.jupyter.api.MimeTypedResult import space.kscience.dataforge.context.Context @@ -21,11 +19,21 @@ import space.kscience.visionforge.html.visionFragment import space.kscience.visionforge.server.VisionServer import space.kscience.visionforge.server.serve import space.kscience.visionforge.visionManager +import kotlin.coroutines.CoroutineContext +import kotlin.random.Random +import kotlin.random.nextUInt + +internal fun TagConsumer<*>.renderScriptForId(id: String) { + script { + type = "text/javascript" + unsafe { +"VisionForge.renderAllVisionsById(\"$id\");" } + } +} /** * A handler class that includes a server and common utilities */ -public class VisionForgeForNotebook(override val context: Context) : ContextAware { +public class VFForNotebook(override val context: Context) : ContextAware, CoroutineScope { private var counter = 0 private var engine: ApplicationEngine? = null @@ -33,6 +41,8 @@ public class VisionForgeForNotebook(override val context: Context) : ContextAwar public var isolateFragments: Boolean = false + override val coroutineContext: CoroutineContext get() = context.coroutineContext + public fun legacyMode() { isolateFragments = true } @@ -68,15 +78,29 @@ public class VisionForgeForNotebook(override val context: Context) : ContextAwar public fun stopServer() { engine?.apply { logger.info { "Stopping VisionForge server" } - }?.stop(1000, 2000) + stop(1000, 2000) + engine = null + server = null + } } private fun produceHtmlString( fragment: HtmlVisionFragment, - ): String = server?.serveVisionsFromFragment("content[${counter++}]", fragment) - ?: createHTML().apply { - visionFragment(context, fragment = fragment) - }.finalize() + ): String = createHTML().apply { + val server = server + val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]" + div { + this.id = id + if (server != null) { + //if server exist, serve dynamically + server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment) + } else { + //if not, use static rendering + visionFragment(context, fragment = fragment) + } + } + renderScriptForId(id) + }.finalize() public fun produceHtml(isolated: Boolean? = null, fragment: HtmlVisionFragment): MimeTypedResult = HTML(produceHtmlString(fragment), isolated ?: isolateFragments) diff --git a/jupyter/src/jvmMain/kotlin/JupyterPluginBase.kt b/jupyter/src/jvmMain/kotlin/VFIntegrationBase.kt similarity index 66% rename from jupyter/src/jvmMain/kotlin/JupyterPluginBase.kt rename to jupyter/src/jvmMain/kotlin/VFIntegrationBase.kt index 944f1493..c8f6831d 100644 --- a/jupyter/src/jvmMain/kotlin/JupyterPluginBase.kt +++ b/jupyter/src/jvmMain/kotlin/VFIntegrationBase.kt @@ -1,8 +1,7 @@ package space.kscience.visionforge.jupyter -import kotlinx.html.p +import kotlinx.html.* import kotlinx.html.stream.createHTML -import kotlinx.html.style import org.jetbrains.kotlinx.jupyter.api.HTML import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration @@ -11,11 +10,16 @@ import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.misc.DFExperimental import space.kscience.visionforge.Vision import space.kscience.visionforge.html.* +import kotlin.random.Random +import kotlin.random.nextUInt +/** + * A base class for different Jupyter VF integrations + */ @DFExperimental -public abstract class JupyterPluginBase(final override val context: Context) : JupyterIntegration(), ContextAware { +public abstract class VFIntegrationBase(final override val context: Context) : JupyterIntegration(), ContextAware { - protected val handler: VisionForgeForNotebook = VisionForgeForNotebook(context) + protected val handler: VFForNotebook = VFForNotebook(context) protected abstract fun Builder.afterLoaded() @@ -50,7 +54,24 @@ public abstract class JupyterPluginBase(final override val context: Context) : J } render { page -> - HTML(page.render(createHTML()), true) + HTML(createHTML().apply { + head { + meta { + charset = "utf-8" + } + page.pageHeaders.values.forEach { + fragment(it) + } + } + body { + val id = "fragment[${page.hashCode()}/${Random.nextUInt()}]" + div { + this.id = id + visionFragment(context, fragment = page.content) + } + renderScriptForId(id) + } + }.finalize(), true) } render { fragment -> diff --git a/jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt b/jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt index 0a112ba2..0f430053 100644 --- a/jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt +++ b/jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt @@ -5,11 +5,11 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.misc.DFExperimental import space.kscience.gdml.Gdml import space.kscience.visionforge.gdml.toVision -import space.kscience.visionforge.jupyter.JupyterPluginBase +import space.kscience.visionforge.jupyter.VFIntegrationBase import space.kscience.visionforge.solid.Solids @DFExperimental -internal class GdmlForJupyter : JupyterPluginBase( +internal class GdmlForJupyter : VFIntegrationBase( Context("GDML") { plugin(Solids) } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt index 5a4395af..e8c91b1e 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt @@ -8,8 +8,6 @@ import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.Name import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager -import kotlin.random.Random -import kotlin.random.nextUInt public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit @@ -17,9 +15,6 @@ public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit public fun HtmlVisionFragment(content: VisionTagConsumer<*>.() -> Unit): HtmlVisionFragment = content -internal const val RENDER_FUNCTION_NAME = "renderAllVisionsById" - - /** * Render a fragment in the given consumer and return a map of extracted visions * @param context a context used to create a vision fragment @@ -35,7 +30,6 @@ public fun TagConsumer<*>.visionFragment( fetchDataUrl: String? = null, fetchUpdatesUrl: String? = null, idPrefix: String? = null, - renderScript: Boolean = true, fragment: HtmlVisionFragment, ): Map { val visionMap = HashMap() @@ -63,19 +57,9 @@ public fun TagConsumer<*>.visionFragment( } } } - if (renderScript) { - val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]" - div { - this.id = id - fragment(consumer) - } - script { - type = "text/javascript" - unsafe { +"window.${RENDER_FUNCTION_NAME}(\"$id\");" } - } - } else { - fragment(consumer) - } + + fragment(consumer) + return visionMap } @@ -83,16 +67,14 @@ public fun FlowContent.visionFragment( context: Context = Global, embedData: Boolean = true, fetchDataUrl: String? = null, - fetchUpdatesUrl: String? = null, + flowDataUrl: String? = null, idPrefix: String? = null, - renderScript: Boolean = true, fragment: HtmlVisionFragment, ): Map = consumer.visionFragment( context, embedData, fetchDataUrl, - fetchUpdatesUrl, + flowDataUrl, idPrefix, - renderScript, fragment ) \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionPage.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionPage.kt index cb284b14..1f297abe 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionPage.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionPage.kt @@ -13,20 +13,6 @@ public data class VisionPage( public val pageHeaders: Map = emptyMap(), public val content: HtmlVisionFragment, ) { - public fun render(root: TagConsumer): R = root.apply { - head { - meta { - charset = "utf-8" - } - pageHeaders.values.forEach { - fragment(it) - } - } - body { - visionFragment(context, fragment = content) - } - }.finalize() - public companion object{ /** * Use a script with given [src] as a global header for all pages. diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/Application.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/Application.kt index 5e1e2470..85ead127 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/Application.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/Application.kt @@ -2,7 +2,7 @@ package space.kscience.visionforge import kotlinx.browser.document import kotlinx.coroutines.CoroutineScope -import kotlinx.dom.hasClass +import org.w3c.dom.Document import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -36,7 +36,7 @@ public interface Application: CoroutineScope { * Starting point for an application. * @param state Initial state between Hot Module Replacement (HMR). */ - public fun start(state: Map) + public fun start(document: Document, state: Map) /** * Ending point for an application. @@ -46,17 +46,13 @@ public interface Application: CoroutineScope { } public fun startApplication(builder: () -> Application) { - fun start(state: dynamic): Application? { - return if (document.body?.hasClass("application") == true) { - val application = builder() + fun start(document: Document, state: dynamic): Application{ + val application = builder() - @Suppress("UnsafeCastFromDynamic") - application.start(state?.appState ?: emptyMap()) + @Suppress("UnsafeCastFromDynamic") + application.start(document, state?.appState ?: emptyMap()) - application - } else { - null - } + return application } var application: Application? = null @@ -73,9 +69,9 @@ public fun startApplication(builder: () -> Application) { } if (document.body != null) { - application = start(state) + application = start(document, state) } else { application = null - document.addEventListener("DOMContentLoaded", { application = start(state) }) + document.addEventListener("DOMContentLoaded", { application = start(document, state) }) } } \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt index f54b7d5d..a84a5a42 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -13,7 +13,6 @@ import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int import space.kscience.dataforge.names.Name -import space.kscience.visionforge.html.RENDER_FUNCTION_NAME import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE @@ -46,18 +45,16 @@ public class VisionClient : AbstractPlugin() { return attribute?.value } - private fun getRenderers() = context.gather(ElementVisionRenderer.TYPE).values + private val renderers by lazy { context.gather(ElementVisionRenderer.TYPE).values } - private fun findRendererFor(vision: Vision): ElementVisionRenderer? { - return getRenderers().mapNotNull { - val rating = it.rateVision(vision) - if (rating > 0) { - rating to it - } else { - null - } - }.maxByOrNull { it.first }?.second - } + private fun findRendererFor(vision: Vision): ElementVisionRenderer? = renderers.mapNotNull { + val rating = it.rateVision(vision) + if (rating > 0) { + rating to it + } else { + null + } + }.maxByOrNull { it.first }?.second private fun Element.getEmbeddedData(className: String): String? = getElementsByClassName(className)[0]?.innerHTML @@ -78,7 +75,7 @@ public class VisionClient : AbstractPlugin() { if (vision != null) { vision.setAsRoot(visionManager) val renderer = findRendererFor(vision) - ?: error("Could not find renderer for ${visionManager.encodeToString(vision)}") + ?: error("Could not find renderer for ${vision::class}") renderer.render(element, vision, outputMeta) element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr -> @@ -228,7 +225,7 @@ public class VisionClient : AbstractPlugin() { private fun whenDocumentLoaded(block: Document.() -> Unit): Unit { - if (document.readyState == DocumentReadyState.COMPLETE) { + if (document.body != null) { block(document) } else { document.addEventListener("DOMContentLoaded", { block(document) }) @@ -267,14 +264,29 @@ public fun VisionClient.renderAllVisions(): Unit = whenDocumentLoaded { renderAllVisionsIn(element) } +public class VisionClientApplication(public val context: Context) : Application { + private val client = context.fetch(VisionClient) + + override fun start(document: Document, state: Map) { + console.info("Starting Vision Client") + val element = document.body ?: error("Document does not have a body") + client.renderAllVisionsIn(element) + } +} + + /** * Create a vision client context and render all visions on the page. */ public fun runVisionClient(contextBuilder: ContextBuilder.() -> Unit) { console.info("Starting VisionForge context") - val context = Context("VisionForge", contextBuilder) - val visionClient = context.fetch(VisionClient) - window.asDynamic()[RENDER_FUNCTION_NAME] = visionClient::renderAllVisionsById - //visionClient.renderAllVisions() + val context = Context("VisionForge") { + plugin(VisionClient) + contextBuilder() + } + + startApplication { + VisionClientApplication(context) + } } \ No newline at end of file diff --git a/visionforge-core/src/jvmMain/kotlin/space/kscience/visionforge/html/htmlExport.kt b/visionforge-core/src/jvmMain/kotlin/space/kscience/visionforge/html/htmlExport.kt index b3063eb1..223a9473 100644 --- a/visionforge-core/src/jvmMain/kotlin/space/kscience/visionforge/html/htmlExport.kt +++ b/visionforge-core/src/jvmMain/kotlin/space/kscience/visionforge/html/htmlExport.kt @@ -1,9 +1,14 @@ package space.kscience.visionforge +import kotlinx.html.body +import kotlinx.html.head +import kotlinx.html.meta import kotlinx.html.stream.createHTML import space.kscience.dataforge.misc.DFExperimental import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.VisionPage +import space.kscience.visionforge.html.fragment +import space.kscience.visionforge.html.visionFragment import java.awt.Desktop import java.nio.file.Files import java.nio.file.Path @@ -56,20 +61,34 @@ import java.nio.file.Path /** * Export a [VisionPage] to a file + * + * @param fileHeaders additional file-system specific headers. */ @DFExperimental public fun VisionPage.makeFile( path: Path?, - defaultHeaders: ((Path) -> Map)? = null, + fileHeaders: ((Path) -> Map)? = null, ): Path { val actualFile = path?.let { Path.of(System.getProperty("user.home")).resolve(path) } ?: Files.createTempFile("tempPlot", ".html") - val actualDefaultHeaders = defaultHeaders?.invoke(actualFile) - val actualPage = if (actualDefaultHeaders == null) this else copy(pageHeaders = actualDefaultHeaders + pageHeaders) + val actualDefaultHeaders = fileHeaders?.invoke(actualFile) + val actualHeaders = if (actualDefaultHeaders == null) pageHeaders else actualDefaultHeaders + pageHeaders - val htmlString = actualPage.render(createHTML()) + val htmlString = createHTML().apply { + head { + meta { + charset = "utf-8" + } + actualHeaders.values.forEach { + fragment(it) + } + } + body { + visionFragment(context, fragment = content) + } + }.finalize() Files.writeString(actualFile, htmlString) return actualFile diff --git a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt index 364d3162..a4bfa7b9 100644 --- a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt @@ -115,7 +115,7 @@ public class VisionServer internal constructor( context = visionManager.context, embedData = dataMode == DataServeMode.EMBED, fetchDataUrl = if (dataMode != DataServeMode.EMBED) "$serverUrl$pagePath/data" else null, - fetchUpdatesUrl = if (dataMode == DataServeMode.UPDATE) "$serverUrl$pagePath/ws" else null, + flowDataUrl = if (dataMode == DataServeMode.UPDATE) "$serverUrl$pagePath/ws" else null, fragment = visionFragment ) } @@ -192,18 +192,19 @@ public class VisionServer internal constructor( * Compile a fragment to string and serve visions from it */ public fun serveVisionsFromFragment( + consumer: TagConsumer<*>, route: String, fragment: HtmlVisionFragment, - ): String = createHTML().apply { - val visions = visionFragment( + ): Unit { + val visions = consumer.visionFragment( visionManager.context, embedData = true, fetchUpdatesUrl = "$serverUrl$route/ws", - renderScript = true, fragment = fragment ) + serveVisions(route, visions) - }.finalize() + } /** * Serve a page, potentially containing any number of visions at a given [route] with given [header]. @@ -248,7 +249,11 @@ public class VisionServer internal constructor( title: String = "VisionForge server page '$route'", visionFragment: HtmlVisionFragment, ) { - page(route, mapOf("title" to VisionPage.title(title)) + headers.associateBy { it.hashCode().toString() }, visionFragment) + page( + route, + mapOf("title" to VisionPage.title(title)) + headers.associateBy { it.hashCode().toString() }, + visionFragment + ) } /**