1use rquickjs::function::{Func, Opt, Rest};
9use rquickjs::{Ctx, Object};
10
11fn normalize_str(path: &str) -> String {
12 let absolute = path.starts_with('/');
13 let mut out: Vec<&str> = Vec::new();
14 for seg in path.split('/') {
15 match seg {
16 "" | "." => {},
17 ".." => {
18 if matches!(out.last(), Some(&"..")) || (out.is_empty() && !absolute) {
19 out.push("..");
20 } else {
21 out.pop();
22 }
23 },
24 s => out.push(s),
25 }
26 }
27 let joined = out.join("/");
28 let trailing = path.len() > 1 && path.ends_with('/') && !joined.is_empty();
29 match (absolute, joined.is_empty()) {
30 (true, true) => "/".to_string(),
31 (true, false) => format!("/{joined}{}", if trailing { "/" } else { "" }),
32 (false, true) => ".".to_string(),
33 (false, false) => format!("{joined}{}", if trailing { "/" } else { "" }),
34 }
35}
36
37fn join_segments(segments: &[String]) -> String {
38 let parts: Vec<&str> = segments.iter().map(String::as_str).filter(|s| !s.is_empty()).collect();
39 if parts.is_empty() {
40 return ".".to_string();
41 }
42 normalize_str(&parts.join("/"))
43}
44
45fn dirname_str(path: &str) -> String {
46 let trimmed = path.trim_end_matches('/');
47 match trimmed.rfind('/') {
48 Some(0) => "/".to_string(),
49 Some(i) => trimmed[..i].to_string(),
50 None => {
51 if path.starts_with('/') {
52 "/".to_string()
53 } else {
54 ".".to_string()
55 }
56 },
57 }
58}
59
60fn basename_str(path: &str, ext: Option<&str>) -> String {
61 let trimmed = path.trim_end_matches('/');
62 let base = trimmed.rsplit('/').next().unwrap_or(trimmed);
63 match ext {
64 Some(e) if base.len() > e.len() && base.ends_with(e) => base[..base.len() - e.len()].to_string(),
65 _ => base.to_string(),
66 }
67}
68
69fn extname_str(path: &str) -> String {
70 let base = basename_str(path, None);
71 match base.rfind('.') {
72 Some(i) if i > 0 => base[i..].to_string(),
74 _ => String::new(),
75 }
76}
77
78fn resolve_segments(cwd: &str, segments: &[String]) -> String {
79 let mut acc = cwd.to_string();
80 for seg in segments {
81 if seg.is_empty() {
82 continue;
83 }
84 if seg.starts_with('/') {
85 acc.clone_from(seg);
86 } else {
87 acc = format!("{acc}/{seg}");
88 }
89 }
90 let n = normalize_str(&acc);
91 if n.len() > 1 {
93 n.trim_end_matches('/').to_string()
94 } else {
95 n
96 }
97}
98
99fn relative_str(from: &str, to: &str) -> String {
100 let f = normalize_str(from);
101 let t = normalize_str(to);
102 let fp: Vec<&str> = f.split('/').filter(|s| !s.is_empty()).collect();
103 let tp: Vec<&str> = t.split('/').filter(|s| !s.is_empty()).collect();
104 let common = fp.iter().zip(tp.iter()).take_while(|(a, b)| a == b).count();
105 let mut out: Vec<&str> = vec![".."; fp.len() - common];
106 out.extend(&tp[common..]);
107 out.join("/")
108}
109
110fn js_cwd(ctx: &Ctx<'_>) -> String {
113 let cwd: rquickjs::Result<String> = (|| {
114 let process: Object<'_> = ctx.globals().get("process")?;
115 let cwd_fn: rquickjs::Function<'_> = process.get("cwd")?;
116 cwd_fn.call(())
117 })();
118 cwd.unwrap_or_else(|_| "/".to_string())
119}
120
121pub fn path_object<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
124 let o = Object::new(ctx.clone())?;
125 o.set("sep", "/")?;
126 o.set("delimiter", ":")?;
127 o.set("join", Func::from(|segs: Rest<String>| join_segments(&segs.0)))?;
128 o.set(
129 "resolve",
130 Func::from(|ctx: Ctx<'_>, segs: Rest<String>| -> String { resolve_segments(&js_cwd(&ctx), &segs.0) }),
131 )?;
132 o.set("normalize", Func::from(|p: String| normalize_str(&p)))?;
133 o.set("dirname", Func::from(|p: String| dirname_str(&p)))?;
134 o.set(
135 "basename",
136 Func::from(|p: String, ext: Opt<String>| basename_str(&p, ext.0.as_deref())),
137 )?;
138 o.set("extname", Func::from(|p: String| extname_str(&p)))?;
139 o.set(
140 "relative",
141 Func::from(|ctx: Ctx<'_>, from: String, to: String| -> String {
142 let cwd = js_cwd(&ctx);
143 relative_str(&resolve_segments(&cwd, &[from]), &resolve_segments(&cwd, &[to]))
144 }),
145 )?;
146 o.set("isAbsolute", Func::from(|p: String| p.starts_with('/')))?;
147 Ok(o)
148}