robinpath_modules/modules/
xml_mod.rs1use 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 Ok(Value::Bool(is_valid_xml(&s)))
18 });
19
20 rp.register_builtin("xml.query", |args, _| {
21 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
22 let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
23 let parsed = parse_xml(&s);
24 Ok(get_by_path(&parsed, &path))
25 });
26
27 rp.register_builtin("xml.toJSON", |args, _| {
28 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
29 let parsed = parse_xml(&s);
30 let json_val: serde_json::Value = parsed.into();
31 Ok(Value::String(serde_json::to_string_pretty(&json_val).unwrap_or_default()))
32 });
33
34 rp.register_builtin("xml.fromJSON", |args, _| {
35 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36 match serde_json::from_str::<serde_json::Value>(&s) {
37 Ok(v) => {
38 let val = Value::from(v);
39 Ok(Value::String(stringify_xml(&val, 0, 2)))
40 }
41 Err(e) => Err(format!("xml.fromJSON error: {}", e)),
42 }
43 });
44
45 rp.register_builtin("xml.parseFile", |args, _| {
46 let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
47 match std::fs::read_to_string(&path) {
48 Ok(content) => Ok(parse_xml(&content)),
49 Err(e) => Err(format!("xml.parseFile error: {}", e)),
50 }
51 });
52
53 rp.register_builtin("xml.writeFile", |args, _| {
54 let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
55 let val = args.get(1).cloned().unwrap_or(Value::Null);
56 let xml_str = stringify_xml(&val, 0, 2);
57 match std::fs::write(&path, &xml_str) {
58 Ok(()) => Ok(Value::Bool(true)),
59 Err(e) => Err(format!("xml.writeFile error: {}", e)),
60 }
61 });
62
63 rp.register_builtin("xml.count", |args, _| {
64 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
65 let tag = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
66 let open_tag = format!("<{}", tag);
67 let count = s.matches(&open_tag).count();
68 Ok(Value::Number(count as f64))
69 });
70
71 rp.register_builtin("xml.getAttribute", |args, _| {
72 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
73 let _element = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
74 let attr = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
75 let pattern = format!("{}=\"", attr);
76 if let Some(start) = s.find(&pattern) {
77 let rest = &s[start + pattern.len()..];
78 if let Some(end) = rest.find('"') {
79 return Ok(Value::String(rest[..end].to_string()));
80 }
81 }
82 Ok(Value::Null)
83 });
84}
85
86fn parse_xml(xml: &str) -> Value {
87 use quick_xml::Reader;
88
89 let trimmed = xml.trim();
90 if trimmed.is_empty() {
91 return Value::Null;
92 }
93
94 let mut reader = Reader::from_str(trimmed);
95
96 match parse_element_from_reader(&mut reader) {
98 Some(val) => val,
99 None => Value::Null,
100 }
101}
102
103fn parse_element_from_reader(reader: &mut quick_xml::Reader<&[u8]>) -> Option<Value> {
104 use quick_xml::events::Event;
105
106 let mut root = indexmap::IndexMap::new();
107
108 loop {
109 match reader.read_event() {
110 Ok(Event::Start(ref e)) => {
111 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
112 let mut attrs = indexmap::IndexMap::new();
113
114 for attr_result in e.attributes() {
116 if let Ok(attr) = attr_result {
117 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
118 let val = String::from_utf8_lossy(&attr.value).to_string();
119 attrs.insert(key, Value::String(val));
120 }
121 }
122
123 let children = parse_children_from_reader(reader, &tag_name);
125
126 if attrs.is_empty() {
127 if root.contains_key(&tag_name) {
129 let existing = root.swap_remove(&tag_name).unwrap();
130 let arr = match existing {
131 Value::Array(mut a) => { a.push(children); a }
132 other => vec![other, children],
133 };
134 root.insert(tag_name, Value::Array(arr));
135 } else {
136 root.insert(tag_name, children);
137 }
138 } else {
139 if let Value::Object(child_map) = children {
141 for (k, v) in child_map {
142 attrs.insert(k, v);
143 }
144 } else if children != Value::String(String::new()) {
145 attrs.insert("#text".to_string(), children);
146 }
147 if root.contains_key(&tag_name) {
148 let existing = root.swap_remove(&tag_name).unwrap();
149 let arr = match existing {
150 Value::Array(mut a) => { a.push(Value::Object(attrs)); a }
151 other => vec![other, Value::Object(attrs)],
152 };
153 root.insert(tag_name, Value::Array(arr));
154 } else {
155 root.insert(tag_name, Value::Object(attrs));
156 }
157 }
158 }
159 Ok(Event::Empty(ref e)) => {
160 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
161 let mut attrs = indexmap::IndexMap::new();
162 for attr_result in e.attributes() {
163 if let Ok(attr) = attr_result {
164 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
165 let val = String::from_utf8_lossy(&attr.value).to_string();
166 attrs.insert(key, Value::String(val));
167 }
168 }
169 let val = if attrs.is_empty() {
170 Value::String(String::new())
171 } else {
172 Value::Object(attrs)
173 };
174 root.insert(tag_name, val);
175 }
176 Ok(Event::Text(ref e)) => {
177 let text = e.unescape().map(|s| s.to_string()).unwrap_or_default();
178 let trimmed = text.trim();
179 if !trimmed.is_empty() {
180 return Some(Value::String(trimmed.to_string()));
181 }
182 }
183 Ok(Event::CData(ref e)) => {
184 let text = String::from_utf8_lossy(e.as_ref()).to_string();
185 if !text.trim().is_empty() {
186 return Some(Value::String(text));
187 }
188 }
189 Ok(Event::End(_)) | Ok(Event::Eof) => break,
190 Ok(Event::Decl(_)) | Ok(Event::PI(_)) | Ok(Event::Comment(_)) => continue,
191 Ok(Event::DocType(_)) => continue,
192 Err(_) => break,
193 }
194 }
195
196 if root.is_empty() {
197 None
198 } else {
199 Some(Value::Object(root))
200 }
201}
202
203fn parse_children_from_reader(reader: &mut quick_xml::Reader<&[u8]>, parent_tag: &str) -> Value {
204 use quick_xml::events::Event;
205
206 let mut children = indexmap::IndexMap::new();
207 let mut text_parts = Vec::new();
208
209 loop {
210 match reader.read_event() {
211 Ok(Event::Start(ref e)) => {
212 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
213 let mut attrs = indexmap::IndexMap::new();
214
215 for attr_result in e.attributes() {
216 if let Ok(attr) = attr_result {
217 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
218 let val = String::from_utf8_lossy(&attr.value).to_string();
219 attrs.insert(key, Value::String(val));
220 }
221 }
222
223 let inner = parse_children_from_reader(reader, &tag_name);
224
225 let child_val = if attrs.is_empty() {
226 inner
227 } else {
228 if let Value::Object(child_map) = inner {
229 for (k, v) in child_map {
230 attrs.insert(k, v);
231 }
232 } else if inner != Value::String(String::new()) {
233 attrs.insert("#text".to_string(), inner);
234 }
235 Value::Object(attrs)
236 };
237
238 if children.contains_key(&tag_name) {
239 let existing = children.swap_remove(&tag_name).unwrap();
240 let arr = match existing {
241 Value::Array(mut a) => { a.push(child_val); a }
242 other => vec![other, child_val],
243 };
244 children.insert(tag_name, Value::Array(arr));
245 } else {
246 children.insert(tag_name, child_val);
247 }
248 }
249 Ok(Event::Empty(ref e)) => {
250 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
251 let mut attrs = indexmap::IndexMap::new();
252 for attr_result in e.attributes() {
253 if let Ok(attr) = attr_result {
254 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
255 let val = String::from_utf8_lossy(&attr.value).to_string();
256 attrs.insert(key, Value::String(val));
257 }
258 }
259 let val = if attrs.is_empty() {
260 Value::String(String::new())
261 } else {
262 Value::Object(attrs)
263 };
264 children.insert(tag_name, val);
265 }
266 Ok(Event::Text(ref e)) => {
267 let text = e.unescape().map(|s| s.to_string()).unwrap_or_default();
268 let trimmed = text.trim();
269 if !trimmed.is_empty() {
270 text_parts.push(trimmed.to_string());
271 }
272 }
273 Ok(Event::CData(ref e)) => {
274 let text = String::from_utf8_lossy(e.as_ref()).to_string();
275 if !text.trim().is_empty() {
276 text_parts.push(text);
277 }
278 }
279 Ok(Event::End(ref e)) => {
280 let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
281 if name == parent_tag {
282 break;
283 }
284 }
285 Ok(Event::Eof) => break,
286 Ok(Event::Decl(_)) | Ok(Event::PI(_)) | Ok(Event::Comment(_)) | Ok(Event::DocType(_)) => continue,
287 Err(_) => break,
288 }
289 }
290
291 if !children.is_empty() {
292 if !text_parts.is_empty() {
293 children.insert("#text".to_string(), Value::String(text_parts.join(" ")));
294 }
295 Value::Object(children)
296 } else if !text_parts.is_empty() {
297 Value::String(text_parts.join(" "))
298 } else {
299 Value::String(String::new())
300 }
301}
302
303fn is_valid_xml(s: &str) -> bool {
304 use quick_xml::events::Event;
305 use quick_xml::Reader;
306
307 let trimmed = s.trim();
308 if trimmed.is_empty() || !trimmed.contains('<') {
309 return false;
310 }
311
312 let mut reader = Reader::from_str(trimmed);
313 let mut depth = 0i32;
314 let mut had_element = false;
315
316 loop {
317 match reader.read_event() {
318 Ok(Event::Start(_)) => { depth += 1; had_element = true; }
319 Ok(Event::End(_)) => {
320 depth -= 1;
321 if depth < 0 { return false; }
322 }
323 Ok(Event::Empty(_)) => { had_element = true; }
324 Ok(Event::Eof) => break,
325 Ok(_) => continue,
326 Err(_) => return false,
327 }
328 }
329
330 had_element && depth == 0
331}
332
333fn stringify_xml(val: &Value, depth: usize, indent: usize) -> String {
334 let pad = " ".repeat(depth * indent);
335 match val {
336 Value::Object(obj) => {
337 let mut lines = Vec::new();
338 for (key, value) in obj {
339 if key.starts_with('@') || key == "#text" {
340 continue; }
342 match value {
343 Value::Object(inner) => {
344 let mut attr_str = String::new();
346 let mut has_children = false;
347 let mut text_val = None;
348 for (ik, iv) in inner {
349 if ik.starts_with('@') {
350 attr_str.push_str(&format!(" {}=\"{}\"", &ik[1..], iv.to_display_string()));
351 } else if ik == "#text" {
352 text_val = Some(iv.to_display_string());
353 } else {
354 has_children = true;
355 }
356 }
357 if has_children {
358 lines.push(format!("{}<{}{}>", pad, key, attr_str));
359 lines.push(stringify_xml(value, depth + 1, indent));
360 lines.push(format!("{}</{}>", pad, key));
361 } else if let Some(text) = text_val {
362 lines.push(format!("{}<{}{}>{}</{}>", pad, key, attr_str, text, key));
363 } else {
364 lines.push(format!("{}<{}{}></{}>", pad, key, attr_str, key));
365 }
366 }
367 Value::Array(arr) => {
368 for item in arr {
369 match item {
370 Value::Object(_) => {
371 lines.push(format!("{}<{}>", pad, key));
372 lines.push(stringify_xml(item, depth + 1, indent));
373 lines.push(format!("{}</{}>", pad, key));
374 }
375 _ => {
376 lines.push(format!("{}<{}>{}</{}>", pad, key, item.to_display_string(), key));
377 }
378 }
379 }
380 }
381 _ => {
382 lines.push(format!("{}<{}>{}</{}>", pad, key, value.to_display_string(), key));
383 }
384 }
385 }
386 lines.join("\n")
387 }
388 _ => format!("{}{}", pad, val.to_display_string()),
389 }
390}
391
392fn get_by_path(val: &Value, path: &str) -> Value {
393 let parts: Vec<&str> = path.split('.').collect();
394 let mut current = val.clone();
395 for part in parts {
396 if let Value::Object(obj) = ¤t {
397 current = obj.get(part).cloned().unwrap_or(Value::Null);
398 } else {
399 return Value::Null;
400 }
401 }
402 current
403}