Skip to main content

tatara_lisp_script/stdlib/
string.rs

1//! String helpers beyond what tatara-lisp-eval::primitive provides.
2//!
3//!   (string-replace STR FROM TO)  → new string
4//!   (string-split STR SEP)        → list of strings
5//!   (string-contains? STR NEEDLE) → bool
6//!   (string-lowercase STR)        → new string
7//!   (string-format TMPL &rest VS) → printf-ish, `{}` placeholders consumed left-to-right
8//!   (string-trim STR)             → new string with leading/trailing whitespace removed
9
10use std::sync::Arc;
11
12use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
13
14use crate::script_ctx::ScriptCtx;
15use crate::stdlib::env::str_arg;
16
17pub fn install(interp: &mut Interpreter<ScriptCtx>) {
18    interp.register_fn(
19        "string-replace",
20        Arity::Exact(3),
21        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
22            let s = str_arg(&args[0], "string-replace", sp)?;
23            let from = str_arg(&args[1], "string-replace", sp)?;
24            let to = str_arg(&args[2], "string-replace", sp)?;
25            Ok(Value::Str(Arc::from(s.replace(&*from, &to))))
26        },
27    );
28
29    interp.register_fn(
30        "string-split",
31        Arity::Exact(2),
32        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
33            let s = str_arg(&args[0], "string-split", sp)?;
34            let sep = str_arg(&args[1], "string-split", sp)?;
35            Ok(Value::list(
36                s.split(&*sep)
37                    .map(|p| Value::Str(Arc::from(p)))
38                    .collect::<Vec<_>>(),
39            ))
40        },
41    );
42
43    interp.register_fn(
44        "string-contains?",
45        Arity::Exact(2),
46        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
47            let s = str_arg(&args[0], "string-contains?", sp)?;
48            let needle = str_arg(&args[1], "string-contains?", sp)?;
49            Ok(Value::Bool(s.contains(&*needle)))
50        },
51    );
52
53    interp.register_fn(
54        "string-lowercase",
55        Arity::Exact(1),
56        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
57            let s = str_arg(&args[0], "string-lowercase", sp)?;
58            Ok(Value::Str(Arc::from(s.to_lowercase())))
59        },
60    );
61
62    interp.register_fn(
63        "string-trim",
64        Arity::Exact(1),
65        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
66            let s = str_arg(&args[0], "string-trim", sp)?;
67            Ok(Value::Str(Arc::from(s.trim())))
68        },
69    );
70
71    interp.register_fn(
72        "string-format",
73        Arity::AtLeast(1),
74        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
75            let tmpl = str_arg(&args[0], "string-format", sp)?;
76            let mut out = String::with_capacity(tmpl.len());
77            let mut remaining = &*tmpl;
78            let mut idx = 1usize;
79            while let Some(pos) = remaining.find("{}") {
80                out.push_str(&remaining[..pos]);
81                let Some(arg) = args.get(idx) else {
82                    return Err(EvalError::native_fn(
83                        "string-format",
84                        format!("template has more {{}} than args ({idx})"),
85                        sp,
86                    ));
87                };
88                out.push_str(&display_value(arg));
89                remaining = &remaining[pos + 2..];
90                idx += 1;
91            }
92            out.push_str(remaining);
93            Ok(Value::Str(Arc::from(out)))
94        },
95    );
96}
97
98fn display_value(v: &Value) -> String {
99    match v {
100        Value::Nil => "nil".to_string(),
101        Value::Bool(b) => b.to_string(),
102        Value::Int(n) => n.to_string(),
103        Value::Float(n) => n.to_string(),
104        Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.as_ref().to_string(),
105        other => format!("{other:?}"),
106    }
107}