use std::collections::{HashMap, HashSet};
use super::roff_escape::visible_text;
use libmandoc_rs::{Node, NodeKind};
use mant_ast::{Block, Diagnostic, DiagnosticLevel, Inline, Section};
type SectionTargets = HashMap<String, Option<String>>;
pub(super) fn resolve_navigation(
sections: &mut [Section],
explicit_targets: &HashSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut targets = SectionTargets::new();
collect_section_targets(sections, &mut targets);
for section in sections {
resolve_section(section, &targets, explicit_targets, diagnostics);
}
}
pub(super) fn explicit_targets(root: &Node) -> HashSet<String> {
let mut nodes = Vec::new();
flatten_nodes(root, &mut nodes);
let mut targets = HashSet::new();
for (index, node) in nodes.iter().enumerate() {
if node.macro_name.as_deref() != Some("Tg") {
continue;
}
let target = first_text(node).map(visible_text).or_else(|| {
nodes[index + 1..]
.iter()
.find(|candidate| candidate.flags.deep_link_target)
.and_then(|candidate| navigation_name(candidate))
});
if let Some(target) = target.filter(|target| !target.is_empty()) {
targets.insert(target);
}
}
targets
}
fn flatten_nodes<'a>(node: &'a Node, output: &mut Vec<&'a Node>) {
output.push(node);
for child in &node.children {
flatten_nodes(child, output);
}
}
fn first_text(node: &Node) -> Option<&str> {
if node.kind == NodeKind::Text {
return node.text.as_deref();
}
node.children.iter().find_map(first_text)
}
fn navigation_name(node: &Node) -> Option<String> {
node.tag.as_deref().map(visible_text).or_else(|| {
first_text(node).and_then(|value| {
let sanitized = visible_text(value);
sanitized
.trim_start_matches('-')
.split_whitespace()
.next()
.map(str::to_owned)
})
})
}
fn collect_section_targets(sections: &[Section], targets: &mut SectionTargets) {
for section in sections {
targets
.entry(section.title.clone())
.and_modify(|target| *target = None)
.or_insert_with(|| Some(section.id.clone()));
collect_section_targets(§ion.children, targets);
}
}
fn resolve_section(
section: &mut Section,
targets: &SectionTargets,
explicit_targets: &HashSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
resolve_blocks(&mut section.blocks, targets, explicit_targets, diagnostics);
for child in &mut section.children {
resolve_section(child, targets, explicit_targets, diagnostics);
}
}
fn resolve_blocks(
blocks: &mut [Block],
targets: &SectionTargets,
explicit_targets: &HashSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
for block in blocks {
match block {
Block::Paragraph { children, .. } | Block::Preformatted { children, .. } => {
resolve_inlines(children, targets, explicit_targets, diagnostics);
}
Block::List { items, .. } => {
for item in items {
resolve_blocks(&mut item.blocks, targets, explicit_targets, diagnostics);
}
}
Block::DefinitionList { items, .. } => {
for item in items {
for term in &mut item.terms {
resolve_inlines(term, targets, explicit_targets, diagnostics);
}
resolve_blocks(
&mut item.description,
targets,
explicit_targets,
diagnostics,
);
}
}
Block::Table { rows, .. } => {
for row in rows {
for cell in &mut row.cells {
resolve_blocks(&mut cell.blocks, targets, explicit_targets, diagnostics);
}
}
}
Block::Equation { .. }
| Block::VerticalSpace { .. }
| Block::ThematicBreak { .. }
| Block::Unsupported { .. } => {}
}
}
}
fn resolve_inlines(
nodes: &mut Vec<Inline>,
targets: &SectionTargets,
explicit_targets: &HashSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut resolved = Vec::with_capacity(nodes.len());
for node in std::mem::take(nodes) {
match node {
Inline::Strong { mut children } => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
resolved.push(Inline::Strong { children });
}
Inline::Emphasis { mut children } => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
resolved.push(Inline::Emphasis { children });
}
Inline::ExternalLink {
uri,
title,
mut children,
} => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
resolved.push(Inline::ExternalLink {
uri,
title,
children,
});
}
Inline::EmailLink {
address,
mut children,
} => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
resolved.push(Inline::EmailLink { address, children });
}
Inline::ManualReference {
name,
section,
mut children,
} => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
resolved.push(Inline::ManualReference {
name,
section,
children,
});
}
Inline::SectionReference {
target,
mut children,
} => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
if let Some(Some(section_id)) = targets.get(&target) {
resolved.push(Inline::SectionReference {
target: section_id.clone(),
children,
});
} else {
diagnostics.push(Diagnostic {
level: DiagnosticLevel::Warning,
code: Some("unresolved-section-reference".to_owned()),
message: format!("cannot resolve section reference: {target}"),
source: None,
});
resolved.extend(children);
}
}
Inline::Anchor { id } if explicit_targets.contains(&id) => {
resolved.push(Inline::Anchor { id });
}
Inline::Anchor { .. } => {}
leaf => resolved.push(leaf),
}
}
*nodes = resolved;
}