use super::{
model::{DocumentReference, EntrySummary, SemanticDocumentTarget, SemanticEntry, ValueDomain},
walk::{owner_child_step, visit_child_entry_locations},
};
use crate::{Block, Document, EntryOwner, Inline, NodeId};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SemanticIndex {
root: Vec<SemanticEntry>,
sections: BTreeMap<Vec<usize>, Vec<SemanticEntry>>,
section_paths: BTreeMap<NodeId, Option<Vec<usize>>>,
owner_locations: BTreeMap<crate::OutlinePath, crate::ContentReveal>,
}
impl SemanticIndex {
#[must_use]
pub fn build(document: &Document) -> Self {
let mut owner_locations = BTreeMap::new();
let root = entries_with_locations(&document.blocks, &[], &[], &[], &mut owner_locations);
let mut sections = BTreeMap::new();
let mut section_paths = BTreeMap::new();
collect_section_entries(
&document.sections,
&[],
&mut sections,
&mut section_paths,
&mut owner_locations,
);
let mut result = Self {
root,
sections,
section_paths,
owner_locations,
};
if result
.root
.iter()
.chain(result.sections.values().flatten())
.any(has_alias_of)
{
let rejected = crate::entry_relation_issues(document)
.into_iter()
.filter(|issue| {
matches!(
issue.kind,
crate::EntryRelationIssueKind::AliasOf
| crate::EntryRelationIssueKind::Cycle
)
})
.map(|issue| issue.owner)
.collect::<std::collections::BTreeSet<_>>();
clear_rejected_relations(&mut result.root, &rejected);
for entries in result.sections.values_mut() {
clear_rejected_relations(entries, &rejected);
}
}
result
}
#[must_use]
pub fn root(&self) -> &[SemanticEntry] {
&self.root
}
#[must_use]
pub fn section(&self, id: &str) -> &[SemanticEntry] {
self.section_paths
.get(id)
.and_then(Option::as_deref)
.map_or(&[], |path| self.section_at(path))
}
#[must_use]
pub fn section_at(&self, path: &[usize]) -> &[SemanticEntry] {
self.sections.get(path).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn owner_at(&self, path: &crate::OutlinePath) -> Option<&crate::ContentReveal> {
self.owner_locations.get(path)
}
#[must_use]
pub fn root_summary(&self) -> EntrySummary {
EntrySummary::for_entries(&self.root)
}
#[must_use]
pub fn section_summary(&self, id: &str) -> EntrySummary {
EntrySummary::for_entries(self.section(id))
}
}
fn has_alias_of(entry: &SemanticEntry) -> bool {
entry.alias_of.is_some() || entry.children.iter().any(has_alias_of)
}
fn clear_rejected_relations(
entries: &mut [SemanticEntry],
rejected: &std::collections::BTreeSet<NodeId>,
) {
for entry in entries {
if rejected.contains(&entry.id) {
entry.alias_of = None;
}
clear_rejected_relations(&mut entry.children, rejected);
}
}
fn collect_section_entries(
sections: &[crate::Section],
parent: &[usize],
output: &mut BTreeMap<Vec<usize>, Vec<SemanticEntry>>,
paths: &mut BTreeMap<NodeId, Option<Vec<usize>>>,
owners: &mut BTreeMap<crate::OutlinePath, crate::ContentReveal>,
) {
for (index, section) in sections.iter().enumerate() {
let mut path = parent.to_vec();
path.push(index);
paths
.entry(section.id.clone())
.and_modify(|path| *path = None)
.or_insert_with(|| Some(path.clone()));
output.insert(
path.clone(),
entries_with_locations(§ion.blocks, &path, &[], &[], owners),
);
collect_section_entries(§ion.children, &path, output, paths, owners);
}
}
#[cfg(test)]
fn entries_in_blocks(blocks: &[Block]) -> Vec<SemanticEntry> {
let mut entries = Vec::new();
super::walk::visit_child_entries(blocks, &mut |item| {
if let Some(entry) = entry_from_owner(item) {
entries.push(entry);
}
});
entries
}
#[cfg(test)]
pub(super) fn entry_from_definition(item: &crate::DefinitionItem) -> Option<SemanticEntry> {
entry_from_owner(EntryOwner::Definition(item))
}
#[cfg(test)]
fn entry_from_owner(item: EntryOwner<'_>) -> Option<SemanticEntry> {
let mut entry = SemanticEntry::from_owner_shallow(item)?;
entry.children = entries_in_blocks(item.blocks());
Some(entry)
}
fn entries_with_locations(
blocks: &[Block],
sections: &[usize],
parent: &[usize],
prefix: &[crate::ContentBlockStep],
owners: &mut BTreeMap<crate::OutlinePath, crate::ContentReveal>,
) -> Vec<SemanticEntry> {
let mut entries = Vec::new();
visit_child_entry_locations(blocks, prefix, &mut |item, _, path, item_index| {
let Some(mut entry) = SemanticEntry::from_owner_shallow(item) else {
return;
};
let mut coordinates = parent.to_vec();
coordinates.push(entries.len() + 1);
let section_path: Vec<_> = sections.iter().map(|index| index + 1).collect();
if let Some(key) = crate::OutlinePath::nested_entry(
(!section_path.is_empty()).then_some(section_path.as_slice()),
&coordinates,
) {
owners.insert(
key,
crate::ContentReveal::Owner {
sections: sections
.iter()
.map(|index| u32::try_from(*index).unwrap_or(u32::MAX))
.collect(),
blocks: path.to_vec(),
item_index,
},
);
}
let mut child_path = path.to_vec();
child_path.push(owner_child_step(item, item_index));
entry.children =
entries_with_locations(item.blocks(), sections, &coordinates, &child_path, owners);
entries.push(entry);
});
entries
}
impl SemanticEntry {
#[must_use]
pub fn from_owner_shallow(item: EntryOwner<'_>) -> Option<Self> {
let identity = item.facts()?;
let forms = item.forms().unwrap_or_default();
let value_domain = identity.value_domain.clone().or_else(|| {
item.has_value_choices()
.then_some(ValueDomain::Choices { exhaustive: false })
});
Some(SemanticEntry {
id: identity.id.clone(),
kind: identity.kind,
names: item.validated_names().unwrap_or_default().to_vec(),
alias_groups: if identity.alias_groups.is_empty() {
Vec::new()
} else {
item.validated_alias_groups().unwrap_or_default().to_vec()
},
alias_of: identity.alias_of.clone(),
case: identity.case,
forms: forms.iter().map(inline_text).collect(),
document_targets: document_targets(&forms),
children: Vec::new(),
value_domain,
})
}
}
fn document_targets(terms: &crate::EntryForms<'_>) -> Vec<SemanticDocumentTarget> {
let mut targets = Vec::new();
for term in terms.iter() {
collect_document_targets(term, &mut targets);
}
targets
}
fn collect_document_targets(inlines: &[Inline], output: &mut Vec<SemanticDocumentTarget>) {
for inline in inlines {
match inline {
Inline::Link {
target, children, ..
} if DocumentReference::from_link_target(target).is_some() => {
let candidate = SemanticDocumentTarget {
label: inline_text(children),
reference: DocumentReference::from_link_target(target)
.expect("the match guard accepts a document reference"),
};
if !output.contains(&candidate) {
output.push(candidate);
}
}
Inline::Strong { children }
| Inline::Emphasis { children }
| Inline::Link { children, .. } => collect_document_targets(children, output),
Inline::Text { .. }
| Inline::Code { .. }
| Inline::Anchor { .. }
| Inline::LineBreak => {}
}
}
}
pub(super) fn inline_text(inlines: &[Inline]) -> String {
let mut output = String::new();
for inline in inlines {
match inline {
Inline::Text { value } | Inline::Code { value } => output.push_str(value),
Inline::Strong { children }
| Inline::Emphasis { children }
| Inline::Link { children, .. } => output.push_str(&inline_text(children)),
Inline::Anchor { .. } => {}
Inline::LineBreak => output.push('\n'),
}
}
output
}