From d307ed2a04874e103aab76fdb79a73581594c461 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 19 Nov 2025 23:56:31 +0100 Subject: [PATCH] inject back button inline for docs' H1 headers, fetch markdown titles progressively, and add deployment script --- bin/deploy_site | 111 +++++++++++++++++++++++++++++++++ site/src/jsMain/kotlin/Main.kt | 74 +++++++++++++++++++++- 2 files changed, 183 insertions(+), 2 deletions(-) create mode 100755 bin/deploy_site diff --git a/bin/deploy_site b/bin/deploy_site new file mode 100755 index 0000000..6fc4342 --- /dev/null +++ b/bin/deploy_site @@ -0,0 +1,111 @@ +#!/bin/bash + +# +# 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. +# +# + +function checkState() { + if [[ $? != 0 ]]; then + echo + echo -- rsync failed. deploy was not finished. deployed version has not been affected + echo + exit 100 + fi + +} + +# default target settings +case $1 in + com) + SSH_HOST=sergeych@lynglang.com # host to deploy to + SSH_PORT=22 # ssh port on it + ROOT=/bigstore/sergeych_pub/lyng # directory to rsync to + ;; +# com) +# SSH_HOST=vvk@front-01.neurodatalab.com +# ROOT=/home/vvk +# ;; + *) + echo "*** ERROR: target not specified (use deploy com | dev)" + echo "*** stop" + exit 101 +esac + +die() { echo "ERROR: $*" 1>&2 ; exit 1; } +#rm build/distributions/*.js > /dev/null +#rm build/distributions/*.js.map > /dev/null + +#./gradlew clean incrementRevision jsBrowserDistribution || die "compilation failed" +#./gradlew incrementRevision jsBrowserDistribution +#./gradlew jsBrowserDistribution +./gradlew incrementRevision jsBrowserDistribution +#./gradlew jsBrowserDistribution + +if [[ $? != 0 ]]; then + echo + echo -- build failed. deploy aborted. + echo + exit 100 +fi + +#exit 0 + +# Prepare working dir +ssh -p ${SSH_PORT} ${SSH_HOST} " + cd ${ROOT} + rm -rd build 2>/dev/null + if [ -d release ]; then + echo copying current release + cp -r release build + else + echo creating first release + mkdir release + mkdir build + fi +"; + +# sync files +SRC=./build/dist/js/productionExecutable +rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete ${SRC}/* ${SSH_HOST}:${ROOT}/build/dist +checkState +rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist +#checkState +#rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete private_data/* ${SSH_HOST}:${ROOT}/build/private_data +#checkState + +echo +echo finalizing the deploy... +ssh -p ${SSH_PORT} ${SSH_HOST} " + cd ${ROOT} + rm -rd release~ + mv release release~ + mv build release + cd release + # in this project we needn't restart back when we deploy the front + # ~/bin/restart_service +"; + +if [[ $? != 0 ]]; then + echo + echo -- finalization failed. the rease might be corrupt. rolling back is not yet implemented. + echo + exit 100 +fi + +echo +echo Deploy successful +echo + diff --git a/site/src/jsMain/kotlin/Main.kt b/site/src/jsMain/kotlin/Main.kt index 2762363..85ccd97 100644 --- a/site/src/jsMain/kotlin/Main.kt +++ b/site/src/jsMain/kotlin/Main.kt @@ -371,7 +371,9 @@ private fun DocsPage( } } - PageTemplate(title = title, showBack = true) { + // Do not render a separate header here. We will show the markdown's own H1 + // and inject a back button inline to that H1 after the content mounts. + PageTemplate(title = null, showBack = false) { if (error != null) { Div({ classes("alert", "alert-danger") }) { Text(error) } } else if (html == null) { @@ -395,6 +397,31 @@ private fun DocsPage( val el = contentEl ?: return@LaunchedEffect if (html == null) return@LaunchedEffect window.requestAnimationFrame { + // Insert an inline back button to the left of the first H1 + try { + val firstH1 = el.querySelector("h1") as? HTMLElement + if (firstH1 != null && firstH1.querySelector(".doc-back-btn") == null) { + val back = el.ownerDocument!!.createElement("div") as HTMLDivElement + back.className = "btn btn-outline btn-sm me-2 align-middle doc-back-btn " + back.setAttribute("aria-label","Back") + back.onclick = { ev -> + ev.preventDefault() + try { + if (window.history.length > 1) window.history.back() else window.location.hash = "#" + } catch (e: dynamic) { + window.location.hash = "#" + } + null + } + val icon = el.ownerDocument!!.createElement("i") as HTMLElement + icon.className = "bi bi-arrow-left" + back.appendChild(icon) + // Insert at the start of H1 content + firstH1.insertBefore(back, firstH1.firstChild) + } + } catch (_: Throwable) { + // best-effort; ignore DOM issues + } val currentPath = routeToPath(route) // without fragment val basePath = currentPath.substringBeforeLast('/', "docs") rewriteImages(el, basePath) @@ -872,6 +899,8 @@ private object MainScopeProvider { private fun ReferencePage() { var docs by remember { mutableStateOf?>(null) } var error by remember { mutableStateOf(null) } + // Titles resolved from the first H1 of each markdown document + var titles by remember { mutableStateOf>(emptyMap()) } // Load docs index once LaunchedEffect(Unit) { @@ -890,6 +919,28 @@ private fun ReferencePage() { } } + // 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.") } @@ -897,7 +948,24 @@ private fun ReferencePage() { error != null -> Div({ classes("alert", "alert-danger") }) { Text(error!!) } docs == null -> P { Text("Loading index…") } docs!!.isEmpty() -> P { Text("No documents found.") } - else -> UnsafeRawHtml(renderReferenceListHtml(docs!!)) + 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") }) + } + } + } + } } } @@ -1316,6 +1384,8 @@ internal fun rewriteAnchors( val asEl = root.querySelectorAll("a") for (i in 0 until asEl.length) { val a = asEl.item(i) as? HTMLAnchorElement ?: continue + // Skip the inline Docs back button we inject before the first H1 + if (a.classList.contains("doc-back-btn") || a.getAttribute("data-no-spa") == "true") continue val href = a.getAttribute("href") ?: continue // Skip external and already-SPA hashes if (