canonrs-core 0.1.0

CanonRS core types, traits and primitives
//! Navigation Context - Shared state for doc navigation

use std::collections::HashMap;
use crate::primitives::structural::toc_item::TocItem;

#[derive(Clone, Debug, Default)]
pub struct NavigationState {
    pub current_heading_id: Option<String>,
    pub heading_hierarchy: HeadingHierarchy,
    pub scroll_progress: f32,
    pub active_branch: Vec<String>,
    pub depth_current: u8,
}

#[derive(Clone, Debug, Default)]
pub struct HeadingHierarchy {
    pub headings: Vec<HeadingNode>,
    pub id_to_index: HashMap<String, usize>,
}

#[derive(Clone, Debug)]
pub struct HeadingNode {
    pub id: String,
    pub text: String,
    pub level: u8,
    pub parent_id: Option<String>,
    pub children_ids: Vec<String>,
}

impl NavigationState {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn update_active_heading(&mut self, heading_id: String) {
        self.current_heading_id = Some(heading_id.clone());
        self.active_branch = self.heading_hierarchy.get_ancestor_chain(&heading_id);
        self.depth_current = self.heading_hierarchy
            .get_level(&heading_id)
            .unwrap_or(1);
    }

    pub fn update_scroll_progress(&mut self, progress: f32) {
        self.scroll_progress = progress.clamp(0.0, 1.0);
    }
}

impl HeadingHierarchy {
    pub fn from_toc_items(items: &[TocItem]) -> Self {
        let mut hierarchy = HeadingHierarchy::default();
        let mut stack: Vec<(usize, u8)> = Vec::new();

        for item in items {
            let index = hierarchy.headings.len();
            let level = item.level;

            while stack.last().map(|(_, l)| *l >= level).unwrap_or(false) {
                stack.pop();
            }

            let parent_id = stack.last().map(|(idx, _)| {
                hierarchy.headings[*idx].id.clone()
            });

            let node = HeadingNode {
                id: item.id.clone(),
                text: item.text.clone(),
                level: item.level,
                parent_id: parent_id.clone(),
                children_ids: Vec::new(),
            };

            if let Some((parent_idx, _)) = stack.last() {
                hierarchy.headings[*parent_idx].children_ids.push(item.id.clone());
            }

            hierarchy.id_to_index.insert(item.id.clone(), index);
            hierarchy.headings.push(node);
            stack.push((index, level));
        }

        hierarchy
    }

    pub fn get_ancestor_chain(&self, id: &str) -> Vec<String> {
        let mut chain = Vec::new();
        let mut current_id = Some(id.to_string());

        while let Some(id) = current_id {
            chain.push(id.clone());
            current_id = self.id_to_index
                .get(&id)
                .and_then(|idx| self.headings[*idx].parent_id.clone());
        }

        chain.reverse();
        chain
    }

    pub fn get_level(&self, id: &str) -> Option<u8> {
        self.id_to_index
            .get(id)
            .map(|idx| self.headings[*idx].level)
    }

    pub fn get_breadcrumb(&self, id: &str) -> Vec<(String, String)> {
        self.get_ancestor_chain(id)
            .into_iter()
            .filter_map(|id| {
                self.id_to_index.get(&id).map(|idx| {
                    let node = &self.headings[*idx];
                    (node.id.clone(), node.text.clone())
                })
            })
            .collect()
    }
}