use asciidoc_parser::{
attributes::{
Attrlist,
ElementAttribute,
},
blocks::{
Block,
FindBlocks,
IsBlock,
SectionBlock,
SectionType,
TocBlock,
},
};
use adocers_render_core::numbering::Numbering;
use crate::{
Renderer,
html::escape_attr,
};
struct Entry {
id: Option<String>,
title: String,
children: Vec<Entry>,
}
#[derive(Default)]
struct Overrides<'src> {
id: Option<&'src str>,
class: Option<&'src str>,
title: Option<&'src str>,
levels: Option<usize>,
title_class: bool,
}
impl<'src> Overrides<'src> {
fn from_macro(attrlist: &'src Attrlist<'src>) -> Self {
let named = |name: &str| {
attrlist
.named_attribute(name)
.map(ElementAttribute::value)
.filter(|value| !value.is_empty())
};
Self {
id: named("id"),
class: named("role"),
title: named("title"),
levels: named("levels").and_then(|levels| levels.parse().ok()),
title_class: true,
}
}
}
impl Renderer<'_> {
pub(super) fn toc(&mut self) {
self.toc_with(&Overrides::default());
}
fn toc_with(&mut self, overrides: &Overrides<'_>) -> bool {
let depth = overrides
.levels
.unwrap_or_else(|| self.document.toc_levels());
let entries = entries(self.document.child_blocks(), 1, depth, &self.numbering);
if entries.is_empty() {
return false;
}
let id = overrides.id.unwrap_or("toc");
let class = overrides.class.map_or_else(
|| {
if self.options.fragment {
"toc".to_string()
} else {
self.document.toc_class().to_string()
}
},
str::to_string,
);
let title = overrides
.title
.map_or_else(|| self.document.toc_title().to_string(), str::to_string);
let title_class = if overrides.title_class {
" class=\"title\""
} else {
""
};
self.out.open("div", Some(id), &[&class]);
self.out.line(&format!(
"<div id=\"{}title\"{title_class}>{title}</div>",
escape_attr(id)
));
self.render_entries(&entries, 1);
self.out.close("div");
true
}
pub(super) fn toc_macro(&mut self, toc: &TocBlock<'_>) {
let placed = !self.toc_rendered
&& self.document.toc_mode() == asciidoc_parser::document::TocMode::Macro
&& self.toc_with(&Overrides::from_macro(toc.macro_attrlist()));
if placed {
self.toc_rendered = true;
} else {
self.out.line("<!-- toc disabled -->");
}
}
fn render_entries(&mut self, entries: &[Entry], level: usize) {
if entries.is_empty() {
return;
}
self.out.open("ul", None, &[&format!("sectlevel{level}")]);
for entry in entries {
let item = match &entry.id {
Some(id) => format!("<li><a href=\"#{}\">{}</a>", escape_attr(id), entry.title),
None => format!("<li>{}", entry.title),
};
if entry.children.is_empty() {
self.out.line(&format!("{item}</li>"));
} else {
self.out.line(&item);
self.render_entries(&entry.children, level + 1);
self.out.line("</li>");
}
}
self.out.close("ul");
}
}
fn entries<'src>(
blocks: impl Iterator<Item = &'src Block<'src>>,
level: usize,
depth: usize,
numbering: &Numbering,
) -> Vec<Entry> {
if level > depth {
return Vec::new();
}
let mut outline = Vec::new();
for block in blocks {
if let Block::Section(section) = block
&& !is_excluded(section)
{
outline.push(Entry {
id: section.id().map(str::to_string),
title: format!("{}{}", numbering.prefix(section), section.section_title()),
children: entries(section.child_blocks(), level + 1, depth, numbering),
});
}
}
outline
}
fn is_excluded(section: &SectionBlock<'_>) -> bool {
section.section_type() == SectionType::Discrete
}