//! A document: a tree of nodes under one root, loose nodes beside it, and //! cross-links between any of them. use std::collections::BTreeMap; use thiserror::Error; use super::geometry::CanvasPos; use super::ids::{DocUid, NodeId}; use super::link::{Link, LinkKey}; use super::look::LineLook; use super::node::{Content, Node}; use super::portal::{Backlink, Portal}; use super::text::Title; /// Where a node sits in its document's tree. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Place { /// The document's main topic. There is exactly one, and its title is the /// document's name. Root, /// A top-level idea beside the root's tree, not attached to anything yet. Loose, /// Attached under another node. Child { /// The node above this one. parent: NodeId, /// How the line from the parent is drawn. branch: LineLook, }, } /// A node, with where it sits in the tree. #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct Entry { pub(super) node: Node, pub(super) place: Place, /// Its children, in order. pub(super) children: Vec, } /// One brainstorm: a tree of nodes under a single root, loose nodes beside /// it, cross-links between any two nodes, and the backlinks of portals in /// other documents that lead here. /// /// The fields are private and every change goes through /// [`Document::edit`](Document::edit), so these rules always hold: /// - there is exactly one root, and it can't be removed or given a parent; /// - a node has at most one parent, and there are no cycles; /// - a link joins two different nodes that both exist, at most one link per /// pair. #[derive(Debug, Clone)] pub struct Document { uid: DocUid, root: NodeId, pub(super) entries: BTreeMap, /// The loose nodes, in order. pub(super) loose: Vec, pub(super) links: BTreeMap, backlinks: Vec, /// The number the next new node gets. It only ever grows, so ids aren't /// handed out twice, not even after an undo. pub(super) next_id: u32, } impl PartialEq for Document { /// Two documents are equal when they hold the same things. `next_id` is /// left out: it only says which number comes next. fn eq(&self, other: &Document) -> bool { self.uid == other.uid && self.root == other.root && self.entries == other.entries && self.loose == other.loose && self.links == other.links && self.backlinks == other.backlinks } } impl Eq for Document {} /// A node as read from a file: the node, plus where the file says it goes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NodeRecord { pub id: NodeId, pub parent: Option, /// How the line from the parent is drawn (ignored without a parent). pub branch: LineLook, pub node: Node, } /// Why the nodes and links read from a file don't make a valid document. #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum StructureError { #[error("node {0} appears twice")] DuplicateNode(NodeId), #[error("the root (node {0}) isn't in the file")] MissingRoot(NodeId), #[error("the root (node {0}) can't have a parent")] RootWithParent(NodeId), #[error("node {child} has parent {parent}, which isn't in the file")] UnknownParent { child: NodeId, parent: NodeId }, #[error("node {0} is inside its own branch")] Cycle(NodeId), #[error("node ids must be smaller than {}", u32::MAX)] IdTooLarge, #[error("a link points at node {0}, which isn't in the file")] UnknownLinkEnd(NodeId), #[error("nodes {0} and {1} are linked twice")] DuplicateLink(NodeId, NodeId), } impl Document { /// A new document with just its root, at the middle of the canvas. pub fn new(title: Title) -> Document { Document::with_uid(title, DocUid::random()) } /// A new document with just its root, and a uid chosen by the caller. pub fn with_uid(title: Title, uid: DocUid) -> Document { let root = NodeId::new(1); let entry = Entry { node: Node::idea(title, CanvasPos::default()), place: Place::Root, children: Vec::new() }; Document { uid, root, entries: BTreeMap::from([(root, entry)]), loose: Vec::new(), links: BTreeMap::new(), backlinks: Vec::new(), next_id: 2, } } /// Builds a document from what a file holds, checking every rule. /// Siblings keep the order they have in `records`. pub fn from_records( uid: DocUid, root: NodeId, records: Vec, links: Vec, backlinks: Vec, ) -> Result { let mut entries = BTreeMap::new(); let mut order = Vec::with_capacity(records.len()); for record in records { if record.id.get() == u32::MAX { return Err(StructureError::IdTooLarge); } let place = match record.parent { Some(_) if record.id == root => return Err(StructureError::RootWithParent(root)), Some(parent) => Place::Child { parent, branch: record.branch }, None if record.id == root => Place::Root, None => Place::Loose, }; order.push((record.id, record.parent)); let entry = Entry { node: record.node, place, children: Vec::new() }; if entries.insert(record.id, entry).is_some() { return Err(StructureError::DuplicateNode(record.id)); } } if !entries.contains_key(&root) { return Err(StructureError::MissingRoot(root)); } let mut loose = Vec::new(); for (id, parent) in order { match parent { Some(parent) => match entries.get_mut(&parent) { Some(entry) => entry.children.push(id), None => return Err(StructureError::UnknownParent { child: id, parent }), }, None if id != root => loose.push(id), None => {} } } // Climbing from any node must reach the top level. A node that keeps // climbing for longer than there are nodes is going round in a cycle. for &id in entries.keys() { let mut current = id; for steps in 0.. { match entries.get(¤t).map(|entry| &entry.place) { Some(Place::Child { parent, .. }) => current = *parent, _ => break, } if steps > entries.len() { return Err(StructureError::Cycle(id)); } } } let mut link_map = BTreeMap::new(); for link in links { for end in [link.start(), link.end()] { if !entries.contains_key(&end) { return Err(StructureError::UnknownLinkEnd(end)); } } let key = link.key(); if link_map.insert(key, link).is_some() { let (a, b) = key.nodes(); return Err(StructureError::DuplicateLink(a, b)); } } // Ids are below u32::MAX (checked above), so this can't overflow. let next_id = entries.keys().next_back().map_or(1, |id| id.get() + 1); Ok(Document { uid, root, entries, loose, links: link_map, backlinks, next_id }) } /// The document's uid, which stays the same when its file is renamed. pub fn uid(&self) -> DocUid { self.uid } /// The root: the document's main topic. pub fn root(&self) -> NodeId { self.root } /// The document's name: its root's title. pub fn title(&self) -> &Title { // The root can't be removed (see `Edit::remove_branch`), so it's // always there. self.entries[&self.root].node.title() } /// How many nodes the document has, the root included. pub fn node_count(&self) -> usize { self.entries.len() } /// Whether the document has a node with this id. pub fn contains(&self, id: NodeId) -> bool { self.entries.contains_key(&id) } /// A node, if it exists. pub fn node(&self, id: NodeId) -> Option<&Node> { self.entries.get(&id).map(|entry| &entry.node) } /// Where a node sits in the tree. pub fn place(&self, id: NodeId) -> Option<&Place> { self.entries.get(&id).map(|entry| &entry.place) } /// A node's parent (none for the root and loose nodes). pub fn parent(&self, id: NodeId) -> Option { match self.place(id)? { Place::Child { parent, .. } => Some(*parent), Place::Root | Place::Loose => None, } } /// A node's children, in order (empty for an unknown id). pub fn children(&self, id: NodeId) -> &[NodeId] { self.entries.get(&id).map_or(&[], |entry| &entry.children) } /// The loose nodes, in order. pub fn loose(&self) -> &[NodeId] { &self.loose } /// The top level: the root, then the loose nodes. pub fn top_level(&self) -> impl Iterator + '_ { std::iter::once(self.root).chain(self.loose.iter().copied()) } /// The node after `id` among its siblings. For the root, that's the first /// loose node: the top level counts as one row of siblings. pub fn next_sibling(&self, id: NodeId) -> Option { let (siblings, index) = self.siblings(id)?; siblings.get(index + 1).copied() } /// The node before `id` among its siblings. pub fn previous_sibling(&self, id: NodeId) -> Option { let (siblings, index) = self.siblings(id)?; siblings.get(index.checked_sub(1)?).copied() } /// The row of siblings `id` is in, and its index there. fn siblings(&self, id: NodeId) -> Option<(Vec, usize)> { let siblings: Vec = match self.place(id)? { Place::Child { parent, .. } => self.children(*parent).to_vec(), Place::Root | Place::Loose => self.top_level().collect(), }; let index = siblings.iter().position(|&sibling| sibling == id)?; Some((siblings, index)) } /// The nodes above `id`: its parent, grandparent and so on up to the top. pub fn ancestors(&self, id: NodeId) -> impl Iterator + '_ { std::iter::successors(self.parent(id), |&node| self.parent(node)) } /// Whether `id` is somewhere below `branch` (not `branch` itself). pub fn is_inside(&self, id: NodeId, branch: NodeId) -> bool { self.ancestors(id).any(|ancestor| ancestor == branch) } /// How deep a node is: 0 at the top level, 1 for their children… pub fn depth(&self, id: NodeId) -> usize { self.ancestors(id).count() } /// Every node below `id`, in reading order (each node before its /// children). pub fn descendants(&self, id: NodeId) -> Vec { let mut found = Vec::new(); let mut to_visit: Vec = self.children(id).iter().rev().copied().collect(); while let Some(node) = to_visit.pop() { found.push(node); to_visit.extend(self.children(node).iter().rev()); } found } /// `id` and every node below it, in reading order (empty for an unknown /// id). pub fn branch(&self, id: NodeId) -> Vec { if !self.contains(id) { return Vec::new(); } let mut branch = vec![id]; branch.extend(self.descendants(id)); branch } /// Every node in reading order: the root's branch, then each loose node's /// branch. Files list their nodes in this order. pub fn outline(&self) -> Vec { self.top_level().flat_map(|top| self.branch(top)).collect() } /// Every cross-link, in the order of their keys. pub fn links(&self) -> impl Iterator { self.links.values() } /// The link between two nodes, if there is one. pub fn link(&self, key: LinkKey) -> Option<&Link> { self.links.get(&key) } /// The cross-links touching `id`, whichever way they point. pub fn links_of(&self, id: NodeId) -> impl Iterator + '_ { self.links.values().filter(move |link| link.key().touches(id)) } /// The portals in other documents that lead here. pub fn backlinks(&self) -> &[Backlink] { &self.backlinks } /// Replaces the backlinks. They're bookkeeping, kept in step with other /// documents when they're saved, so this isn't an undoable edit. pub fn set_backlinks(&mut self, backlinks: Vec) { self.backlinks = backlinks; } /// The portals in this document, with the nodes they sit on. pub fn portals(&self) -> impl Iterator + '_ { self.entries.iter().filter_map(|(id, entry)| match &entry.node.content { Content::Portal(portal) => Some((*id, portal)), Content::Idea(_) => None, }) } } #[cfg(test)] impl Document { /// Panics if one of the document's rules is broken. Tests call this /// after edits. pub(crate) fn assert_rules(&self) { let roots: Vec<_> = self.entries.iter().filter(|(_, e)| e.place == Place::Root).map(|(id, _)| *id).collect(); assert_eq!(roots, vec![self.root], "exactly one root"); let loose: Vec<_> = self.entries.iter().filter(|(_, e)| e.place == Place::Loose).map(|(id, _)| *id).collect(); let mut listed = self.loose.clone(); listed.sort(); assert_eq!(listed, loose, "the loose list matches the loose nodes"); for (id, entry) in &self.entries { if let Place::Child { parent, .. } = &entry.place { let siblings = self.children(*parent); assert_eq!( siblings.iter().filter(|&&s| s == *id).count(), 1, "{id} is listed once by its parent" ); } for child in &entry.children { assert_eq!(self.parent(*child), Some(*id), "{child} knows its parent {id}"); } assert!(id.get() < self.next_id, "{id} is below next_id"); } let mut outline = self.outline(); assert_eq!(outline.len(), self.entries.len(), "every node is reachable once (no cycles)"); outline.sort(); outline.dedup(); assert_eq!(outline.len(), self.entries.len(), "no node is reached twice"); for (key, link) in &self.links { assert_eq!(*key, link.key()); assert!(self.contains(link.start()) && self.contains(link.end()), "link ends exist"); } } } #[cfg(test)] mod tests { use super::*; use crate::model::test_support::{garden, idea, title}; fn record(id: u32, parent: Option, name: &str) -> NodeRecord { NodeRecord { id: NodeId::new(id), parent: parent.map(NodeId::new), branch: LineLook::default(), node: idea(name), } } fn build(records: Vec) -> Result { Document::from_records( DocUid::parse("0000000000000001").unwrap(), NodeId::new(1), records, vec![], vec![], ) } #[test] fn records_become_a_tree_in_file_order() { let doc = build(vec![ record(1, None, "root"), record(3, Some(1), "b"), record(2, Some(1), "a"), record(9, None, "loose"), record(4, Some(3), "b1"), ]) .unwrap(); doc.assert_rules(); let ids = |raw: &[u32]| raw.iter().map(|&n| NodeId::new(n)).collect::>(); assert_eq!(doc.children(NodeId::new(1)), ids(&[3, 2]).as_slice()); assert_eq!(doc.loose(), ids(&[9]).as_slice()); assert_eq!(doc.outline(), ids(&[1, 3, 4, 2, 9])); assert_eq!(doc.title().as_str(), "root"); } #[test] fn broken_records_are_refused() { use StructureError as E; let n = NodeId::new; assert_eq!(build(vec![record(2, None, "x")]), Err(E::MissingRoot(n(1)))); assert_eq!(build(vec![record(1, Some(2), "r"), record(2, None, "x")]), Err(E::RootWithParent(n(1)))); assert_eq!(build(vec![record(1, None, "r"), record(1, None, "again")]), Err(E::DuplicateNode(n(1)))); assert_eq!( build(vec![record(1, None, "r"), record(2, Some(5), "x")]), Err(E::UnknownParent { child: n(2), parent: n(5) }) ); assert!(matches!( build(vec![record(1, None, "r"), record(2, Some(3), "x"), record(3, Some(2), "y")]), Err(E::Cycle(_)) )); } #[test] fn siblings_ancestors_and_branches() { let (doc, g) = garden(); assert_eq!(doc.next_sibling(g.soil), Some(g.beds)); assert_eq!(doc.previous_sibling(g.soil), None); assert_eq!( doc.next_sibling(g.root), Some(g.compost), "the root's next sibling is the first loose node" ); assert_eq!(doc.ancestors(g.cedar).collect::>(), vec![g.beds, g.root]); assert!(doc.is_inside(g.cedar, g.root) && !doc.is_inside(g.root, g.cedar)); assert_eq!(doc.depth(g.cedar), 2); assert_eq!(doc.branch(g.beds), vec![g.beds, g.cedar]); assert_eq!(doc.outline(), vec![g.root, g.soil, g.beds, g.cedar, g.watering, g.compost]); assert_eq!(Document::new(title("x")).node_count(), 1); } }