support for poetry and epigraphs

This commit is contained in:
sergeych committed 2026-05-24 09:31:32 +03:00
1 parent 316b10d015
commit de04b9cbed
11 files changed
+787 -10

No files matched your search

@@ -15,6 +15,7 @@ data class Fb2Book(
val documentInfo: Fb2DocumentInfo = Fb2DocumentInfo(),
val bodyTitle: List<String> = emptyList(),
val bodyImages: List<Fb2ImageRef> = emptyList(),
val bodyEpigraphs: List<Fb2Epigraph> = emptyList(),
val sections: List<Fb2Section> = emptyList(),
val binaries: List<Fb2Binary> = emptyList(),
) {
@@ -69,10 +70,50 @@ data class Fb2Binary(
sealed interface Fb2Block {
data class Paragraph(val content: Fb2Text) : Fb2Block
data class Subtitle(val content: Fb2Text) : Fb2Block
data class Epigraph(val epigraph: Fb2Epigraph) : Fb2Block
data class Cite(val cite: Fb2Cite) : Fb2Block
data class Poem(val poem: Fb2Poem) : Fb2Block
data class Image(val image: Fb2ImageRef) : Fb2Block
data object EmptyLine : Fb2Block
}
data class Fb2Epigraph(
val blocks: List<Fb2EpigraphBlock> = emptyList(),
val textAuthors: List<Fb2Text> = emptyList(),
)
data class Fb2Cite(
val blocks: List<Fb2EpigraphBlock> = emptyList(),
val textAuthors: List<Fb2Text> = emptyList(),
)
sealed interface Fb2EpigraphBlock {
data class Paragraph(val content: Fb2Text) : Fb2EpigraphBlock
data class Subtitle(val content: Fb2Text) : Fb2EpigraphBlock
data class Poem(val poem: Fb2Poem) : Fb2EpigraphBlock
data class Cite(val cite: Fb2Cite) : Fb2EpigraphBlock
data object EmptyLine : Fb2EpigraphBlock
}
data class Fb2Poem(
val title: List<Fb2Text> = emptyList(),
val epigraphs: List<Fb2Epigraph> = emptyList(),
val blocks: List<Fb2PoemBlock> = emptyList(),
val textAuthors: List<Fb2Text> = emptyList(),
val date: String? = null,
)
sealed interface Fb2PoemBlock {
data class Subtitle(val content: Fb2Text) : Fb2PoemBlock
data class Stanza(val stanza: Fb2Stanza) : Fb2PoemBlock
}
data class Fb2Stanza(
val title: List<Fb2Text> = emptyList(),
val subtitle: Fb2Text? = null,
val verses: List<Fb2Text> = emptyList(),
)
data class Fb2Text(
val spans: List<Fb2TextSpan>,
) {
@@ -32,6 +32,7 @@ internal object Fb2XmlMapper {
documentInfo = documentInfoFrom(documentInfo),
bodyTitle = body?.first("title")?.children("p")?.mapNotNull { it.text().ifBlank { null } }.orEmpty(),
bodyImages = body?.children("image")?.mapNotNull(::imageRefFrom).orEmpty(),
bodyEpigraphs = body?.children("epigraph")?.map(::epigraphFrom).orEmpty(),
sections = body?.children("section")?.map(::sectionFrom).orEmpty(),
binaries = root.children("binary").mapNotNull(::binaryFrom),
)
@@ -124,6 +125,9 @@ internal object Fb2XmlMapper {
when (child.localName) {
"p" -> Fb2Block.Paragraph(textFrom(child))
"subtitle" -> Fb2Block.Subtitle(textFrom(child))
"epigraph" -> Fb2Block.Epigraph(epigraphFrom(child))
"cite" -> Fb2Block.Cite(citeFrom(child))
"poem" -> Fb2Block.Poem(poemFrom(child))
"image" -> imageRefFrom(child)?.let(Fb2Block::Image)
"empty-line" -> Fb2Block.EmptyLine
else -> null
@@ -140,8 +144,64 @@ internal object Fb2XmlMapper {
)
}
private fun poemFrom(element: XmlElement): Fb2Poem =
Fb2Poem(
title = titleFrom(element.first("title")),
epigraphs = element.children("epigraph").map(::epigraphFrom),
blocks = element.nodes.mapNotNull { node ->
val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
when (child.localName) {
"subtitle" -> Fb2PoemBlock.Subtitle(textFrom(child))
"stanza" -> Fb2PoemBlock.Stanza(stanzaFrom(child))
else -> null
}
},
textAuthors = element.children("text-author").map(::textFrom),
date = element.first("date")?.text()?.ifBlank { null },
)
private fun epigraphFrom(element: XmlElement): Fb2Epigraph =
Fb2Epigraph(
blocks = element.nodes.mapNotNull { node ->
val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
when (child.localName) {
"p" -> Fb2EpigraphBlock.Paragraph(textFrom(child))
"poem" -> Fb2EpigraphBlock.Poem(poemFrom(child))
"cite" -> Fb2EpigraphBlock.Cite(citeFrom(child))
"empty-line" -> Fb2EpigraphBlock.EmptyLine
else -> null
}
},
textAuthors = element.children("text-author").map(::textFrom),
)
private fun citeFrom(element: XmlElement): Fb2Cite =
Fb2Cite(
blocks = element.nodes.mapNotNull { node ->
val child = (node as? XmlNode.ElementNode)?.element ?: return@mapNotNull null
when (child.localName) {
"p" -> Fb2EpigraphBlock.Paragraph(textFrom(child))
"subtitle" -> Fb2EpigraphBlock.Subtitle(textFrom(child))
"poem" -> Fb2EpigraphBlock.Poem(poemFrom(child))
"empty-line" -> Fb2EpigraphBlock.EmptyLine
else -> null
}
},
textAuthors = element.children("text-author").map(::textFrom),
)
private fun stanzaFrom(element: XmlElement): Fb2Stanza =
Fb2Stanza(
title = titleFrom(element.first("title")),
subtitle = element.first("subtitle")?.let(::textFrom),
verses = element.children("v").map(::textFrom),
)
private fun titleFrom(element: XmlElement?): List<Fb2Text> =
element?.children("p")?.map(::textFrom).orEmpty()
private fun textFrom(element: XmlElement): Fb2Text =
Fb2Text(spansFrom(element.nodes).mergeAdjacent())
Fb2Text(spansFrom(element.nodes).mergeAdjacent().trimBoundaryWhitespace())
private fun spansFrom(nodes: List<XmlNode>, styles: Set<Fb2TextStyle> = emptySet()): List<Fb2TextSpan> =
nodes.flatMap { node ->
@@ -179,6 +239,14 @@ internal object Fb2XmlMapper {
return merged
}
private fun List<Fb2TextSpan>.trimBoundaryWhitespace(): List<Fb2TextSpan> {
if (isEmpty()) return emptyList()
val trimmed = toMutableList()
trimmed[0] = trimmed[0].copy(text = trimmed[0].text.trimStart())
trimmed[trimmed.lastIndex] = trimmed.last().copy(text = trimmed.last().text.trimEnd())
return trimmed.filter { it.text.isNotEmpty() }
}
private fun StringBuilder.appendSection(section: Fb2Section) {
append("<section>")
section.title?.takeIf { it.isNotBlank() }?.let {