Skip to main content

robinpath_modules/modules/
xml_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("xml.parse", |args, _| {
5        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6        Ok(parse_xml(&s))
7    });
8
9    rp.register_builtin("xml.stringify", |args, _| {
10        let val = args.first().cloned().unwrap_or(Value::Null);
11        let indent = args.get(1).map(|v| v.to_number() as usize).unwrap_or(2);
12        Ok(Value::String(stringify_xml(&val, 0, indent)))
13    });
14
15    rp.register_builtin("xml.isValid", |args, _| {
16        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
17        let trimmed = s.trim();
18        Ok(Value::Bool(
19            trimmed.starts_with('<') && trimmed.ends_with('>') && is_balanced_xml(trimmed),
20        ))
21    });
22
23    rp.register_builtin("xml.query", |args, _| {
24        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
25        let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
26        let parsed = parse_xml(&s);
27        Ok(get_by_path(&parsed, &path))
28    });
29
30    rp.register_builtin("xml.toJSON", |args, _| {
31        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
32        let parsed = parse_xml(&s);
33        let json_val: serde_json::Value = parsed.into();
34        Ok(Value::String(serde_json::to_string_pretty(&json_val).unwrap_or_default()))
35    });
36
37    rp.register_builtin("xml.fromJSON", |args, _| {
38        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
39        match serde_json::from_str::<serde_json::Value>(&s) {
40            Ok(v) => {
41                let val = Value::from(v);
42                Ok(Value::String(stringify_xml(&val, 0, 2)))
43            }
44            Err(e) => Err(format!("xml.fromJSON error: {}", e)),
45        }
46    });
47
48    rp.register_builtin("xml.parseFile", |args, _| {
49        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
50        match std::fs::read_to_string(&path) {
51            Ok(content) => Ok(parse_xml(&content)),
52            Err(e) => Err(format!("xml.parseFile error: {}", e)),
53        }
54    });
55
56    rp.register_builtin("xml.writeFile", |args, _| {
57        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
58        let val = args.get(1).cloned().unwrap_or(Value::Null);
59        let xml_str = stringify_xml(&val, 0, 2);
60        match std::fs::write(&path, &xml_str) {
61            Ok(()) => Ok(Value::Bool(true)),
62            Err(e) => Err(format!("xml.writeFile error: {}", e)),
63        }
64    });
65
66    rp.register_builtin("xml.count", |args, _| {
67        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
68        let tag = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
69        let open_tag = format!("<{}", tag);
70        let count = s.matches(&open_tag).count();
71        Ok(Value::Number(count as f64))
72    });
73
74    rp.register_builtin("xml.getAttribute", |args, _| {
75        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
76        let _element = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
77        let attr = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
78        let pattern = format!("{}=\"", attr);
79        if let Some(start) = s.find(&pattern) {
80            let rest = &s[start + pattern.len()..];
81            if let Some(end) = rest.find('"') {
82                return Ok(Value::String(rest[..end].to_string()));
83            }
84        }
85        Ok(Value::Null)
86    });
87}
88
89fn parse_xml(xml: &str) -> Value {
90    let mut result = indexmap::IndexMap::new();
91    let trimmed = xml.trim();
92    // Skip XML declaration
93    let content = if trimmed.starts_with("<?") {
94        if let Some(end) = trimmed.find("?>") {
95            trimmed[end + 2..].trim()
96        } else {
97            trimmed
98        }
99    } else {
100        trimmed
101    };
102    parse_element(content, &mut result);
103    Value::Object(result)
104}
105
106fn parse_element(xml: &str, result: &mut indexmap::IndexMap<String, Value>) {
107    let trimmed = xml.trim();
108    if !trimmed.starts_with('<') {
109        return;
110    }
111    // Find tag name
112    if let Some(gt) = trimmed.find('>') {
113        let tag_part = &trimmed[1..gt];
114        let (tag_name, _attrs) = if let Some(sp) = tag_part.find(' ') {
115            (&tag_part[..sp], &tag_part[sp..])
116        } else {
117            (tag_part, "")
118        };
119        // Self-closing
120        if tag_part.ends_with('/') {
121            let name = tag_name.trim_end_matches('/');
122            result.insert(name.to_string(), Value::String(String::new()));
123            return;
124        }
125        let close_tag = format!("</{}>", tag_name);
126        if let Some(close_pos) = trimmed.find(&close_tag) {
127            let inner = &trimmed[gt + 1..close_pos];
128            let inner_trimmed = inner.trim();
129            if inner_trimmed.starts_with('<') {
130                // Nested elements
131                let mut children = indexmap::IndexMap::new();
132                parse_children(inner_trimmed, &mut children);
133                result.insert(tag_name.to_string(), Value::Object(children));
134            } else {
135                result.insert(tag_name.to_string(), Value::String(inner.to_string()));
136            }
137            // Parse siblings
138            let after = &trimmed[close_pos + close_tag.len()..];
139            let after_trimmed = after.trim();
140            if !after_trimmed.is_empty() && after_trimmed.starts_with('<') {
141                parse_element(after_trimmed, result);
142            }
143        }
144    }
145}
146
147fn parse_children(xml: &str, result: &mut indexmap::IndexMap<String, Value>) {
148    let mut remaining = xml.trim();
149    while !remaining.is_empty() && remaining.starts_with('<') {
150        if remaining.starts_with("</") {
151            break;
152        }
153        if let Some(gt) = remaining.find('>') {
154            let tag_part = &remaining[1..gt];
155            let tag_name = if let Some(sp) = tag_part.find(' ') {
156                &tag_part[..sp]
157            } else {
158                tag_part.trim_end_matches('/')
159            };
160            if tag_part.ends_with('/') {
161                result.insert(tag_name.to_string(), Value::String(String::new()));
162                remaining = remaining[gt + 1..].trim();
163                continue;
164            }
165            let close_tag = format!("</{}>", tag_name);
166            if let Some(close_pos) = remaining.find(&close_tag) {
167                let inner = &remaining[gt + 1..close_pos];
168                let inner_trimmed = inner.trim();
169                if inner_trimmed.starts_with('<') {
170                    let mut children = indexmap::IndexMap::new();
171                    parse_children(inner_trimmed, &mut children);
172                    result.insert(tag_name.to_string(), Value::Object(children));
173                } else {
174                    result.insert(tag_name.to_string(), Value::String(inner.to_string()));
175                }
176                remaining = remaining[close_pos + close_tag.len()..].trim();
177            } else {
178                break;
179            }
180        } else {
181            break;
182        }
183    }
184}
185
186fn stringify_xml(val: &Value, depth: usize, indent: usize) -> String {
187    let pad = " ".repeat(depth * indent);
188    match val {
189        Value::Object(obj) => {
190            let mut lines = Vec::new();
191            for (key, value) in obj {
192                match value {
193                    Value::Object(_) => {
194                        lines.push(format!("{}<{}>", pad, key));
195                        lines.push(stringify_xml(value, depth + 1, indent));
196                        lines.push(format!("{}</{}>", pad, key));
197                    }
198                    _ => {
199                        lines.push(format!("{}<{}>{}</{}>", pad, key, value.to_display_string(), key));
200                    }
201                }
202            }
203            lines.join("\n")
204        }
205        _ => format!("{}{}", pad, val.to_display_string()),
206    }
207}
208
209fn is_balanced_xml(s: &str) -> bool {
210    let mut stack: Vec<String> = Vec::new();
211    let re = regex::Regex::new(r"<(/?)(\w+)[^>]*/?>").unwrap();
212    for cap in re.captures_iter(s) {
213        let is_close = &cap[1] == "/";
214        let tag = cap[2].to_string();
215        let full = &cap[0];
216        if full.ends_with("/>") {
217            continue; // self-closing
218        }
219        if is_close {
220            if stack.last() == Some(&tag) {
221                stack.pop();
222            } else {
223                return false;
224            }
225        } else {
226            stack.push(tag);
227        }
228    }
229    stack.is_empty()
230}
231
232fn get_by_path(val: &Value, path: &str) -> Value {
233    let parts: Vec<&str> = path.split('.').collect();
234    let mut current = val.clone();
235    for part in parts {
236        if let Value::Object(obj) = &current {
237            current = obj.get(part).cloned().unwrap_or(Value::Null);
238        } else {
239            return Value::Null;
240        }
241    }
242    current
243}