Skip to main content

blockworx_editor/
content_path.rs

1//! The content path: the chain of block names from the document root down to
2//! the level being drawn, as a plain string — `top/Thing 1/Core`.
3//!
4//! It reads along the status strip as a breadcrumb, is printed across the
5//! bottom of every PDF sheet, and is parsed back — by the navigator's tree,
6//! which carries the same names — for the command palette's `go <path>`, so
7//! a path can be copied out of one window and pasted into another to
8//! navigate there.
9
10use crate::path::BlockPath;
11use blockworx_doc::{document::IndexedDocument, id::BlockId};
12
13/// The document read a path walks: the whole document with its index, since
14/// resolving a name needs each scope's children.
15type Doc<'a> = IndexedDocument<'a>;
16
17/// What separates two names in a path string.
18pub const SEPARATOR: char = '/';
19/// The same separator, for a caller measuring what it costs in a line.
20pub const SEPARATOR_STR: &str = "/";
21
22/// A block's name for path purposes: its title, or its id when untitled. The
23/// navigator's richer `name/type` label would only make a path harder to read
24/// and to retype.
25pub fn name_of(document: &Doc<'_>, id: BlockId) -> String {
26    document
27        .doc
28        .block(&id)
29        .map(|block| block.title.name.trim().to_owned())
30        .filter(|name| !name.is_empty())
31        .unwrap_or_else(|| id.to_string())
32}
33
34/// The path's names, outermost first. Every segment is a block the path
35/// descended into — the document root is the unnamed scope they hang from,
36/// so it contributes nothing and the empty path (the root itself) has no
37/// segments at all.
38///
39/// The segments rather than the joined string: the printed sheet links each
40/// ancestor segment to that ancestor's page, so it needs to know where one
41/// name ends and the next begins.
42pub fn segments(document: &Doc<'_>, path: &BlockPath) -> Vec<String> {
43    path.segments()
44        .iter()
45        .map(|&id| name_of(document, id))
46        .collect()
47}
48
49/// The path as a string, outermost first: `top/Thing 1/Core`.
50pub fn to_string(document: &Doc<'_>, path: &BlockPath) -> String {
51    join(&segments(document, path))
52}
53
54/// The one spelling of a path: its names, separated.
55pub fn join(segments: &[String]) -> String {
56    segments.join(&SEPARATOR.to_string())
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::path::Scope;
63    use crate::widget::test_fixtures::{self as fx, Scene};
64    use blockworx_doc::fixtures::block_id;
65    use blockworx_geom::{Rect, pos2};
66
67    fn body() -> Rect {
68        Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0))
69    }
70
71    /// `top → Thing 1 → Core`, returning the ids in path order.
72    fn nested() -> (Scene, Vec<BlockId>) {
73        let scene = Scene::new(vec![
74            fx::block_in(1, Scope::Root, body()),
75            fx::titled(1, "top"),
76            fx::block_in(2, Scope::Block(block_id(1)), body()),
77            fx::titled(2, "Thing 1"),
78            fx::block_in(3, Scope::Block(block_id(2)), body()),
79            fx::titled(3, "Core"),
80        ]);
81        (scene, vec![block_id(1), block_id(2), block_id(3)])
82    }
83
84    fn path_of(ids: &[BlockId]) -> BlockPath {
85        let mut path = BlockPath::empty();
86        for &id in ids {
87            path.push(id);
88        }
89        path
90    }
91
92    #[test]
93    fn a_path_reads_as_names_from_the_root_down() {
94        let (mut scene, ids) = nested();
95        let indexed = scene.indexed();
96        assert_eq!(to_string(&indexed, &path_of(&ids)), "top/Thing 1/Core");
97        assert_eq!(
98            to_string(&indexed, &BlockPath::empty()),
99            "",
100            "the document root is the unnamed scope the path hangs from"
101        );
102    }
103
104    /// An untitled block still names a level: its id stands in, so the string
105    /// never has an empty segment to parse back.
106    #[test]
107    fn an_untitled_block_falls_back_to_its_id() {
108        let id = block_id(1);
109        let mut scene = Scene::new(vec![
110            fx::block_in(1, Scope::Root, body()),
111            fx::titled(1, ""),
112        ]);
113        let indexed = scene.indexed();
114        let text = to_string(&indexed, &path_of(&[id]));
115        assert_eq!(text, id.to_string());
116    }
117
118    /// The document root names no level, so it contributes no segments for a
119    /// breadcrumb — or a printed sheet — to draw a line from.
120    #[test]
121    fn the_root_contributes_no_segments() {
122        let (mut scene, _) = nested();
123        let indexed = scene.indexed();
124        assert!(segments(&indexed, &BlockPath::empty()).is_empty());
125    }
126}