diff --git a/site/src/jsMain/kotlin/App.kt b/site/src/jsMain/kotlin/App.kt index 4e5cc18..31b1565 100644 --- a/site/src/jsMain/kotlin/App.kt +++ b/site/src/jsMain/kotlin/App.kt @@ -176,7 +176,7 @@ private fun TocNav( Ul({ classes("list-unstyled", "mb-0") }) { toc.forEach { item -> Li({ classes("mb-1") }) { - val pad = when (item.level) { 1 -> "0"; 2 -> "0.75rem"; else -> "1.5rem" } + val pad = "${(item.level - 1) * 0.75}rem" val routeNoFrag = route.substringBefore('#') val tocHref = "#/$routeNoFrag#${item.id}" A(attrs = { diff --git a/site/src/jsMain/kotlin/Main.kt b/site/src/jsMain/kotlin/Main.kt index 0b3430f..ffbe629 100644 --- a/site/src/jsMain/kotlin/Main.kt +++ b/site/src/jsMain/kotlin/Main.kt @@ -1016,17 +1016,21 @@ fun rewriteAnchors( } } +private data class TocTreeNode( + val headingLevel: Int, + val id: String, + val title: String, + val children: MutableList = mutableListOf(), +) + fun buildToc(root: HTMLElement): List { - val out = mutableListOf() val used = hashSetOf() - val hs = root.querySelectorAll("h1, h2, h3") + val tocRoot = TocTreeNode(headingLevel = 0, id = "", title = "") + val stack = mutableListOf(tocRoot) + val hs = root.querySelectorAll("h1, h2, h3, h4, h5, h6") for (i in 0 until hs.length) { val h = hs.item(i) as? HTMLHeadingElement ?: continue - val level = when (h.tagName.uppercase()) { - "H1" -> 1 - "H2" -> 2 - else -> 3 - } + val level = h.tagName.removePrefix("H").toIntOrNull() ?: continue var id = h.id.ifBlank { slugify(h.textContent ?: "") } if (id.isBlank()) id = "section-${i + 1}" var unique = id @@ -1036,8 +1040,19 @@ fun buildToc(root: HTMLElement): List { n++ } h.id = unique - out += TocItem(level, unique, h.textContent ?: "") + + while (stack.last().headingLevel >= level) stack.removeAt(stack.lastIndex) + val node = TocTreeNode(level, unique, h.textContent ?: "") + stack.last().children += node + stack += node } + + val out = mutableListOf() + fun visit(node: TocTreeNode, normalizedLevel: Int) { + if (node.id.isNotEmpty()) out += TocItem(normalizedLevel, node.id, node.title) + node.children.forEach { child -> visit(child, normalizedLevel + 1) } + } + tocRoot.children.forEach { child -> visit(child, 1) } return out } diff --git a/site/src/jsTest/kotlin/RouteAndDomRewriteTest.kt b/site/src/jsTest/kotlin/RouteAndDomRewriteTest.kt index 4177c94..cb9ebb5 100644 --- a/site/src/jsTest/kotlin/RouteAndDomRewriteTest.kt +++ b/site/src/jsTest/kotlin/RouteAndDomRewriteTest.kt @@ -71,5 +71,37 @@ class RouteAndDomRewriteTest { val el = root.ownerDocument?.getElementById(id) ?: root.querySelector("#${id}") assertNotNull(el, "Heading with id $id should be present in DOM") } + + assertEquals(listOf(1, 2, 2, 3), toc.map { it.level }) + } + + @Test + fun testBuildToc_normalizesStartingLevelAndGaps() { + val root = document.createElement("div") as HTMLElement + root.innerHTML = """ +

Top

+
Deep child
+

Middle sibling

+
Nested
+ """.trimIndent() + + val toc = buildToc(root) + + assertEquals(listOf("Top", "Deep child", "Middle sibling", "Nested"), toc.map { it.title }) + assertEquals(listOf(1, 2, 2, 3), toc.map { it.level }) + } + + @Test + fun testBuildToc_supportsAllHeadingLevels() { + val root = document.createElement("div") as HTMLElement + root.innerHTML = """ +

Start

+
Child
+
Grandchild
+ """.trimIndent() + + val toc = buildToc(root) + + assertEquals(listOf(1, 2, 3), toc.map { it.level }) } }