lyng/site/src/jsMain/kotlin/ReferencePage.kt

182 lines
8.1 KiB
Kotlin

/*
* 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.browser.window
import kotlinx.coroutines.await
import net.sergeych.lyng.miniast.*
import org.jetbrains.compose.web.dom.*
@Composable
fun ReferencePage() {
var docs by remember { mutableStateOf<List<String>?>(null) }
var error by remember { mutableStateOf<String?>(null) }
// Titles resolved from the first H1 of each markdown document
var titles by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
// Load docs index once
LaunchedEffect(Unit) {
try {
val resp = window.fetch("docs-index.json").await()
if (!resp.ok) {
error = "Failed to load docs index (${resp.status})"
} else {
val text = resp.text().await()
// Simple JSON parse into dynamic array of strings
val arr = js("JSON.parse(text)") as Array<String>
docs = arr.toList()
}
} catch (t: Throwable) {
error = t.message
}
}
// Once we have the docs list, fetch their titles (H1) progressively
LaunchedEffect(docs) {
val list = docs ?: return@LaunchedEffect
// Reset titles when list changes
titles = emptyMap()
// Fetch sequentially to avoid flooding; fast enough for small/medium doc sets
for (path in list) {
try {
val mdPath = if (path.startsWith("docs/")) path else "docs/$path"
val url = "./" + encodeURI(mdPath)
val resp = window.fetch(url).await()
if (!resp.ok) continue
val text = resp.text().await()
val title = extractTitleFromMarkdown(text) ?: path.substringAfterLast('/')
// Update state incrementally
titles = titles + (path to title)
} catch (_: Throwable) {
// ignore individual failures; fallback will be filename
}
}
}
H2({ classes("h5", "mb-3") }) { Text("Reference") }
P({ classes("text-muted") }) { Text("Browse all documentation pages included in this build.") }
when {
error != null -> Div({ classes("alert", "alert-danger") }) { Text(error!!) }
docs == null -> P { Text("Loading index…") }
docs!!.isEmpty() -> P { Text("No documents found.") }
else -> {
Ul({ classes("list-group") }) {
docs!!.sorted().forEach { path ->
val displayTitle = titles[path] ?: path.substringAfterLast('/')
Li({ classes("list-group-item", "d-flex", "justify-content-between", "align-items-center") }) {
Div({}) {
A(attrs = {
classes("link-body-emphasis", "text-decoration-none")
attr("href", "#/$path")
}) { Text(displayTitle) }
Br()
Small({ classes("text-muted") }) { Text(path) }
}
I({ classes("bi", "bi-chevron-right") })
}
}
}
}
}
// Built-in APIs (from registry)
Hr()
H2({ classes("h5", "mb-3", "mt-4") }) { Text("Built-in APIs") }
val modules = remember { BuiltinDocRegistry.allModules().sorted() }
if (modules.isEmpty()) {
P({ classes("text-muted") }) { Text("No built-in modules registered.") }
} else {
modules.forEach { modName ->
val decls = BuiltinDocRegistry.docsForModule(modName)
if (decls.isEmpty()) return@forEach
H3({ classes("h6", "mt-3") }) { Text(modName) }
Ul({ classes("list-group", "mb-3") }) {
decls.forEach { d ->
Li({ classes("list-group-item") }) {
when (d) {
is MiniFunDecl -> {
val sig = signatureOf(d)
Div { Text("fun ${d.name}$sig") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
}
is MiniValDecl -> {
val kind = if (d.mutable) "var" else "val"
val t = typeOf(d.type)
Div { Text("$kind ${d.name}$t") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
}
is MiniClassDecl -> {
Div { Text("class ${d.name}") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
if (d.members.isNotEmpty()) {
Ul({ classes("mt-2") }) {
d.members.forEach { m ->
when (m) {
is MiniMemberFunDecl, -> {
val params = m.params.joinToString(", ") { p ->
val ts = typeOf(p.type)
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
}
val ret = typeOf(m.returnType)
val staticStr = if (m.isStatic) "static " else ""
Li { Text("${staticStr}method ${d.name}.${m.name}(${params})${ret}") }
}
is MiniInitDecl -> {
// we don't doc init {} blocks at all
}
is MiniMemberValDecl -> {
val ts = typeOf(m.type)
val kindM = if (m.mutable) "var" else "val"
val staticStr = if (m.isStatic) "static " else ""
Li { Text("${staticStr}${kindM} ${d.name}.${m.name}${ts}") }
}
}
}
}
}
}
}
}
}
}
}
}
}
// --- helpers (mirror IDE provider minimal renderers) ---
private fun typeOf(t: MiniTypeRef?): String = when (t) {
is MiniTypeName -> ": " + t.segments.joinToString(".") { it.name } + if (t.nullable) "?" else ""
is MiniGenericType -> {
val base = typeOf(t.base).removePrefix(": ")
val args = t.args.joinToString(", ") { typeOf(it).removePrefix(": ") }
": ${base}<${args}>" + if (t.nullable) "?" else ""
}
is MiniFunctionType -> ": (..) -> .." + if (t.nullable) "?" else ""
is MiniTypeVar -> ": ${t.name}" + if (t.nullable) "?" else ""
null -> ""
}
private fun signatureOf(fn: MiniFunDecl): String {
val params = fn.params.joinToString(", ") { p ->
val ts = typeOf(p.type)
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
}
val ret = typeOf(fn.returnType)
return "(${params})${ret}"
}