use std::collections::BTreeSet;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use super::{Graph, Target};
use crate::content::ContentFormat;
use crate::document::is_opaque_payload;
use crate::error::Result;
use crate::fs::ReadStorage;
use crate::index::IdIndex;
use crate::link::{self, Link};
use crate::title::{self, TitleIndex};
impl<FS: ReadStorage, Ix: IdIndex> Graph<FS, Ix> {
pub async fn title_index(&self) -> Result<TitleIndex> {
let mut index = TitleIndex::new();
self.scan_titles(PathBuf::new(), &[], &mut index).await?;
Ok(index)
}
pub async fn title_index_scoped(&self, start: &Path, parked: &[PathBuf]) -> Result<TitleIndex> {
let (dirs, needs_full) = self.title_scope(start, parked).await?;
if needs_full {
let mut index = TitleIndex::new();
self.scan_titles(PathBuf::new(), parked, &mut index).await?;
return Ok(index);
}
let mut index = TitleIndex::new();
let files = self.direct_child_files(&dirs).await?;
let listing: BTreeSet<PathBuf> = files.iter().cloned().collect();
for rel in files {
if !is_document_path(&rel) || self.is_shadowed_payload(&rel, &listing).await {
continue;
}
if let Some(stem) = rel.file_stem().and_then(|s| s.to_str()) {
index.insert(stem, rel.clone());
}
if let Ok((_, doc)) = self.load(&rel).await {
let meta = fig::Value::from(&doc.meta);
if let Some(title) = meta.get("title").and_then(fig::Value::as_str) {
index.insert(title, rel.clone());
}
}
}
Ok(index)
}
async fn title_scope(
&self,
start: &Path,
parked: &[PathBuf],
) -> Result<(BTreeSet<PathBuf>, bool)> {
let spanning = self.relations().spanning_relation().map(str::to_owned);
let dir_of = |p: &Path| p.parent().unwrap_or(Path::new("")).to_path_buf();
let is_parked = |dir: &Path| parked.iter().any(|p| dir.starts_with(p));
let mut dirs: BTreeSet<PathBuf> = BTreeSet::new();
let mut visited: BTreeSet<PathBuf> = BTreeSet::new();
let mut queue = vec![link::normalize(start)];
while let Some(path) = queue.pop() {
if !visited.insert(path.clone()) {
continue;
}
let dir = dir_of(&path);
if is_parked(&dir) {
continue;
}
dirs.insert(dir);
let Ok((_, doc)) = self.load(&path).await else {
continue;
};
let meta = fig::Value::from(&doc.meta);
for edge in self.relations().edges(&meta) {
let link = Link::parse(&edge.target);
let is_spanning = Some(edge.relation.as_str()) == spanning.as_deref();
if link.is_external() {
continue;
}
if title::is_alias_shaped(&link.target) {
if is_spanning {
return Ok((BTreeSet::new(), true));
}
continue;
}
if let Target::Path(target) = self.resolve_link(&path, &link) {
let dir = dir_of(&target);
if is_parked(&dir) {
continue;
}
dirs.insert(dir);
if is_spanning {
queue.push(target);
}
}
}
for body_link in link::scan_body_links(&path, &doc.body) {
let link = body_link.link;
if link.is_external() || title::is_alias_shaped(&link.target) {
continue;
}
if let Target::Path(target) = self.resolve_link(&path, &link) {
let dir = dir_of(&target);
if !is_parked(&dir) {
dirs.insert(dir);
}
}
}
}
Ok((dirs, false))
}
pub async fn scan_ids(&self) -> Result<Vec<(crate::identity::Id, PathBuf)>> {
let mut ids = Vec::new();
self.scan_ids_dir(PathBuf::new(), &mut ids).await?;
Ok(ids)
}
pub async fn content_documents(&self) -> Result<Vec<PathBuf>> {
let mut docs = Vec::new();
self.scan_content_dir(PathBuf::new(), &mut docs).await?;
docs.sort();
Ok(docs)
}
pub async fn direct_child_files(&self, dirs: &BTreeSet<PathBuf>) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for dir in dirs {
let Ok(entries) = self.listing(dir).await else {
continue;
};
for entry in entries {
let Some(name) = entry
.file_name()
.and_then(|n| n.to_str())
.map(str::to_owned)
else {
continue;
};
if name.starts_with('.') || !entry.file_type().is_file() {
continue;
}
files.push(if dir.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
dir.join(&name)
});
}
}
Ok(files)
}
pub fn reached_dirs(reachable: &BTreeSet<PathBuf>) -> BTreeSet<PathBuf> {
reachable
.iter()
.map(|p| p.parent().unwrap_or(Path::new("")).to_path_buf())
.collect()
}
fn scan_content_dir<'a>(
&'a self,
rel_dir: PathBuf,
docs: &'a mut Vec<PathBuf>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
let Ok(entries) = self.listing(&rel_dir).await else {
return Ok(());
};
for entry in entries {
let Some(name) = entry
.file_name()
.and_then(|n| n.to_str())
.map(str::to_owned)
else {
continue;
};
if name.starts_with('.') {
continue;
}
let rel = if rel_dir.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
rel_dir.join(&name)
};
if entry.file_type().is_dir() {
self.scan_content_dir(rel, docs).await?;
} else if entry.file_type().is_file()
&& ContentFormat::from_extension(&rel).is_some()
{
docs.push(rel);
}
}
Ok(())
})
}
fn scan_ids_dir<'a>(
&'a self,
rel_dir: PathBuf,
ids: &'a mut Vec<(crate::identity::Id, PathBuf)>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
let Ok(entries) = self.listing(&rel_dir).await else {
return Ok(());
};
let listing = file_listing(&rel_dir, &entries);
for entry in entries {
let Some(name) = entry
.file_name()
.and_then(|n| n.to_str())
.map(str::to_owned)
else {
continue;
};
if name.starts_with('.') {
continue;
}
let rel = if rel_dir.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
rel_dir.join(&name)
};
if entry.file_type().is_dir() {
self.scan_ids_dir(rel, ids).await?;
} else if entry.file_type().is_file()
&& is_document_path(&rel)
&& !self.is_shadowed_payload(&rel, &listing).await
&& let Ok((_, doc)) = self.load(&rel).await
{
let meta = fig::Value::from(&doc.meta);
if let Some(id) = meta.get("id").and_then(fig::Value::as_str)
&& !id.trim().is_empty()
{
ids.push((crate::identity::Id(id.trim().to_string()), rel));
}
}
}
Ok(())
})
}
fn scan_titles<'a>(
&'a self,
rel_dir: PathBuf,
parked: &'a [PathBuf],
index: &'a mut TitleIndex,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
if parked.iter().any(|p| rel_dir.starts_with(p)) {
return Ok(());
}
let Ok(entries) = self.listing(&rel_dir).await else {
return Ok(());
};
let listing = file_listing(&rel_dir, &entries);
for entry in entries {
let Some(name) = entry
.file_name()
.and_then(|n| n.to_str())
.map(str::to_owned)
else {
continue;
};
if name.starts_with('.') {
continue;
}
let rel = if rel_dir.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
rel_dir.join(&name)
};
if entry.file_type().is_dir() {
self.scan_titles(rel, parked, index).await?;
} else if entry.file_type().is_file()
&& is_document_path(&rel)
&& !self.is_shadowed_payload(&rel, &listing).await
{
if let Some(stem) = rel.file_stem().and_then(|s| s.to_str()) {
index.insert(stem, rel.clone());
}
if let Ok((_, doc)) = self.load(&rel).await {
let meta = fig::Value::from(&doc.meta);
if let Some(title) = meta.get("title").and_then(fig::Value::as_str) {
index.insert(title, rel.clone());
}
}
}
}
Ok(())
})
}
}
fn is_document_path(path: &Path) -> bool {
!is_opaque_payload(path)
}
fn file_listing(rel_dir: &Path, entries: &[crate::fs::DirEntry]) -> BTreeSet<PathBuf> {
entries
.iter()
.filter(|e| e.file_type().is_file())
.filter_map(|e| e.file_name().and_then(|n| n.to_str()).map(str::to_owned))
.filter(|name| !name.starts_with('.'))
.map(|name| {
if rel_dir.as_os_str().is_empty() {
PathBuf::from(name)
} else {
rel_dir.join(name)
}
})
.collect()
}