1use std::{
4 path::{Component, Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
5 slice::Iter,
6};
7
8use rquickjs::{
9 function::Opt,
10 module::{Declarations, Exports, ModuleDef},
11 prelude::{Func, Rest},
12 Ctx, Object, Result,
13};
14
15use crate::modules::module::export_default;
16
17pub struct PathModule;
18
19#[cfg(windows)]
20const DELIMITER: char = ';';
21#[cfg(not(windows))]
22const DELIMITER: char = ':';
23
24#[cfg(windows)]
25pub const CURRENT_DIR_STR: &str = ".\\";
26#[cfg(not(windows))]
27pub const CURRENT_DIR_STR: &str = "./";
28
29pub fn dirname(path: String) -> String {
30 if path == MAIN_SEPARATOR_STR {
31 return path;
32 }
33 let path = path.strip_suffix(MAIN_SEPARATOR).unwrap_or(&path);
34 match path.rfind(MAIN_SEPARATOR) {
35 Some(idx) => {
36 let parent = &path[..idx];
37 if parent.is_empty() {
38 MAIN_SEPARATOR_STR
39 } else {
40 parent
41 }
42 }
43 None => ".",
44 }
45 .to_string()
46}
47
48fn name_extname(path: &str) -> (&str, &str) {
49 let path = path.strip_suffix(MAIN_SEPARATOR).unwrap_or(path);
50 let path = match path.rfind(MAIN_SEPARATOR) {
51 Some(idx) => &path[idx + 1..],
52 None => path,
53 };
54 if path.starts_with('.') {
55 return (path, "");
56 }
57 match path.rfind('.') {
58 Some(idx) => path.split_at(idx),
59 None => (path, ""),
60 }
61}
62
63fn basename(path: String, suffix: Opt<String>) -> String {
64 if path == MAIN_SEPARATOR_STR {
65 return path;
66 }
67 if path.is_empty() {
68 return String::from(".");
69 }
70 let (base, ext) = name_extname(&path);
71 let name = format!("{}{}", base, ext);
72 if let Some(suffix) = suffix.0 {
73 name.strip_suffix(&suffix).unwrap_or(&name)
74 } else {
75 &name
76 }
77 .to_string()
78}
79
80fn extname(path: String) -> String {
81 let (_, ext) = name_extname(&path);
82 ext.to_string()
83}
84
85fn format(obj: Object) -> String {
86 let dir: String = obj.get("dir").unwrap_or_default();
87 let root: String = obj.get("root").unwrap_or_default();
88 let base: String = obj.get("base").unwrap_or_default();
89 let name: String = obj.get("name").unwrap_or_default();
90 let ext: String = obj.get("ext").unwrap_or_default();
91
92 let mut path = String::new();
93 if !dir.is_empty() {
94 path.push_str(&dir);
95 if !dir.ends_with(MAIN_SEPARATOR) {
96 path.push(MAIN_SEPARATOR);
97 }
98 } else if !root.is_empty() {
99 path.push_str(&root);
100 if !root.ends_with(MAIN_SEPARATOR) {
101 path.push(MAIN_SEPARATOR);
102 }
103 }
104 if !base.is_empty() {
105 path.push_str(&base);
106 } else {
107 path.push_str(&name);
108 if !ext.is_empty() {
109 if !ext.starts_with('.') {
110 path.push('.');
111 }
112 path.push_str(&ext);
113 }
114 }
115 path
116}
117
118fn parse(ctx: Ctx, path_str: String) -> Result<Object> {
119 let obj = Object::new(ctx)?;
120 let path = Path::new(&path_str);
121 let parent = path
122 .parent()
123 .map(|p| p.to_str().unwrap())
124 .unwrap_or_default();
125 let filename = path
126 .file_name()
127 .map(|n| n.to_str().unwrap())
128 .unwrap_or_default();
129
130 let (name, extension) = name_extname(filename);
131
132 let root = path
133 .components()
134 .next()
135 .and_then(|c| match c {
136 Component::Prefix(prefix) => prefix.as_os_str().to_str(),
137 Component::RootDir => c.as_os_str().to_str(),
138 _ => Some(""),
139 })
140 .unwrap_or_default();
141
142 obj.set("root", root)?;
143 obj.set("dir", parent)?;
144 obj.set("base", format!("{}{}", name, extension))?;
145 obj.set("ext", extension)?;
146 obj.set("name", name)?;
147
148 Ok(obj)
149}
150
151fn join(parts: Rest<String>) -> String {
152 join_path(parts.0)
153}
154
155pub fn join_path(parts: Vec<String>) -> String {
156 let mut result = PathBuf::new();
157 let mut empty = true;
158 for part in parts.iter() {
159 if part.starts_with(MAIN_SEPARATOR) && empty {
160 result.push(MAIN_SEPARATOR_STR);
161 empty = false;
162 }
163 for sub_part in part.split(MAIN_SEPARATOR) {
164 if !sub_part.is_empty() {
165 if sub_part.starts_with("..") {
166 empty = false;
167 result.pop();
168 } else {
169 result.push(sub_part.strip_prefix('.').unwrap_or(sub_part));
170 empty = false;
171 }
172 }
173 }
174 }
175 remove_trailing_slash(result)
176}
177
178fn remove_trailing_slash(result: PathBuf) -> String {
179 let path = result.to_string_lossy().to_string();
180 path.strip_suffix(MAIN_SEPARATOR)
181 .unwrap_or(&path)
182 .to_string()
183}
184
185fn resolve(path: Rest<String>) -> String {
186 resolve_path(path.iter())
187}
188
189pub fn resolve_path(iter: Iter<'_, String>) -> String {
190 let mut dir = std::env::current_dir().unwrap();
191 for part in iter {
192 let p = part.strip_prefix(CURRENT_DIR_STR).unwrap_or(part);
193 if p.starts_with(MAIN_SEPARATOR) {
194 dir = PathBuf::from(p);
195 } else {
196 for sub_part in p.split(MAIN_SEPARATOR) {
197 if sub_part.starts_with("..") {
198 dir.pop();
199 } else {
200 dir.push(sub_part.strip_prefix('.').unwrap_or(sub_part))
201 }
202 }
203 }
204 }
205
206 remove_trailing_slash(dir)
207}
208
209fn normalize(path: String) -> String {
210 let path = PathBuf::from(path);
211 let parts = path
212 .components()
213 .map(|c| c.as_os_str().to_string_lossy().to_string())
214 .collect::<Vec<_>>();
215
216 join_path(parts)
217}
218
219pub fn is_absolute(path: String) -> bool {
220 PathBuf::from(path).is_absolute()
221}
222
223impl ModuleDef for PathModule {
224 fn declare(declare: &Declarations<'_>) -> Result<()> {
225 declare.declare("basename")?;
226 declare.declare("dirname")?;
227 declare.declare("extname")?;
228 declare.declare("format")?;
229 declare.declare("parse")?;
230 declare.declare("join")?;
231 declare.declare("resolve")?;
232 declare.declare("normalize")?;
233 declare.declare("isAbsolute")?;
234 declare.declare("delimiter")?;
235
236 declare.declare("default")?;
237 Ok(())
238 }
239
240 fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
241 export_default(ctx, exports, |default| {
242 default.set("dirname", Func::from(dirname))?;
243 default.set("basename", Func::from(basename))?;
244 default.set("extname", Func::from(extname))?;
245 default.set("format", Func::from(format))?;
246 default.set("parse", Func::from(parse))?;
247 default.set("join", Func::from(join))?;
248 default.set("resolve", Func::from(resolve))?;
249 default.set("normalize", Func::from(normalize))?;
250 default.set("isAbsolute", Func::from(is_absolute))?;
251 default.prop("delimiter", DELIMITER.to_string())?;
252 Ok(())
253 })
254 }
255}