blockworx/tools/
content_path.rs1use crate::path::{BlockPath, Scope, child_blocks};
10use blockworx_doc::{document::IndexedDocument, id::BlockId};
11
12type Doc<'a> = IndexedDocument<'a>;
15
16const SEPARATOR: char = '/';
18pub const SEPARATOR_STR: &str = "/";
20
21fn name_of(document: &Doc<'_>, id: BlockId) -> String {
25 document
26 .doc
27 .block(&id)
28 .map(|block| block.title.name.trim().to_owned())
29 .filter(|name| !name.is_empty())
30 .unwrap_or_else(|| id.to_string())
31}
32
33pub fn segments(document: &Doc<'_>, path: &BlockPath) -> Vec<String> {
42 path.segments()
43 .iter()
44 .map(|&id| name_of(document, id))
45 .collect()
46}
47
48pub fn to_string(document: &Doc<'_>, path: &BlockPath) -> String {
50 join(&segments(document, path))
51}
52
53pub fn join(segments: &[String]) -> String {
55 segments.join(&SEPARATOR.to_string())
56}
57
58pub fn parse(document: &Doc<'_>, text: &str) -> Option<BlockPath> {
62 let names = text
63 .split(SEPARATOR)
64 .map(str::trim)
65 .filter(|name| !name.is_empty());
66 let mut path = BlockPath::empty();
67 let mut current = Scope::Root;
68 for name in names {
69 let child = child_blocks(document, current)
70 .into_iter()
71 .find(|&id| name_of(document, id).eq_ignore_ascii_case(name))?;
72 path.push(child);
73 current = Scope::Block(child);
74 }
75 Some(path)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::widget::test_fixtures::{self as fx, Scene};
82 use blockworx_doc::fixtures::block_id;
83 use blockworx_geom::{Rect, pos2};
84
85 fn body() -> Rect {
86 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0))
87 }
88
89 fn nested() -> (Scene, Vec<BlockId>) {
91 let scene = Scene::new(vec![
92 fx::block_in(1, Scope::Root, body()),
93 fx::titled(1, "top"),
94 fx::block_in(2, Scope::Block(block_id(1)), body()),
95 fx::titled(2, "Thing 1"),
96 fx::block_in(3, Scope::Block(block_id(2)), body()),
97 fx::titled(3, "Core"),
98 ]);
99 (scene, vec![block_id(1), block_id(2), block_id(3)])
100 }
101
102 fn path_of(ids: &[BlockId]) -> BlockPath {
103 let mut path = BlockPath::empty();
104 for &id in ids {
105 path.push(id);
106 }
107 path
108 }
109
110 #[test]
111 fn a_path_reads_as_names_from_the_root_down() {
112 let (mut scene, ids) = nested();
113 let indexed = scene.indexed();
114 assert_eq!(to_string(&indexed, &path_of(&ids)), "top/Thing 1/Core");
115 assert_eq!(
116 to_string(&indexed, &BlockPath::empty()),
117 "",
118 "the document root is the unnamed scope the path hangs from"
119 );
120 }
121
122 #[test]
125 fn an_untitled_block_falls_back_to_its_id() {
126 let id = block_id(1);
127 let mut scene = Scene::new(vec![
128 fx::block_in(1, Scope::Root, body()),
129 fx::titled(1, ""),
130 ]);
131 let indexed = scene.indexed();
132 let text = to_string(&indexed, &path_of(&[id]));
133 assert_eq!(text, id.to_string());
134 assert_eq!(parse(&indexed, &text), Some(path_of(&[id])));
135 }
136
137 #[test]
140 fn a_displayed_path_parses_back_to_itself() {
141 let (mut scene, ids) = nested();
142 let indexed = scene.indexed();
143 let path = path_of(&ids);
144 assert_eq!(
145 parse(&indexed, &to_string(&indexed, &path)),
146 Some(path.clone())
147 );
148 assert_eq!(parse(&indexed, " top / thing 1 / core "), Some(path));
150 assert_eq!(parse(&indexed, "top"), Some(path_of(&ids[..1])));
151 }
152
153 #[test]
156 fn the_root_contributes_no_segments() {
157 let (mut scene, _) = nested();
158 let indexed = scene.indexed();
159 assert!(segments(&indexed, &BlockPath::empty()).is_empty());
160 }
161
162 #[test]
163 fn a_name_that_names_nothing_fails_to_parse() {
164 let (mut scene, _) = nested();
165 let indexed = scene.indexed();
166 assert_eq!(parse(&indexed, "top/Thing 1/Nope"), None);
167 assert_eq!(
168 parse(&indexed, "Core"),
169 None,
170 "names resolve level by level"
171 );
172 }
173}