From fa3fda144b40dbd09545f4d22b12d88b2a434c08 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 21 Nov 2025 00:38:43 +0100 Subject: [PATCH] added tryling, v0 --- site/src/jsMain/kotlin/App.kt | 1 + site/src/jsMain/kotlin/HomePage.kt | 8 ++ site/src/jsMain/kotlin/Main.kt | 7 +- site/src/jsMain/kotlin/TryLyngPage.kt | 181 ++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 site/src/jsMain/kotlin/TryLyngPage.kt diff --git a/site/src/jsMain/kotlin/App.kt b/site/src/jsMain/kotlin/App.kt index 2b6beb1..5cb23c3 100644 --- a/site/src/jsMain/kotlin/App.kt +++ b/site/src/jsMain/kotlin/App.kt @@ -105,6 +105,7 @@ fun App() { Div({ classes("col-12", if (isDocsRoute) "col-lg-9" else "col-lg-12") }) { when { route.isBlank() -> HomePage() + route == "tryling" -> TryLyngPage() !isDocsRoute -> ReferencePage() else -> DocsPage( route = route, diff --git a/site/src/jsMain/kotlin/HomePage.kt b/site/src/jsMain/kotlin/HomePage.kt index 54c65ff..6881f63 100644 --- a/site/src/jsMain/kotlin/HomePage.kt +++ b/site/src/jsMain/kotlin/HomePage.kt @@ -56,6 +56,14 @@ fun HomePage() { I({ classes("bi", "bi-journal-text", "me-1") }) Text("Browse reference") } + A(attrs = { + classes("btn", "btn-success", "btn-lg") + // Use the hash path requested by the user: "#tryling" + attr("href", "#tryling") + }) { + I({ classes("bi", "bi-code-slash", "me-1") }) + Text("Try Lyng") + } } } } diff --git a/site/src/jsMain/kotlin/Main.kt b/site/src/jsMain/kotlin/Main.kt index 462e5c0..3f62164 100644 --- a/site/src/jsMain/kotlin/Main.kt +++ b/site/src/jsMain/kotlin/Main.kt @@ -194,7 +194,12 @@ fun ensureDocsLayoutStyles() { // DocLink and UnsafeRawHtml moved to Components.kt -fun currentRoute(): String = window.location.hash.removePrefix("#/") +fun currentRoute(): String { + val h = window.location.hash + // Support both "#/path" and "#path" formats + val noHash = if (h.startsWith("#")) h.substring(1) else h + return noHash.removePrefix("/") +} fun routeToPath(route: String): String { val noParams = stripQuery(stripFragment(route)) diff --git a/site/src/jsMain/kotlin/TryLyngPage.kt b/site/src/jsMain/kotlin/TryLyngPage.kt new file mode 100644 index 0000000..9c15309 --- /dev/null +++ b/site/src/jsMain/kotlin/TryLyngPage.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import androidx.compose.runtime.* +import kotlinx.coroutines.launch +import org.jetbrains.compose.web.attributes.placeholder +import org.jetbrains.compose.web.dom.* +import net.sergeych.lyng.Scope + +@Composable +fun TryLyngPage() { + val scope = rememberCoroutineScope() + var code by remember { + mutableStateOf( + """ + // Welcome to Lyng! Edit and run. + // Try changing the data and press Ctrl+Enter or click Run. + import lyng.stdlib + + val data = [1, 2, 3, 4, 5] + val evens = data.filter { it % 2 == 0 }.map { it * it } + evens + """.trimIndent() + ) + } + var running by remember { mutableStateOf(false) } + var output by remember { mutableStateOf(null) } + var error by remember { mutableStateOf(null) } + + fun runCode() { + if (running) return + running = true + output = null + error = null + scope.launch { + try { + // Create a fresh module scope each run so imports and vars are clean + val s = Scope.new() + // Capture printed output from Lyng `print`/`println` into the UI result window + val printed = StringBuilder() + s.addVoidFn("print") { + for ((i, a) in args.withIndex()) { + if (i > 0) printed.append(' ') + printed.append(a.toString(this).value) + } + } + s.addVoidFn("println") { + for ((i, a) in args.withIndex()) { + if (i > 0) printed.append(' ') + printed.append(a.toString(this).value) + } + printed.append('\n') + } + val result = s.eval(code) + // Render with inspect for nice, user-facing representation + val text = try { + result.inspect(s) + } catch (_: Throwable) { + // Fallback if some object lacks inspect override + result.toString() + } + val combined = buildString { + if (printed.isNotEmpty()) append(printed) + // Always show the final expression value, like a REPL + if (isNotEmpty()) append('\n') + append(">>> ") + append(text) + } + output = combined + } catch (t: Throwable) { + // Show error, but also keep anything that has been printed so far + error = t.message ?: t.toString() + } finally { + running = false + } + } + } + + fun resetCode() { + code = """ + // Welcome to Lyng! Edit and run. + import lyng.stdlib + [1,2,3].map { it * 10 } + """.trimIndent() + output = null + error = null + } + + PageTemplate(title = "Try Lyng", showBack = true) { + // Intro + P({ classes("lead", "text-muted", "mb-3") }) { + Text("Type or paste Lyng code and run it right in your browser.") + } + + // Editor + Div({ classes("mb-3") }) { + Div({ classes("form-label", "fw-semibold") }) { Text("Code") } + TextArea(value = code, attrs = { + classes("form-control", "font-monospace") + attr("style", "min-height: 220px; tab-size: 2;") + placeholder("Write some Lyng code…") + onInput { ev -> code = ev.value } + onKeyDown { ev -> + val ctrlEnter = (ev.ctrlKey || ev.metaKey) && ev.key == "Enter" + if (ctrlEnter) { + ev.preventDefault() + runCode() + } + } + }) + } + + // Actions + Div({ classes("d-flex", "gap-2", "mb-3") }) { + Button(attrs = { + classes("btn", "btn-primary") + if (running) attr("disabled", "disabled") + onClick { + it.preventDefault() + runCode() + } + }) { + I({ classes("bi", "bi-play-fill", "me-1") }) + Text(if (running) "Running…" else "Run") + } + + Button(attrs = { + classes("btn", "btn-outline-secondary") + if (running) attr("disabled", "disabled") + onClick { + it.preventDefault() + resetCode() + } + }) { + I({ classes("bi", "bi-arrow-counterclockwise", "me-1") }) + Text("Reset") + } + } + + // Results + if (error != null) { + Div({ classes("alert", "alert-danger") }) { + I({ classes("bi", "bi-exclamation-triangle-fill", "me-2") }) + Span({ classes("fw-semibold", "me-1") }) { Text("Error:") } + Span { Text(" ${'$'}{error}") } + } + } + + if (output != null) { + Div({ classes("card", "mb-3") }) { + Div({ classes("card-header", "d-flex", "align-items-center", "gap-2") }) { + I({ classes("bi", "bi-terminal") }) + Span({ classes("fw-semibold") }) { Text("Result") } + } + Div({ classes("card-body", "bg-body-tertiary") }) { + Pre({ classes("mb-0") }) { Code { Text(output!!) } } + } + } + } + + // Tips + P({ classes("text-muted", "small") }) { + I({ classes("bi", "bi-info-circle", "me-1") }) + Text("Tip: press Ctrl+Enter (or ⌘+Enter on Mac) to run.") + } + } +}