canonrs-server 0.1.0

CanonRS server-side rendering support
use pulldown_cmark::{Parser, Event, Tag, TagEnd, HeadingLevel};
use canonrs_core::TocItem;

pub struct TocExtractor;

impl TocExtractor {
    pub fn extract_toc(markdown: &str) -> Vec<TocItem> {
        let parser = Parser::new(markdown);
        let mut toc_items = Vec::new();
        let mut in_heading = false;
        let mut current_level = 0u8;
        let mut current_text = String::new();
        let mut heading_counter = 0;

        for event in parser {
            match event {
                Event::Start(Tag::Heading { level, .. }) => {
                    in_heading = true;
                    current_level = match level {
                        HeadingLevel::H1 => 1,
                        HeadingLevel::H2 => 2,
                        HeadingLevel::H3 => 3,
                        HeadingLevel::H4 => 4,
                        HeadingLevel::H5 => 5,
                        HeadingLevel::H6 => 6,
                    };
                    current_text.clear();
                }
                Event::End(TagEnd::Heading { .. }) => {
                    if in_heading {
                        let id = if current_text.is_empty() {
                            format!("heading-{}", heading_counter)
                        } else {
                            slugify(current_text.trim())
                        };
                        toc_items.push(TocItem::new(id, current_text.clone(), current_level));
                        heading_counter += 1;
                        in_heading = false;
                    }
                }
                Event::Text(text) if in_heading => {
                    current_text.push_str(&text);
                }
                _ => {}
            }
        }

        toc_items
    }
}

pub fn slugify(text: &str) -> String {
    text.to_lowercase()
        .chars()
        .map(|c| {
            if c.is_alphanumeric() { c }
            else if c.is_whitespace() || c == '-' { '-' }
            else { '_' }
        })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}

impl TocExtractor {
    pub fn extract_toc_with_prefix(markdown: &str, id_prefix: &str) -> Vec<TocItem> {
        let parser = Parser::new(markdown);
        let mut toc_items = Vec::new();
        let mut in_heading = false;
        let mut current_level = 0u8;
        let mut current_text = String::new();
        let mut heading_counter = 0;

        for event in parser {
            match event {
                Event::Start(Tag::Heading { level, .. }) => {
                    in_heading = true;
                    current_level = match level {
                        HeadingLevel::H1 => 1,
                        HeadingLevel::H2 => 2,
                        HeadingLevel::H3 => 3,
                        HeadingLevel::H4 => 4,
                        HeadingLevel::H5 => 5,
                        HeadingLevel::H6 => 6,
                    };
                    current_text.clear();
                }
                Event::End(TagEnd::Heading { .. }) => {
                    if in_heading {
                        let slug = if current_text.is_empty() {
                            format!("heading-{}", heading_counter)
                        } else {
                            slugify(current_text.trim())
                        };
                        let id = if id_prefix.is_empty() {
                            slug
                        } else {
                            format!("{}-{}", id_prefix, slug)
                        };
                        toc_items.push(TocItem::new(id, current_text.clone(), current_level));
                        heading_counter += 1;
                        in_heading = false;
                    }
                }
                Event::Text(text) if in_heading => {
                    current_text.push_str(&text);
                }
                _ => {}
            }
        }

        toc_items
    }
}