diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b53c23e4..b2eaa43272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. Take a look ## [Unreleased] +### Added + +* The EPUB 2 `` element is now parsed into the RWPM `landmarks` subcollection when no EPUB 3 `landmarks` navigation document is declared (contributed by [@erkasraim](https://github.com/readium/kotlin-toolkit/pull/628)). + ### Changed * Jetifier is not required anymore, you can remove `android.enableJetifier=true` from your `gradle.properties` if you were using Readium as a local clone. diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt index d00b780cfd..61a979498c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt @@ -58,6 +58,16 @@ internal class ManifestAdapter( } } .mapValues { listOf(PublicationCollection(links = it.value)) } + .toMutableMap() + + // EPUB 3 Reading Systems must ignore the guide element when provided in EPUB 3 Publications + // whose EPUB Navigation Document includes the landmarks feature. + // https://idpf.org/epub/30/spec/epub30-publications.html#sec-guide-elem + if (!subcollections.contains("landmarks") && packageDocument.guide.isNotEmpty()) { + // EPUB 2.0 doesn't have a landmarks collection, so we use the guide as a fallback + // If an EPUB 3.0+ file does not have landmarks, it will use guide instead. + subcollections["landmarks"] = listOf(PublicationCollection(links = packageDocument.guide)) + } // Build Publication object return Manifest( @@ -66,7 +76,7 @@ internal class ManifestAdapter( readingOrder = readingOrder, resources = resources, tableOfContents = toc, - subcollections = subcollections + subcollections = subcollections.toMap() ) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt index 26b3d1f934..481489e5f4 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt @@ -52,15 +52,15 @@ internal object NavigationDocumentParser { DEFAULT_VOCAB.TYPE ) } - val links = nav.getFirst("ol", Namespaces.XHTML)?.let { parseOlElement(it, filePath) } + val links = nav.getFirst("ol", Namespaces.XHTML)?.let { parseOlElement(it, filePath, prefixMap) } return if (types.isNotEmpty() && !links.isNullOrEmpty()) Pair(types, links) else null } - private fun parseOlElement(element: ElementNode, filePath: Url): List = - element.get("li", Namespaces.XHTML).mapNotNull { parseLiElement(it, filePath) } + private fun parseOlElement(element: ElementNode, filePath: Url, prefixMap: Map): List = + element.get("li", Namespaces.XHTML).mapNotNull { parseLiElement(it, filePath, prefixMap) } @OptIn(DelicateReadiumApi::class) - private fun parseLiElement(element: ElementNode, filePath: Url): Link? { + private fun parseLiElement(element: ElementNode, filePath: Url, prefixMap: Map): Link? { val first = element.getAll().firstOrNull() ?: return null // should be , , or
    val title = if (first.name == "ol") { "" @@ -76,7 +76,26 @@ internal object NavigationDocumentParser { } else { Url("#")!! } - val children = element.getFirst("ol", Namespaces.XHTML)?.let { parseOlElement(it, filePath) }.orEmpty() + val children = element.getFirst("ol", Namespaces.XHTML)?.let { + parseOlElement( + it, + filePath, + prefixMap + ) + }.orEmpty() + + val typeAttr = first.getAttrNs("type", Namespaces.OPS) ?: "" + val rels = if (typeAttr.isNotEmpty()) { + parseProperties(typeAttr).map { + resolveProperty( + it, + prefixMap, + DEFAULT_VOCAB.TYPE + ) + }.toSet() + } else { + emptySet() + } return if (children.isEmpty() && (href.toString() == "#" || title == "")) { null @@ -84,7 +103,8 @@ internal object NavigationDocumentParser { Link( title = title, href = href, - children = children + children = children, + rels = rels ) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt index de9df9d061..c1b00a514d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt @@ -9,6 +9,7 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.fromEpubHref @@ -21,6 +22,7 @@ internal data class PackageDocument( val metadata: List, val manifest: List, val spine: Spine, + val guide: List, ) { companion object { @@ -34,6 +36,7 @@ internal data class PackageDocument( ?: return null val spineElement = document.getFirst("spine", Namespaces.OPF) ?: return null + val guideElement = document.getFirst("guide", Namespaces.OPF) return PackageDocument( path = filePath, @@ -42,7 +45,8 @@ internal data class PackageDocument( metadata = metadata, manifest = manifestElement.get("item", Namespaces.OPF) .mapNotNull { Item.parse(it, filePath, prefixMap) }, - spine = Spine.parse(spineElement, prefixMap, epubVersion) + spine = Spine.parse(spineElement, prefixMap, epubVersion), + guide = Guide.parse(guideElement, filePath, prefixMap), ) } } @@ -106,6 +110,46 @@ internal data class Spine( } } +internal data class Guide( + val links: List, +) { + companion object { + // Epub 3.0+ does not support the guide element + // https://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#TOC2.6 + fun parse(element: ElementNode?, filePath: Url, prefixMap: Map): List { + if (element == null) return emptyList() + + return element.get("reference", Namespaces.OPF).mapNotNull { node -> + val href = node.getAttr("href") + ?.let { Url.fromEpubHref(it) } + ?.let { filePath.resolve(it) } + ?: return@mapNotNull null + val rels = node.getAttr("type")?.let { + setOf(mapToEPUB3Spec(it, prefixMap)) + } ?: emptySet() + + Link( + href = href, + title = node.getAttr("title"), + rels = rels, + ) + } + } + + private fun mapToEPUB3Spec(type: String, prefixMap: Map): String { + return when (type) { + "title-page" -> "titlepage" + "text" -> "bodymatter" + "acknowledgements" -> "acknowledgments" // American English + "notes" -> "endnotes" // endnotes or footnotes. https://www.w3.org/TR/epub-ssv-11/#notes + else -> type + }.let { + resolveProperty(it, prefixMap, DEFAULT_VOCAB.TYPE) + } + } + } +} + internal data class Itemref( val idref: String, val linear: Boolean, diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt index 24bde97940..1443467e6c 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt @@ -135,8 +135,8 @@ class NavigationDocumentParserTest { @Test fun `landmarks are rightly parsed`() { assertThat(navComplex["landmarks"]).containsExactly( - Link(title = "Table of Contents", href = Href("OEBPS/xhtml/nav.xhtml#toc")!!), - Link(title = "Begin Reading", href = Href("OEBPS/xhtml/chapter1.xhtml")!!) + Link(title = "Table of Contents", href = Href("OEBPS/xhtml/nav.xhtml#toc")!!, rels = setOf("http://idpf.org/epub/vocab/structure/#toc")), + Link(title = "Begin Reading", href = Href("OEBPS/xhtml/chapter1.xhtml")!!, rels = setOf("http://idpf.org/epub/vocab/structure/#bodymatter")) ) } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt index 3c766d433d..9a70986678 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt @@ -12,12 +12,14 @@ package org.readium.r2.streamer.parser.epub import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.entry import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.PublicationCollection import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.epub.contains @@ -231,3 +233,38 @@ class LinkMiscTest { parsePackageDocument("package/fallbacks-termination.opf") } } + +@RunWith(RobolectricTestRunner::class) +class GuideTest { + private val guidePub = parsePackageDocument("package/guide-epub2.opf") + + @Test + fun `Guide is rightly computed`() { + assertThat(guidePub.subcollections).containsExactly( + entry( + "landmarks", + listOf( + PublicationCollection( + links = listOf( + Link( + href = Href("OEBPS/toc.html")!!, + title = "Table of Contents", + rels = setOf("http://idpf.org/epub/vocab/structure/#toc") + ), + Link( + href = Href("OEBPS/toc.html#figures")!!, + title = "List Of Illustrations", + rels = setOf("http://idpf.org/epub/vocab/structure/#loi") + ), + Link( + href = Href("OEBPS/beginpage.html")!!, + title = "Introduction", + rels = setOf("http://idpf.org/epub/vocab/structure/#bodymatter") + ), + ) + ) + ) + ) + ) + } +} diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/package/guide-epub2.opf b/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/package/guide-epub2.opf new file mode 100644 index 0000000000..34772a4855 --- /dev/null +++ b/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/package/guide-epub2.opf @@ -0,0 +1,24 @@ + + + + Alice's Adventures in Wonderland + + + + + + + + + + + + + + + + +