A lot of small refactoring in html

This commit is contained in:
Alexander Nozik 2020-12-12 10:44:45 +03:00
parent a38d70bade
commit 734d1e1168
14 changed files with 351 additions and 160 deletions

View File

@ -18,6 +18,13 @@ kscience{
val ktorVersion: String by rootProject.extra
kotlin {
js{
browser {
webpackTask {
this.outputFileName = "visionforge-solid.js"
}
}
}
afterEvaluate {
val jsBrowserDistribution by tasks.getting

View File

@ -6,34 +6,10 @@ import hep.dataforge.vision.client.fetchAndRenderAllVisions
import hep.dataforge.vision.solid.three.ThreePlugin
import kotlinx.browser.window
//private class SatDemoApp : Application {
//
// override fun start(state: Map<String, Any>) {
// val element = document.getElementById("canvas") as? HTMLElement
// ?: error("Element with id 'canvas' not found on page")
// val three = Global.plugins.fetch(ThreePlugin)
//
// val sat = visionOfSatellite(
// ySegments = 3,
// )
// three.render(element, sat){
// minSize = 500
// axes{
// size = 500.0
// visible = true
// }
// }
// }
//
//}
//
//fun main() {
// startApplication(::SatDemoApp)
//}
fun main() {
//Loading three-js renderer
Global.plugins.load(ThreePlugin)
//Fetch from server and render visions for all outputs
window.onload = {
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions()
}

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Three js demo for particle physics</title>
<script type="text/javascript" src="sat-demo.js"></script>
</head>
<body class="application">
<div id="canvas"></div>
</body>
</html>

View File

@ -2,12 +2,9 @@ package ru.mipt.npm.sat
import hep.dataforge.context.Global
import hep.dataforge.names.asName
import hep.dataforge.names.toName
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.server.close
import hep.dataforge.vision.server.serve
import hep.dataforge.vision.server.show
import hep.dataforge.vision.server.*
import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.color
@ -15,8 +12,8 @@ import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.script
import kotlin.random.Random
@OptIn(KtorExperimentalAPI::class)
@ -30,27 +27,27 @@ fun main() {
}
val server = context.plugins.fetch(VisionManager).serve {
header {
script {
src = "sat-demo.js"
}
}
useScript("visionforge-solid.js")
useCss("css/styles.css")
page {
div("flex-column") {
h1 { +"Satellite detector demo" }
vision("main".asName(), sat)
}
launch {
delay(1000)
while (isActive) {
val target = "layer[${Random.nextInt(1,10)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName()
(sat[target] as? Solid)?.color("red")
delay(300)
(sat[target] as? Solid)?.color = "green"
vision(sat)
}
}
}
server.show()
context.launch {
while (isActive) {
val target = "layer[${Random.nextInt(1,11)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName()
(sat[target] as? Solid)?.color("red")
delay(300)
(sat[target] as? Solid)?.color = "green"
delay(10)
}
}
println("Press Enter to close server")
while (readLine()!="exit"){
//

View File

@ -0,0 +1,16 @@
body{
width: 100%;
height: 100%;
overflow: hidden;
}
.flex-column{
width: calc(100% - 15px);
height: calc(100% - 15px);
display: flex;
flex-direction: column;
}
.visionforge-output{
flex-grow: 1;
}

View File

@ -16,12 +16,12 @@ kotlin {
dependencies {
api("hep.dataforge:dataforge-context:$dataforgeVersion")
api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion")
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
}
}
jsMain {
dependencies {
api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion")
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
}
}
}

View File

@ -35,6 +35,7 @@ public class VisionChangeBuilder : VisionContainerBuilder<Vision> {
propertyChange,
childrenChange.mapValues { it.value?.isolate(manager) }
)
//TODO optimize isolation for visions without parents?
}
private fun Vision.isolate(manager: VisionManager): Vision {
@ -77,12 +78,15 @@ private fun CoroutineScope.collectChange(
}
}
coroutineContext[Job]?.invokeOnCompletion {
source.config.removeListener(mutex)
}
if (source is VisionGroup) {
//Subscribe for children changes
source.children.forEach { (token, child) ->
collectChange(name + token, child, mutex, collector)
}
//TODO update styles?
//Subscribe for structure change
if (source is MutableVisionGroup) {
@ -94,6 +98,9 @@ private fun CoroutineScope.collectChange(
}
collector()[name + token] = after
}
coroutineContext[Job]?.invokeOnCompletion {
source.removeStructureChangeListener(mutex)
}
}
}
}
@ -102,12 +109,12 @@ private fun CoroutineScope.collectChange(
public fun Vision.flowChanges(
manager: VisionManager,
collectionDuration: Duration,
scope: CoroutineScope = manager.context,
): Flow<VisionChange> = flow {
supervisorScope {
val mutex = Mutex()
var collector = VisionChangeBuilder()
scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
while (currentCoroutineContext().isActive) {
//Wait for changes to accumulate
@ -123,3 +130,4 @@ public fun Vision.flowChanges(
}
}
}
}

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision.html
import hep.dataforge.meta.Meta
import hep.dataforge.names.Name
import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent
@ -13,7 +14,7 @@ public class BindingOutputTagConsumer<T, V : Vision>(
private val _bindings = HashMap<Name, V>()
public val bindings: Map<Name, V> get() = _bindings
override fun FlowContent.renderVision(name: Name, vision: V) {
override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta) {
_bindings[name] = vision
}
}

View File

@ -1,18 +1,24 @@
package hep.dataforge.vision.html
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionManager
import kotlinx.html.*
/**
* An HTML div wrapper that includes the output [name] and inherited [render] function
* A placeholder object to attach inline vision builders.
*/
public class OutputDiv<in V : Vision>(
private val div: DIV,
public val name: Name,
public val render: (V) -> Unit,
) : HtmlBlockTag by div
@DFExperimental
public class VisionOutput {
public var meta: Meta = Meta.EMPTY
public inline fun meta(block: MetaBuilder.() -> Unit) {
this.meta = Meta(block)
}
}
/**
* Modified [TagConsumer] that allows rendering output fragments and visions in them
@ -26,33 +32,54 @@ public abstract class OutputTagConsumer<R, V : Vision>(
/**
* Render a vision inside the output fragment
* @param name name of the output container
* @param vision an object to be rendered
* @param outputMeta optional configuration for the output container
*/
protected abstract fun FlowContent.renderVision(name: Name, vision: V)
protected abstract fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta)
/**
* Create a placeholder for an output window
* Create a placeholder for a vision output with optional [Vision] in it
*/
public fun <T> TagConsumer<T>.visionOutput(
public fun <T> TagConsumer<T>.vision(
name: Name,
block: OutputDiv<V>.() -> Unit = {},
vision: V? = null,
outputMeta: Meta = Meta.EMPTY,
): T = div {
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
OutputDiv<V>(this, name) { renderVision(name, it) }.block()
if (!outputMeta.isEmpty()) {
//Hard-code output configuration
script {
attributes["class"] = OUTPUT_META_CLASS
unsafe {
+VisionManager.defaultJson.encodeToString(MetaSerializer, outputMeta)
}
}
}
vision?.let { renderVision(name, it, outputMeta) }
}
public fun <T> TagConsumer<T>.visionOutput(
@OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision(
name: Name,
visionProvider: VisionOutput.() -> V,
): T {
val output = VisionOutput()
val vision = output.visionProvider()
return vision(name, vision, output.meta)
}
@OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision(
name: String,
block: OutputDiv<V>.() -> Unit = {},
): T = visionOutput(name.toName(), block)
visionProvider: VisionOutput.() -> V,
): T = vision(name.toName(), visionProvider)
public fun <T> TagConsumer<T>.vision(name: Name, vision: V): Unit {
visionOutput(name) {
render(vision)
}
}
public inline fun <T> TagConsumer<T>.vision(
vision: V,
): T = vision("vision[${vision.hashCode()}]".toName(), vision)
/**
* Process the resulting object produced by [TagConsumer]
@ -67,6 +94,9 @@ public abstract class OutputTagConsumer<R, V : Vision>(
public companion object {
public const val OUTPUT_CLASS: String = "visionforge-output"
public const val OUTPUT_META_CLASS: String = "visionforge-output-meta"
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data"
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
public const val DEFAULT_ENDPOINT: String = "."

View File

@ -1,12 +1,16 @@
package hep.dataforge.vision.html
import hep.dataforge.meta.Meta
import hep.dataforge.names.Name
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionManager
import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer
import kotlinx.html.script
import kotlinx.html.stream.createHTML
import kotlinx.html.unsafe
public typealias HtmlVisionRenderer<V> = FlowContent.(V) -> Unit
public typealias HtmlVisionRenderer<V> = FlowContent.(V, Meta) -> Unit
/**
* An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer]
@ -16,8 +20,18 @@ public class StaticOutputTagConsumer<R, V : Vision>(
prefix: String? = null,
private val renderer: HtmlVisionRenderer<V>,
) : OutputTagConsumer<R, V>(root, prefix) {
override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta): Unit = renderer(vision, outputMeta)
override fun FlowContent.renderVision(name: Name, vision: V): Unit = renderer(vision)
public companion object {
public fun embed(manager: VisionManager): HtmlVisionRenderer<Vision> = { vision: Vision, _: Meta ->
script {
attributes["class"] = OUTPUT_DATA_CLASS
unsafe {
+manager.encodeToString(vision)
}
}
}
}
}
public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
@ -26,5 +40,21 @@ public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
renderer: HtmlVisionRenderer<Vision>,
): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize()
/**
* Render an object to HTML embedding the data as script bodies
*/
public fun <T : Any> HtmlVisionFragment<Vision>.embedToObject(
manager: VisionManager,
root: TagConsumer<T>,
prefix: String? = null,
): T = renderToObject(root, prefix, StaticOutputTagConsumer.embed(manager))
public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
renderToObject(createHTML(), null, renderer)
/**
* Convert a fragment to a string, embedding all visions data
*/
public fun HtmlVisionFragment<Vision>.embedToString(manager: VisionManager): String =
embedToObject(manager, createHTML())

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision.html
import hep.dataforge.meta.DFExperimental
import hep.dataforge.meta.configure
import hep.dataforge.meta.set
import hep.dataforge.vision.Vision
@ -10,14 +11,18 @@ import kotlin.test.Test
class HtmlTagTest {
fun OutputDiv<Vision>.visionBase(block: VisionBase.() -> Unit) =
render(VisionBase().apply(block))
@OptIn(DFExperimental::class)
fun VisionOutput.base(block: VisionBase.() -> Unit) =
VisionBase().apply(block)
val fragment = buildVisionFragment {
div {
h1 { +"Head" }
visionOutput("ddd") {
visionBase {
vision("ddd") {
meta{
"metaProperty" put 87
}
base {
configure {
set("myProp", 82)
set("otherProp", false)
@ -27,7 +32,7 @@ class HtmlTagTest {
}
}
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision ->
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision, _ ->
div {
h2 { +"Properties" }
ul {
@ -41,7 +46,7 @@ class HtmlTagTest {
}
}
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group ->
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group, _ ->
p { +"This is group" }
}

View File

@ -2,8 +2,7 @@ package hep.dataforge.vision.client
import hep.dataforge.context.*
import hep.dataforge.meta.Meta
import hep.dataforge.meta.get
import hep.dataforge.meta.node
import hep.dataforge.meta.MetaSerializer
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager
@ -42,16 +41,31 @@ public class VisionClient : AbstractPlugin() {
public fun findRendererFor(vision: Vision): ElementVisionRenderer? =
getRenderers().maxByOrNull { it.rateVision(vision) }
private fun Element.getEmbeddedData(className: String): String? = getElementsByClassName(className)[0]?.innerHTML
/**
* Fetch from server and render a vision, described in a given with [OutputTagConsumer.OUTPUT_CLASS] class.
*/
public fun fetchAndRenderVision(element: Element, requestUpdates: Boolean = true) {
public fun renderVisionAt(element: Element, requestUpdates: Boolean = true) {
val name = resolveName(element) ?: error("The element is not a vision output")
console.info("Found DF output with name $name")
if (!element.classList.contains(OutputTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
val endpoint = resolveEndpoint(element)
console.info("Vision server is resolved to $endpoint")
val outputMeta = element.getEmbeddedData(OutputTagConsumer.OUTPUT_META_CLASS)?.let {
VisionManager.defaultJson.decodeFromString(MetaSerializer, it)
} ?: Meta.EMPTY
//Trying to render embedded vision
val embeddedVision = element.getEmbeddedData(OutputTagConsumer.OUTPUT_DATA_CLASS)?.let {
visionManager.decodeFromString(it)
}
if (embeddedVision != null) {
val renderer = findRendererFor(embeddedVision) ?: error("Could nof find renderer for $embeddedVision")
renderer.render(element, embeddedVision, outputMeta)
}
val fetchUrl = URL(endpoint).apply {
searchParams.append("name", name)
pathname += "/vision"
@ -63,15 +77,14 @@ public class VisionClient : AbstractPlugin() {
response.text().then { text ->
val vision = visionManager.decodeFromString(text)
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
val rendererConfiguration = vision.properties[RENDERER_CONFIGURATION_META_KEY].node ?: Meta.EMPTY
renderer.render(element, vision, rendererConfiguration)
renderer.render(element, vision, outputMeta)
if (requestUpdates) {
val wsUrl = URL(endpoint).apply {
pathname += "/ws"
protocol = "ws"
searchParams.append("name", name)
}
val ws = WebSocket(wsUrl.toString()).apply {
WebSocket(wsUrl.toString()).apply {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
@ -106,8 +119,6 @@ public class VisionClient : AbstractPlugin() {
public companion object : PluginFactory<VisionClient> {
public const val RENDERER_CONFIGURATION_META_KEY: String = "@renderer"
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()
override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP)
@ -123,7 +134,7 @@ public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates:
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
console.info("Finished search for outputs. Found ${elements.length} items")
elements.asList().forEach { child ->
fetchAndRenderVision(child, requestUpdates)
renderVisionAt(child, requestUpdates)
}
}

View File

@ -0,0 +1,67 @@
//package hep.dataforge.vision.export
//
//package kscience.plotly
//
//import kotlinx.html.*
//import kotlinx.html.stream.createHTML
//
///**
// * A custom HTML fragment including plotly container reference
// */
//public class PlotlyFragment(public val render: FlowContent.(renderer: PlotlyRenderer) -> Unit)
//
///**
// * A complete page including headers and title
// */
//public data class PlotlyPage(
// val headers: Collection<HtmlFragment>,
// val fragment: PlotlyFragment,
// val title: String = "Plotly.kt",
// val renderer: PlotlyRenderer = StaticPlotlyRenderer
//) {
// public fun render(): String = createHTML().html {
// head {
// meta {
// charset = "utf-8"
// }
// title(this@PlotlyPage.title)
// headers.distinct().forEach { it.visit(consumer) }
// }
// body {
// fragment.render(this, renderer)
// }
// }
//}
//
//public fun Plotly.fragment(content: FlowContent.(renderer: PlotlyRenderer) -> Unit): PlotlyFragment = PlotlyFragment(content)
//
///**
// * Create a complete page including plots
// */
//public fun Plotly.page(
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
// title: String = "Plotly.kt",
// renderer: PlotlyRenderer = StaticPlotlyRenderer,
// content: FlowContent.(renderer: PlotlyRenderer) -> Unit
//): PlotlyPage = PlotlyPage(headers.toList(), fragment(content), title, renderer)
//
///**
// * Convert an html plot fragment to page
// */
//public fun PlotlyFragment.toPage(
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
// title: String = "Plotly.kt",
// renderer: PlotlyRenderer = StaticPlotlyRenderer
//): PlotlyPage = PlotlyPage(headers.toList(), this, title, renderer)
//
///**
// * Convert a plot to the sigle-plot page
// */
//public fun Plot.toPage(
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
// config: PlotlyConfig = PlotlyConfig.empty(),
// title: String = "Plotly.kt",
// renderer: PlotlyRenderer = StaticPlotlyRenderer
//): PlotlyPage = PlotlyFragment {
// plot(this@toPage, config = config, renderer = renderer)
//}.toPage(*headers, title = title)

View File

@ -1,10 +1,7 @@
package hep.dataforge.vision.server
import hep.dataforge.context.Context
import hep.dataforge.meta.Config
import hep.dataforge.meta.Configurable
import hep.dataforge.meta.boolean
import hep.dataforge.meta.long
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
@ -25,21 +22,16 @@ import io.ktor.http.content.static
import io.ktor.http.withCharset
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.application
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.routing.*
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.error
import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import java.awt.Desktop
@ -91,25 +83,13 @@ public class VisionServer internal constructor(
return visionMap
}
public fun page(
visionFragment: HtmlVisionFragment<Vision>,
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(),
) {
val visions = HashMap<Name, Vision>()
/**
* Server a map of visions without providing explicit html page for them
*/
@OptIn(DFExperimental::class)
public fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route {
application.log.info("Serving visions $visions at $route")
val cachedHtml: String? = if (cacheFragments) {
createHTML(true).html {
visions.putAll(buildPage(visionFragment, title, headers))
}
} else {
null
}
application.routing {
route(rootRoute) {
route(route) {
//Update websocket
webSocket("ws") {
val name: String = call.request.queryParameters["name"]
@ -117,12 +97,19 @@ public class VisionServer internal constructor(
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
vision.flowChanges(visionManager, updateInterval.milliseconds).onEach { update ->
val json = VisionManager.defaultJson.encodeToString(VisionChange.serializer(), update)
try {
withContext(visionManager.context.coroutineContext) {
vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update ->
val json = VisionManager.defaultJson.encodeToString(
VisionChange.serializer(),
update
)
outgoing.send(Frame.Text(json))
}.catch { ex ->
application.log.error(ex)
}.launchIn(visionManager.context).join()
}
}
} catch (t: Throwable) {
application.log.info("WebSocket update channel for $name is closed with exception: $t")
}
}
//Plots in their json representation
get("vision") {
@ -140,13 +127,56 @@ public class VisionServer internal constructor(
)
}
}
//filled pages
get {
if (cachedHtml == null) {
call.respondHtml {
}
/**
* Serv visions in a given [route] without providing a page template
*/
public fun serveVisions(route: String, visions: Map<Name, Vision>): Unit {
application.routing {
route(rootRoute) {
route(route) {
serveVisions(this, visions)
}
}
}
}
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [headers].
*
*/
public fun servePage(
visionFragment: HtmlVisionFragment<Vision>,
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(),
) {
val visions = HashMap<Name, Vision>()
val cachedHtml: String? = if (cacheFragments) {
//Create and cache page html and map of visions
createHTML(true).html {
visions.putAll(buildPage(visionFragment, title, headers))
}
} else {
null
}
application.routing {
route(rootRoute) {
route(route) {
serveVisions(this, visions)
//filled pages
get {
if (cachedHtml == null) {
//re-create html and vision list on each call
call.respondHtml {
visions.clear()
visions.putAll(buildPage(visionFragment, title, headers))
}
} else {
//Use cached html
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
}
}
@ -155,13 +185,16 @@ public class VisionServer internal constructor(
}
}
/**
* A shortcut method to easily create Complete pages filled with visions
*/
public fun page(
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(),
content: OutputTagConsumer<*, Vision>.() -> Unit,
) {
page(buildVisionFragment(content), route, title, headers)
servePage(buildVisionFragment(content), route, title, headers)
}
@ -171,11 +204,33 @@ public class VisionServer internal constructor(
}
}
/**
* Use a script with given [src] as a global header for all pages.
*/
public inline fun VisionServer.useScript(src: String, crossinline block: SCRIPT.() -> Unit = {}) {
header {
script {
type = "text/javascript"
this.src = src
block()
}
}
}
public inline fun VisionServer.useCss(href: String, crossinline block: LINK.() -> Unit = {}) {
header {
link {
rel = "stylesheet"
this.href = href
block()
}
}
}
/**
* Attach plotly application to given server
*/
public fun Application.visionModule(context: Context, route: String = DEFAULT_PAGE): VisionServer {
public fun Application.visionServer(context: Context, route: String = DEFAULT_PAGE): VisionServer {
if (featureOrNull(WebSockets) == null) {
install(WebSockets)
}
@ -209,7 +264,7 @@ public fun VisionManager.serve(
port: Int = 7777,
block: VisionServer.() -> Unit,
): ApplicationEngine = context.embeddedServer(CIO, port, host) {
visionModule(context).apply(block)
visionServer(context).apply(block)
}.start()
public fun ApplicationEngine.show() {