use std::collections::{HashMap, HashSet};
use super::reference::{is_manual_reference_name, is_manual_section};
use super::roff_escape::visible_text;
use libmandoc_rs::{Node, NodeKind};
use mant_ir::{Block, Diagnostic, DiagnosticLevel, Inline, LinkTarget, 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.to_string()));
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);
promote_manual_references(&mut section.blocks);
for child in &mut section.children {
resolve_section(child, targets, explicit_targets, diagnostics);
}
}
fn promote_manual_references(blocks: &mut [Block]) {
for block in blocks {
match block {
Block::Paragraph { children, .. } | Block::Preformatted { children, .. } => {
promote_manual_reference_inlines(children);
}
Block::List { items, .. } => {
for item in items {
promote_manual_references(&mut item.blocks);
}
}
Block::DefinitionList { items, .. } => {
for item in items {
for term in &mut item.terms {
promote_manual_reference_inlines(term);
}
promote_manual_references(&mut item.description);
}
}
Block::Table { rows, .. } => {
for cell in rows.iter_mut().flat_map(|row| &mut row.cells) {
promote_manual_references(&mut cell.blocks);
}
}
Block::Equation { .. }
| Block::VerticalSpace { .. }
| Block::ThematicBreak { .. }
| Block::Unsupported { .. } => {}
}
}
}
fn promote_manual_reference_inlines(nodes: &mut Vec<Inline>) {
let mut promoted = Vec::with_capacity(nodes.len());
let mut source = std::mem::take(nodes).into_iter().peekable();
while let Some(node) = source.next() {
let (Inline::Strong { children } | Inline::Emphasis { children }) = &node else {
promoted.push(node);
continue;
};
let name = crate::inline::plain_text(children);
let Some(Inline::Text { value }) = source.peek() else {
promoted.push(node);
continue;
};
let Some((section, remainder)) = manual_section_suffix(value) else {
promoted.push(node);
continue;
};
if !is_manual_reference_name(&name) {
promoted.push(node);
continue;
}
source.next();
promoted.push(Inline::Link {
target: LinkTarget::Manual {
name: name.clone(),
manual_section: Some(section.clone()),
},
title: None,
children: vec![Inline::Text {
value: format!("{name}({section})"),
}],
});
let remainder = remainder.strip_prefix(" <>").unwrap_or(&remainder);
if !remainder.is_empty() {
promoted.push(Inline::Text {
value: remainder.to_owned(),
});
}
}
*nodes = promoted;
}
fn manual_section_suffix(value: &str) -> Option<(String, String)> {
let value = value.strip_prefix('(')?;
let closing = value.find(')')?;
let section = &value[..closing];
if !is_manual_section(section) {
return None;
}
Some((section.to_owned(), value[closing + 1..].to_owned()))
}
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::Link {
target: LinkTarget::Section { id },
title,
mut children,
} => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
if let Some(Some(section_id)) = targets.get(id.as_str()) {
resolved.push(Inline::Link {
target: LinkTarget::Section {
id: section_id.as_str().into(),
},
title,
children,
});
} else {
diagnostics.push(Diagnostic {
level: DiagnosticLevel::Warning,
code: Some("unresolved-section-reference".to_owned()),
message: format!("cannot resolve section reference: {id}"),
source: None,
});
resolved.extend(children);
}
}
Inline::Link {
target,
title,
mut children,
} => {
resolve_inlines(&mut children, targets, explicit_targets, diagnostics);
resolved.push(Inline::Link {
target,
title,
children,
});
}
Inline::Anchor { id } if explicit_targets.contains(id.as_str()) => {
resolved.push(Inline::Anchor { id });
}
Inline::Anchor { .. } => {}
leaf => resolved.push(leaf),
}
}
*nodes = resolved;
}
#[cfg(test)]
mod tests {
use mant_ir::Inline;
use super::promote_manual_reference_inlines;
#[test]
fn promotes_traditional_see_also_pairs_without_consuming_punctuation() {
let mut nodes = vec![
Inline::Strong {
children: vec![Inline::Text {
value: "printf".to_owned(),
}],
},
Inline::Text {
value: "(3), next".to_owned(),
},
];
promote_manual_reference_inlines(&mut nodes);
assert!(matches!(
&nodes[0],
Inline::Link { target: mant_ir::LinkTarget::Manual { name, manual_section: Some(manual_section) }, .. }
if name == "printf" && manual_section == "3"
));
assert!(matches!(&nodes[1], Inline::Text { value } if value == ", next"));
}
#[test]
fn promotes_manual_pairs_outside_see_also_sections() {
let mut nodes = vec![
Inline::Strong {
children: vec![Inline::Text {
value: "git-add".to_owned(),
}],
},
Inline::Text {
value: "(1)".to_owned(),
},
];
promote_manual_reference_inlines(&mut nodes);
assert!(matches!(
&nodes[0],
Inline::Link { target: mant_ir::LinkTarget::Manual { name, manual_section: Some(manual_section) }, children, .. }
if name == "git-add"
&& manual_section == "1"
&& crate::inline::plain_text(children) == "git-add(1)"
));
}
#[test]
fn promotes_groff_mr_fallback_pairs_from_emphasis() {
let mut nodes = vec![
Inline::Emphasis {
children: vec![Inline::Text {
value: "groff_man".to_owned(),
}],
},
Inline::Text {
value: "(7), next".to_owned(),
},
];
promote_manual_reference_inlines(&mut nodes);
assert!(matches!(
&nodes[0],
Inline::Link { target: mant_ir::LinkTarget::Manual { name, manual_section: Some(manual_section) }, .. }
if name == "groff_man" && manual_section == "7"
));
assert!(matches!(&nodes[1], Inline::Text { value } if value == ", next"));
}
#[test]
fn removes_empty_sphinx_destination_after_styled_reference() {
let mut nodes = vec![
Inline::Strong {
children: vec![Inline::Text {
value: "btrfs".to_owned(),
}],
},
Inline::Text {
value: "(5) <>, next".to_owned(),
},
];
promote_manual_reference_inlines(&mut nodes);
assert!(matches!(
&nodes[0],
Inline::Link { target: mant_ir::LinkTarget::Manual { name, manual_section: Some(manual_section) }, .. }
if name == "btrfs" && manual_section == "5"
));
assert!(matches!(&nodes[1], Inline::Text { value } if value == ", next"));
}
#[test]
fn leaves_prose_and_malformed_sections_unchanged() {
for suffix in [" documentation", "()", "(0)", "(section one)"] {
let mut nodes = vec![
Inline::Strong {
children: vec![Inline::Text {
value: "tool".to_owned(),
}],
},
Inline::Text {
value: suffix.to_owned(),
},
];
promote_manual_reference_inlines(&mut nodes);
assert!(matches!(nodes[0], Inline::Strong { .. }));
}
let mut emphasized_prose = vec![
Inline::Emphasis {
children: vec![Inline::Text {
value: "tool".to_owned(),
}],
},
Inline::Text {
value: " documentation".to_owned(),
},
];
promote_manual_reference_inlines(&mut emphasized_prose);
assert!(matches!(emphasized_prose[0], Inline::Emphasis { .. }));
}
}