use crate::engine::ast::{Block, DocTree, InlineContent};
use crate::engine::provenance::ProvMap;
use crate::engine::strip::StripState;
use crate::model::{Section, Span};
use super::plaintext;
struct Builder {
level: u8,
heading_span: Span,
title_span: Span,
title_text: String,
body_start: usize,
children: Vec<Section>,
}
impl Builder {
fn finish(self, body_end: usize) -> Section {
Section::new(
self.level,
self.heading_span,
self.title_span,
Span {
start: self.body_start,
end: body_end,
},
self.title_text,
self.children,
)
}
}
pub(crate) fn build(
tree: &DocTree,
flat: &str,
strips: &StripState,
prov: &ProvMap,
) -> Vec<Section> {
let mut roots: Vec<Section> = Vec::new();
let mut stack: Vec<Builder> = Vec::new();
for block in &tree.blocks {
let Block::Heading {
level,
line,
inner,
content,
} = block
else {
continue;
};
let heading_span = to_span(prov, line.start, line.end);
while stack.last().is_some_and(|b| b.level >= *level) {
if let Some(top) = stack.pop() {
let done = top.finish(heading_span.start);
attach(&mut stack, &mut roots, done);
}
}
stack.push(Builder {
level: *level,
heading_span,
title_span: to_span(prov, inner.start, inner.end),
title_text: title_of(content, flat, strips),
body_start: heading_span.start,
children: Vec::new(),
});
}
let end = prov.src_len() as usize;
while let Some(top) = stack.pop() {
let done = top.finish(end);
attach(&mut stack, &mut roots, done);
}
roots
}
fn attach(stack: &mut [Builder], roots: &mut Vec<Section>, done: Section) {
match stack.last_mut() {
Some(parent) => parent.children.push(done),
None => roots.push(done),
}
}
fn to_span(prov: &ProvMap, start: u32, end: u32) -> Span {
Span {
start: prov.source_offset(start) as usize,
end: prov.source_offset(end) as usize,
}
}
fn title_of(content: &InlineContent, flat: &str, strips: &StripState) -> String {
match content {
InlineContent::Parsed(nodes) => plaintext::label_text(nodes, flat, strips),
InlineContent::Raw(_) => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::ast::TextRange;
use crate::engine::source::{SourceId, Span as SrcSpan};
#[test]
fn raw_heading_content_yields_an_empty_title() {
let mut prov = ProvMap::new(8);
prov.push_text(0, SrcSpan::new(SourceId(0), 0, 8));
prov.seal(8);
let tree = DocTree {
blocks: vec![Block::Heading {
level: 2,
line: TextRange::new(0, 8),
inner: TextRange::new(3, 5),
content: InlineContent::Raw(TextRange::new(3, 5)),
}],
};
let sections = build(&tree, "== A ==", &StripState::new(), &prov);
assert_eq!(sections.len(), 1);
assert_eq!(sections[0].title_text(), "");
assert_eq!(sections[0].level(), 2);
assert_eq!(sections[0].body_span(), Span { start: 0, end: 8 });
}
#[test]
fn documents_without_headings_have_no_sections() {
let prov = ProvMap::new(0);
let tree = DocTree {
blocks: vec![Block::HorizontalRule],
};
assert!(build(&tree, "", &StripState::new(), &prov).is_empty());
}
}