Skip to main content

robinpath_modules/modules/
path_mod.rs

1use robinpath::{RobinPath, Value};
2use std::path::{Path, MAIN_SEPARATOR};
3
4pub fn register(rp: &mut RobinPath) {
5    rp.register_builtin("path.join", |args, _| {
6        let parts: Vec<String> = args.iter().map(|v| v.to_display_string()).collect();
7        if parts.is_empty() {
8            return Ok(Value::String(".".to_string()));
9        }
10        let mut result = std::path::PathBuf::from(&parts[0]);
11        for part in &parts[1..] {
12            result.push(part);
13        }
14        Ok(Value::String(normalize_slashes(&result.to_string_lossy())))
15    });
16
17    rp.register_builtin("path.resolve", |args, _| {
18        let base = std::env::current_dir().unwrap_or_default();
19        let mut result = base;
20        for arg in args {
21            let s = arg.to_display_string();
22            let p = Path::new(&s);
23            if p.is_absolute() {
24                result = p.to_path_buf();
25            } else {
26                result.push(p);
27            }
28        }
29        Ok(Value::String(normalize_slashes(&result.to_string_lossy())))
30    });
31
32    rp.register_builtin("path.dirname", |args, _| {
33        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
34        let p = Path::new(&s);
35        match p.parent() {
36            Some(parent) => {
37                let d = parent.to_string_lossy();
38                if d.is_empty() {
39                    Ok(Value::String(".".to_string()))
40                } else {
41                    Ok(Value::String(normalize_slashes(&d)))
42                }
43            }
44            None => Ok(Value::String(".".to_string())),
45        }
46    });
47
48    rp.register_builtin("path.basename", |args, _| {
49        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
50        let ext = args.get(1).map(|v| v.to_display_string());
51        let p = Path::new(&s);
52        let name = p
53            .file_name()
54            .map(|n| n.to_string_lossy().to_string())
55            .unwrap_or_default();
56        if let Some(ext) = ext {
57            if name.ends_with(&ext) {
58                return Ok(Value::String(name[..name.len() - ext.len()].to_string()));
59            }
60        }
61        Ok(Value::String(name))
62    });
63
64    rp.register_builtin("path.extname", |args, _| {
65        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
66        let p = Path::new(&s);
67        match p.extension() {
68            Some(ext) => Ok(Value::String(format!(".{}", ext.to_string_lossy()))),
69            None => Ok(Value::String(String::new())),
70        }
71    });
72
73    rp.register_builtin("path.normalize", |args, _| {
74        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
75        // Simple normalization: resolve . and .. components
76        let parts: Vec<&str> = s.split(['/', '\\']).collect();
77        let mut stack: Vec<&str> = Vec::new();
78        for part in parts {
79            match part {
80                "." | "" => {}
81                ".." => {
82                    if !stack.is_empty() {
83                        stack.pop();
84                    }
85                }
86                _ => stack.push(part),
87            }
88        }
89        let result = if stack.is_empty() {
90            ".".to_string()
91        } else {
92            stack.join("/")
93        };
94        // Preserve leading slash for absolute paths
95        if s.starts_with('/') || s.starts_with('\\') {
96            Ok(Value::String(format!("/{}", result)))
97        } else {
98            Ok(Value::String(result))
99        }
100    });
101
102    rp.register_builtin("path.isAbsolute", |args, _| {
103        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
104        Ok(Value::Bool(Path::new(&s).is_absolute()))
105    });
106
107    rp.register_builtin("path.relative", |args, _| {
108        let from = args.first().map(|v| v.to_display_string()).unwrap_or_default();
109        let to = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
110        // Simple relative path: split both, skip common prefix, add .. for remaining from
111        let from_parts: Vec<&str> = from.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
112        let to_parts: Vec<&str> = to.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
113        let mut common = 0;
114        for (a, b) in from_parts.iter().zip(to_parts.iter()) {
115            if a == b {
116                common += 1;
117            } else {
118                break;
119            }
120        }
121        let ups = from_parts.len() - common;
122        let mut parts: Vec<&str> = Vec::new();
123        for _ in 0..ups {
124            parts.push("..");
125        }
126        for part in &to_parts[common..] {
127            parts.push(part);
128        }
129        if parts.is_empty() {
130            Ok(Value::String(".".to_string()))
131        } else {
132            Ok(Value::String(parts.join("/")))
133        }
134    });
135
136    rp.register_builtin("path.parse", |args, _| {
137        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
138        let p = Path::new(&s);
139        let mut obj = indexmap::IndexMap::new();
140        let dir = p
141            .parent()
142            .map(|d| d.to_string_lossy().to_string())
143            .unwrap_or_default();
144        let base = p
145            .file_name()
146            .map(|n| n.to_string_lossy().to_string())
147            .unwrap_or_default();
148        let ext = p
149            .extension()
150            .map(|e| format!(".{}", e.to_string_lossy()))
151            .unwrap_or_default();
152        let name = p
153            .file_stem()
154            .map(|n| n.to_string_lossy().to_string())
155            .unwrap_or_default();
156        // Determine root
157        let root = if s.starts_with('/') {
158            "/".to_string()
159        } else if s.len() >= 3 && s.as_bytes().get(1) == Some(&b':') {
160            s[..3].to_string()
161        } else {
162            String::new()
163        };
164        obj.insert("root".to_string(), Value::String(root));
165        obj.insert("dir".to_string(), Value::String(normalize_slashes(&dir)));
166        obj.insert("base".to_string(), Value::String(base));
167        obj.insert("ext".to_string(), Value::String(ext));
168        obj.insert("name".to_string(), Value::String(name));
169        Ok(Value::Object(obj))
170    });
171
172    rp.register_builtin("path.separator", |_args, _| {
173        Ok(Value::String(MAIN_SEPARATOR.to_string()))
174    });
175}
176
177fn normalize_slashes(s: &str) -> String {
178    s.replace('\\', "/")
179}