use std::collections::BTreeMap;
use super::{file_name, normalize_import_path, strip_extension};
#[derive(Debug, Clone)]
enum LookupValue {
Found(String),
Ambiguous,
}
#[derive(Debug, Default)]
struct PathLookup {
exact: BTreeMap<String, String>,
names: BTreeMap<String, LookupValue>,
}
impl PathLookup {
fn register(&mut self, path: &str, value: &str, extension_optional: bool) {
let path = lookup_key(path);
self.exact.insert(path.clone(), value.to_string());
self.register_name(file_name(&path), value);
if extension_optional {
let path = strip_extension(&path);
self.exact.insert(path.to_string(), value.to_string());
self.register_name(file_name(path), value);
}
}
fn register_alias(&mut self, alias: &str, value: &str) {
self.register_name(&lookup_key(alias), value);
}
fn register_name(&mut self, name: &str, value: &str) {
let key = lookup_key(name);
match self.names.get(&key) {
Some(LookupValue::Found(existing)) if existing != value => {
self.names.insert(key, LookupValue::Ambiguous);
}
None => {
self.names.insert(key, LookupValue::Found(value.to_string()));
}
_ => {}
}
}
fn resolve(
&self,
target: &str,
current_path: &str,
extension: Option<&str>,
preferred_root: Option<&str>,
) -> Option<&str> {
let target = lookup_key(target);
if target.is_empty() {
return None;
}
let with_extension = extension
.filter(|extension| !target.to_lowercase().ends_with(extension))
.map(|extension| format!("{target}{extension}"));
let base = current_path.rsplit_once('/').map(|(base, _)| base).unwrap_or("");
let relative = lookup_key(&format!("{base}/{target}"));
let relative_with_extension = with_extension
.as_ref()
.map(|target| lookup_key(&format!("{base}/{target}")));
let explicit_path = target.contains('/');
let candidates = if explicit_path {
[
Some(target.as_str()),
with_extension.as_deref(),
Some(relative.as_str()),
relative_with_extension.as_deref(),
]
} else {
[
Some(relative.as_str()),
relative_with_extension.as_deref(),
Some(target.as_str()),
with_extension.as_deref(),
]
};
for candidate in candidates.into_iter().flatten() {
if let Some(value) = self.exact.get(candidate) {
return Some(value);
}
}
if let Some(root) = preferred_root {
for candidate in [Some(target.as_str()), with_extension.as_deref()].into_iter().flatten() {
let candidate = lookup_key(&format!("{root}/{candidate}"));
if let Some(value) = self.exact.get(&candidate) {
return Some(value);
}
}
}
let name = file_name(&target);
match self.names.get(name) {
Some(LookupValue::Found(value)) => Some(value),
_ => with_extension
.as_deref()
.and_then(|path| self.names.get(file_name(path)))
.and_then(|value| match value {
LookupValue::Found(value) => Some(value.as_str()),
LookupValue::Ambiguous => None,
}),
}
}
}
#[derive(Debug, Default)]
pub(super) struct VaultIndex {
pages: PathLookup,
assets: PathLookup,
anchors: BTreeMap<String, BTreeMap<String, String>>,
attachment_folder: Option<String>,
}
impl VaultIndex {
pub(super) fn register_page(&mut self, path: &str, doc_id: &str) {
self.pages.register(path, doc_id, true);
}
pub(super) fn register_page_alias(&mut self, alias: &str, doc_id: &str) {
self.pages.register_alias(alias, doc_id);
}
pub(super) fn register_asset(&mut self, path: &str, blob_id: &str) {
self.assets.register(path, blob_id, false);
}
pub(super) fn register_anchors(&mut self, doc_id: &str, anchors: BTreeMap<String, String>) {
self.anchors.insert(doc_id.to_string(), anchors);
}
pub(super) fn set_attachment_folder(&mut self, path: Option<String>) {
self.attachment_folder = path.filter(|path| !path.is_empty() && path != ".");
}
pub(super) fn resolve_page(&self, target: &str, current_path: &str) -> Option<&str> {
self.pages.resolve(target, current_path, Some(".md"), None)
}
pub(super) fn resolve_asset(&self, target: &str, current_path: &str) -> Option<&str> {
self
.assets
.resolve(target, current_path, None, self.attachment_folder.as_deref())
}
pub(super) fn resolve_anchor(&self, doc_id: &str, fragment: &str) -> Option<&str> {
self
.anchors
.get(doc_id)
.and_then(|anchors| anchors.get(&fragment_key(fragment)))
.map(String::as_str)
}
}
pub(super) fn fragment_key(fragment: &str) -> String {
fragment.trim().trim_start_matches('#').to_lowercase()
}
fn lookup_key(path: &str) -> String {
normalize_import_path(path.trim().trim_start_matches('/')).to_lowercase()
}