Skip to main content

stryke/
builtins_data.rs

1//! HTML / JSON / XML / CSS primitives.
2//! Uses `serde_json` and `scraper` (both already deps). XML/CSS ops
3//! that don't have a clean crate-backed implementation use lightweight
4//! regex/string parsing — pragmatic, not RFC-perfect.
5
6use crate::value::StrykeValue;
7use parking_lot::RwLock;
8use std::sync::Arc;
9
10fn arg_str(args: &[StrykeValue]) -> String {
11    args.first().map(|v| v.to_string()).unwrap_or_default()
12}
13
14fn arr(vs: Vec<StrykeValue>) -> StrykeValue {
15    StrykeValue::array_ref(Arc::new(RwLock::new(vs)))
16}
17
18// ══════════════════════════════════════════════════════════════════════
19// JSON jq-like operations (serde_json backed)
20// ══════════════════════════════════════════════════════════════════════
21
22fn parse_json(s: &str) -> Option<serde_json::Value> {
23    serde_json::from_str(s).ok()
24}
25
26fn json_to_stryke(v: &serde_json::Value) -> StrykeValue {
27    use indexmap::IndexMap;
28    match v {
29        serde_json::Value::Null => StrykeValue::UNDEF,
30        serde_json::Value::Bool(b) => StrykeValue::integer(if *b { 1 } else { 0 }),
31        serde_json::Value::Number(n) => {
32            if let Some(i) = n.as_i64() {
33                StrykeValue::integer(i)
34            } else if let Some(f) = n.as_f64() {
35                StrykeValue::float(f)
36            } else {
37                StrykeValue::string(n.to_string())
38            }
39        }
40        serde_json::Value::String(s) => StrykeValue::string(s.clone()),
41        serde_json::Value::Array(items) => {
42            let elems: Vec<StrykeValue> = items.iter().map(json_to_stryke).collect();
43            arr(elems)
44        }
45        serde_json::Value::Object(m) => {
46            let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
47            for (k, v) in m {
48                h.insert(k.clone(), json_to_stryke(v));
49            }
50            StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
51        }
52    }
53}
54
55fn split_path(path: &str) -> Vec<&str> {
56    path.split('.').filter(|s| !s.is_empty()).collect()
57}
58
59/// `jq_get(JSON_STR, "path.to.key")` — extract value at path. Path
60/// uses dot notation; numeric segments index arrays.
61pub fn jq_get(args: &[StrykeValue]) -> StrykeValue {
62    let s = arg_str(args);
63    let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
64    let Some(mut v) = parse_json(&s) else {
65        return StrykeValue::UNDEF;
66    };
67    for seg in split_path(&path) {
68        if let Ok(idx) = seg.parse::<usize>() {
69            v = match v {
70                serde_json::Value::Array(arr) => {
71                    arr.into_iter().nth(idx).unwrap_or(serde_json::Value::Null)
72                }
73                _ => serde_json::Value::Null,
74            };
75        } else {
76            v = match v {
77                serde_json::Value::Object(mut m) => {
78                    m.remove(seg).unwrap_or(serde_json::Value::Null)
79                }
80                _ => serde_json::Value::Null,
81            };
82        }
83    }
84    json_to_stryke(&v)
85}
86
87pub fn jq_set(args: &[StrykeValue]) -> StrykeValue {
88    let s = arg_str(args);
89    let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
90    let new_val = args.get(2).map(|v| v.to_string()).unwrap_or_default();
91    let Some(mut v) = parse_json(&s) else {
92        return StrykeValue::UNDEF;
93    };
94    let new_v = parse_json(&new_val).unwrap_or(serde_json::Value::String(new_val.clone()));
95    let segs = split_path(&path);
96    fn set_path(v: &mut serde_json::Value, segs: &[&str], new_v: serde_json::Value) {
97        if segs.is_empty() {
98            *v = new_v;
99            return;
100        }
101        let seg = segs[0];
102        let rest = &segs[1..];
103        if let Ok(idx) = seg.parse::<usize>() {
104            if let serde_json::Value::Array(arr) = v {
105                while arr.len() <= idx {
106                    arr.push(serde_json::Value::Null);
107                }
108                set_path(&mut arr[idx], rest, new_v);
109            }
110        } else {
111            if !v.is_object() {
112                *v = serde_json::Value::Object(serde_json::Map::new());
113            }
114            if let serde_json::Value::Object(m) = v {
115                let entry = m.entry(seg.to_string()).or_insert(serde_json::Value::Null);
116                set_path(entry, rest, new_v);
117            }
118        }
119    }
120    set_path(&mut v, &segs, new_v);
121    StrykeValue::string(serde_json::to_string(&v).unwrap_or_default())
122}
123
124pub fn jq_delete(args: &[StrykeValue]) -> StrykeValue {
125    let s = arg_str(args);
126    let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
127    let Some(mut v) = parse_json(&s) else {
128        return StrykeValue::UNDEF;
129    };
130    let segs = split_path(&path);
131    if segs.is_empty() {
132        return StrykeValue::string("null".to_string());
133    }
134    let last = segs[segs.len() - 1];
135    let parents = &segs[..segs.len() - 1];
136    fn descend<'a>(
137        v: &'a mut serde_json::Value,
138        segs: &[&str],
139    ) -> Option<&'a mut serde_json::Value> {
140        let mut cur = v;
141        for seg in segs {
142            if let Ok(idx) = seg.parse::<usize>() {
143                if let serde_json::Value::Array(arr) = cur {
144                    cur = arr.get_mut(idx)?;
145                } else {
146                    return None;
147                }
148            } else if let serde_json::Value::Object(m) = cur {
149                cur = m.get_mut(*seg)?;
150            } else {
151                return None;
152            }
153        }
154        Some(cur)
155    }
156    if let Some(target) = descend(&mut v, parents) {
157        if let Ok(idx) = last.parse::<usize>() {
158            if let serde_json::Value::Array(arr) = target {
159                if idx < arr.len() {
160                    arr.remove(idx);
161                }
162            }
163        } else if let serde_json::Value::Object(m) = target {
164            m.remove(last);
165        }
166    }
167    StrykeValue::string(serde_json::to_string(&v).unwrap_or_default())
168}
169
170pub fn jq_select(args: &[StrykeValue]) -> StrykeValue {
171    jq_get(args)
172}
173
174pub fn jq_keys_at(args: &[StrykeValue]) -> StrykeValue {
175    let v = jq_get(args);
176    if let Some(h) = v.as_hash_ref() {
177        let g = h.read();
178        return arr(g.keys().map(|k| StrykeValue::string(k.clone())).collect());
179    }
180    StrykeValue::UNDEF
181}
182
183pub fn jq_values_at(args: &[StrykeValue]) -> StrykeValue {
184    let v = jq_get(args);
185    if let Some(h) = v.as_hash_ref() {
186        let g = h.read();
187        return arr(g.values().cloned().collect());
188    }
189    if let Some(a) = v.as_array_ref() {
190        return StrykeValue::array_ref(a);
191    }
192    StrykeValue::UNDEF
193}
194
195pub fn jq_length_at(args: &[StrykeValue]) -> StrykeValue {
196    let v = jq_get(args);
197    if let Some(h) = v.as_hash_ref() {
198        return StrykeValue::integer(h.read().len() as i64);
199    }
200    if let Some(a) = v.as_array_ref() {
201        return StrykeValue::integer(a.read().len() as i64);
202    }
203    if let Some(s) = v.as_str() {
204        return StrykeValue::integer(s.chars().count() as i64);
205    }
206    StrykeValue::integer(0)
207}
208
209pub fn jq_type(args: &[StrykeValue]) -> StrykeValue {
210    let s = arg_str(args);
211    let Some(v) = parse_json(&s) else {
212        return StrykeValue::string("invalid".to_string());
213    };
214    let t = match v {
215        serde_json::Value::Null => "null",
216        serde_json::Value::Bool(_) => "boolean",
217        serde_json::Value::Number(_) => "number",
218        serde_json::Value::String(_) => "string",
219        serde_json::Value::Array(_) => "array",
220        serde_json::Value::Object(_) => "object",
221    };
222    StrykeValue::string(t.to_string())
223}
224
225pub fn jq_has(args: &[StrykeValue]) -> StrykeValue {
226    let v = jq_get(args);
227    StrykeValue::integer(if v.is_undef() { 0 } else { 1 })
228}
229
230pub fn jq_paths(args: &[StrykeValue]) -> StrykeValue {
231    let s = arg_str(args);
232    let Some(v) = parse_json(&s) else {
233        return StrykeValue::UNDEF;
234    };
235    fn walk(v: &serde_json::Value, prefix: String, out: &mut Vec<String>) {
236        match v {
237            serde_json::Value::Object(m) => {
238                for (k, vv) in m {
239                    let p = if prefix.is_empty() {
240                        k.clone()
241                    } else {
242                        format!("{}.{}", prefix, k)
243                    };
244                    out.push(p.clone());
245                    walk(vv, p, out);
246                }
247            }
248            serde_json::Value::Array(a) => {
249                for (i, vv) in a.iter().enumerate() {
250                    let p = if prefix.is_empty() {
251                        i.to_string()
252                    } else {
253                        format!("{}.{}", prefix, i)
254                    };
255                    out.push(p.clone());
256                    walk(vv, p, out);
257                }
258            }
259            _ => {}
260        }
261    }
262    let mut paths: Vec<String> = Vec::new();
263    walk(&v, String::new(), &mut paths);
264    arr(paths.into_iter().map(StrykeValue::string).collect())
265}
266
267pub fn jq_leaf_paths(args: &[StrykeValue]) -> StrykeValue {
268    let s = arg_str(args);
269    let Some(v) = parse_json(&s) else {
270        return StrykeValue::UNDEF;
271    };
272    fn walk(v: &serde_json::Value, prefix: String, out: &mut Vec<String>) {
273        match v {
274            serde_json::Value::Object(m) => {
275                for (k, vv) in m {
276                    let p = if prefix.is_empty() {
277                        k.clone()
278                    } else {
279                        format!("{}.{}", prefix, k)
280                    };
281                    walk(vv, p, out);
282                }
283            }
284            serde_json::Value::Array(a) => {
285                for (i, vv) in a.iter().enumerate() {
286                    let p = if prefix.is_empty() {
287                        i.to_string()
288                    } else {
289                        format!("{}.{}", prefix, i)
290                    };
291                    walk(vv, p, out);
292                }
293            }
294            _ => out.push(prefix),
295        }
296    }
297    let mut paths: Vec<String> = Vec::new();
298    walk(&v, String::new(), &mut paths);
299    arr(paths.into_iter().map(StrykeValue::string).collect())
300}
301
302pub fn jq_walk(args: &[StrykeValue]) -> StrykeValue {
303    let s = arg_str(args);
304    let Some(v) = parse_json(&s) else {
305        return StrykeValue::UNDEF;
306    };
307    json_to_stryke(&v)
308}
309
310pub fn jq_map_values(args: &[StrykeValue]) -> StrykeValue {
311    let s = arg_str(args);
312    let Some(v) = parse_json(&s) else {
313        return StrykeValue::UNDEF;
314    };
315    json_to_stryke(&v)
316}
317
318pub fn jq_filter(args: &[StrykeValue]) -> StrykeValue {
319    jq_get(args)
320}
321
322pub fn jq_to_entries(args: &[StrykeValue]) -> StrykeValue {
323    let s = arg_str(args);
324    let Some(v) = parse_json(&s) else {
325        return StrykeValue::UNDEF;
326    };
327    if let serde_json::Value::Object(m) = v {
328        use indexmap::IndexMap;
329        let entries: Vec<StrykeValue> = m
330            .into_iter()
331            .map(|(k, v)| {
332                let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
333                h.insert("key".to_string(), StrykeValue::string(k));
334                h.insert("value".to_string(), json_to_stryke(&v));
335                StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
336            })
337            .collect();
338        return arr(entries);
339    }
340    StrykeValue::UNDEF
341}
342
343pub fn jq_from_entries(args: &[StrykeValue]) -> StrykeValue {
344    use indexmap::IndexMap;
345    let Some(arr_ref) = args.first().and_then(|v| v.as_array_ref()) else {
346        return StrykeValue::UNDEF;
347    };
348    let g = arr_ref.read();
349    let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
350    for entry in g.iter() {
351        if let Some(eh) = entry.as_hash_ref() {
352            let eg = eh.read();
353            let key = eg.get("key").map(|v| v.to_string()).unwrap_or_default();
354            let val = eg.get("value").cloned().unwrap_or(StrykeValue::UNDEF);
355            h.insert(key, val);
356        }
357    }
358    StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
359}
360
361pub fn jq_with_entries(args: &[StrykeValue]) -> StrykeValue {
362    jq_to_entries(args)
363}
364
365pub fn jq_recurse(args: &[StrykeValue]) -> StrykeValue {
366    jq_paths(args)
367}
368
369pub fn jq_min_by(args: &[StrykeValue]) -> StrykeValue {
370    let v = jq_get(args);
371    if let Some(arr_ref) = v.as_array_ref() {
372        let g = arr_ref.read();
373        return g
374            .iter()
375            .min_by(|a, b| {
376                a.to_number()
377                    .partial_cmp(&b.to_number())
378                    .unwrap_or(std::cmp::Ordering::Equal)
379            })
380            .cloned()
381            .unwrap_or(StrykeValue::UNDEF);
382    }
383    StrykeValue::UNDEF
384}
385
386pub fn jq_max_by(args: &[StrykeValue]) -> StrykeValue {
387    let v = jq_get(args);
388    if let Some(arr_ref) = v.as_array_ref() {
389        let g = arr_ref.read();
390        return g
391            .iter()
392            .max_by(|a, b| {
393                a.to_number()
394                    .partial_cmp(&b.to_number())
395                    .unwrap_or(std::cmp::Ordering::Equal)
396            })
397            .cloned()
398            .unwrap_or(StrykeValue::UNDEF);
399    }
400    StrykeValue::UNDEF
401}
402
403pub fn jq_sort_by(args: &[StrykeValue]) -> StrykeValue {
404    let v = jq_get(args);
405    if let Some(arr_ref) = v.as_array_ref() {
406        let mut g: Vec<StrykeValue> = arr_ref.read().clone();
407        g.sort_by(|a, b| {
408            a.to_number()
409                .partial_cmp(&b.to_number())
410                .unwrap_or(std::cmp::Ordering::Equal)
411        });
412        return arr(g);
413    }
414    StrykeValue::UNDEF
415}
416
417pub fn jq_group_by(args: &[StrykeValue]) -> StrykeValue {
418    use indexmap::IndexMap;
419    let v = jq_get(args);
420    if let Some(arr_ref) = v.as_array_ref() {
421        let mut m: IndexMap<String, Vec<StrykeValue>> = IndexMap::new();
422        for x in arr_ref.read().iter() {
423            m.entry(x.to_string()).or_default().push(x.clone());
424        }
425        let groups: Vec<StrykeValue> = m.into_values().map(arr).collect();
426        return arr(groups);
427    }
428    StrykeValue::UNDEF
429}
430
431pub fn jq_unique_by(args: &[StrykeValue]) -> StrykeValue {
432    use std::collections::HashSet;
433    let v = jq_get(args);
434    if let Some(arr_ref) = v.as_array_ref() {
435        let mut seen: HashSet<String> = HashSet::new();
436        let out: Vec<StrykeValue> = arr_ref
437            .read()
438            .iter()
439            .filter(|v| seen.insert(v.to_string()))
440            .cloned()
441            .collect();
442        return arr(out);
443    }
444    StrykeValue::UNDEF
445}
446
447pub fn jq_any(args: &[StrykeValue]) -> StrykeValue {
448    let v = jq_get(args);
449    if let Some(arr_ref) = v.as_array_ref() {
450        return StrykeValue::integer(if arr_ref.read().iter().any(|v| v.is_true()) {
451            1
452        } else {
453            0
454        });
455    }
456    StrykeValue::integer(0)
457}
458
459pub fn jq_all(args: &[StrykeValue]) -> StrykeValue {
460    let v = jq_get(args);
461    if let Some(arr_ref) = v.as_array_ref() {
462        let g = arr_ref.read();
463        if g.is_empty() {
464            return StrykeValue::integer(1);
465        }
466        return StrykeValue::integer(if g.iter().all(|v| v.is_true()) { 1 } else { 0 });
467    }
468    StrykeValue::integer(0)
469}
470
471pub fn jq_flatten(args: &[StrykeValue]) -> StrykeValue {
472    let v = jq_get(args);
473    if let Some(arr_ref) = v.as_array_ref() {
474        let mut out: Vec<StrykeValue> = Vec::new();
475        fn walk(v: &StrykeValue, out: &mut Vec<StrykeValue>) {
476            if let Some(a) = v.as_array_ref() {
477                for x in a.read().iter() {
478                    walk(x, out);
479                }
480            } else {
481                out.push(v.clone());
482            }
483        }
484        for x in arr_ref.read().iter() {
485            walk(x, &mut out);
486        }
487        return arr(out);
488    }
489    v
490}
491
492pub fn jq_index(args: &[StrykeValue]) -> StrykeValue {
493    let v = jq_get(args);
494    let needle = args.get(2).map(|v| v.to_string()).unwrap_or_default();
495    if let Some(arr_ref) = v.as_array_ref() {
496        let g = arr_ref.read();
497        for (i, x) in g.iter().enumerate() {
498            if x.to_string() == needle {
499                return StrykeValue::integer(i as i64);
500            }
501        }
502        return StrykeValue::integer(-1);
503    }
504    StrykeValue::integer(-1)
505}
506
507pub fn jq_indices(args: &[StrykeValue]) -> StrykeValue {
508    let v = jq_get(args);
509    let needle = args.get(2).map(|v| v.to_string()).unwrap_or_default();
510    if let Some(arr_ref) = v.as_array_ref() {
511        let g = arr_ref.read();
512        let mut out: Vec<StrykeValue> = Vec::new();
513        for (i, x) in g.iter().enumerate() {
514            if x.to_string() == needle {
515                out.push(StrykeValue::integer(i as i64));
516            }
517        }
518        return arr(out);
519    }
520    StrykeValue::UNDEF
521}
522
523pub fn jq_first(args: &[StrykeValue]) -> StrykeValue {
524    let v = jq_get(args);
525    if let Some(arr_ref) = v.as_array_ref() {
526        return arr_ref
527            .read()
528            .first()
529            .cloned()
530            .unwrap_or(StrykeValue::UNDEF);
531    }
532    v
533}
534
535pub fn jq_last(args: &[StrykeValue]) -> StrykeValue {
536    let v = jq_get(args);
537    if let Some(arr_ref) = v.as_array_ref() {
538        return arr_ref.read().last().cloned().unwrap_or(StrykeValue::UNDEF);
539    }
540    v
541}
542
543pub fn jq_split_at(args: &[StrykeValue]) -> StrykeValue {
544    let v = jq_get(args);
545    let n = args.get(2).map(|v| v.to_int() as usize).unwrap_or(0);
546    if let Some(arr_ref) = v.as_array_ref() {
547        let g = arr_ref.read();
548        let (l, r): (Vec<_>, Vec<_>) = g.iter().enumerate().partition(|(i, _)| *i < n);
549        let left: Vec<StrykeValue> = l.into_iter().map(|(_, v)| v.clone()).collect();
550        let right: Vec<StrykeValue> = r.into_iter().map(|(_, v)| v.clone()).collect();
551        return arr(vec![arr(left), arr(right)]);
552    }
553    StrykeValue::UNDEF
554}
555
556pub fn jq_chunks(args: &[StrykeValue]) -> StrykeValue {
557    let v = jq_get(args);
558    let n = args.get(2).map(|v| v.to_int().max(1) as usize).unwrap_or(1);
559    if let Some(arr_ref) = v.as_array_ref() {
560        let g = arr_ref.read();
561        let out: Vec<StrykeValue> = g.chunks(n).map(|c| arr(c.to_vec())).collect();
562        return arr(out);
563    }
564    StrykeValue::UNDEF
565}
566
567pub fn jq_zip(args: &[StrykeValue]) -> StrykeValue {
568    let a = jq_get(args);
569    let b = jq_get(&[
570        args.get(2).cloned().unwrap_or(StrykeValue::UNDEF),
571        args.get(3).cloned().unwrap_or(StrykeValue::UNDEF),
572    ]);
573    let (Some(a_arr), Some(b_arr)) = (a.as_array_ref(), b.as_array_ref()) else {
574        return StrykeValue::UNDEF;
575    };
576    let ag = a_arr.read();
577    let bg = b_arr.read();
578    let n = ag.len().min(bg.len());
579    let out: Vec<StrykeValue> = (0..n)
580        .map(|i| arr(vec![ag[i].clone(), bg[i].clone()]))
581        .collect();
582    arr(out)
583}
584
585pub fn jq_combinations(args: &[StrykeValue]) -> StrykeValue {
586    let v = jq_get(args);
587    let k = args.get(2).map(|v| v.to_int() as usize).unwrap_or(2);
588    let Some(arr_ref) = v.as_array_ref() else {
589        return StrykeValue::UNDEF;
590    };
591    let g = arr_ref.read();
592    let n = g.len();
593    if k > n {
594        return arr(vec![]);
595    }
596    let mut indices: Vec<usize> = (0..k).collect();
597    let mut out: Vec<StrykeValue> = Vec::new();
598    loop {
599        out.push(arr(indices.iter().map(|i| g[*i].clone()).collect()));
600        let mut i = k;
601        while i > 0 {
602            i -= 1;
603            if indices[i] < n - k + i {
604                indices[i] += 1;
605                for j in i + 1..k {
606                    indices[j] = indices[j - 1] + 1;
607                }
608                break;
609            }
610            if i == 0 {
611                return arr(out);
612            }
613        }
614    }
615}
616
617pub fn json_diff(args: &[StrykeValue]) -> StrykeValue {
618    let a = arg_str(args);
619    let b = args.get(1).map(|v| v.to_string()).unwrap_or_default();
620    let (Some(va), Some(vb)) = (parse_json(&a), parse_json(&b)) else {
621        return StrykeValue::UNDEF;
622    };
623    if va == vb {
624        StrykeValue::string("[]".to_string())
625    } else {
626        // Simple text diff representation
627        StrykeValue::string(format!(
628            "[{{\"old\":{}}},{{\"new\":{}}}]",
629            serde_json::to_string(&va).unwrap_or_default(),
630            serde_json::to_string(&vb).unwrap_or_default()
631        ))
632    }
633}
634
635pub fn json_patch(args: &[StrykeValue]) -> StrykeValue {
636    // RFC 6902 — applied naively: only "replace" / "add" / "remove" ops.
637    let s = arg_str(args);
638    let patches_s = args.get(1).map(|v| v.to_string()).unwrap_or_default();
639    let Some(mut v) = parse_json(&s) else {
640        return StrykeValue::UNDEF;
641    };
642    let Some(patches) = parse_json(&patches_s) else {
643        return StrykeValue::UNDEF;
644    };
645    let serde_json::Value::Array(ops) = patches else {
646        return StrykeValue::UNDEF;
647    };
648    for op in ops {
649        let serde_json::Value::Object(m) = op else {
650            continue;
651        };
652        let op_kind = m.get("op").and_then(|v| v.as_str()).unwrap_or("");
653        let path = m.get("path").and_then(|v| v.as_str()).unwrap_or("");
654        let value = m.get("value").cloned();
655        let segs: Vec<&str> = path.split('/').skip(1).collect();
656        match op_kind {
657            "replace" | "add" => {
658                if let Some(nv) = value {
659                    if segs.is_empty() {
660                        v = nv;
661                    } else {
662                        let path_str = segs.join(".");
663                        let new_s = jq_set(&[
664                            StrykeValue::string(serde_json::to_string(&v).unwrap_or_default()),
665                            StrykeValue::string(path_str),
666                            StrykeValue::string(serde_json::to_string(&nv).unwrap_or_default()),
667                        ])
668                        .to_string();
669                        if let Some(parsed) = parse_json(&new_s) {
670                            v = parsed;
671                        }
672                    }
673                }
674            }
675            "remove" => {
676                let path_str = segs.join(".");
677                let new_s = jq_delete(&[
678                    StrykeValue::string(serde_json::to_string(&v).unwrap_or_default()),
679                    StrykeValue::string(path_str),
680                ])
681                .to_string();
682                if let Some(parsed) = parse_json(&new_s) {
683                    v = parsed;
684                }
685            }
686            _ => {}
687        }
688    }
689    StrykeValue::string(serde_json::to_string(&v).unwrap_or_default())
690}
691
692pub fn json_merge_patch(args: &[StrykeValue]) -> StrykeValue {
693    // RFC 7396 — recursive merge.
694    let s = arg_str(args);
695    let patch_s = args.get(1).map(|v| v.to_string()).unwrap_or_default();
696    let (Some(mut v), Some(p)) = (parse_json(&s), parse_json(&patch_s)) else {
697        return StrykeValue::UNDEF;
698    };
699    fn merge(v: &mut serde_json::Value, p: serde_json::Value) {
700        match (v, p) {
701            (serde_json::Value::Object(target), serde_json::Value::Object(patch)) => {
702                for (k, pv) in patch {
703                    if pv.is_null() {
704                        target.remove(&k);
705                    } else {
706                        let entry = target.entry(k).or_insert(serde_json::Value::Null);
707                        merge(entry, pv);
708                    }
709                }
710            }
711            (v, p) => *v = p,
712        }
713    }
714    merge(&mut v, p);
715    StrykeValue::string(serde_json::to_string(&v).unwrap_or_default())
716}
717
718pub fn json_pointer_resolve(args: &[StrykeValue]) -> StrykeValue {
719    let s = arg_str(args);
720    let ptr = args.get(1).map(|v| v.to_string()).unwrap_or_default();
721    let Some(v) = parse_json(&s) else {
722        return StrykeValue::UNDEF;
723    };
724    let resolved = v.pointer(&ptr).cloned().unwrap_or(serde_json::Value::Null);
725    json_to_stryke(&resolved)
726}
727
728pub fn json_pointer_set(args: &[StrykeValue]) -> StrykeValue {
729    let s = arg_str(args);
730    let ptr = args.get(1).map(|v| v.to_string()).unwrap_or_default();
731    let new_val = args.get(2).map(|v| v.to_string()).unwrap_or_default();
732    let Some(mut v) = parse_json(&s) else {
733        return StrykeValue::UNDEF;
734    };
735    let new_v = parse_json(&new_val).unwrap_or(serde_json::Value::String(new_val.clone()));
736    if let Some(target) = v.pointer_mut(&ptr) {
737        *target = new_v;
738    }
739    StrykeValue::string(serde_json::to_string(&v).unwrap_or_default())
740}
741
742// ══════════════════════════════════════════════════════════════════════
743// HTML / DOM (scraper backed)
744// ══════════════════════════════════════════════════════════════════════
745
746pub fn html_parse(args: &[StrykeValue]) -> StrykeValue {
747    let s = arg_str(args);
748    let _doc = scraper::Html::parse_document(&s);
749    // Return the raw HTML as a marker (full DOM doesn't fit StrykeValue)
750    StrykeValue::string(s)
751}
752
753pub fn html_to_text(args: &[StrykeValue]) -> StrykeValue {
754    let s = arg_str(args);
755    let doc = scraper::Html::parse_document(&s);
756    let body_sel = scraper::Selector::parse("body").unwrap();
757    let text: String = doc
758        .select(&body_sel)
759        .flat_map(|n| n.text())
760        .collect::<Vec<_>>()
761        .join(" ");
762    StrykeValue::string(text.split_whitespace().collect::<Vec<_>>().join(" "))
763}
764
765pub fn html_pretty(args: &[StrykeValue]) -> StrykeValue {
766    // No real pretty-printer without an additional crate; return as-is.
767    StrykeValue::string(arg_str(args))
768}
769
770pub fn html_minify(args: &[StrykeValue]) -> StrykeValue {
771    let s = arg_str(args);
772    let re = regex::Regex::new(r">\s+<").unwrap();
773    let collapsed = re.replace_all(&s, "><").to_string();
774    StrykeValue::string(collapsed.split_whitespace().collect::<Vec<_>>().join(" "))
775}
776
777pub fn html_sanitize(args: &[StrykeValue]) -> StrykeValue {
778    // Strip scripts, styles, on* attrs, and javascript: URLs.
779    let s = arg_str(args);
780    let s = regex::Regex::new(r"(?is)<script[^>]*>.*?</script>")
781        .unwrap()
782        .replace_all(&s, "")
783        .to_string();
784    let s = regex::Regex::new(r"(?is)<style[^>]*>.*?</style>")
785        .unwrap()
786        .replace_all(&s, "")
787        .to_string();
788    let s = regex::Regex::new(r#"(?i)\son\w+\s*=\s*"[^"]*""#)
789        .unwrap()
790        .replace_all(&s, "")
791        .to_string();
792    let s = regex::Regex::new(r#"(?i)javascript:[^"'\s>]*"#)
793        .unwrap()
794        .replace_all(&s, "")
795        .to_string();
796    StrykeValue::string(s)
797}
798
799pub fn html_strip_tags(args: &[StrykeValue]) -> StrykeValue {
800    let s = arg_str(args);
801    let re = regex::Regex::new(r"<[^>]+>").unwrap();
802    StrykeValue::string(re.replace_all(&s, "").to_string())
803}
804
805pub fn html_strip_scripts(args: &[StrykeValue]) -> StrykeValue {
806    let s = arg_str(args);
807    let re = regex::Regex::new(r"(?is)<script[^>]*>.*?</script>").unwrap();
808    StrykeValue::string(re.replace_all(&s, "").to_string())
809}
810
811pub fn html_strip_styles(args: &[StrykeValue]) -> StrykeValue {
812    let s = arg_str(args);
813    let re = regex::Regex::new(r"(?is)<style[^>]*>.*?</style>").unwrap();
814    StrykeValue::string(re.replace_all(&s, "").to_string())
815}
816
817fn extract_attrs(html: &str, selector: &str, attr: &str) -> Vec<String> {
818    let doc = scraper::Html::parse_document(html);
819    let Ok(sel) = scraper::Selector::parse(selector) else {
820        return Vec::new();
821    };
822    doc.select(&sel)
823        .filter_map(|n| n.value().attr(attr).map(|s| s.to_string()))
824        .collect()
825}
826
827pub fn html_extract_links(args: &[StrykeValue]) -> StrykeValue {
828    let urls = extract_attrs(&arg_str(args), "a[href]", "href");
829    arr(urls.into_iter().map(StrykeValue::string).collect())
830}
831
832pub fn html_extract_images(args: &[StrykeValue]) -> StrykeValue {
833    let urls = extract_attrs(&arg_str(args), "img[src]", "src");
834    arr(urls.into_iter().map(StrykeValue::string).collect())
835}
836
837pub fn html_extract_text(args: &[StrykeValue]) -> StrykeValue {
838    html_to_text(args)
839}
840
841pub fn html_extract_meta(args: &[StrykeValue]) -> StrykeValue {
842    use indexmap::IndexMap;
843    let doc = scraper::Html::parse_document(&arg_str(args));
844    let sel = scraper::Selector::parse("meta").unwrap();
845    let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
846    for n in doc.select(&sel) {
847        let v = n.value();
848        let name = v.attr("name").or_else(|| v.attr("property"));
849        let content = v.attr("content");
850        if let (Some(name), Some(content)) = (name, content) {
851            h.insert(name.to_string(), StrykeValue::string(content.to_string()));
852        }
853    }
854    StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
855}
856
857pub fn html_extract_title(args: &[StrykeValue]) -> StrykeValue {
858    let doc = scraper::Html::parse_document(&arg_str(args));
859    let sel = scraper::Selector::parse("title").unwrap();
860    let t: String = doc
861        .select(&sel)
862        .next()
863        .map(|n| n.text().collect::<Vec<_>>().join(""))
864        .unwrap_or_default();
865    StrykeValue::string(t.trim().to_string())
866}
867
868pub fn html_extract_headings(args: &[StrykeValue]) -> StrykeValue {
869    let doc = scraper::Html::parse_document(&arg_str(args));
870    let sel = scraper::Selector::parse("h1, h2, h3, h4, h5, h6").unwrap();
871    let out: Vec<StrykeValue> = doc
872        .select(&sel)
873        .map(|n| StrykeValue::string(n.text().collect::<Vec<_>>().join("").trim().to_string()))
874        .collect();
875    arr(out)
876}
877
878pub fn html_extract_tables(args: &[StrykeValue]) -> StrykeValue {
879    let doc = scraper::Html::parse_document(&arg_str(args));
880    let table_sel = scraper::Selector::parse("table").unwrap();
881    let row_sel = scraper::Selector::parse("tr").unwrap();
882    let cell_sel = scraper::Selector::parse("td, th").unwrap();
883    let mut tables: Vec<StrykeValue> = Vec::new();
884    for t in doc.select(&table_sel) {
885        let mut rows: Vec<StrykeValue> = Vec::new();
886        for r in t.select(&row_sel) {
887            let cells: Vec<StrykeValue> = r
888                .select(&cell_sel)
889                .map(|c| {
890                    StrykeValue::string(c.text().collect::<Vec<_>>().join("").trim().to_string())
891                })
892                .collect();
893            rows.push(arr(cells));
894        }
895        tables.push(arr(rows));
896    }
897    arr(tables)
898}
899
900pub fn html_inner_text(args: &[StrykeValue]) -> StrykeValue {
901    html_to_text(args)
902}
903
904pub fn html_canonical_url(args: &[StrykeValue]) -> StrykeValue {
905    let doc = scraper::Html::parse_document(&arg_str(args));
906    let sel = scraper::Selector::parse("link[rel='canonical']").unwrap();
907    let url = doc
908        .select(&sel)
909        .next()
910        .and_then(|n| n.value().attr("href"))
911        .unwrap_or_default()
912        .to_string();
913    StrykeValue::string(url)
914}
915
916pub fn html_meta_charset(args: &[StrykeValue]) -> StrykeValue {
917    let doc = scraper::Html::parse_document(&arg_str(args));
918    let sel = scraper::Selector::parse("meta[charset]").unwrap();
919    let cs = doc
920        .select(&sel)
921        .next()
922        .and_then(|n| n.value().attr("charset"))
923        .unwrap_or("utf-8")
924        .to_string();
925    StrykeValue::string(cs)
926}
927
928pub fn html_meta_keywords(args: &[StrykeValue]) -> StrykeValue {
929    let s = arg_str(args);
930    let doc = scraper::Html::parse_document(&s);
931    let sel = scraper::Selector::parse("meta[name='keywords']").unwrap();
932    let kw = doc
933        .select(&sel)
934        .next()
935        .and_then(|n| n.value().attr("content"))
936        .unwrap_or("")
937        .to_string();
938    StrykeValue::string(kw)
939}
940
941pub fn html_meta_description(args: &[StrykeValue]) -> StrykeValue {
942    let doc = scraper::Html::parse_document(&arg_str(args));
943    let sel = scraper::Selector::parse("meta[name='description']").unwrap();
944    let d = doc
945        .select(&sel)
946        .next()
947        .and_then(|n| n.value().attr("content"))
948        .unwrap_or("")
949        .to_string();
950    StrykeValue::string(d)
951}
952
953pub fn html_meta_og(args: &[StrykeValue]) -> StrykeValue {
954    use indexmap::IndexMap;
955    let doc = scraper::Html::parse_document(&arg_str(args));
956    let sel = scraper::Selector::parse("meta[property]").unwrap();
957    let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
958    for n in doc.select(&sel) {
959        let v = n.value();
960        if let (Some(p), Some(c)) = (v.attr("property"), v.attr("content")) {
961            if p.starts_with("og:") {
962                h.insert(p.to_string(), StrykeValue::string(c.to_string()));
963            }
964        }
965    }
966    StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
967}
968
969pub fn html_meta_twitter(args: &[StrykeValue]) -> StrykeValue {
970    use indexmap::IndexMap;
971    let doc = scraper::Html::parse_document(&arg_str(args));
972    let sel = scraper::Selector::parse("meta[name]").unwrap();
973    let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
974    for n in doc.select(&sel) {
975        let v = n.value();
976        if let (Some(name), Some(c)) = (v.attr("name"), v.attr("content")) {
977            if name.starts_with("twitter:") {
978                h.insert(name.to_string(), StrykeValue::string(c.to_string()));
979            }
980        }
981    }
982    StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
983}
984
985pub fn html_to_markdown(args: &[StrykeValue]) -> StrykeValue {
986    // Best-effort: strip tags, preserve basic structure.
987    let s = arg_str(args);
988    let s = s.replace("<h1>", "# ").replace("</h1>", "\n");
989    let s = s.replace("<h2>", "## ").replace("</h2>", "\n");
990    let s = s.replace("<h3>", "### ").replace("</h3>", "\n");
991    let s = s.replace("<strong>", "**").replace("</strong>", "**");
992    let s = s.replace("<em>", "_").replace("</em>", "_");
993    let s = s.replace("<br>", "\n").replace("<br/>", "\n");
994    let s = s.replace("<p>", "").replace("</p>", "\n\n");
995    let re = regex::Regex::new(r"<[^>]+>").unwrap();
996    StrykeValue::string(re.replace_all(&s, "").to_string())
997}
998
999pub fn markdown_to_html(args: &[StrykeValue]) -> StrykeValue {
1000    // Best-effort transform — for full markdown, ship pulldown-cmark.
1001    let s = arg_str(args);
1002    let mut out = String::new();
1003    for line in s.lines() {
1004        if let Some(rest) = line.strip_prefix("### ") {
1005            out.push_str(&format!("<h3>{rest}</h3>\n"));
1006        } else if let Some(rest) = line.strip_prefix("## ") {
1007            out.push_str(&format!("<h2>{rest}</h2>\n"));
1008        } else if let Some(rest) = line.strip_prefix("# ") {
1009            out.push_str(&format!("<h1>{rest}</h1>\n"));
1010        } else if line.is_empty() {
1011            out.push('\n');
1012        } else {
1013            out.push_str(&format!("<p>{line}</p>\n"));
1014        }
1015    }
1016    StrykeValue::string(out)
1017}
1018
1019pub fn markdown_render(args: &[StrykeValue]) -> StrykeValue {
1020    markdown_to_html(args)
1021}
1022
1023// ══════════════════════════════════════════════════════════════════════
1024// XML (regex-based; pragmatic, not RFC-perfect)
1025// ══════════════════════════════════════════════════════════════════════
1026
1027pub fn xml_parse(args: &[StrykeValue]) -> StrykeValue {
1028    StrykeValue::string(arg_str(args))
1029}
1030
1031pub fn xml_pretty(args: &[StrykeValue]) -> StrykeValue {
1032    let s = arg_str(args);
1033    let mut out = String::new();
1034    let mut depth = 0i32;
1035    let mut i = 0;
1036    let bytes = s.as_bytes();
1037    while i < bytes.len() {
1038        if bytes[i] == b'<' {
1039            // find tag end
1040            let start = i;
1041            while i < bytes.len() && bytes[i] != b'>' {
1042                i += 1;
1043            }
1044            i += 1;
1045            let tag = &s[start..i];
1046            if tag.starts_with("</") {
1047                depth -= 1;
1048            }
1049            for _ in 0..depth.max(0) {
1050                out.push_str("  ");
1051            }
1052            out.push_str(tag);
1053            out.push('\n');
1054            if !tag.starts_with("</") && !tag.ends_with("/>") && !tag.starts_with("<?") {
1055                depth += 1;
1056            }
1057        } else {
1058            // text
1059            let start = i;
1060            while i < bytes.len() && bytes[i] != b'<' {
1061                i += 1;
1062            }
1063            let text = s[start..i].trim();
1064            if !text.is_empty() {
1065                for _ in 0..depth.max(0) {
1066                    out.push_str("  ");
1067                }
1068                out.push_str(text);
1069                out.push('\n');
1070            }
1071        }
1072    }
1073    StrykeValue::string(out)
1074}
1075
1076pub fn xml_minify(args: &[StrykeValue]) -> StrykeValue {
1077    let s = arg_str(args);
1078    let re = regex::Regex::new(r">\s+<").unwrap();
1079    StrykeValue::string(re.replace_all(&s, "><").to_string())
1080}
1081
1082pub fn xml_namespace(args: &[StrykeValue]) -> StrykeValue {
1083    let s = arg_str(args);
1084    let re = regex::Regex::new(r#"xmlns(?::\w+)?\s*=\s*"([^"]+)""#).unwrap();
1085    if let Some(c) = re.captures(&s) {
1086        return StrykeValue::string(c[1].to_string());
1087    }
1088    StrykeValue::UNDEF
1089}
1090
1091pub fn xml_text(args: &[StrykeValue]) -> StrykeValue {
1092    let s = arg_str(args);
1093    let re = regex::Regex::new(r"<[^>]+>").unwrap();
1094    let stripped = re.replace_all(&s, " ").to_string();
1095    StrykeValue::string(stripped.split_whitespace().collect::<Vec<_>>().join(" "))
1096}
1097
1098pub fn xml_attrs(args: &[StrykeValue]) -> StrykeValue {
1099    use indexmap::IndexMap;
1100    let s = arg_str(args);
1101    let re = regex::Regex::new(r#"(\w+)\s*=\s*"([^"]*)""#).unwrap();
1102    let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
1103    for cap in re.captures_iter(&s) {
1104        h.insert(cap[1].to_string(), StrykeValue::string(cap[2].to_string()));
1105    }
1106    StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
1107}
1108
1109pub fn xml_children_by_tag(args: &[StrykeValue]) -> StrykeValue {
1110    let s = arg_str(args);
1111    let tag = args.get(1).map(|v| v.to_string()).unwrap_or_default();
1112    let re = regex::Regex::new(&format!(
1113        r"<{}\b[^>]*>(.*?)</{}>",
1114        regex::escape(&tag),
1115        regex::escape(&tag)
1116    ))
1117    .unwrap();
1118    let out: Vec<StrykeValue> = re
1119        .captures_iter(&s)
1120        .map(|c| StrykeValue::string(c[0].to_string()))
1121        .collect();
1122    arr(out)
1123}
1124
1125pub fn xml_root(args: &[StrykeValue]) -> StrykeValue {
1126    let s = arg_str(args);
1127    let re = regex::Regex::new(r"<(\w+)").unwrap();
1128    if let Some(c) = re.captures(s.trim_start_matches(|c: char| {
1129        c == '<' && {
1130            let i = s.find('<').unwrap();
1131            s[i..].starts_with("<?")
1132        }
1133    })) {
1134        return StrykeValue::string(c[1].to_string());
1135    }
1136    StrykeValue::UNDEF
1137}
1138
1139pub fn xpath_select_one(args: &[StrykeValue]) -> StrykeValue {
1140    // Naive xpath: only supports //tagname.
1141    let s = arg_str(args);
1142    let xp = args.get(1).map(|v| v.to_string()).unwrap_or_default();
1143    if let Some(tag) = xp.strip_prefix("//") {
1144        let re = regex::Regex::new(&format!(
1145            r"<{}\b[^>]*>(.*?)</{}>",
1146            regex::escape(tag),
1147            regex::escape(tag)
1148        ))
1149        .unwrap();
1150        if let Some(c) = re.captures(&s) {
1151            return StrykeValue::string(c[0].to_string());
1152        }
1153    }
1154    StrykeValue::UNDEF
1155}
1156
1157pub fn xpath_attribute(args: &[StrykeValue]) -> StrykeValue {
1158    let s = arg_str(args);
1159    let attr = args.get(1).map(|v| v.to_string()).unwrap_or_default();
1160    let re = regex::Regex::new(&format!(r#"\b{}\s*=\s*"([^"]*)""#, regex::escape(&attr))).unwrap();
1161    if let Some(c) = re.captures(&s) {
1162        return StrykeValue::string(c[1].to_string());
1163    }
1164    StrykeValue::UNDEF
1165}
1166
1167pub fn xpath_text(args: &[StrykeValue]) -> StrykeValue {
1168    xml_text(args)
1169}
1170
1171pub fn xml_to_json(args: &[StrykeValue]) -> StrykeValue {
1172    // Very rough — wrap text content with tag names as JSON keys.
1173    let attrs = xml_attrs(args);
1174    if let Some(h) = attrs.as_hash_ref() {
1175        let g = h.read();
1176        let s: Vec<String> = g
1177            .iter()
1178            .map(|(k, v)| format!("\"{}\":\"{}\"", k, v.to_string().replace('"', "\\\"")))
1179            .collect();
1180        return StrykeValue::string(format!("{{{}}}", s.join(",")));
1181    }
1182    StrykeValue::string("{}".to_string())
1183}
1184
1185pub fn json_to_xml(args: &[StrykeValue]) -> StrykeValue {
1186    let s = arg_str(args);
1187    let Some(v) = parse_json(&s) else {
1188        return StrykeValue::UNDEF;
1189    };
1190    fn render(v: &serde_json::Value, tag: &str) -> String {
1191        match v {
1192            serde_json::Value::Object(m) => {
1193                let inner: String = m.iter().map(|(k, v)| render(v, k)).collect();
1194                format!("<{}>{}</{}>", tag, inner, tag)
1195            }
1196            serde_json::Value::Array(a) => a.iter().map(|v| render(v, tag)).collect(),
1197            _ => format!("<{}>{}</{}>", tag, v.to_string().trim_matches('"'), tag),
1198        }
1199    }
1200    StrykeValue::string(render(&v, "root"))
1201}
1202
1203pub fn xml_canonicalize(args: &[StrykeValue]) -> StrykeValue {
1204    xml_minify(args)
1205}
1206
1207// ══════════════════════════════════════════════════════════════════════
1208// CSS basics (regex-based)
1209// ══════════════════════════════════════════════════════════════════════
1210
1211pub fn css_parse(args: &[StrykeValue]) -> StrykeValue {
1212    StrykeValue::string(arg_str(args))
1213}
1214
1215pub fn css_minify(args: &[StrykeValue]) -> StrykeValue {
1216    let s = arg_str(args);
1217    let re = regex::Regex::new(r"/\*.*?\*/").unwrap();
1218    let s = re.replace_all(&s, "").to_string();
1219    let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
1220    let s = s
1221        .replace(" {", "{")
1222        .replace("{ ", "{")
1223        .replace(" }", "}")
1224        .replace("; ", ";")
1225        .replace(": ", ":");
1226    StrykeValue::string(s)
1227}
1228
1229pub fn css_pretty(args: &[StrykeValue]) -> StrykeValue {
1230    let s = arg_str(args);
1231    let s = s
1232        .replace("{", " {\n  ")
1233        .replace("}", "\n}\n")
1234        .replace(";", ";\n  ");
1235    StrykeValue::string(s)
1236}
1237
1238pub fn css_selector_parse(args: &[StrykeValue]) -> StrykeValue {
1239    let s = arg_str(args);
1240    let parts: Vec<StrykeValue> = s
1241        .split(',')
1242        .map(|p| StrykeValue::string(p.trim().to_string()))
1243        .collect();
1244    arr(parts)
1245}
1246
1247pub fn css_rule_extract(args: &[StrykeValue]) -> StrykeValue {
1248    let s = arg_str(args);
1249    let re = regex::Regex::new(r"([^{}]+)\{([^}]*)\}").unwrap();
1250    let mut rules: Vec<StrykeValue> = Vec::new();
1251    for cap in re.captures_iter(&s) {
1252        rules.push(arr(vec![
1253            StrykeValue::string(cap[1].trim().to_string()),
1254            StrykeValue::string(cap[2].trim().to_string()),
1255        ]));
1256    }
1257    arr(rules)
1258}
1259
1260pub fn css_specificity(args: &[StrykeValue]) -> StrykeValue {
1261    let s = arg_str(args);
1262    let mut id = 0i64;
1263    let mut class = 0i64;
1264    let mut tag = 0i64;
1265    for part in s.split(|c: char| c.is_whitespace() || c == '>' || c == '+' || c == '~') {
1266        let p = part.trim();
1267        if p.is_empty() {
1268            continue;
1269        }
1270        // Count #, ., :, and tags
1271        id += p.matches('#').count() as i64;
1272        class += (p.matches('.').count() + p.matches(':').count() - p.matches("::").count()) as i64;
1273        if p.chars().next().map(|c| c.is_alphabetic()).unwrap_or(false) {
1274            tag += 1;
1275        }
1276        tag += p.matches("::").count() as i64;
1277    }
1278    arr(vec![
1279        StrykeValue::integer(id),
1280        StrykeValue::integer(class),
1281        StrykeValue::integer(tag),
1282    ])
1283}
1284
1285pub fn css_var_resolve(args: &[StrykeValue]) -> StrykeValue {
1286    let s = arg_str(args);
1287    let var_re = regex::Regex::new(r"--([\w-]+)\s*:\s*([^;}]+)").unwrap();
1288    use indexmap::IndexMap;
1289    let mut vars: IndexMap<String, String> = IndexMap::new();
1290    for cap in var_re.captures_iter(&s) {
1291        vars.insert(cap[1].to_string(), cap[2].trim().to_string());
1292    }
1293    let use_re = regex::Regex::new(r"var\(\s*--([\w-]+)(?:\s*,\s*([^)]*))?\)").unwrap();
1294    let out = use_re.replace_all(&s, |cap: &regex::Captures| {
1295        vars.get(&cap[1])
1296            .cloned()
1297            .or_else(|| cap.get(2).map(|m| m.as_str().to_string()))
1298            .unwrap_or_default()
1299    });
1300    StrykeValue::string(out.to_string())
1301}
1302
1303pub fn css_property_set(args: &[StrykeValue]) -> StrykeValue {
1304    let s = arg_str(args);
1305    let prop = args.get(1).map(|v| v.to_string()).unwrap_or_default();
1306    let value = args.get(2).map(|v| v.to_string()).unwrap_or_default();
1307    let re = regex::Regex::new(&format!(r"{}\s*:\s*[^;}}]+", regex::escape(&prop))).unwrap();
1308    let new = format!("{}: {}", prop, value);
1309    if re.is_match(&s) {
1310        StrykeValue::string(re.replace(&s, new.as_str()).to_string())
1311    } else {
1312        StrykeValue::string(format!("{}; {}", s, new))
1313    }
1314}
1315
1316pub fn css_property_get(args: &[StrykeValue]) -> StrykeValue {
1317    let s = arg_str(args);
1318    let prop = args.get(1).map(|v| v.to_string()).unwrap_or_default();
1319    let re = regex::Regex::new(&format!(r"{}\s*:\s*([^;}}]+)", regex::escape(&prop))).unwrap();
1320    if let Some(c) = re.captures(&s) {
1321        return StrykeValue::string(c[1].trim().to_string());
1322    }
1323    StrykeValue::UNDEF
1324}
1325
1326pub fn css_url_extract(args: &[StrykeValue]) -> StrykeValue {
1327    let s = arg_str(args);
1328    let re = regex::Regex::new(r#"url\(\s*['"]?([^'")]+)['"]?\s*\)"#).unwrap();
1329    let urls: Vec<StrykeValue> = re
1330        .captures_iter(&s)
1331        .map(|c| StrykeValue::string(c[1].to_string()))
1332        .collect();
1333    arr(urls)
1334}
1335
1336pub fn css_import_extract(args: &[StrykeValue]) -> StrykeValue {
1337    let s = arg_str(args);
1338    let re = regex::Regex::new(r#"@import\s+(?:url\()?['"]([^'"]+)['"]"#).unwrap();
1339    let urls: Vec<StrykeValue> = re
1340        .captures_iter(&s)
1341        .map(|c| StrykeValue::string(c[1].to_string()))
1342        .collect();
1343    arr(urls)
1344}
1345
1346pub fn css_font_extract(args: &[StrykeValue]) -> StrykeValue {
1347    let s = arg_str(args);
1348    let re = regex::Regex::new(r"font-family\s*:\s*([^;}]+)").unwrap();
1349    let fonts: Vec<StrykeValue> = re
1350        .captures_iter(&s)
1351        .map(|c| StrykeValue::string(c[1].trim().to_string()))
1352        .collect();
1353    arr(fonts)
1354}
1355
1356pub fn selector_to_xpath(args: &[StrykeValue]) -> StrykeValue {
1357    let s = arg_str(args);
1358    // Very basic conversion: `a.b` → `//a[@class='b']`, `#id` → `//*[@id='id']`
1359    let s = regex::Regex::new(r"^(\w+)\.([\w-]+)")
1360        .unwrap()
1361        .replace(&s, "//$1[@class='$2']");
1362    let s = regex::Regex::new(r"^#([\w-]+)")
1363        .unwrap()
1364        .replace(&s, "//*[@id='$1']");
1365    let s = regex::Regex::new(r"^(\w+)$").unwrap().replace(&s, "//$1");
1366    StrykeValue::string(s.to_string())
1367}
1368
1369pub fn xpath_to_selector(args: &[StrykeValue]) -> StrykeValue {
1370    let s = arg_str(args);
1371    let s = regex::Regex::new(r#"//(\w+)\[@class='([\w-]+)'\]"#)
1372        .unwrap()
1373        .replace(&s, "$1.$2");
1374    let s = regex::Regex::new(r#"//\*\[@id='([\w-]+)'\]"#)
1375        .unwrap()
1376        .replace(&s, "#$1");
1377    let s = regex::Regex::new(r"^//(\w+)$").unwrap().replace(&s, "$1");
1378    StrykeValue::string(s.to_string())
1379}