use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use super::Graph;
use crate::error::Result;
use crate::fs::ReadStorage;
use crate::index::IdIndex;
use crate::link::{self, Link};
use super::Target;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeKind {
Doc,
Missing,
Cycle,
Unreadable(String),
UnresolvedId(crate::identity::Id),
AmbiguousAlias(String),
Foreign {
workspace: String,
id: crate::identity::Id,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TreeOptions {
pub ignore_missing: bool,
}
#[derive(Debug, Clone)]
pub struct Node {
pub path: PathBuf,
pub title: Option<String>,
pub label: Option<String>,
pub kind: NodeKind,
pub children: Vec<Node>,
}
fn is_missing(error: &crate::error::Error) -> bool {
match error {
crate::error::Error::NotFound(_) => true,
crate::error::Error::Io(e) => e.kind() == std::io::ErrorKind::NotFound,
_ => false,
}
}
struct Walk<'a> {
root: &'a Path,
options: TreeOptions,
parked: &'a [PathBuf],
}
impl<FS: ReadStorage, Ix: IdIndex> Graph<FS, Ix> {
pub async fn tree(&self, start: impl AsRef<Path>) -> Result<Node> {
self.tree_with(start, TreeOptions::default()).await
}
pub async fn tree_with(&self, start: impl AsRef<Path>, options: TreeOptions) -> Result<Node> {
self.tree_within(start, options, &[]).await
}
pub async fn tree_within(
&self,
start: impl AsRef<Path>,
options: TreeOptions,
parked: &[PathBuf],
) -> Result<Node> {
let _scope = self.read_scope();
let start = link::normalize(start);
let mut titles: Option<crate::title::TitleIndex> = None;
let mut trail: Vec<PathBuf> = Vec::new();
let root = start.clone();
let cx = Walk {
root: &root,
options,
parked,
};
self.tree_node(start, None, &cx, &mut titles, &mut trail)
.await
}
fn tree_node<'a>(
&'a self,
path: PathBuf,
label: Option<String>,
cx: &'a Walk<'a>,
titles: &'a mut Option<crate::title::TitleIndex>,
trail: &'a mut Vec<PathBuf>,
) -> Pin<Box<dyn Future<Output = Result<Node>> + 'a>> {
Box::pin(async move {
if trail.contains(&path) {
return Ok(Node {
path,
title: None,
label,
kind: NodeKind::Cycle,
children: Vec::new(),
});
}
let doc = match self.load(&path).await {
Ok((_, doc)) => doc,
Err(e) if is_missing(&e) => {
return Ok(Node {
path,
title: None,
label,
kind: NodeKind::Missing,
children: Vec::new(),
});
}
Err(e) => {
return Ok(Node {
path,
title: None,
label,
kind: NodeKind::Unreadable(e.to_string()),
children: Vec::new(),
});
}
};
let meta = fig::Value::from(&doc.meta);
let title = meta
.get("title")
.and_then(fig::Value::as_str)
.map(str::to_owned);
trail.push(path.clone());
let mut children = Vec::new();
for raw in self.relations().children(&meta) {
let child = Link::parse(&raw);
if titles.is_none() && crate::title::is_alias_shaped(&child.target) {
*titles = Some(self.title_index_scoped(cx.root, cx.parked).await?);
}
let child_path = match self.resolve_link_with(&path, &child, titles.as_ref()) {
Target::External => continue,
Target::UnresolvedId(id) => {
children.push(Node {
path: PathBuf::from(child.target.clone()),
title: None,
label: child.label,
kind: NodeKind::UnresolvedId(id),
children: Vec::new(),
});
continue;
}
Target::AmbiguousAlias(name) => {
children.push(Node {
path: PathBuf::from(name.clone()),
title: None,
label: child.label,
kind: NodeKind::AmbiguousAlias(name),
children: Vec::new(),
});
continue;
}
Target::Foreign { workspace, id } => {
children.push(Node {
path: PathBuf::from(child.target.clone()),
title: None,
label: child.label,
kind: NodeKind::Foreign { workspace, id },
children: Vec::new(),
});
continue;
}
Target::Path(p) => p,
};
let child_node = self
.tree_node(child_path, child.label, cx, titles, trail)
.await?;
if !(cx.options.ignore_missing && child_node.kind == NodeKind::Missing) {
children.push(child_node);
}
}
trail.pop();
Ok(Node {
path,
title,
label,
kind: NodeKind::Doc,
children,
})
})
}
}
#[cfg(all(test, feature = "yaml"))]
mod tests {
use super::*;
use crate::exec::block_on;
use crate::fs::StdFs;
use crate::graph::ReadSettings;
use crate::index::NoIndex;
fn write(dir: &Path, rel: &str, text: &str) {
let p = dir.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
}
fn tempdir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("prov-tree-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn walks_the_spanning_tree_with_labels_and_titles() {
let dir = tempdir("walk");
write(
&dir,
"index.md",
"---\ntitle: Root\ncontents:\n- '[A](notes/a.md)'\n- missing.md\n---\n",
);
write(
&dir,
"notes/a.md",
"---\ntitle: A\npart_of: ../index.md\n---\n",
);
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let root = block_on(ws.tree("index.md")).unwrap();
assert_eq!(root.title.as_deref(), Some("Root"));
assert_eq!(root.children.len(), 2);
assert_eq!(root.children[0].path, PathBuf::from("notes/a.md"));
assert_eq!(root.children[0].label.as_deref(), Some("A"));
assert_eq!(root.children[0].kind, NodeKind::Doc);
assert_eq!(root.children[1].kind, NodeKind::Missing);
}
#[test]
fn spanning_alias_links_resolve_through_the_title_index() {
let dir = tempdir("alias");
write(
&dir,
"index.md",
"---\ntitle: Root\ncontents:\n- '[[Alpha]]'\n- '[[Dup]]'\n- '[[Ghost]]'\n---\n",
);
write(&dir, "notes/alpha.md", "---\ntitle: Alpha\n---\n");
write(&dir, "one.md", "---\ntitle: Dup\n---\n");
write(&dir, "two.md", "---\ntitle: Dup\n---\n");
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let root = block_on(ws.tree("index.md")).unwrap();
assert_eq!(root.children.len(), 3);
assert_eq!(root.children[0].kind, NodeKind::Doc);
assert_eq!(root.children[0].path, PathBuf::from("notes/alpha.md"));
assert_eq!(
root.children[1].kind,
NodeKind::AmbiguousAlias("Dup".into())
);
assert_eq!(root.children[2].kind, NodeKind::Missing);
}
#[test]
fn a_target_that_exists_but_cannot_be_read_is_unreadable_not_missing() {
let dir = tempdir("unreadable");
write(
&dir,
"index.md",
"---\ncontents:\n- sub\n- ../outside.md\n---\n",
);
std::fs::create_dir_all(dir.join("sub")).unwrap();
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let root = block_on(ws.tree("index.md")).unwrap();
assert_eq!(root.children.len(), 2);
assert!(
matches!(root.children[0].kind, NodeKind::Unreadable(_)),
"a directory is not a missing document: {:?}",
root.children[0].kind
);
assert!(
matches!(root.children[1].kind, NodeKind::Unreadable(_)),
"an escaping target is refused, not reported absent: {:?}",
root.children[1].kind
);
}
#[test]
fn cycles_are_marked_not_followed() {
let dir = tempdir("cycle");
write(&dir, "a.md", "---\ncontents:\n- b.md\n---\n");
write(&dir, "b.md", "---\ncontents:\n- a.md\n---\n");
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let root = block_on(ws.tree("a.md")).unwrap();
let b = &root.children[0];
assert_eq!(b.kind, NodeKind::Doc);
assert_eq!(b.children[0].kind, NodeKind::Cycle);
assert_eq!(b.children[0].path, PathBuf::from("a.md"));
}
#[test]
fn default_tree_materializes_a_missing_node_for_a_broken_contents_link() {
let dir = tempdir("missing-default");
write(
&dir,
"index.md",
"---\ntitle: Root\ncontents:\n- '[A](notes/a.md)'\n- gone.md\n---\n",
);
write(&dir, "notes/a.md", "---\ntitle: A\n---\n");
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let root = block_on(ws.tree("index.md")).unwrap();
assert_eq!(root.children.len(), 2);
assert_eq!(root.children[1].kind, NodeKind::Missing);
let root = block_on(ws.tree_with("index.md", TreeOptions::default())).unwrap();
assert_eq!(root.children.len(), 2);
assert_eq!(root.children[1].kind, NodeKind::Missing);
}
#[test]
fn ignore_missing_drops_the_broken_link_entirely() {
let dir = tempdir("missing-ignore");
write(
&dir,
"index.md",
"---\ntitle: Root\ncontents:\n- '[A](notes/a.md)'\n- gone.md\n---\n",
);
write(&dir, "notes/a.md", "---\ntitle: A\n---\n");
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let options = TreeOptions {
ignore_missing: true,
};
let root = block_on(ws.tree_with("index.md", options)).unwrap();
assert_eq!(root.children.len(), 1);
assert_eq!(root.children[0].path, PathBuf::from("notes/a.md"));
}
#[test]
fn ignore_missing_only_filters_missing_not_other_marker_kinds() {
let dir = tempdir("missing-ignore-cycle");
write(&dir, "a.md", "---\ncontents:\n- b.md\n- gone.md\n---\n");
write(&dir, "b.md", "---\ncontents:\n- a.md\n---\n");
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let options = TreeOptions {
ignore_missing: true,
};
let root = block_on(ws.tree_with("a.md", options)).unwrap();
assert_eq!(root.children.len(), 1);
let b = &root.children[0];
assert_eq!(b.kind, NodeKind::Doc);
assert_eq!(b.children.len(), 1);
assert_eq!(b.children[0].kind, NodeKind::Cycle);
}
#[test]
fn fs_path_joins_a_node_path_onto_the_workspace_root() {
let dir = tempdir("fs-path");
write(&dir, "notes/a.md", "---\ntitle: A\n---\n");
let ws = Graph::new(StdFs, &dir, NoIndex, ReadSettings::default());
let node = block_on(ws.tree("notes/a.md")).unwrap();
assert_eq!(ws.fs_path(&node.path), dir.join("notes/a.md"));
}
}