1use std::fmt;
5
6use blockworx_doc::{
7 block_model::Block,
8 document::{Document, IndexedDocument, chronological},
9 id::BlockId,
10};
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16pub enum Scope {
17 Root,
18 Block(BlockId),
19}
20
21impl Scope {
22 pub fn from_wire(id: BlockId) -> Scope {
26 if id == BlockId::NULL {
27 Scope::Root
28 } else {
29 Scope::Block(id)
30 }
31 }
32
33 pub fn wire_id(self) -> BlockId {
35 match self {
36 Scope::Root => BlockId::NULL,
37 Scope::Block(id) => id,
38 }
39 }
40
41 pub fn block(self) -> Option<BlockId> {
43 match self {
44 Scope::Root => None,
45 Scope::Block(id) => Some(id),
46 }
47 }
48}
49
50pub enum Resolved<'a> {
54 Root,
55 Block(&'a Block),
56 Absent,
57}
58
59pub fn resolve<'a>(indexed: &IndexedDocument<'a>, scope: Scope) -> Resolved<'a> {
62 match scope {
63 Scope::Root => Resolved::Root,
64 Scope::Block(id) => indexed
65 .doc
66 .block(&id)
67 .map_or(Resolved::Absent, Resolved::Block),
68 }
69}
70
71#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
75pub struct BlockPath(Vec<BlockId>);
76
77impl BlockPath {
78 pub fn empty() -> Self {
79 Self(Vec::new())
80 }
81
82 pub fn opening(doc: &Document) -> Self {
85 match Scope::from_wire(doc.title_block().top) {
86 Scope::Root => Self::empty(),
87 Scope::Block(top) => Self(vec![top]),
88 }
89 }
90
91 pub fn to_parent_of(doc: &Document, target: BlockId) -> Option<Self> {
94 let mut trail = Vec::new();
95 let mut current = Scope::from_wire(doc.block(&target)?.parent);
96 while let Scope::Block(id) = current {
97 trail.push(id);
98 current = Scope::from_wire(doc.block(&id)?.parent);
99 }
100 trail.reverse();
101 Some(Self(trail))
102 }
103
104 pub fn showing(doc: &Document, scope: Scope) -> Option<Self> {
108 match scope {
109 Scope::Root => Some(Self::empty()),
110 Scope::Block(id) => {
111 let mut path = Self::to_parent_of(doc, id)?;
112 path.push(id);
113 Some(path)
114 }
115 }
116 }
117
118 pub fn scope(&self) -> Scope {
122 self.0.last().copied().map_or(Scope::Root, Scope::Block)
123 }
124
125 pub fn push(&mut self, id: BlockId) {
126 self.0.push(id);
127 }
128
129 pub fn pop(&mut self) -> Option<BlockId> {
130 self.0.pop()
131 }
132
133 pub fn segments(&self) -> &[BlockId] {
134 &self.0
135 }
136
137 pub fn is_held_by(&self, doc: &Document) -> bool {
142 self.0.iter().all(|id| doc.block(id).is_some())
143 }
144}
145
146pub fn tree_root(document: &IndexedDocument<'_>) -> Scope {
151 Scope::from_wire(document.doc.title_block().top)
152}
153
154pub fn child_blocks(indexed: &IndexedDocument<'_>, scope: Scope) -> Vec<BlockId> {
159 let Some(entry) = indexed.index.scope(scope.wire_id()) else {
160 return Vec::new();
161 };
162 chronological(
163 entry
164 .children
165 .iter()
166 .filter_map(|id| Some((*id, indexed.doc.block(id)?))),
167 )
168}
169
170#[derive(Clone, Copy, PartialEq, Eq, Debug)]
174pub enum Structure {
175 Leaf,
176 Nested,
177}
178
179impl Structure {
180 pub fn opens_a_scope(self) -> bool {
181 self == Structure::Nested
182 }
183}
184
185pub fn structure(indexed: &IndexedDocument<'_>, id: BlockId) -> Structure {
189 let holds_a_block = indexed.index.scope(id).is_some_and(|entry| {
190 entry
191 .children
192 .iter()
193 .any(|child| indexed.doc.block(child).is_some())
194 });
195 if holds_a_block {
196 Structure::Nested
197 } else {
198 Structure::Leaf
199 }
200}
201
202impl fmt::Display for BlockPath {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 let mut first = true;
205 for seg in &self.0 {
206 if !first {
207 f.write_str(":")?;
208 }
209 write!(f, "{seg}")?;
210 first = false;
211 }
212 Ok(())
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use blockworx_doc::fixtures::block_id;
220
221 #[test]
222 fn push_pop_segments() {
223 let mut p = BlockPath::empty();
224 assert!(p.segments().is_empty());
225 assert_eq!(p.scope(), Scope::Root, "the empty path is the root scope");
226 p.push(block_id(1));
227 p.push(block_id(3));
228 assert_eq!(p.segments(), &[block_id(1), block_id(3)]);
229 assert_eq!(
230 p.scope(),
231 Scope::Block(block_id(3)),
232 "the scope is the last segment"
233 );
234 assert_eq!(p.pop(), Some(block_id(3)));
235 assert_eq!(p.segments(), &[block_id(1)]);
236 }
237
238 #[test]
241 fn wire_conversions_round_trip() {
242 assert_eq!(Scope::from_wire(BlockId::NULL), Scope::Root);
243 assert_eq!(Scope::Root.wire_id(), BlockId::NULL);
244 assert_eq!(Scope::from_wire(block_id(2)), Scope::Block(block_id(2)));
245 assert_eq!(Scope::Block(block_id(2)).wire_id(), block_id(2));
246 assert_eq!(Scope::Root.block(), None);
247 }
248
249 #[test]
250 fn display() {
251 let mut p = BlockPath::empty();
252 assert_eq!(p.to_string(), "");
253 p.push(block_id(1));
254 p.push(block_id(7));
255 assert_eq!(p.to_string(), format!("{}:{}", block_id(1), block_id(7)));
256 }
257}