use fig::Value;
use flower_core::Seg;
use prov::Link;
use prov::link::IdRef;
use crate::facets::{Facet, Facets};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TargetKind {
Path,
Id,
Foreign {
workspace: String,
},
External,
SameDocument,
MalformedId,
}
#[derive(Debug, Clone)]
pub struct MetaLink {
pub path: Vec<Seg>,
pub relation: crate::facets::RelationFacet,
pub link: Link,
pub kind: TargetKind,
}
impl MetaLink {
pub fn target(&self) -> &str {
&self.link.target
}
pub fn locator(&self) -> Option<&str> {
self.link.locator()
}
pub fn display(&self) -> &str {
self.link.label.as_deref().unwrap_or(&self.link.target)
}
pub fn leaves_the_workspace(&self) -> bool {
matches!(self.kind, TargetKind::External | TargetKind::Foreign { .. })
}
}
fn kind_of(link: &Link) -> TargetKind {
if link.is_same_document() {
return TargetKind::SameDocument;
}
match link.id_ref() {
Some(IdRef::Local(_)) => return TargetKind::Id,
Some(IdRef::Foreign { workspace, .. }) => {
return TargetKind::Foreign {
workspace: workspace.to_string(),
};
}
Some(IdRef::Malformed) => return TargetKind::MalformedId,
None => {}
}
if link.is_external() {
return TargetKind::External;
}
TargetKind::Path
}
pub fn links_in(meta: &Value, facets: &Facets) -> Vec<MetaLink> {
let mut links = Vec::new();
for relation in facets.relations().relations() {
let Facet::Relation(facet) = facets.of_key(&relation.name) else {
continue;
};
let Some(value) = meta.get(relation.name.as_str()) else {
continue;
};
let at = |path: Vec<Seg>, raw: &str| MetaLink {
path,
relation: facet.clone(),
link: parse(raw),
kind: TargetKind::Path, };
match value {
Value::Seq(items) => {
for (index, item) in items.iter().enumerate() {
if let Some(raw) = item.as_str() {
let path = vec![Seg::Key(relation.name.clone()), Seg::Index(index)];
links.push(finish(at(path, raw)));
}
}
}
other => {
if let Some(raw) = other.as_str() {
let path = vec![Seg::Key(relation.name.clone())];
links.push(finish(at(path, raw)));
}
}
}
}
links
}
pub fn link_at(meta: &Value, facets: &Facets, path: &[Seg]) -> Option<MetaLink> {
links_in(meta, facets)
.into_iter()
.find(|link| link.path == path)
}
pub fn links_under(meta: &Value, facets: &Facets, path: &[Seg]) -> Vec<MetaLink> {
links_in(meta, facets)
.into_iter()
.filter(|link| link.path.starts_with(path))
.collect()
}
fn parse(raw: &str) -> Link {
Link::parse(raw)
}
fn finish(mut link: MetaLink) -> MetaLink {
link.kind = kind_of(&link.link);
link
}
#[cfg(test)]
mod tests {
use super::*;
use prov::{Document, WorkspaceConfig};
const DOC: &str = "\
---
title: A Note
contents:
- '[Child](child.md)'
- '[[notes/other.md|Other]]'
- 'id:ajp7eq'
part_of: '[Root](/README.md)'
links:
- 'https://example.com/'
- '#section-2'
- 'id:otherws/bkq8fr'
config: prov.yaml
mood: rainy
---
# Note
";
fn meta() -> Value {
let doc = Document::parse("notes/note.md", DOC).expect("parse");
Value::from(&doc.meta)
}
fn facets() -> Facets {
Facets::from_config(&WorkspaceConfig::default())
}
fn at(links: &[MetaLink], path: &[Seg]) -> MetaLink {
links
.iter()
.find(|l| l.path == path)
.unwrap_or_else(|| panic!("no link at {path:?}"))
.clone()
}
#[test]
fn every_link_knows_where_it_sits() {
let meta = meta();
let links = links_in(&meta, &facets());
let paths: Vec<Vec<Seg>> = links.iter().map(|l| l.path.clone()).collect();
assert!(paths.contains(&vec![Seg::Key("contents".into()), Seg::Index(1)]));
assert!(paths.contains(&vec![Seg::Key("part_of".into())]));
assert!(paths.contains(&vec![Seg::Key("config".into())]));
assert!(!paths.iter().any(|p| p == &vec![Seg::Key("mood".into())]));
}
#[test]
fn the_label_and_the_wrapper_survive_the_round_trip() {
let meta = meta();
let links = links_in(&meta, &facets());
let child = at(&links, &[Seg::Key("contents".into()), Seg::Index(0)]);
assert_eq!(child.display(), "Child");
assert_eq!(child.target(), "child.md");
assert_eq!(child.link.render(), "[Child](child.md)");
let other = at(&links, &[Seg::Key("contents".into()), Seg::Index(1)]);
assert!(other.link.wikilink);
assert_eq!(other.display(), "Other");
assert_eq!(other.link.render(), "[[notes/other.md|Other]]");
}
#[test]
fn a_target_is_classified_by_syntax_alone() {
let meta = meta();
let links = links_in(&meta, &facets());
assert_eq!(
at(&links, &[Seg::Key("contents".into()), Seg::Index(0)]).kind,
TargetKind::Path
);
assert_eq!(
at(&links, &[Seg::Key("contents".into()), Seg::Index(2)]).kind,
TargetKind::Id
);
assert_eq!(
at(&links, &[Seg::Key("links".into()), Seg::Index(0)]).kind,
TargetKind::External
);
assert_eq!(
at(&links, &[Seg::Key("links".into()), Seg::Index(1)]).kind,
TargetKind::SameDocument
);
assert_eq!(
at(&links, &[Seg::Key("links".into()), Seg::Index(2)]).kind,
TargetKind::Foreign {
workspace: "otherws".into()
}
);
assert!(at(&links, &[Seg::Key("links".into()), Seg::Index(2)]).leaves_the_workspace());
}
#[test]
fn a_link_carries_what_its_relation_is() {
let meta = meta();
let links = links_in(&meta, &facets());
let child = at(&links, &[Seg::Key("contents".into()), Seg::Index(0)]);
assert!(child.relation.spanning);
assert!(!child.relation.pointer);
let config = at(&links, &[Seg::Key("config".into())]);
assert!(config.relation.pointer);
assert!(!config.relation.spanning);
}
#[test]
fn the_cursor_question_is_exact() {
let meta = meta();
let facets = facets();
assert!(
link_at(&meta, &facets, &[Seg::Key("part_of".into())]).is_some(),
"a scalar relation row is a link"
);
assert!(
link_at(&meta, &facets, &[Seg::Key("contents".into())]).is_none(),
"standing on the list is not standing on a link"
);
assert_eq!(
links_under(&meta, &facets, &[Seg::Key("contents".into())]).len(),
3,
"the list's own links, for a caller that asks for them"
);
}
#[test]
fn a_non_string_item_is_skipped_rather_than_guessed_at() {
const ODD: &str = "---\ncontents:\n- ok.md\n- {a: b}\n---\n# x\n";
let doc = Document::parse("note.md", ODD).expect("parse");
let links = links_in(&Value::from(&doc.meta), &facets());
assert_eq!(links.len(), 1);
assert_eq!(links[0].target(), "ok.md");
}
}