Coverage Summary for Class: OutlineKt (io.heapy.kotmark.ext.toc.internal)

Class Method, % Branch, % Line, % Instruction, %
OutlineKt 100% (4/4) 96% (48/50) 100% (45/45) 98.8% (342/346)
OutlineKt$documentNodes$1 100% (1/1) 100% (12/12) 100% (10/10) 100% (64/64)
Total 100% (5/5) 96.8% (60/62) 100% (55/55) 99% (406/410)


 package io.heapy.kotmark.ext.toc.internal
 
 import io.heapy.kotmark.node.BlockQuote
 import io.heapy.kotmark.node.BulletList
 import io.heapy.kotmark.node.Code
 import io.heapy.kotmark.node.Document
 import io.heapy.kotmark.node.HardLineBreak
 import io.heapy.kotmark.node.Heading
 import io.heapy.kotmark.node.Link
 import io.heapy.kotmark.node.ListBlock
 import io.heapy.kotmark.node.ListItem
 import io.heapy.kotmark.node.Node
 import io.heapy.kotmark.node.Paragraph
 import io.heapy.kotmark.node.SoftLineBreak
 import io.heapy.kotmark.node.Text
 
 /** Walk standard block containers iteratively. Custom blocks may hide or defer their children. */
 internal fun documentNodes(root: Node): Sequence<Node> = sequence {
     val pending = ArrayDeque<Node>()
     pending.add(root)
     while (pending.isNotEmpty()) {
         val node = pending.removeLast()
         yield(node)
         if (node is Document || node is BlockQuote || node is ListBlock || node is ListItem) {
             var child = node.lastChild
             while (child != null) {
                 pending.add(child)
                 child = child.previous
             }
         }
     }
 }
 
 internal fun headingText(heading: Heading): String = buildString {
     val pending = ArrayDeque<Node>()
     pending.add(heading)
     while (pending.isNotEmpty()) {
         val node = pending.removeLast()
         when (node) {
             is Text -> append(node.literal)
             is Code -> append(node.literal)
             is SoftLineBreak, is HardLineBreak -> append(' ')
         }
         var child = node.lastChild
         while (child != null) {
             pending.add(child)
             child = child.previous
         }
     }
 }.trim()
 
 internal data class Entry(val level: Int, val label: String, val destination: String? = null)
 
 /** The nearest preceding lower-level entry is the parent; skipped levels create no empty items. */
 internal fun outline(entries: List<Entry>): BulletList {
     val root = BulletList().apply { isTight = true; marker = "-" }
     val ancestors = mutableListOf<Pair<Int, ListItem>>()
     for (entry in entries) {
         while (ancestors.isNotEmpty() && ancestors.last().first >= entry.level) ancestors.removeAt(ancestors.lastIndex)
         val parent = ancestors.lastOrNull()?.second
         val list = if (parent == null) root else {
             parent.lastChild as? BulletList ?: BulletList().apply {
                 isTight = true
                 marker = "-"
                 parent.appendChild(this)
             }
         }
         val item = ListItem()
         val paragraph = Paragraph()
         val text = Text(entry.label)
         if (entry.destination == null) paragraph.appendChild(text) else {
             paragraph.appendChild(Link(entry.destination, null).apply { appendChild(text) })
         }
         item.appendChild(paragraph)
         list.appendChild(item)
         ancestors.add(entry.level to item)
     }
     return root
 }
 
 /** Encode a literal ID as a URI fragment, including percent signs and fragment delimiters. */
 internal fun fragment(id: String): String = buildString {
     append('#')
     for (byte in id.encodeToByteArray()) {
         val value = byte.toInt() and 0xff
         val char = value.toChar()
         if (char in 'a'..'z' || char in 'A'..'Z' || char in '0'..'9' || char in "-._~") append(char) else {
             append('%')
             append("0123456789ABCDEF"[value shr 4])
             append("0123456789ABCDEF"[value and 15])
         }
     }
 }