diff --git a/README-CHANGES.xml b/README-CHANGES.xml index 44b9b2b..2928996 100644 --- a/README-CHANGES.xml +++ b/README-CHANGES.xml @@ -171,7 +171,7 @@ - + @@ -180,6 +180,15 @@ + + + + + + + + + diff --git a/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2NavigationGraphs.kt b/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2NavigationGraphs.kt index d18fb80..567ca57 100644 --- a/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2NavigationGraphs.kt +++ b/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2NavigationGraphs.kt @@ -5,19 +5,32 @@ import org.librarysimplified.r2.vanilla.internal.SR2NavigationNode.SR2Navigation import org.librarysimplified.r2.vanilla.internal.SR2NavigationNode.SR2NavigationResourceNode import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication +import org.slf4j.LoggerFactory +import java.net.URI object SR2NavigationGraphs { + private val logger = + LoggerFactory.getLogger(SR2NavigationGraphs::class.java) + + private data class FlatTOCLink( + val depth: Int, + val link: Link, + ) + fun create( publication: Publication, ): SR2NavigationGraph { + val flatToc = mutableListOf() + flattenTOC(flatToc, publication.tableOfContents, depth = 0) + val readingOrder = publication.readingOrder.mapIndexed { index, link -> - this.makeReadingOrderNode(publication, index, link) + this.makeReadingOrderNode(flatToc, index, link) } val resources = publication.resources.map { link -> - this.makeResourceNode(publication, link) + this.makeResourceNode(flatToc, link) } return SR2NavigationGraph( @@ -26,31 +39,48 @@ object SR2NavigationGraphs { ) } + private fun flattenTOC( + flatToc: MutableList, + tableOfContents: List, + depth: Int, + ) { + for (entry in tableOfContents) { + flatToc.add(FlatTOCLink(depth, entry)) + flattenTOC( + flatToc = flatToc, + tableOfContents = entry.children, + depth = depth + 1, + ) + } + } + private fun makeResourceNode( - publication: Publication, + flatTOC: List, link: Link, ): SR2NavigationResourceNode { return SR2NavigationResourceNode( - navigationPoint = this.makeNavigationPoint(publication, link), + navigationPoint = this.makeNavigationPoint(flatTOC, link), ) } private fun makeReadingOrderNode( - publication: Publication, + flatTOC: List, index: Int, link: Link, ): SR2NavigationReadingOrderNode { return SR2NavigationReadingOrderNode( - navigationPoint = this.makeNavigationPoint(publication, link), + navigationPoint = this.makeNavigationPoint(flatTOC, link), index = index, ) } - private fun makeNavigationPoint(publication: Publication, link: Link): SR2NavigationPoint { - val title = - link.title?.takeIf(String::isNotBlank) - ?: titleFromTOC(null, publication.tableOfContents, link) - ?: "" + private fun makeNavigationPoint( + flatTOC: List, + link: Link, + ): SR2NavigationPoint { + val title = this.findTitleFor(flatTOC, link) + this.logger.debug("Title: {} -> {}", link.href, title) + return SR2NavigationPoint( title, SR2Locator.SR2LocatorPercent.create(link.href, 0.0), @@ -59,25 +89,69 @@ object SR2NavigationGraphs { /** * Perform a depth-first search for a title in the toc tree. - * In case of multiples matches, deeper items have precedence because they're likely to be more specific. + * In case of multiples matches, deeper items have precedence because they're likely to be + * more specific. */ - private fun titleFromTOC(tocEntryParent: Link?, toc: List, link: Link): String? { - for (entry in toc) { - this.titleFromTOC(entry, entry.children, link)?.let { - return it - } + private fun findTitleFor( + flatTOC: List, + link: Link, + ): String { + val linkTitle = link.title?.trim() + if (!linkTitle.isNullOrBlank()) { + return linkTitle } - for (entry in toc) { - if (entry.href == link.href && entry.title != null) { - return entry.title - } + val bestLink = + flatTOC.filter { flatLink -> hasSuitableTitle(candidateEntry = flatLink.link, link) } + .sortedBy(FlatTOCLink::depth) + .lastOrNull() + + return bestLink?.link?.title ?: "" + } + + private fun hasSuitableTitle( + candidateEntry: Link, + link: Link, + ): Boolean { + /* + * If there isn't a non-blank title, then we know this entry isn't suitable. + */ + + val title = candidateEntry.title ?: return false + if (title.isBlank()) { + return false + } + + /* + * If the URLs match exactly, then we know this entry is suitable. + */ + + if (candidateEntry.href == link.href) { + return true } - if (tocEntryParent?.href == link.href && tocEntryParent.title != null) { - return tocEntryParent.title + /* + * Otherwise, if we _don't_ have a fragment, and the target link body matches ours but + * _does_ have a fragment, we treat it as suitable. + */ + + try { + val linkAsURI = URI.create(link.href.toString()) + if (!linkAsURI.fragment.isNullOrBlank()) { + return false + } + + val linkWithoutFragment = + URI.create(linkAsURI.toString().substringBefore('#')) + val candidateEntryAsURI = + URI.create(candidateEntry.href.toString()) + val candidateEntryWithoutFragment = + URI.create(candidateEntryAsURI.toString().substringBefore('#')) + + return candidateEntryWithoutFragment == linkWithoutFragment + } catch (e: Exception) { + return false } - return null } }