robinpath_modules/modules/
path_mod.rs1use 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 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 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 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 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}