mant-loader 0.11.0

Read-only local document discovery and bounded loading for ManT
Documentation
//! Resolves logical document names across registered sources, manuals, and tldr.

use super::{
    DocumentAddress, FullDocumentMode, LoadError, LoadHost, LoadPolicy, LoadedManual,
    ManualLoadError, ManualRequest, QuickReferenceMode, RegisteredLookupPhase, RegisteredSelection,
    RegisteredSelectionGroup, ResolvedContent, TldrDocument, load_markdown_text,
};

pub(super) fn query_named_document(
    name: &str,
    requested_source: Option<&str>,
    requested_manual_section: Option<&str>,
    policy: LoadPolicy,
    host: &dyn LoadHost,
) -> Result<ResolvedContent, LoadError> {
    let name = name.trim();
    if name.is_empty() {
        return Err(LoadError::EmptyName);
    }
    if let Some(address) = DocumentAddress::parse_catalog_path(name) {
        if requested_source.is_some() || requested_manual_section.is_some() {
            return Err(LoadError::ConflictingSourceSelectors);
        }
        return query_catalog_address(name, &address, policy, host);
    }
    let section = requested_manual_section.map(str::trim);
    if section.is_some_and(|section| !crate::is_manual_section(section)) {
        return Err(LoadError::InvalidManualSection);
    }
    let section = section.map(ToOwned::to_owned);
    let source = requested_source.map(str::trim);
    if source.is_some_and(str::is_empty) {
        return Err(LoadError::InvalidSource);
    }
    let plan = policy.named_resolution_plan(section.is_some());
    if source.is_some() && (section.is_some() || plan.document == FullDocumentMode::NativeManual) {
        return Err(LoadError::ConflictingSourceSelectors);
    }
    let candidates = host.name_candidates(name);

    if plan.quick_reference == QuickReferenceMode::Only {
        if let Some(section) = section.as_deref()
            && !crate::is_command_manual_section(section)
        {
            return Err(LoadError::TldrManualSection {
                section: section.to_owned(),
            });
        }
        return query_tldr_only(name, &candidates, source, host);
    }

    // Personal documents and positive-priority sources form the preferred
    // registration phase. Explicit source selection always wins regardless of
    // its configured rank. Non-positive sources are consulted only after the
    // priority-zero native-manual phase fails.
    if plan.document == FullDocumentMode::Priority {
        let registered = host
            .locate_registered_document(&candidates, source, RegisteredLookupPhase::BeforeBuiltin)
            .map_err(|detail| LoadError::Registry { detail })?;
        if let Some(registered) = registered {
            return query_registered_document(name, &registered, host);
        }
        if source.is_some() {
            return Err(LoadError::NoReadableContent {
                name: name.to_owned(),
            });
        }
    }

    if plan.document == FullDocumentMode::NativeManual && !host.native_available() {
        return Err(LoadError::NativeBackendUnavailable { tldr_topic: None });
    }
    let mut manual = if host.native_available() {
        load_manual(name, &candidates, section.as_deref(), host).map_err(LoadError::Manual)
    } else {
        Err(LoadError::NativeBackendUnavailable { tldr_topic: None })
    };

    // A malformed page may omit its own section metadata. Preserve the
    // requested section so labels stay `name(N)`.
    if let (Ok(manual), Some(section)) = (&mut manual, section.as_deref())
        && manual.document.meta.manual_section.is_none()
    {
        manual.document.meta.manual_section = Some(section.to_owned());
    }

    let tldr = match plan.quick_reference {
        QuickReferenceMode::AttachToCommandManual => match &manual {
            Ok(manual) if manual_accepts_tldr(manual) => host.read_tldr(name).ok().flatten(),
            Err(_)
                if section
                    .as_deref()
                    .is_none_or(crate::is_command_manual_section) =>
            {
                host.read_tldr(name).ok().flatten()
            }
            Ok(_) | Err(_) => None,
        },
        QuickReferenceMode::Exclude => None,
        QuickReferenceMode::Only => unreachable!("tldr-only queries returned before manual I/O"),
    };

    match plan.document {
        FullDocumentMode::Priority => {
            finish_unqualified_manual(name, &candidates, manual, tldr, host)
        }
        FullDocumentMode::NativeManual => finish_selected_manual(name, manual, tldr),
        FullDocumentMode::None => unreachable!("tldr-only queries returned before manual I/O"),
    }
}

fn manual_accepts_tldr(manual: &LoadedManual) -> bool {
    let DocumentAddress::Manual { manual_section, .. } = &manual.address else {
        return false;
    };
    crate::is_command_manual_section(manual_section)
}

fn query_catalog_address(
    selector: &str,
    address: &DocumentAddress,
    policy: LoadPolicy,
    host: &dyn LoadHost,
) -> Result<ResolvedContent, LoadError> {
    match address {
        DocumentAddress::Markdown { .. } if policy == LoadPolicy::TldrOnly => {
            let registered = host
                .locate_registered_address(address)
                .map_err(|detail| LoadError::Registry { detail })?
                .ok_or_else(|| LoadError::TldrNotFound {
                    topic: selector.to_owned(),
                })?;
            query_registered_tldr(selector, &registered, host)?.ok_or_else(|| {
                LoadError::TldrNotFound {
                    topic: selector.to_owned(),
                }
            })
        }
        DocumentAddress::Markdown { .. } if policy == LoadPolicy::ManualOnly => {
            Err(LoadError::ConflictingSourceSelectors)
        }
        DocumentAddress::Markdown { .. } => {
            let registered = host
                .locate_registered_address(address)
                .map_err(|detail| LoadError::Registry { detail })?
                .ok_or_else(|| LoadError::NoReadableContent {
                    name: selector.to_owned(),
                })?;
            query_registered_document(selector, &registered, host)
        }
        DocumentAddress::Manual {
            name,
            manual_section,
        } => query_named_document(name, None, Some(manual_section), policy, host),
    }
}

fn query_tldr_only(
    name: &str,
    candidates: &[String],
    source: Option<&str>,
    host: &dyn LoadHost,
) -> Result<ResolvedContent, LoadError> {
    let before = host
        .locate_registered_document_groups(candidates, source, RegisteredLookupPhase::BeforeBuiltin)
        .map_err(|detail| LoadError::Registry { detail })?;
    if let Some(tldr) = first_registered_tldr(name, before, host)? {
        return Ok(tldr);
    }
    if source.is_some() {
        return Err(LoadError::TldrNotFound {
            topic: name.to_owned(),
        });
    }

    if let Some(tldr) = host.read_tldr(name).map_err(|detail| LoadError::Tldr {
        topic: name.to_owned(),
        detail,
    })? {
        return Ok(ResolvedContent {
            address: None,
            label: name.to_owned(),
            document: None,
            tldr: Some(tldr),
        });
    }

    let after = host
        .locate_registered_document_groups(candidates, None, RegisteredLookupPhase::AfterBuiltin)
        .map_err(|detail| LoadError::Registry { detail })?;
    first_registered_tldr(name, after, host)?.ok_or_else(|| LoadError::TldrNotFound {
        topic: name.to_owned(),
    })
}

fn first_registered_tldr(
    name: &str,
    groups: Vec<RegisteredSelectionGroup>,
    host: &dyn LoadHost,
) -> Result<Option<ResolvedContent>, LoadError> {
    for group in groups {
        let mut matches = Vec::new();
        for registered in group.documents {
            if let Some(tldr) = query_registered_tldr(name, &registered, host)? {
                matches.push(tldr);
            }
        }
        match matches.len() {
            0 => {}
            1 => return Ok(matches.pop()),
            _ => {
                let choices = matches
                    .iter()
                    .filter_map(|candidate| candidate.address.as_ref())
                    .map(DocumentAddress::catalog_path)
                    .collect::<Vec<_>>()
                    .join("', '");
                return Err(LoadError::Registry {
                    detail: format!(
                        "tldr selector '{name}' is ambiguous at one document priority: '{choices}'"
                    ),
                });
            }
        }
    }
    Ok(None)
}

fn query_registered_tldr(
    name: &str,
    registered: &RegisteredSelection,
    host: &dyn LoadHost,
) -> Result<Option<ResolvedContent>, LoadError> {
    let resolved = query_registered_document(name, registered, host)?;
    let Some(tldr) = resolved.tldr else {
        return Ok(None);
    };
    Ok(Some(ResolvedContent {
        address: resolved.address,
        label: resolved.label,
        document: None,
        tldr: Some(tldr),
    }))
}

fn finish_selected_manual(
    name: &str,
    manual: Result<LoadedManual, LoadError>,
    tldr: Option<TldrDocument>,
) -> Result<ResolvedContent, LoadError> {
    match manual {
        Ok(manual) => Ok(ResolvedContent {
            address: Some(manual.address),
            label: name.to_owned(),
            document: Some(manual.document),
            tldr,
        }),
        Err(error) if tldr.is_some() => Err(with_tldr_hint(error, name)),
        Err(error) => Err(error),
    }
}

fn finish_unqualified_manual(
    name: &str,
    candidates: &[String],
    manual: Result<LoadedManual, LoadError>,
    tldr: Option<TldrDocument>,
    host: &dyn LoadHost,
) -> Result<ResolvedContent, LoadError> {
    match manual {
        Ok(manual) => Ok(ResolvedContent {
            address: Some(manual.address),
            label: name.to_owned(),
            document: Some(manual.document),
            tldr,
        }),
        Err(error) => {
            let registered = host
                .locate_registered_document(candidates, None, RegisteredLookupPhase::AfterBuiltin)
                .map_err(|detail| LoadError::Registry { detail })?;
            if let Some(registered) = registered {
                query_registered_document(name, &registered, host)
            } else if tldr.is_some() {
                Err(with_tldr_hint(error, name))
            } else {
                Err(error)
            }
        }
    }
}

fn with_tldr_hint(error: LoadError, topic: &str) -> LoadError {
    match error {
        LoadError::Manual(error) => LoadError::ManualWithTldr {
            error,
            topic: topic.to_owned(),
        },
        LoadError::NativeBackendUnavailable { .. } => LoadError::NativeBackendUnavailable {
            tldr_topic: Some(topic.to_owned()),
        },
        other => other,
    }
}

fn query_registered_document(
    name: &str,
    registered: &RegisteredSelection,
    host: &dyn LoadHost,
) -> Result<ResolvedContent, LoadError> {
    let path = &registered.path;
    let source_path = path.to_string_lossy().into_owned();
    let source = host
        .read_markdown(path)
        .map_err(|detail| LoadError::Markdown {
            path: source_path.clone(),
            detail,
        })?;
    let mut query = load_markdown_text(&source, Some(source_path))?;
    name.clone_into(&mut query.label);
    query.address = Some(registered.address.clone());
    Ok(query)
}

fn load_manual(
    requested_name: &str,
    candidates: &[String],
    section: Option<&str>,
    host: &dyn LoadHost,
) -> Result<LoadedManual, ManualLoadError> {
    let mut first_locate_error = None;
    let mut located = None;
    for candidate in candidates {
        let request = ManualRequest::new(candidate, section.map(ToOwned::to_owned));
        match host.locate_manual(&request) {
            Ok(page) => {
                located = Some(page);
                break;
            }
            Err(error) => {
                first_locate_error.get_or_insert(error);
            }
        }
    }
    let Some(page) = located else {
        let error =
            first_locate_error.unwrap_or_else(|| "no name candidates were available".to_owned());
        return Err(ManualLoadError::NotFound {
            name: requested_name.to_owned(),
            detail: error,
        });
    };

    let source_path = page.path.clone();
    let address = DocumentAddress::Manual {
        name: page.name.clone(),
        manual_section: page.section.clone(),
    };
    let document = host
        .parse_manual(&page)
        .map_err(|detail| ManualLoadError::Parse {
            name: requested_name.to_owned(),
            detail,
        })?;
    if document.sections.is_empty() && document.blocks.is_empty() {
        let diagnostics = document
            .diagnostics
            .iter()
            .map(|diagnostic| {
                let location = diagnostic.source.map_or_else(String::new, |source| {
                    format!(" at {}:{}", source.line, source.column)
                });
                format!("{:?}{location}: {}", diagnostic.level, diagnostic.message)
            })
            .collect::<Vec<_>>();
        return Err(ManualLoadError::Empty {
            name: requested_name.to_owned(),
            path: source_path,
            diagnostics,
        });
    }
    Ok(LoadedManual { document, address })
}