footnotes support (as notes)

This commit is contained in:
sergeych committed 2026-05-24 11:05:19 +03:00
1 parent 50f201d3c6
commit 8e7dcc0307
5 files changed
+270 -11

No files matched your search

@@ -17,6 +17,7 @@ data class Fb2Book(
val bodyImages: List<Fb2ImageRef> = emptyList(),
val bodyEpigraphs: List<Fb2Epigraph> = emptyList(),
val sections: List<Fb2Section> = emptyList(),
val notes: Map<String, Fb2Section> = emptyMap(),
val binaries: List<Fb2Binary> = emptyList(),
) {
fun binaryFor(image: Fb2ImageRef): Fb2Binary? =
@@ -47,6 +48,7 @@ data class Fb2DocumentInfo(
)
data class Fb2Section(
val id: String? = null,
val title: String? = null,
val paragraphs: List<String> = emptyList(),
val images: List<Fb2ImageRef> = emptyList(),
@@ -124,6 +126,8 @@ data class Fb2Text(
data class Fb2TextSpan(
val text: String,
val styles: Set<Fb2TextStyle> = emptySet(),
val href: String? = null,
val linkType: String? = null,
)
enum class Fb2TextStyle {
@@ -13,8 +13,15 @@ internal object Fb2XmlMapper {
?: throw Fb2ParseException("FB2 document is missing description/title-info")
val documentInfo = description.first("document-info")
val body = root.children("body").firstOrNull { it.attributes["name"] != "notes" }
val bodies = root.children("body")
val body = bodies.firstOrNull { it.attributes["name"] != "notes" }
?: root.first("body")
val notes = bodies
.filter { it.attributes["name"] == "notes" }
.flatMap { it.children("section") }
.map(::sectionFrom)
.mapNotNull { section -> section.id?.let { it to section } }
.toMap()
return Fb2Book(
title = titleInfo.firstText("book-title")
@@ -34,6 +41,7 @@ internal object Fb2XmlMapper {
bodyImages = body?.children("image")?.mapNotNull(::imageRefFrom).orEmpty(),
bodyEpigraphs = body?.children("epigraph")?.map(::epigraphFrom).orEmpty(),
sections = body?.children("section")?.map(::sectionFrom).orEmpty(),
notes = notes,
binaries = root.children("binary").mapNotNull(::binaryFrom),
)
}
@@ -135,6 +143,7 @@ internal object Fb2XmlMapper {
}
return Fb2Section(
id = element.attributes["id"]?.takeIf { it.isNotBlank() },
title = element.first("title")?.text()?.ifBlank { null },
paragraphs = blocks.filterIsInstance<Fb2Block.Paragraph>()
.mapNotNull { it.content.plainText.ifBlank { null } },
@@ -203,11 +212,23 @@ internal object Fb2XmlMapper {
private fun textFrom(element: XmlElement): Fb2Text =
Fb2Text(spansFrom(element.nodes).mergeAdjacent().trimBoundaryWhitespace())
private fun spansFrom(nodes: List<XmlNode>, styles: Set<Fb2TextStyle> = emptySet()): List<Fb2TextSpan> =
private fun spansFrom(
nodes: List<XmlNode>,
styles: Set<Fb2TextStyle> = emptySet(),
href: String? = null,
linkType: String? = null,
): List<Fb2TextSpan> =
nodes.flatMap { node ->
when (node) {
is XmlNode.TextNode -> listOfNotNull(
node.text.takeIf { it.isNotEmpty() }?.let { Fb2TextSpan(it, styles) }
node.text.takeIf { it.isNotEmpty() }?.let {
Fb2TextSpan(
text = it,
styles = styles,
href = href,
linkType = linkType,
)
}
)
is XmlNode.ElementNode -> {
val element = node.element
@@ -220,7 +241,13 @@ internal object Fb2XmlMapper {
"sub" -> styles + Fb2TextStyle.Subscript
else -> styles
}
spansFrom(element.nodes, nestedStyles)
val nestedHref = element.attributeLocal("href")
?.takeIf { element.localName == "a" && it.isNotBlank() }
?: href
val nestedLinkType = element.attributes["type"]
?.takeIf { element.localName == "a" && it.isNotBlank() }
?: linkType
spansFrom(element.nodes, nestedStyles, nestedHref, nestedLinkType)
}
}
}
@@ -230,7 +257,11 @@ internal object Fb2XmlMapper {
val merged = mutableListOf<Fb2TextSpan>()
forEach { span ->
val previous = merged.lastOrNull()
if (previous != null && previous.styles == span.styles) {
if (previous != null &&
previous.styles == span.styles &&
previous.href == span.href &&
previous.linkType == span.linkType
) {
merged[merged.lastIndex] = previous.copy(text = previous.text + span.text)
} else {
merged += span