Skip to main content

huff_neo_utils/file/
file_source.rs

1use crate::prelude::Span;
2use crate::time;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fmt;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9/// File Encapsulation
10#[derive(Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
11#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
12#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
13pub struct FileSource {
14    /// File Path
15    pub path: String,
16    /// File Source
17    pub source: Option<String>,
18    /// Last File Access Time
19    #[cfg_attr(feature = "wasm", tsify(type = "number | undefined"))]
20    pub access: Option<time::Time>,
21    /// An Ordered List of File Dependencies
22    pub dependencies: Vec<Arc<FileSource>>,
23}
24
25// Reduce the size of the debug output by not printing the source code and dependencies
26impl fmt::Debug for FileSource {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.debug_struct("FileSource")
29            .field("path", &self.path)
30            .field("source", &"...")
31            .field("access", &self.access)
32            .field("dependencies", &"...")
33            .finish()
34    }
35}
36
37impl FileSource {
38    /// Visit all dependencies and return a list of none duplicate [FileSource] in order of occurrence.
39    fn discover_all_file_sources(node_file_source: Arc<FileSource>, visited_nodes: &mut HashSet<String>) -> Vec<Arc<FileSource>> {
40        if visited_nodes.contains(&node_file_source.path) {
41            return vec![];
42        }
43        visited_nodes.insert(node_file_source.path.clone());
44
45        let mut result = vec![];
46        for child_file_source in node_file_source.dependencies.iter() {
47            let mut child_result = FileSource::discover_all_file_sources(child_file_source.clone(), visited_nodes);
48            result.append(&mut child_result);
49        }
50
51        result.push(node_file_source);
52
53        result
54    }
55
56    /// Generates a fully flattened source code for the given `FileSource` and all its dependencies
57    ///
58    /// ### Examples
59    ///
60    /// Let's say you have a file, `a.txt` with two dependencies, `b.txt` and `c.txt`,
61    /// `fully_flatten()` will generate a source code string with the contents of `b.txt` and
62    /// `c.txt` appended to the end of the contents of `a.txt`.
63    ///
64    /// Note: This preserves #include statements in the output to maintain correct span positions
65    pub fn fully_flatten(node_file_source: Arc<FileSource>) -> (String, Vec<(Arc<FileSource>, Span)>) {
66        let all_file_sources = FileSource::discover_all_file_sources(node_file_source, &mut HashSet::new());
67
68        let mut full_source = "".to_string();
69        let mut relative_positions = vec![];
70        let mut shift_pos = 0;
71
72        // Append all child elements
73        for (i, child_file_source) in all_file_sources.into_iter().enumerate() {
74            let Some(source) = child_file_source.source.clone() else {
75                continue;
76            };
77
78            // Add a newline before each file after the first to ensure proper separation
79            if i > 0 && !full_source.ends_with('\n') {
80                full_source.push('\n');
81                shift_pos += 1;
82            }
83
84            let span = Span::new(shift_pos..(shift_pos + source.len() - 1), Some(child_file_source.clone()));
85            relative_positions.push((child_file_source, span));
86            shift_pos += source.len();
87            full_source = format!("{full_source}{source}");
88        }
89
90        (full_source, relative_positions)
91    }
92
93    /// Derives a File Path's directory
94    pub fn derive_dir(path: &str) -> Option<String> {
95        let path = PathBuf::from(path);
96        match path.parent() {
97            Some(p) => p.to_str().map(String::from),
98            None => None,
99        }
100    }
101
102    /// Localizes a file path, if path is relative
103    pub fn localize_file(parent: &str, child: &str) -> Option<String> {
104        let mut prefixed_parent;
105        // A relative parent is relative to the working directory; an absolute one stays absolute
106        if !parent.starts_with('.') && !Path::new(parent).is_absolute() {
107            prefixed_parent = "./".to_owned();
108            prefixed_parent.push_str(parent);
109        } else {
110            prefixed_parent = parent.to_owned();
111        }
112        let mut prefix = match FileSource::derive_dir(prefixed_parent.as_str()) {
113            Some(p) => {
114                if p.is_empty() {
115                    String::from(".")
116                } else {
117                    p
118                }
119            }
120            None => String::from("."),
121        };
122        if child.starts_with("../") {
123            let mut res_str = child.to_string();
124            while res_str.starts_with("../") {
125                let path = PathBuf::from(prefix.clone());
126                match path.parent() {
127                    Some(p) => match p.to_str().map(String::from) {
128                        Some(pref) => {
129                            if pref.is_empty() || prefix.ends_with("..") {
130                                if prefix.is_empty() || prefix == "." {
131                                    prefix = "..".to_string();
132                                } else {
133                                    prefix = format!("../{prefix}");
134                                }
135                            } else {
136                                prefix = pref
137                            }
138                        }
139                        None => {
140                            tracing::warn!("Failed to convert path to string");
141                            return None;
142                        }
143                    },
144                    None => {
145                        tracing::warn!("Failed to find parent for path: {:?}", path);
146                        return None;
147                    }
148                }
149                res_str = res_str.replacen("../", "", 1);
150            }
151            Some(format!("{}/{res_str}", prefix.trim_end_matches('/')))
152        } else if child.starts_with("./") {
153            Some(child.replacen("./", &format!("{}/", prefix.trim_end_matches('/')), 1))
154        } else if child.starts_with('/') {
155            Some(child.to_string())
156        } else {
157            Some(format!("{}/{child}", prefix.trim_end_matches('/')))
158        }
159    }
160}