use std::collections::HashMap;
use std::sync::Arc;
use crate::import::ChapterId;
use crate::model::{AnchorTarget, Chapter, GlobalNodeId, Role};
#[derive(Debug, Default)]
pub struct ResolvedLinks {
links: HashMap<GlobalNodeId, AnchorTarget>,
internal_targets: HashMap<GlobalNodeId, Vec<GlobalNodeId>>,
chapter_targets: HashMap<ChapterId, Vec<GlobalNodeId>>,
broken: Vec<(GlobalNodeId, String)>,
}
impl ResolvedLinks {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, source: GlobalNodeId) -> Option<&AnchorTarget> {
self.links.get(&source)
}
pub fn is_internal_target(&self, node: GlobalNodeId) -> bool {
self.internal_targets.contains_key(&node)
}
pub fn is_chapter_target(&self, chapter: ChapterId) -> bool {
self.chapter_targets.contains_key(&chapter)
}
pub fn links_to(&self, target: GlobalNodeId) -> &[GlobalNodeId] {
self.internal_targets
.get(&target)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn links_to_chapter(&self, chapter: ChapterId) -> &[GlobalNodeId] {
self.chapter_targets
.get(&chapter)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn broken_links(&self) -> &[(GlobalNodeId, String)] {
&self.broken
}
pub fn iter(&self) -> impl Iterator<Item = (GlobalNodeId, &AnchorTarget)> {
self.links.iter().map(|(&k, v)| (k, v))
}
pub fn len(&self) -> usize {
self.links.len()
}
pub fn is_empty(&self) -> bool {
self.links.is_empty()
}
}
pub(crate) struct ResolvedLinksBuilder {
resolved: ResolvedLinks,
}
impl ResolvedLinksBuilder {
pub fn new() -> Self {
Self {
resolved: ResolvedLinks::new(),
}
}
pub fn add_internal(&mut self, source: GlobalNodeId, target: GlobalNodeId) {
self.resolved
.links
.insert(source, AnchorTarget::Internal(target));
self.resolved
.internal_targets
.entry(target)
.or_default()
.push(source);
}
pub fn add_chapter(&mut self, source: GlobalNodeId, chapter: ChapterId) {
self.resolved
.links
.insert(source, AnchorTarget::Chapter(chapter));
self.resolved
.chapter_targets
.entry(chapter)
.or_default()
.push(source);
}
pub fn add_external(&mut self, source: GlobalNodeId, url: String) {
self.resolved
.links
.insert(source, AnchorTarget::External(url));
}
pub fn add_broken(&mut self, source: GlobalNodeId, href: String) {
self.resolved.broken.push((source, href));
}
pub fn build(self) -> ResolvedLinks {
self.resolved
}
}
pub(crate) fn resolve_book_links(book: &mut crate::model::Book) -> std::io::Result<ResolvedLinks> {
let mut builder = ResolvedLinksBuilder::new();
let spine: Vec<_> = book.spine().to_vec();
let mut chapters: Vec<(ChapterId, Arc<Chapter>)> = Vec::new();
for entry in &spine {
let chapter = book.load_chapter_cached(entry.id)?;
chapters.push((entry.id, chapter));
}
book.index_anchors(&chapters);
book.resolve_toc();
book.resolve_toc_targets();
for (chapter_id, chapter) in &chapters {
for node_id in chapter.iter_dfs() {
let node = match chapter.node(node_id) {
Some(n) => n,
None => continue,
};
if node.role != Role::Link {
continue;
}
let href = match chapter.semantics.href(node_id) {
Some(h) => h,
None => continue,
};
let source = GlobalNodeId::new(*chapter_id, node_id);
match book.resolve_href(*chapter_id, href) {
Some(AnchorTarget::Internal(target)) => {
builder.add_internal(source, target);
}
Some(AnchorTarget::Chapter(target_chapter)) => {
builder.add_chapter(source, target_chapter);
}
Some(AnchorTarget::External(url)) => {
builder.add_external(source, url);
}
None => {
builder.add_broken(source, href.to_string());
}
}
}
}
Ok(builder.build())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::NodeId;
#[test]
fn test_resolved_links_empty() {
let resolved = ResolvedLinks::new();
assert!(resolved.is_empty());
assert_eq!(resolved.len(), 0);
assert!(resolved.broken_links().is_empty());
}
#[test]
fn test_builder_internal_link() {
let mut builder = ResolvedLinksBuilder::new();
let source = GlobalNodeId::new(ChapterId(0), NodeId(5));
let target = GlobalNodeId::new(ChapterId(1), NodeId(23));
builder.add_internal(source, target);
let resolved = builder.build();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved.get(source), Some(&AnchorTarget::Internal(target)));
assert!(resolved.is_internal_target(target));
assert_eq!(resolved.links_to(target), &[source]);
}
#[test]
fn test_builder_chapter_link() {
let mut builder = ResolvedLinksBuilder::new();
let source = GlobalNodeId::new(ChapterId(0), NodeId(10));
let target_chapter = ChapterId(2);
builder.add_chapter(source, target_chapter);
let resolved = builder.build();
assert_eq!(
resolved.get(source),
Some(&AnchorTarget::Chapter(target_chapter))
);
assert_eq!(resolved.links_to_chapter(target_chapter), &[source]);
}
#[test]
fn test_builder_external_link() {
let mut builder = ResolvedLinksBuilder::new();
let source = GlobalNodeId::new(ChapterId(0), NodeId(15));
let url = "https://example.com".to_string();
builder.add_external(source, url.clone());
let resolved = builder.build();
assert_eq!(resolved.get(source), Some(&AnchorTarget::External(url)));
}
#[test]
fn test_builder_broken_link() {
let mut builder = ResolvedLinksBuilder::new();
let source = GlobalNodeId::new(ChapterId(0), NodeId(20));
let href = "nonexistent.xhtml#missing".to_string();
builder.add_broken(source, href.clone());
let resolved = builder.build();
assert_eq!(resolved.broken_links(), &[(source, href)]);
}
#[test]
fn test_multiple_links_to_same_target() {
let mut builder = ResolvedLinksBuilder::new();
let target = GlobalNodeId::new(ChapterId(1), NodeId(100));
let source1 = GlobalNodeId::new(ChapterId(0), NodeId(5));
let source2 = GlobalNodeId::new(ChapterId(2), NodeId(10));
builder.add_internal(source1, target);
builder.add_internal(source2, target);
let resolved = builder.build();
assert!(resolved.is_internal_target(target));
let links = resolved.links_to(target);
assert_eq!(links.len(), 2);
assert!(links.contains(&source1));
assert!(links.contains(&source2));
}
#[test]
fn test_is_chapter_target() {
let mut builder = ResolvedLinksBuilder::new();
let source = GlobalNodeId::new(ChapterId(0), NodeId(5));
let target_chapter = ChapterId(2);
builder.add_chapter(source, target_chapter);
let resolved = builder.build();
assert!(resolved.is_chapter_target(target_chapter));
assert!(!resolved.is_chapter_target(ChapterId(99)));
}
}