robinpath_modules/modules/
template_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 rp.register_builtin("template.render", |args, _| {
5 let tmpl = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6 let data = args.get(1).cloned().unwrap_or(Value::Null);
7 Ok(Value::String(render_template(&tmpl, &data)))
8 });
9
10 rp.register_builtin("template.escape", |args, _| {
11 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
12 Ok(Value::String(html_escape(&s)))
13 });
14
15 rp.register_builtin("template.compile", |args, _| {
16 let tmpl = args.first().map(|v| v.to_display_string()).unwrap_or_default();
17 let vars = extract_variables(&tmpl);
20 let mut obj = indexmap::IndexMap::new();
21 obj.insert("template".to_string(), Value::String(tmpl));
22 obj.insert("variables".to_string(), Value::Array(vars));
23 Ok(Value::Object(obj))
24 });
25
26 rp.register_builtin("template.extractVariables", |args, _| {
27 let tmpl = args.first().map(|v| v.to_display_string()).unwrap_or_default();
28 Ok(Value::Array(extract_variables(&tmpl)))
29 });
30
31 rp.register_builtin("template.renderString", |args, _| {
32 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
33 let data = args.get(1).cloned().unwrap_or(Value::Null);
34 Ok(Value::String(render_template(&s, &data)))
35 });
36}
37
38fn render_template(tmpl: &str, data: &Value) -> String {
39 let mut result = String::new();
40 let chars: Vec<char> = tmpl.chars().collect();
41 let len = chars.len();
42 let mut i = 0;
43
44 while i < len {
45 if i + 1 < len && chars[i] == '{' && chars[i + 1] == '{' {
46 let start = i + 2;
48 if let Some(end) = find_closing_braces(&chars, start) {
49 let tag = chars[start..end].iter().collect::<String>().trim().to_string();
50
51 if tag.starts_with('!') {
52 } else if tag.starts_with('#') {
54 let section_name = tag[1..].trim();
56 let close_tag = format!("{{{{/{}}}}}", section_name);
57 if let Some(close_pos) = tmpl[end + 2..].find(&close_tag) {
58 let inner = &tmpl[end + 2..end + 2 + close_pos];
59 let val = resolve_value(data, section_name);
60 match &val {
61 Value::Bool(true) => {
62 result.push_str(&render_template(inner, data));
63 }
64 Value::Array(arr) => {
65 for item in arr {
66 result.push_str(&render_template(inner, item));
67 }
68 }
69 Value::Object(_) => {
70 result.push_str(&render_template(inner, &val));
71 }
72 v if v.is_truthy() => {
73 result.push_str(&render_template(inner, data));
74 }
75 _ => {} }
77 i = end + 2 + close_pos + close_tag.len();
78 continue;
79 }
80 } else if tag.starts_with('^') {
81 let section_name = tag[1..].trim();
83 let close_tag = format!("{{{{/{}}}}}", section_name);
84 if let Some(close_pos) = tmpl[end + 2..].find(&close_tag) {
85 let inner = &tmpl[end + 2..end + 2 + close_pos];
86 let val = resolve_value(data, section_name);
87 if !val.is_truthy() {
88 result.push_str(&render_template(inner, data));
89 }
90 i = end + 2 + close_pos + close_tag.len();
91 continue;
92 }
93 } else if tag.starts_with('{') && tag.ends_with('}') {
94 let var_name = tag[1..tag.len() - 1].trim();
96 let val = resolve_value(data, var_name);
97 result.push_str(&val.to_display_string());
98 i = end + 2;
100 if i < len && chars.get(i) == Some(&'}') {
101 i += 1;
102 }
103 continue;
104 } else {
105 let val = resolve_value(data, &tag);
107 let display = val.to_display_string();
108 if display != "null" && display != "undefined" {
109 result.push_str(&html_escape(&display));
110 }
111 }
112
113 i = end + 2;
114 continue;
115 }
116 }
117
118 result.push(chars[i]);
119 i += 1;
120 }
121
122 result
123}
124
125fn find_closing_braces(chars: &[char], start: usize) -> Option<usize> {
126 let len = chars.len();
127 let mut i = start;
128 while i + 1 < len {
129 if chars[i] == '}' && chars[i + 1] == '}' {
130 return Some(i);
131 }
132 i += 1;
133 }
134 None
135}
136
137fn resolve_value(data: &Value, path: &str) -> Value {
138 let parts: Vec<&str> = path.split('.').collect();
139 let mut current = data.clone();
140 for part in parts {
141 match ¤t {
142 Value::Object(obj) => {
143 current = obj.get(part).cloned().unwrap_or(Value::Null);
144 }
145 _ => return Value::Null,
146 }
147 }
148 current
149}
150
151fn extract_variables(tmpl: &str) -> Vec<Value> {
152 let mut vars = Vec::new();
153 let mut seen = std::collections::HashSet::new();
154 let chars: Vec<char> = tmpl.chars().collect();
155 let len = chars.len();
156 let mut i = 0;
157
158 while i + 1 < len {
159 if chars[i] == '{' && chars[i + 1] == '{' {
160 let start = i + 2;
161 if let Some(end) = find_closing_braces(&chars, start) {
162 let tag = chars[start..end].iter().collect::<String>().trim().to_string();
163 let name = if tag.starts_with('!') || tag.starts_with('/') {
165 None
166 } else if tag.starts_with('#') || tag.starts_with('^') {
167 Some(tag[1..].trim().to_string())
168 } else if tag.starts_with('{') && tag.ends_with('}') {
169 Some(tag[1..tag.len() - 1].trim().to_string())
170 } else {
171 Some(tag)
172 };
173 if let Some(name) = name {
174 if !name.is_empty() && seen.insert(name.clone()) {
175 vars.push(Value::String(name));
176 }
177 }
178 i = end + 2;
179 continue;
180 }
181 }
182 i += 1;
183 }
184
185 vars
186}
187
188fn html_escape(s: &str) -> String {
189 s.replace('&', "&")
190 .replace('<', "<")
191 .replace('>', ">")
192 .replace('"', """)
193 .replace('\'', "'")
194}