lssg_lib/sitetree/
site_tree.rs

1use core::fmt;
2use std::{
3    collections::HashMap,
4    ops::{Index, IndexMut},
5};
6
7use log::{debug, warn};
8
9use crate::{tree::Tree, LssgError};
10
11use super::{
12    page::Page,
13    relational_graph::RelationalGraph,
14    relational_graph::{Link, Relation},
15    stylesheet::Stylesheet,
16    Input, Resource, SiteNode, SiteNodeKind,
17};
18
19fn absolute_path(nodes: &Vec<SiteNode>, to: SiteId) -> String {
20    let mut names = vec![nodes[to].name.clone()];
21    let mut parent = nodes[to].parent;
22    while let Some(p) = parent {
23        names.push(nodes[p].name.clone());
24        parent = nodes[p].parent;
25    }
26    names.pop(); // pop root
27    names.reverse();
28    return format!("/{}", names.join("/"));
29}
30
31/// Get the relative path between two nodes
32fn rel_path(nodes: &Vec<SiteNode>, from: SiteId, to: SiteId) -> String {
33    let mut visited = HashMap::new();
34    let mut to_path = vec![nodes[to].name.clone()];
35
36    // discover all parents from destination
37    let mut depth = 0;
38    let mut node = nodes[to].parent;
39    while let Some(i) = node {
40        visited.insert(i, depth);
41        depth += 1;
42        node = nodes[i].parent;
43        // if not root (root doesn't have a parent) add to file directories
44        if let Some(_) = nodes[i].parent {
45            to_path.push(nodes[i].name.clone())
46        }
47    }
48
49    // find shared parent and go back till that point
50    depth = 0;
51    let mut to_depth = to_path.len() - 1;
52    let mut node = Some(from);
53    while let Some(i) = node {
54        if let Some(d) = visited.get(&i) {
55            to_depth = *d;
56            break;
57        }
58        depth += 1;
59        node = nodes[i].parent;
60    }
61
62    // don't add anything to path traversal if root
63    to_path.reverse();
64    let to_path = if nodes[to].parent.is_some() {
65        to_path[to_path.len() - 1 - to_depth..to_path.len()].join("/")
66    } else {
67        depth -= 1;
68        "".into()
69    };
70
71    // get remaining path
72    if depth > 0 {
73        return format!("{}{}", "../".repeat(depth), to_path);
74    } else {
75        return format!("./{}", to_path);
76    }
77}
78
79pub type SiteId = usize;
80
81/// Code representation of all nodes within the site (hiarchy and how nodes are related)
82#[derive(Debug)]
83pub struct SiteTree {
84    nodes: Vec<SiteNode>,
85    root: SiteId,
86    // used for detecting if inputs are outside of the root input file
87    root_input: Input,
88
89    /// cannonical paths to node ids
90    input_to_id: HashMap<Input, SiteId>,
91    rel_graph: RelationalGraph,
92}
93
94impl SiteTree {
95    /// `input` is a markdown input file from where to start discovering resources and pages
96    pub fn from_input(input: Input) -> Result<SiteTree, LssgError> {
97        let mut tree = SiteTree {
98            nodes: vec![],
99            root: 0,
100            root_input: input.clone(),
101            input_to_id: HashMap::new(),
102            rel_graph: RelationalGraph::new(),
103        };
104        tree.add_page_with_root(input, None)?;
105        Ok(tree)
106    }
107
108    /// Check if node `id` has `parent_id` as (grand)parent node
109    pub fn is_parent(&self, id: SiteId, parent_id: SiteId) -> bool {
110        let mut parent = self.nodes[id].parent;
111        while let Some(p) = parent {
112            if p == parent_id {
113                return true;
114            }
115            parent = self.nodes[id].parent
116        }
117        return false;
118    }
119
120    /// try and get the input of a node if input exists
121    pub fn get_input(&self, id: SiteId) -> Option<&Input> {
122        self.input_to_id
123            .iter()
124            .find_map(|(input, i)| if *i == id { Some(input) } else { None })
125    }
126
127    // get a node by name by checking the children of `id`
128    pub fn get_by_name(&self, name: &str, id: SiteId) -> Option<&SiteId> {
129        self.nodes[id]
130            .children
131            .iter()
132            .find(|n| &self.nodes[**n].name == name)
133    }
134
135    pub fn root(&self) -> SiteId {
136        self.root
137    }
138
139    pub fn get(&self, id: SiteId) -> Result<&SiteNode, LssgError> {
140        self.nodes.get(id).ok_or(LssgError::sitetree(&format!(
141            "Could not find {id} in SiteTree"
142        )))
143    }
144
145    /// get next parent of page
146    pub fn page_parent(&self, id: SiteId) -> Option<SiteId> {
147        let mut parent = self.nodes[id].parent;
148        let mut parents = vec![];
149        while let Some(p) = parent {
150            if let SiteNodeKind::Page { .. } = self.nodes[p].kind {
151                return Some(p);
152            }
153            parents.push(p);
154            parent = self.nodes[p].parent;
155        }
156        None
157    }
158
159    /// Get all parents from a node
160    pub fn parents(&self, id: SiteId) -> Vec<SiteId> {
161        let mut parent = self.nodes[id].parent;
162        let mut parents = vec![];
163        while let Some(p) = parent {
164            parents.push(p);
165            parent = self.nodes[p].parent;
166        }
167        parents
168    }
169
170    /// Get the absolute path of a node
171    pub fn path(&self, id: SiteId) -> String {
172        absolute_path(&self.nodes, id)
173    }
174
175    /// Get the relative path between two nodes
176    pub fn rel_path(&self, from: SiteId, to: SiteId) -> String {
177        rel_path(&self.nodes, from, to)
178    }
179
180    pub fn ids(&self) -> Vec<SiteId> {
181        (0..self.nodes.len() - 1).collect()
182    }
183
184    pub fn add_link(&mut self, from: SiteId, to: SiteId) {
185        self.rel_graph.add(from, to, Relation::External);
186    }
187
188    /// Get all the relations from a single node to other nodes
189    pub fn links_from(&self, from: SiteId) -> Vec<&Link> {
190        self.rel_graph.links_from(from)
191    }
192
193    /// Utility function to add a node, create a id and add to parent children
194    pub fn add(&mut self, node: SiteNode) -> Result<SiteId, LssgError> {
195        // check for name collisions
196        if let Some(parent) = node.parent {
197            if let Some(id) = self.get_by_name(&node.name, parent) {
198                warn!("{} already exists at {id}", node.name);
199                return Ok(*id);
200            }
201        }
202
203        let id = self.nodes.len();
204        if let Some(parent) = node.parent {
205            self.nodes[parent].children.push(id);
206            self.rel_graph.add(parent, id, Relation::Family);
207        }
208        self.nodes.push(node);
209
210        Ok(id)
211    }
212
213    /// add from Input, will figure out what node to add from input and will register input not to
214    /// be used for other nodes
215    pub fn add_from_input(
216        &mut self,
217        input: Input,
218        mut parent_id: SiteId,
219    ) -> Result<SiteId, LssgError> {
220        // return id if file already exists
221        if let Some(id) = self.input_to_id.get(&input) {
222            warn!(
223                "{} already exists using existing node instead",
224                input.filename()?
225            );
226            return Ok(*id);
227        }
228
229        let id = if SiteNodeKind::input_is_stylesheet(&input) {
230            self.add_stylesheet_from_input(input.clone(), parent_id)?
231        } else if SiteNodeKind::input_is_page(&input) {
232            self.add_page_from_input(input.clone(), parent_id)?
233        } else {
234            parent_id = self.create_folders(&input, parent_id)?;
235            let id = self.add(SiteNode {
236                name: input.filename()?,
237                parent: Some(parent_id),
238                children: vec![],
239                kind: SiteNodeKind::Resource(Resource::from_input(&input)?),
240            })?;
241            self.input_to_id.insert(input.clone(), id);
242            id
243        };
244
245        Ok(id)
246    }
247
248    /// Add a page node to tree and discover any other new pages
249    /// will error if input is not a markdown file
250    fn add_page_from_input(&mut self, input: Input, parent: SiteId) -> Result<SiteId, LssgError> {
251        self.add_page_with_root(input, Some(parent))
252    }
253
254    /// Add a page node to tree and discover any other new pages with possibility of adding root
255    fn add_page_with_root(
256        &mut self,
257        input: Input,
258        mut parent: Option<SiteId>,
259    ) -> Result<SiteId, LssgError> {
260        if let Some(id) = self.input_to_id.get(&input) {
261            // TODO if this page exists should the location of the page be updated?
262            return Ok(*id);
263        }
264
265        if let Some(parent) = &mut parent {
266            *parent = self.create_folders(&input, *parent)?;
267        }
268
269        // create early because of the need of an parent id
270        let page = Page::from_input(&input)?;
271        let id = self.add(SiteNode {
272            name: input.filestem().unwrap_or("root".to_string()),
273            parent,
274            children: vec![],
275            kind: SiteNodeKind::Page(page),
276        })?;
277
278        // register input
279        self.input_to_id.insert(input.clone(), id);
280
281        let links: Vec<(bool, String)> = match &self.nodes[id].kind {
282            SiteNodeKind::Page(page) => page
283                .links()
284                .into_iter()
285                .map(|(text, href)| (text.len() == 0, href.clone()))
286                .collect(),
287            _ => panic!("has to be page"),
288        };
289
290        for (is_empty, href) in links {
291            // if link has no text add whatever is in it
292            if is_empty {
293                let input = input.new(&href)?;
294                let child_id = self.add_from_input(input, id)?;
295                self.rel_graph
296                    .add(id, child_id, Relation::Discovered { raw_path: href });
297                continue;
298            }
299
300            if Page::is_href_to_page(&href) {
301                let input = input.new(&href)?;
302                let child_id = self.add_page_from_input(input, id)?;
303                self.rel_graph
304                    .add(id, child_id, Relation::Discovered { raw_path: href });
305                continue;
306            }
307        }
308
309        return Ok(id);
310    }
311
312    /// Add a stylesheet and all resources needed by the stylesheet
313    pub fn add_stylesheet_from_input(
314        &mut self,
315        input: Input,
316        mut parent: SiteId,
317    ) -> Result<SiteId, LssgError> {
318        parent = self.create_folders(&input, parent)?;
319
320        let stylesheet = Stylesheet::try_from(&input)?;
321        let resources: Vec<String> = stylesheet
322            .resources()
323            .into_iter()
324            .map(|p| p.to_string())
325            .collect();
326
327        let parent = self.create_folders(&input, parent)?;
328        let stylesheet_id = self.add(SiteNode {
329            name: input.filename()?,
330            parent: Some(parent),
331            children: vec![],
332            kind: SiteNodeKind::Stylesheet(stylesheet),
333        })?;
334
335        for resource in resources {
336            let input = input.new(&resource)?;
337            let parent = self.create_folders(&input, parent)?;
338            let resource_id = self.add(SiteNode {
339                name: input.filename()?,
340                parent: Some(parent),
341                children: vec![],
342                kind: SiteNodeKind::Resource(Resource::from_input(&input)?),
343            })?;
344            self.rel_graph.add(
345                stylesheet_id,
346                resource_id,
347                Relation::Discovered { raw_path: resource },
348            );
349            self.input_to_id.insert(input, resource_id);
350        }
351
352        // register input
353        self.input_to_id.insert(input, stylesheet_id);
354
355        Ok(stylesheet_id)
356    }
357
358    /// If local input and not outside of `root_input` it will create some extra folders for
359    /// structuring SiteTree
360    fn create_folders(&mut self, input: &Input, mut parent: SiteId) -> Result<SiteId, LssgError> {
361        if let Some(rel_path) = self.root_input.make_relative(input) {
362            // don't allow backtrack from root
363            if rel_path.starts_with("..") {
364                return Ok(parent);
365            }
366            let parts: Vec<&str> = rel_path.split("/").collect();
367            let parts = &parts[0..parts.len() - 1];
368
369            let mut parents = self.parents(parent);
370            parents.push(parent);
371            parents.reverse();
372            for i in 0..parts.len() {
373                let name = parts[i];
374                if let Some(parent) = parents.get(i) {
375                    if self[*parent].name == name {
376                        continue;
377                    }
378                }
379                if let Some(id) = self.get_by_name(name, parent) {
380                    parent = *id;
381                } else {
382                    debug!("creating folder {name:?} under {parent:?}");
383                    parent = self.add(SiteNode {
384                        name: name.to_string(),
385                        parent: Some(parent),
386                        children: vec![],
387                        kind: SiteNodeKind::Folder,
388                    })?;
389                }
390            }
391        }
392        return Ok(parent);
393    }
394
395    pub fn remove(&mut self, id: SiteId) {
396        self.rel_graph.remove_all(id);
397        todo!("remove from tree");
398    }
399
400    /// Concat resources and minify what can be minified
401    pub fn minify(&mut self) {
402        // TODO
403        todo!()
404    }
405}
406
407impl Tree for SiteTree {
408    type Node = SiteNode;
409
410    fn root(&self) -> SiteId {
411        self.root
412    }
413
414    fn get(&self, id: SiteId) -> &Self::Node {
415        &self[id]
416    }
417}
418
419impl fmt::Display for SiteTree {
420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421        // fill in table
422        let mut row_length = 0;
423        let mut table: Vec<Vec<Option<String>>> = vec![];
424        let mut prev_col = 0;
425        let mut queue = vec![(self.root(), 0)];
426        while let Some((n, col)) = queue.pop() {
427            let node = &self.nodes[n];
428            for c in &node.children {
429                queue.push((c.clone(), col + 1))
430            }
431
432            // create col if not exists
433            if let None = table.get(col) {
434                table.push(vec![]);
435            }
436
437            // fill in until we reach the current row where we are
438            let amount_rows_in_col = table[col].len();
439            // if going back fill all the way
440            if prev_col > col {
441                for _ in amount_rows_in_col..row_length {
442                    table[col].push(None);
443                }
444            } else {
445                // if going forward fill to current row - 1
446                for _ in amount_rows_in_col + 1..row_length {
447                    table[col].push(None);
448                }
449            }
450            prev_col = col;
451
452            let node_name = format!("{}({})({})", node.name, n, node.kind.to_string());
453            table[col].push(Some(node_name));
454
455            let amount_rows_in_col = table[col].len();
456            // update at what row we are
457            if amount_rows_in_col > row_length {
458                row_length = amount_rows_in_col;
459            }
460        }
461
462        // display table
463        let mut out = vec![String::new(); row_length];
464        for col in 0..table.len() {
465            let max_name_length = table[col]
466                .iter()
467                .map(|c| c.as_ref().map(|c| c.len()).unwrap_or(0))
468                .reduce(|a, b| a.max(b))
469                .unwrap_or(0);
470            for (row, entry) in table[col].iter().enumerate() {
471                match entry {
472                    Some(name) => {
473                        out[row] += name;
474                        out[row] += &" ".repeat(max_name_length - name.len());
475                        if let Some(next_column) = table.get(col + 1) {
476                            if let Some(Some(_)) = next_column.get(row) {
477                                out[row] += &" - ";
478                                continue;
479                            }
480                        }
481                        out[row] += &"   ";
482                    }
483                    None => out[row] += &" ".repeat(max_name_length + 3),
484                }
485            }
486            for row in table[col].len()..row_length {
487                out[row] += &" ".repeat(max_name_length + 3);
488            }
489        }
490
491        f.write_str(&out.join("\n"))?;
492        Ok(())
493    }
494}
495
496impl Index<SiteId> for SiteTree {
497    type Output = SiteNode;
498
499    fn index(&self, index: SiteId) -> &Self::Output {
500        &self.nodes[index]
501    }
502}
503impl IndexMut<SiteId> for SiteTree {
504    fn index_mut(&mut self, index: SiteId) -> &mut Self::Output {
505        &mut self.nodes[index]
506    }
507}