Current scope: kotmark-renderer-kotlinx-html| all classes
|
io.heapy.kotmark.renderer.kotlinxhtml.internal
Coverage Summary for Class: FootnoteNodeRenderer (io.heapy.kotmark.renderer.kotlinxhtml.internal)
| Class | Method, % | Branch, % | Line, % | Instruction, % |
|---|---|---|---|---|
| FootnoteNodeRenderer | 94.1% (16/17) | 78.6% (33/42) | 87.1% (54/62) | 91.9% (544/592) |
| FootnoteNodeRenderer$afterRoot$1 | 100% (2/2) | 80% (8/10) | 81.8% (9/11) | 75.3% (58/77) |
| FootnoteNodeRenderer$beforeRoot$1 | 100% (2/2) | 50% (1/2) | 66.7% (2/3) | 83.3% (15/18) |
| FootnoteNodeRenderer$ReferencedDefinition | 100% (1/1) | 100% (1/1) | 100% (31/31) | |
| FootnoteNodeRenderer$ReferenceInfo | 100% (1/1) | 100% (1/1) | 100% (17/17) | |
| Total | 95.7% (22/23) | 77.8% (42/54) | 85.9% (67/78) | 90.5% (665/735) |
package io.heapy.kotmark.renderer.kotlinxhtml.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.kotlinxhtml.KotlinxHtmlNodeRendererContext
/** Mirrors Kotmark's footnote numbering/backlink contract using structured HTML events. */
internal class FootnoteNodeRenderer(private val context: KotlinxHtmlNodeRendererContext) : NodeRenderer {
private val definitions = DefinitionMap(FootnoteDefinition::class)
private val referenced = linkedMapOf<Node, ReferencedDefinition>()
private val references = mutableMapOf<Node, ReferenceInfo>()
override val nodeTypes = setOf(FootnoteReference::class, InlineFootnote::class, FootnoteDefinition::class)
override fun beforeRoot(rootNode: Node) {
rootNode.accept(object : AbstractVisitor() {
override fun visit(customBlock: CustomBlock) {
if (customBlock is FootnoteDefinition) definitions.putIfAbsent(customBlock.label, customBlock)
else super.visit(customBlock)
}
})
}
override fun render(node: Node) {
val info = when (node) {
is FootnoteReference -> references[node] ?: definitions.get(node.label)?.let { register(it, it.label) }
is InlineFootnote -> references[node] ?: register(node, null)
else -> return
}
if (info == null) {
context.text("[^${(node as FootnoteReference).label}]")
return
}
context.element(node, "sup", mapOf("class" to "footnote-ref")) {
context.element(node, "a", linkedMapOf("href" to "#${info.definitionId}", "id" to info.id, "data-footnote-ref" to "")) {
context.text(info.number.toString())
}
}
}
override fun afterRoot(rootNode: Node) {
if (referenced.isEmpty()) return
// Discover references inside definitions before rendering backlinks. This also
// makes repeated/self/cyclic references finite and gives breadth-first numbering.
val queue = referenced.keys.toMutableList()
var index = 0
while (index < queue.size) {
val definition = queue[index++]
definition.accept(object : AbstractVisitor() {
override fun visit(customNode: CustomNode) {
when (customNode) {
is FootnoteReference -> {
val target = definitions.get(customNode.label) ?: return
if (target !in referenced) queue.add(target)
references[customNode] = register(target, target.label)
}
is InlineFootnote -> if (customNode === definition) {
super.visit(customNode)
} else {
queue.add(customNode)
references[customNode] = register(customNode, null)
}
else -> super.visit(customNode)
}
}
})
}
val first = referenced.keys.first()
context.element(first, "section", linkedMapOf("class" to "footnotes", "data-footnotes" to "")) {
context.element(first, "ol") {
for ([definition, info] in referenced) renderDefinition(definition, info)
}
}
}
private fun register(node: Node, label: String?): ReferenceInfo {
val entry = referenced.getOrPut(node) {
val number = referenced.size + 1
ReferencedDefinition(number, if (label == null) number.toString() else "-$label")
}
val count = entry.references.size + 1
val id = "fnref${entry.key}${if (count == 1) "" else "-$count"}"
entry.references.add(id)
return ReferenceInfo(id, "fn${entry.key}", entry.number)
}
private fun renderDefinition(node: Node, info: ReferencedDefinition) {
context.element(node, "li", mapOf("id" to "fn${info.key}")) {
val lastParagraph = node.lastChild as? Paragraph
when {
lastParagraph != null -> {
var child = node.firstChild
while (child != null && child !== lastParagraph) {
val current = child
child = current.next
if (current is Paragraph) {
context.element(current, "p") { context.renderChildren(current) }
} else context.render(current)
}
context.element(lastParagraph, "p") {
context.renderChildren(lastParagraph)
context.text(" ")
backrefs(node, info)
}
}
node is InlineFootnote -> context.element(node, "p") {
context.renderChildren(node)
context.text(" ")
backrefs(node, info)
}
else -> {
context.renderChildren(node)
backrefs(node, info)
}
}
}
}
private fun backrefs(node: Node, info: ReferencedDefinition) {
for ([index, reference] in info.references.withIndex()) {
val count = index + 1
val backlinkIndex = "${info.number}${if (count > 1) "-$count" else ""}"
val attrs = linkedMapOf(
"href" to "#$reference", "class" to "footnote-backref", "data-footnote-backref" to "",
"data-footnote-backref-idx" to backlinkIndex, "aria-label" to "Back to reference $backlinkIndex",
)
context.element(node, "a", attrs) {
if (count > 1) context.element(node, "sup", mapOf("class" to "footnote-ref")) { context.text(count.toString()) }
context.text("\u21A9")
}
if (count < info.references.size) context.text(" ")
}
}
private class ReferencedDefinition(val number: Int, val key: String, val references: MutableList<String> = mutableListOf())
private class ReferenceInfo(val id: String, val definitionId: String, val number: Int)
}