Coverage Summary for Class: FootnoteHtmlNodeRenderer (io.heapy.kotmark.ext.footnotes.internal)

Class Method, % Branch, % Line, % Instruction, %
FootnoteHtmlNodeRenderer 100% (15/15) 78.8% (41/52) 90.7% (147/162) 92% (723/786)
FootnoteHtmlNodeRenderer$DefinitionVisitor 100% (2/2) 50% (1/2) 80% (4/5) 88.5% (23/26)
FootnoteHtmlNodeRenderer$ReferencedDefinition 100% (1/1) 100% (3/3) 100% (20/20)
FootnoteHtmlNodeRenderer$ReferenceInfo 100% (1/1) 100% (4/4) 100% (17/17)
FootnoteHtmlNodeRenderer$ShallowReferenceVisitor 100% (2/2) 75% (6/8) 87.5% (7/8) 92.9% (39/42)
Total 100% (21/21) 77.4% (48/62) 90.7% (165/182) 92.3% (822/891)


 package io.heapy.kotmark.ext.footnotes.internal
 
 import io.heapy.kotmark.ext.footnotes.FootnoteDefinition
 import io.heapy.kotmark.ext.footnotes.FootnoteReference
 import io.heapy.kotmark.ext.footnotes.InlineFootnote
 import io.heapy.kotmark.node.AbstractVisitor
 import io.heapy.kotmark.node.CustomBlock
 import io.heapy.kotmark.node.CustomNode
 import io.heapy.kotmark.node.DefinitionMap
 import io.heapy.kotmark.node.Node
 import io.heapy.kotmark.node.Paragraph
 import io.heapy.kotmark.renderer.NodeRenderer
 import io.heapy.kotmark.renderer.html.HtmlNodeRendererContext
 import kotlin.reflect.KClass
 
 class FootnoteHtmlNodeRenderer(
     private val context: HtmlNodeRendererContext,
 ) : NodeRenderer {
     private val html = context.writer
     private lateinit var definitionMap: DefinitionMap<FootnoteDefinition>
     private val referencedDefinitions = linkedMapOf<Node, ReferencedDefinition>()
     private val references = mutableMapOf<Node, ReferenceInfo>()
 
     override val nodeTypes: Set<KClass<out Node>> =
         setOf(FootnoteReference::class, InlineFootnote::class, FootnoteDefinition::class)
 
     override fun beforeRoot(rootNode: Node) {
         val visitor = DefinitionVisitor()
         rootNode.accept(visitor)
         definitionMap = visitor.definitions
     }
 
     override fun render(node: Node) {
         when (node) {
             is FootnoteReference -> {
                 val info = references[node] ?: tryRegisterReference(node)
                 if (info != null) {
                     renderReference(node, info)
                 } else {
                     html.text("[^${node.label}]")
                 }
             }
             is InlineFootnote -> {
                 val info = references[node] ?: registerReference(node, null)
                 renderReference(node, info)
             }
         }
     }
 
     override fun afterRoot(rootNode: Node) {
         if (referencedDefinitions.isEmpty()) {
             return
         }
 
         val firstDefinition = referencedDefinitions.keys.first()
         html.tag(
             "section",
             context.extendAttributes(
                 firstDefinition,
                 "section",
                 linkedMapOf("class" to "footnotes", "data-footnotes" to null),
             ),
         )
         html.line()
         html.tag("ol")
         html.line()
 
         val queue = referencedDefinitions.keys.toMutableList()
         var queueIndex = 0
         while (queueIndex < queue.size) {
             val definition = queue[queueIndex++]
             definition.accept(
                 ShallowReferenceVisitor(definition) { reference ->
                     when (reference) {
                         is FootnoteReference -> {
                             val nestedDefinition = definitionMap.get(reference.label)
                             if (nestedDefinition != null) {
                                 if (nestedDefinition !in referencedDefinitions) {
                                     queue.add(nestedDefinition)
                                 }
                                 references[reference] =
                                     registerReference(nestedDefinition, nestedDefinition.label)
                             }
                         }
                         is InlineFootnote -> {
                             queue.add(reference)
                             references[reference] = registerReference(reference, null)
                         }
                     }
                 },
             )
         }
 
         for ([definition, referencedDefinition] in referencedDefinitions) {
             renderDefinition(definition, referencedDefinition)
         }
         html.tag("/ol")
         html.line()
         html.tag("/section")
         html.line()
     }
 
     private fun tryRegisterReference(reference: FootnoteReference): ReferenceInfo? {
         val definition = definitionMap.get(reference.label) ?: return null
         return registerReference(definition, definition.label)
     }
 
     private fun registerReference(node: Node, label: String?): ReferenceInfo {
         val referencedDefinition =
             referencedDefinitions.getOrPut(node) {
                 val number = referencedDefinitions.size + 1
                 ReferencedDefinition(number, definitionKey(label, number))
             }
         val referenceNumber = referencedDefinition.references.size + 1
         val id = referenceId(referencedDefinition.definitionKey, referenceNumber)
         referencedDefinition.references.add(id)
         return ReferenceInfo(
             id,
             definitionId(referencedDefinition.definitionKey),
             referencedDefinition.definitionNumber,
         )
     }
 
     private fun renderReference(node: Node, info: ReferenceInfo) {
         html.tag(
             "sup",
             context.extendAttributes(node, "sup", mapOf("class" to "footnote-ref")),
         )
         html.tag(
             "a",
             context.extendAttributes(
                 node,
                 "a",
                 linkedMapOf(
                     "href" to "#${info.definitionId}",
                     "id" to info.id,
                     "data-footnote-ref" to null,
                 ),
             ),
         )
         html.raw(info.definitionNumber.toString())
         html.tag("/a")
         html.tag("/sup")
     }
 
     private fun renderDefinition(
         definition: Node,
         referencedDefinition: ReferencedDefinition,
     ) {
         html.tag(
             "li",
             context.extendAttributes(
                 definition,
                 "li",
                 mapOf("id" to definitionId(referencedDefinition.definitionKey)),
             ),
         )
         html.line()
         val lastParagraph = definition.lastChild as? Paragraph
         if (lastParagraph != null) {
             var child = definition.firstChild
             while (child != lastParagraph) {
                 val current = child ?: break
                 val next = current.next
                 if (current is Paragraph) {
                     html.tag(
                         "p",
                         context.extendAttributes(current, "p", emptyMap()),
                     )
                     renderChildren(current)
                     html.tag("/p")
                     html.line()
                 } else {
                     context.render(current)
                 }
                 child = next
             }
             html.tag(
                 "p",
                 context.extendAttributes(lastParagraph, "p", emptyMap()),
             )
             renderChildren(lastParagraph)
             html.raw(" ")
             renderBackrefs(definition, referencedDefinition)
             html.tag("/p")
             html.line()
         } else if (definition is InlineFootnote) {
             html.tag(
                 "p",
                 context.extendAttributes(definition, "p", emptyMap()),
             )
             renderChildren(definition)
             html.raw(" ")
             renderBackrefs(definition, referencedDefinition)
             html.tag("/p")
             html.line()
         } else {
             renderChildren(definition)
             html.line()
             renderBackrefs(definition, referencedDefinition)
         }
         html.tag("/li")
         html.line()
     }
 
     private fun renderBackrefs(
         definition: Node,
         referencedDefinition: ReferencedDefinition,
     ) {
         for ([index, reference] in referencedDefinition.references.withIndex()) {
             val referenceNumber = index + 1
             val backlinkIndex =
                 referencedDefinition.definitionNumber.toString() +
                     if (referenceNumber > 1) "-$referenceNumber" else ""
             html.tag(
                 "a",
                 context.extendAttributes(
                     definition,
                     "a",
                     linkedMapOf(
                         "href" to "#$reference",
                         "class" to "footnote-backref",
                         "data-footnote-backref" to null,
                         "data-footnote-backref-idx" to backlinkIndex,
                         "aria-label" to "Back to reference $backlinkIndex",
                     ),
                 ),
             )
             if (referenceNumber > 1) {
                 html.tag(
                     "sup",
                     context.extendAttributes(
                         definition,
                         "sup",
                         mapOf("class" to "footnote-ref"),
                     ),
                 )
                 html.raw(referenceNumber.toString())
                 html.tag("/sup")
             }
             html.raw("\u21A9")
             html.tag("/a")
             if (index + 1 < referencedDefinition.references.size) {
                 html.raw(" ")
             }
         }
     }
 
     private fun renderChildren(parent: Node) {
         var child = parent.firstChild
         while (child != null) {
             val next = child.next
             context.render(child)
             child = next
         }
     }
 
     private fun referenceId(definitionKey: String, number: Int): String =
         "fnref$definitionKey${if (number == 1) "" else "-$number"}"
 
     private fun definitionKey(label: String?, number: Int): String =
         if (label != null) "-$label" else number.toString()
 
     private fun definitionId(definitionKey: String): String = "fn$definitionKey"
 
     private class DefinitionVisitor : AbstractVisitor() {
         val definitions = DefinitionMap(FootnoteDefinition::class)
 
         override fun visit(customBlock: CustomBlock) {
             if (customBlock is FootnoteDefinition) {
                 definitions.putIfAbsent(customBlock.label, customBlock)
             } else {
                 super.visit(customBlock)
             }
         }
     }
 
     private class ShallowReferenceVisitor(
         private val parent: Node,
         private val consumer: (Node) -> Unit,
     ) : AbstractVisitor() {
         override fun visit(customNode: CustomNode) {
             when {
                 customNode is FootnoteReference -> consumer(customNode)
                 customNode is InlineFootnote && customNode == parent -> super.visit(customNode)
                 customNode is InlineFootnote -> consumer(customNode)
                 else -> super.visit(customNode)
             }
         }
     }
 
     private class ReferencedDefinition(
         val definitionNumber: Int,
         val definitionKey: String,
         val references: MutableList<String> = mutableListOf(),
     )
 
     private class ReferenceInfo(
         val id: String,
         val definitionId: String,
         val definitionNumber: Int,
     )
 }