use std::collections::{BTreeMap, BTreeSet};
use crate::{
DOCUMENT_ROOT_ID, DefinitionItem, Document, FragmentAlias, Inline, NodeId, Section,
visit::{self, Visit},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IndexedRole {
Section,
Entry,
Anchor,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedNode {
roles: BTreeSet<IndexedRole>,
containing_section: Option<NodeId>,
}
impl IndexedNode {
#[must_use]
pub fn roles(&self) -> &BTreeSet<IndexedRole> {
&self.roles
}
#[must_use]
pub fn containing_section(&self) -> Option<&NodeId> {
self.containing_section.as_ref()
}
#[must_use]
pub fn has_role(&self, role: IndexedRole) -> bool {
self.roles.contains(&role)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DuplicateIdentity {
pub id: NodeId,
pub role: IndexedRole,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DocumentIndex {
nodes: BTreeMap<NodeId, IndexedNode>,
duplicates: Vec<DuplicateIdentity>,
fragment_targets: BTreeMap<FragmentAlias, BTreeSet<NodeId>>,
authored_fragments: BTreeSet<FragmentAlias>,
}
impl DocumentIndex {
#[must_use]
pub fn build(document: &Document) -> Self {
let mut builder = IndexBuilder::default();
builder.visit_document(document);
if (document.heading.is_some()
|| !document.blocks.is_empty()
|| !document.fragment_aliases.is_empty())
&& !builder.index.nodes.contains_key(DOCUMENT_ROOT_ID)
{
builder.register(&NodeId::from(DOCUMENT_ROOT_ID), IndexedRole::Anchor);
}
for alias in &document.fragment_aliases {
builder.register_fragment(alias.clone(), &NodeId::from(DOCUMENT_ROOT_ID), true);
}
builder.index
}
#[must_use]
pub fn get(&self, id: &str) -> Option<&IndexedNode> {
self.nodes.get(id)
}
#[must_use]
pub fn contains(&self, id: &str) -> bool {
self.nodes.contains_key(id)
}
pub fn iter(&self) -> impl Iterator<Item = (&NodeId, &IndexedNode)> {
self.nodes.iter()
}
#[must_use]
pub fn duplicates(&self) -> &[DuplicateIdentity] {
&self.duplicates
}
#[must_use]
pub fn fragment_target(&self, fragment: &str) -> Option<&NodeId> {
let targets = self.fragment_targets.get(fragment)?;
let mut targets = targets.iter();
let target = targets.next()?;
targets.next().is_none().then_some(target)
}
pub fn authored_fragments(&self) -> impl Iterator<Item = &FragmentAlias> {
self.authored_fragments.iter()
}
pub fn ambiguous_fragments(&self) -> impl Iterator<Item = (&FragmentAlias, &BTreeSet<NodeId>)> {
self.fragment_targets
.iter()
.filter(|(_, targets)| targets.len() > 1)
}
}
#[derive(Default)]
struct IndexBuilder {
index: DocumentIndex,
section_stack: Vec<NodeId>,
}
impl IndexBuilder {
fn register(&mut self, id: &NodeId, role: IndexedRole) {
self.register_fragment(FragmentAlias::from(id.as_str()), id, false);
let containing_section = self.section_stack.last().cloned();
let node = self
.index
.nodes
.entry(id.clone())
.or_insert_with(|| IndexedNode {
roles: BTreeSet::new(),
containing_section,
});
if !node.roles.insert(role) {
self.index.duplicates.push(DuplicateIdentity {
id: id.clone(),
role,
});
}
}
fn register_fragment(&mut self, alias: FragmentAlias, id: &NodeId, authored: bool) {
if authored {
self.index.authored_fragments.insert(alias.clone());
}
self.index
.fragment_targets
.entry(alias)
.or_default()
.insert(id.clone());
}
}
impl<'ir> Visit<'ir> for IndexBuilder {
fn visit_list_item(&mut self, item: &'ir crate::ListItem) {
if let Some(facts) = &item.entry {
self.register(&facts.id, IndexedRole::Entry);
}
visit::walk_list_item(self, item);
}
fn visit_section(&mut self, section: &'ir Section) {
self.register(§ion.id, IndexedRole::Section);
for alias in §ion.fragment_aliases {
self.register_fragment(alias.clone(), §ion.id, true);
}
self.section_stack.push(section.id.clone());
visit::walk_section(self, section);
self.section_stack.pop();
}
fn visit_definition_item(&mut self, item: &'ir DefinitionItem) {
if let Some(identity) = &item.entry {
self.register(&identity.id, IndexedRole::Entry);
}
visit::walk_definition_item(self, item);
}
fn visit_inline(&mut self, inline: &'ir Inline) {
if let Inline::Anchor {
id,
fragment_aliases,
..
} = inline
{
self.register(id, IndexedRole::Anchor);
for alias in fragment_aliases {
self.register_fragment(alias.clone(), id, true);
}
}
visit::walk_inline(self, inline);
}
}
#[cfg(test)]
mod tests {
use crate::{DocumentMeta, DocumentSource, EntryFacts, EntryKind, NameCase, SourceFormat};
use super::*;
#[test]
fn indexes_shared_entry_anchors_without_calling_them_duplicates() {
let id = NodeId::from("help");
let document = Document {
heading: None,
parser: None,
source: DocumentSource {
format: SourceFormat::Markdown,
path: None,
},
meta: DocumentMeta::default(),
fragment_aliases: Vec::new(),
diagnostics: Vec::new(),
blocks: vec![crate::Block::DefinitionList {
declaration_groups: Vec::new(),
items: vec![DefinitionItem {
source: None,
entry: Some(EntryFacts {
name_bindings: Vec::new(),
alias_groups: Vec::new(),
alias_of: None,
forms: Vec::new(),
id: id.clone(),
kind: EntryKind::Parameter {
parameter_kind: crate::ParameterKind::Option,
},
case: NameCase::Sensitive,
names: vec!["--help".to_owned()],
value_domain: None,
}),
terms: vec![vec![Inline::anchor(id.clone())]],
description: Vec::new(),
layout: crate::DefinitionLayout {
inline_term: false,
spacing_before_lines: None,
..Default::default()
},
}],
compact: false,
layout: crate::LayoutHint::default(),
source: None,
}],
sections: Vec::new(),
};
let index = DocumentIndex::build(&document);
let indexed = index.get("help").expect("entry must be indexed");
assert_eq!(
indexed.roles(),
&BTreeSet::from([IndexedRole::Entry, IndexedRole::Anchor])
);
assert!(index.duplicates().is_empty());
}
#[test]
fn resolves_exact_fragments_to_normalized_targets_without_guessing() {
let mut section = Section {
id: "mixed-target".into(),
fragment_aliases: vec![FragmentAlias::from("Mixed.Target")],
heading: "Mixed target".into(),
spacing_before_lines: 0,
blocks: Vec::new(),
children: Vec::new(),
source: None,
};
section.blocks.push(crate::Block::Paragraph {
children: vec![Inline::anchor_with_aliases(
"option",
vec![FragmentAlias::from("--option")],
)],
layout: crate::LayoutHint::default(),
source: None,
});
let document = Document {
heading: None,
parser: None,
source: DocumentSource {
format: SourceFormat::Markdown,
path: None,
},
meta: DocumentMeta::default(),
fragment_aliases: Vec::new(),
diagnostics: Vec::new(),
blocks: Vec::new(),
sections: vec![section],
};
let index = DocumentIndex::build(&document);
assert_eq!(
index.fragment_target("Mixed.Target").map(NodeId::as_str),
Some("mixed-target")
);
assert_eq!(
index.fragment_target("--option").map(NodeId::as_str),
Some("option")
);
assert_eq!(
index.fragment_target("mixed-target").map(NodeId::as_str),
Some("mixed-target")
);
}
}