footnotes support (as notes)
This commit is contained in:
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
|
||||
|
||||
@@ -116,6 +116,18 @@ class Fb2FormatTest {
|
||||
assertEquals("Poem quote", (poemEpigraph.blocks.single() as Fb2EpigraphBlock.Paragraph).content.plainText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesNoteLinksAndNotesBody() {
|
||||
val book = Fb2Format.parseXml(notesXml)
|
||||
val paragraph = book.sections.single().blocks.single() as Fb2Block.Paragraph
|
||||
val link = paragraph.content.spans.single { it.href != null }
|
||||
|
||||
assertEquals("[1]", link.text)
|
||||
assertEquals("#n_1", link.href)
|
||||
assertEquals("note", link.linkType)
|
||||
assertEquals("Note text.", (book.notes.getValue("n_1").blocks.single() as Fb2Block.Paragraph).content.plainText)
|
||||
}
|
||||
|
||||
private val sampleXml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
@@ -261,6 +273,35 @@ class Fb2FormatTest {
|
||||
</FictionBook>
|
||||
""".trimIndent()
|
||||
|
||||
private val notesXml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">
|
||||
<description>
|
||||
<title-info>
|
||||
<author><nickname>A</nickname></author>
|
||||
<book-title>Notes</book-title>
|
||||
<lang>en</lang>
|
||||
</title-info>
|
||||
<document-info>
|
||||
<author><nickname>Toread</nickname></author>
|
||||
<date>2026-05-12</date>
|
||||
<id>notes</id>
|
||||
<version>1.0</version>
|
||||
</document-info>
|
||||
</description>
|
||||
<body>
|
||||
<section>
|
||||
<p>Main<a l:href="#n_1" type="note">[1]</a>.</p>
|
||||
</section>
|
||||
</body>
|
||||
<body name="notes">
|
||||
<section id="n_1">
|
||||
<p>Note text.</p>
|
||||
</section>
|
||||
</body>
|
||||
</FictionBook>
|
||||
""".trimIndent()
|
||||
|
||||
private fun String.encodeWindows1251(): ByteArray =
|
||||
ByteArray(length) { index ->
|
||||
val char = this[index]
|
||||
|
||||
Reference in new issue
Block a user