use crate::content::parsing::MarkdownFile;
use crate::content::parsing::ast_utils::extract_text_from_nodes;
use crate::core::markdown_config;
use crate::templates::{NextPrevNavData, SidebarNavData};
use markdown::mdast::Node;
use markdown::to_mdast;
pub trait ContentGenerator {
fn generate_sidebar_nav(&self, _file: &MarkdownFile) -> Option<SidebarNavData> {
None }
fn generate_table_of_contents(&self, _content: &str) -> Option<String> {
None }
fn generate_next_prev_nav(&self, _file: &MarkdownFile) -> Option<NextPrevNavData> {
None }
}
pub struct DefaultContentGenerator;
impl ContentGenerator for DefaultContentGenerator {
fn generate_sidebar_nav(&self, file: &MarkdownFile) -> Option<SidebarNavData> {
let current_path = file
.path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("docs");
Some(SidebarNavData {
current_path: current_path.to_string(),
items: vec![
crate::templates::SidebarNavItem {
title: "Documentation".to_string(),
url: "docs".to_string(),
is_current: current_path == "docs",
is_section: true,
},
crate::templates::SidebarNavItem {
title: "Getting Started".to_string(),
url: "docs/getting-started".to_string(),
is_current: file.path.ends_with("getting-started.md"),
is_section: false,
},
crate::templates::SidebarNavItem {
title: "Frontmatter".to_string(),
url: "docs/frontmatter".to_string(),
is_current: file.path.ends_with("frontmatter.md"),
is_section: false,
},
crate::templates::SidebarNavItem {
title: "CLI Commands".to_string(),
url: "docs/cli-commands".to_string(),
is_current: file.path.ends_with("cli-commands.md"),
is_section: false,
},
crate::templates::SidebarNavItem {
title: "Deployment".to_string(),
url: "docs/deployment".to_string(),
is_current: file.path.ends_with("deployment.md"),
is_section: false,
},
],
})
}
fn generate_table_of_contents(&self, content: &str) -> Option<String> {
let root = to_mdast(content, &markdown_config::default()).ok()?;
let mut toc_html = String::from("<ul class=\"toc-list\">");
let mut has_items = false;
if let Node::Root(root_node) = root {
for child in &root_node.children {
if let Node::Heading(heading) = child
&& heading.depth >= 2
&& heading.depth <= 3
{
has_items = true;
let text = self.extract_text_from_node(child);
let anchor = self.create_anchor(&text);
let class = if heading.depth == 2 {
"toc-h2"
} else {
"toc-h3"
};
toc_html.push_str(&format!(
"<li class=\"{}\"><a href=\"#{}\">{}</a></li>",
class, anchor, text
));
}
}
}
toc_html.push_str("</ul>");
if has_items { Some(toc_html) } else { None }
}
fn generate_next_prev_nav(&self, _file: &MarkdownFile) -> Option<NextPrevNavData> {
None
}
}
impl DefaultContentGenerator {
fn extract_text_from_node(&self, node: &Node) -> String {
match node {
Node::Root(root) => extract_text_from_nodes(&root.children),
Node::Blockquote(quote) => extract_text_from_nodes("e.children),
Node::List(list) => extract_text_from_nodes(&list.children),
Node::ListItem(item) => extract_text_from_nodes(&item.children),
Node::Definition(_def) => String::new(),
Node::Paragraph(para) => extract_text_from_nodes(¶.children),
Node::Heading(heading) => extract_text_from_nodes(&heading.children),
Node::Table(_table) => String::new(),
Node::TableRow(_row) => String::new(),
Node::TableCell(_cell) => String::new(),
Node::Html(_html) => String::new(),
Node::Code(_code) => String::new(),
Node::Yaml(_yaml) => String::new(),
Node::Toml(_toml) => String::new(),
Node::InlineMath(_math) => String::new(),
Node::Math(_math) => String::new(),
Node::MdxJsxFlowElement(_jsx) => String::new(),
Node::MdxFlowExpression(_expr) => String::new(),
Node::MdxTextExpression(_expr) => String::new(),
Node::Emphasis(emphasis) => extract_text_from_nodes(&emphasis.children),
Node::Strong(strong) => extract_text_from_nodes(&strong.children),
Node::Delete(delete) => extract_text_from_nodes(&delete.children),
Node::InlineCode(code) => code.value.clone(),
Node::Break(_break) => String::new(),
Node::Link(link) => extract_text_from_nodes(&link.children),
Node::Image(image) => image.alt.clone(),
Node::LinkReference(_reference) => String::new(),
Node::ImageReference(_reference) => String::new(),
Node::FootnoteReference(_reference) => String::new(),
Node::FootnoteDefinition(_definition) => String::new(),
Node::Text(text) => text.value.clone(),
Node::ThematicBreak(_) => String::new(),
Node::MdxjsEsm(_) => String::new(),
Node::MdxJsxTextElement(_) => String::new(),
}
}
fn create_anchor(&self, text: &str) -> String {
text.to_lowercase()
.chars()
.map(|c| match c {
'a'..='z' | '0'..='9' => c,
' ' | '_' | '-' => '-',
_ => '-',
})
.collect::<String>()
.replace("--", "-")
.trim_matches('-')
.to_string()
}
}