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 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 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 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 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 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 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; }
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) = ¤t {
237 current = obj.get(part).cloned().unwrap_or(Value::Null);
238 } else {
239 return Value::Null;
240 }
241 }
242 current
243}