use rustc_hash::FxHashMap;
use ox_content_ast::{Document, ListItem, Node, Paragraph};
use super::heading::{collect_heading_text, slugify_heading};
#[derive(Debug, Clone)]
pub(super) struct InlineTocEntry {
pub(super) depth: u8,
pub(super) text: String,
pub(super) id: String,
}
pub(super) struct DocumentRenderScan {
pub(super) has_toc_marker: bool,
pub(super) heading_count: usize,
}
pub(super) fn collect_inline_toc_entries(
document: &Document<'_>,
max_depth: u8,
entries: &mut Vec<InlineTocEntry>,
) {
let mut counts = FxHashMap::default();
for node in &document.children {
collect_inline_toc_node(node, max_depth, &mut counts, entries);
}
}
pub(super) fn scan_document_for_render(document: &Document<'_>) -> DocumentRenderScan {
let mut scan = DocumentRenderScan { has_toc_marker: false, heading_count: 0 };
for node in &document.children {
scan_node_for_render(node, &mut scan);
}
scan
}
fn scan_node_for_render(node: &Node<'_>, scan: &mut DocumentRenderScan) {
match node {
Node::Heading(_) => scan.heading_count += 1,
Node::Paragraph(p) if !scan.has_toc_marker && is_toc_marker_paragraph(p) => {
scan.has_toc_marker = true;
}
Node::Paragraph(_) => {}
Node::BlockQuote(bq) => {
for child in &bq.children {
scan_node_for_render(child, scan);
}
}
Node::List(list) => {
for item in &list.children {
scan_list_item_for_render(item, scan);
}
}
Node::ListItem(item) => scan_list_item_for_render(item, scan),
Node::FootnoteDefinition(def) => {
for child in &def.children {
scan_node_for_render(child, scan);
}
}
_ => {}
}
}
fn scan_list_item_for_render(item: &ListItem<'_>, scan: &mut DocumentRenderScan) {
for child in &item.children {
scan_node_for_render(child, scan);
}
}
pub(super) fn is_toc_marker_paragraph(paragraph: &Paragraph<'_>) -> bool {
const MARKER: &[u8] = b"[[toc]]";
let mut matched = 0usize;
let mut after_marker_ws = false;
for child in ¶graph.children {
let Node::Text(text) = child else {
return false;
};
for &byte in text.value.as_bytes() {
let is_ws = matches!(byte, b' ' | b'\t' | b'\n' | b'\r');
if is_ws {
if matched > 0 {
after_marker_ws = true;
}
continue;
}
if after_marker_ws || matched == MARKER.len() || byte != MARKER[matched] {
return false;
}
matched += 1;
}
}
matched == MARKER.len()
}
fn collect_inline_toc_node(
node: &Node<'_>,
max_depth: u8,
counts: &mut FxHashMap<String, usize>,
entries: &mut Vec<InlineTocEntry>,
) {
use std::fmt::Write as _;
match node {
Node::Heading(heading) => {
let include_heading = heading.depth <= max_depth;
let text = collect_heading_text(&heading.children);
let mut slug = slugify_heading(&text);
let id = if let Some(count) = counts.get_mut(slug.as_str()) {
let suffix = *count;
*count += 1;
if include_heading {
let _ = write!(slug, "-{suffix}");
Some(slug)
} else {
None
}
} else if include_heading {
counts.insert(slug.clone(), 1);
Some(slug)
} else {
counts.insert(slug, 1);
None
};
if let Some(id) = id {
entries.push(InlineTocEntry { depth: heading.depth, text, id });
}
}
Node::BlockQuote(block_quote) => {
for child in &block_quote.children {
collect_inline_toc_node(child, max_depth, counts, entries);
}
}
Node::List(list) => {
for item in &list.children {
for child in &item.children {
collect_inline_toc_node(child, max_depth, counts, entries);
}
}
}
Node::ListItem(item) => {
for child in &item.children {
collect_inline_toc_node(child, max_depth, counts, entries);
}
}
Node::FootnoteDefinition(definition) => {
for child in &definition.children {
collect_inline_toc_node(child, max_depth, counts, entries);
}
}
_ => {}
}
}