mod collect;
mod literal;
mod location;
pub use location::resolve_explanation_block;
mod details;
mod matches;
mod materialize;
mod page;
mod plan;
mod positions;
mod preview;
mod relations;
mod scoped;
mod support;
pub(crate) use scoped::explain as explain_scope;
use crate::selectors::{LocatedNode, collect_root_entries, collect_sections};
use mant_ir::{Block, EntryOwner, NameCase, OutlinePath, ResolvedContent, SourceSpan};
use mant_protocol::{EvidenceBasis, ExplanationQuery, QueryExplanation};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExplanationError {
Entry(mant_protocol::ScopeTextError),
ResultLimit,
ContentLimit,
MissingContent,
}
impl std::fmt::Display for ExplanationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Entry(mant_protocol::ScopeTextError::Empty) => {
f.write_str("explanation entry must not be empty")
}
Self::Entry(mant_protocol::ScopeTextError::ControlCharacter) => {
f.write_str("explanation entry must not contain control characters")
}
Self::Entry(mant_protocol::ScopeTextError::TooLong { maximum }) => write!(
f,
"explanation entry must not exceed {maximum} Unicode scalar values"
),
Self::ResultLimit => f.write_str("explanation limit must be between 1 and 256"),
Self::ContentLimit => {
f.write_str("explanation content budget must be between 1 and 4194304 bytes")
}
Self::MissingContent => f.write_str("explanation requires readable content"),
}
}
}
impl std::error::Error for ExplanationError {}
pub fn validate_explanation_query(query: &ExplanationQuery) -> Result<(), ExplanationError> {
mant_protocol::validate_scope_text(&query.entry, mant_protocol::MAX_SEMANTIC_ENTRY_CHARS)
.map_err(ExplanationError::Entry)?;
if !(1..=mant_protocol::MAX_EXPLANATION_RESULTS).contains(&query.options.limit) {
return Err(ExplanationError::ResultLimit);
}
if !(1..=mant_protocol::MAX_EXPLANATION_CONTENT_BYTES).contains(&query.options.content_bytes) {
return Err(ExplanationError::ContentLimit);
}
Ok(())
}
pub fn explain_query(
content: &ResolvedContent,
query: &ExplanationQuery,
) -> Result<QueryExplanation, ExplanationError> {
explain_with_usage(content, query).map(|(response, _)| response)
}
pub(crate) fn explain_with_usage(
content: &ResolvedContent,
query: &ExplanationQuery,
) -> Result<(QueryExplanation, u32), ExplanationError> {
validate_explanation_query(query)?;
let plan = collection_plan(content, query.entry.trim())?;
Ok(materialize::response(plan, query))
}
fn collection_plan<'a>(
content: &'a ResolvedContent,
entry: &str,
) -> Result<plan::CollectionPlan<'a>, ExplanationError> {
if content.document.is_none() && content.tldr.is_none() {
return Err(ExplanationError::MissingContent);
}
let mut located = Vec::new();
if let Some(document) = &content.document {
collect_root_entries(&document.blocks, &mut located);
collect_sections(&document.sections, &[], &[], &mut located);
}
let validation = content
.document
.as_ref()
.map(mant_ir::DocumentValidation::new);
let (mut candidates, orders, supports) =
collect::collect(content, entry, &located, validation.as_ref());
let relations = relations::expand(
validation.as_ref(),
entry,
&located,
&orders,
&mut candidates,
);
let (candidates, truncated) = candidates.finish();
let mut diagnostics = content
.document
.as_ref()
.map(|d| d.diagnostics.clone())
.unwrap_or_default();
let rejected_aliases = validation
.as_ref()
.into_iter()
.flat_map(mant_ir::DocumentValidation::relation_issues)
.filter(|issue| {
matches!(
issue.kind,
mant_ir::EntryRelationIssueKind::AliasOf | mant_ir::EntryRelationIssueKind::Cycle
)
})
.map(|issue| issue.owner.clone())
.collect();
if let Some(validation) = validation {
for diagnostic in validation.into_diagnostics() {
if !diagnostics.contains(&diagnostic) {
diagnostics.push(diagnostic);
}
}
}
Ok(plan::CollectionPlan {
content,
located,
candidates,
supports,
diagnostics,
rejected_aliases,
truncation: mant_protocol::ExplanationTruncation {
candidates: truncated,
relations,
content: false,
},
})
}
pub fn select_explanation(
content: &ResolvedContent,
entry: &str,
) -> Result<QueryExplanation, ExplanationError> {
explain_query(
content,
&ExplanationQuery {
entry: entry.to_owned(),
options: mant_protocol::ExplanationOptions::default(),
},
)
}
struct Candidate<'a> {
order: usize,
located: Option<usize>,
ordinary: Option<&'a Block>,
section: Option<usize>,
block_path: Option<String>,
source: Option<SourceSpan>,
bases: Vec<EvidenceBasis>,
matched: matches::MatchPlan,
hits: Vec<preview::LiteralHit<'a>>,
}
fn same(left: &str, right: &str, case: NameCase) -> bool {
match case {
NameCase::Sensitive => left == right,
NameCase::Insensitive => left.eq_ignore_ascii_case(right),
}
}
fn owner<'a>(node: &LocatedNode<'a>) -> Option<EntryOwner<'a>> {
match node {
LocatedNode::Entry { entry, .. } => Some(entry.owner()),
LocatedNode::Section { .. } => None,
}
}
fn is_identity(node: &LocatedNode<'_>, entry: &str) -> bool {
node.id() == entry
|| entry
.parse::<OutlinePath>()
.is_ok_and(|path| &path == node.path())
}