dotzuki_renderer/layout_engine/
data_context.rs1use crate::layout_engine::types::DataValue;
2use crate::layout_engine::types::DataContext;
3
4impl DataContext {
5 pub fn new() -> Self {
7 Self {
8 values: std::collections::HashMap::new(),
9 }
10 }
11
12 pub fn set<V: Into<DataValue>>(&mut self, key: &str, value: V) {
14 self.values.insert(key.to_string(), value.into());
15 }
16
17 pub fn get(&self, key: &str) -> Option<&DataValue> {
19 self.values.get(key)
20 }
21
22 pub fn lang(&self) -> &str {
27 match self.values.get("__lang") {
28 Some(DataValue::Str(s)) => s.as_str(),
29 _ => "en",
30 }
31 }
32
33 pub fn is_truthy(&self, key: &str) -> bool {
40 match self.values.get(key) {
41 Some(DataValue::Bool(b)) => *b,
42 Some(DataValue::Int(n)) => *n != 0,
43 Some(_) => true,
44 None => false,
45 }
46 }
47
48 pub fn resolve(&self, template: &str) -> String {
65 let mut result = String::with_capacity(template.len());
66 let mut chars = template.chars();
67
68 while let Some(ch) = chars.next() {
69 if ch == '{' {
70 let mut expr = String::new();
71 let mut found_close = false;
72
73 for inner in chars.by_ref() {
74 if inner == '}' {
75 found_close = true;
76 break;
77 }
78 if inner == '{' {
79 break;
80 }
81 expr.push(inner);
82 }
83
84 if found_close && !expr.is_empty() {
85 result.push_str(&self.resolve_expr(&expr));
86 } else {
87 result.push('{');
88 result.push_str(&expr);
89 if found_close {
90 result.push('}');
91 }
92 }
93 } else {
94 result.push(ch);
95 }
96 }
97
98 result
99 }
100
101 fn resolve_expr(&self, expr: &str) -> String {
103 if let Some(colon_pos) = expr.find(':') {
104 let var = &expr[..colon_pos];
105 let format = &expr[colon_pos + 1..];
106 let value = self.values.get(var);
107 return self.format_zero_pad(value, format);
108 }
109
110 if let Some(pct_pos) = expr.find('%') {
111 let var = &expr[..pct_pos];
112 let modulo_str = &expr[pct_pos + 1..];
113 let value = self.values.get(var);
114 return self.format_modulo(value, modulo_str);
115 }
116
117 match self.values.get(expr) {
118 Some(value) => self.data_value_to_string(value),
119 None => "?".to_string(),
120 }
121 }
122
123 fn format_zero_pad(&self, value: Option<&DataValue>, format: &str) -> String {
125 let width: usize = if format.starts_with('0') {
126 format[1..].parse().unwrap_or(0)
127 } else {
128 format.parse().unwrap_or(0)
129 };
130
131 match value {
132 Some(DataValue::Int(n)) => {
133 format!("{:0width$}", n, width = width)
134 }
135 Some(other) => self.data_value_to_string(other),
136 None => "?".to_string(),
137 }
138 }
139
140 fn format_modulo(&self, value: Option<&DataValue>, modulo_str: &str) -> String {
142 let modulo: i64 = modulo_str.parse().unwrap_or(1);
143
144 match value {
145 Some(DataValue::Int(n)) => {
146 let result = if modulo != 0 { n % modulo } else { *n };
147 result.to_string()
148 }
149 Some(other) => self.data_value_to_string(other),
150 None => "?".to_string(),
151 }
152 }
153
154 fn data_value_to_string(&self, value: &DataValue) -> String {
156 match value {
157 DataValue::Str(s) => s.clone(),
158 DataValue::Int(n) => n.to_string(),
159 DataValue::Float(f) => f.to_string(),
160 DataValue::Bool(b) => b.to_string(),
161 DataValue::TileId(t) => t.to_string(),
162 DataValue::List(v) => v
163 .iter()
164 .map(|dv| self.data_value_to_string(dv))
165 .collect::<Vec<_>>()
166 .join(", "),
167 }
168 }
169}
170
171#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::layout_engine::types::DataValue;
179
180 #[test]
181 fn test_simple_substitution() {
182 let mut ctx = DataContext::new();
183 ctx.set("name", "LEAFKIT");
184 assert_eq!(ctx.resolve("{name}"), "LEAFKIT");
185 }
186
187 #[test]
188 fn test_zero_padded() {
189 let mut ctx = DataContext::new();
190 ctx.set("num", 1i64);
191 assert_eq!(ctx.resolve("№•{num:03}"), "№•001");
192 }
193
194 #[test]
195 fn test_multiple_vars() {
196 let mut ctx = DataContext::new();
197 ctx.set("feet", 2i64);
198 ctx.set("inches", 4i64);
199 assert_eq!(ctx.resolve("{feet}′{inches:02}″"), "2′04″");
200 }
201
202 #[test]
203 fn test_modulo() {
204 let mut ctx = DataContext::new();
205 ctx.set("weight", 150i64);
206 assert_eq!(ctx.resolve("{weight%10}"), "0");
207 }
208
209 #[test]
210 fn test_modulo_non_zero() {
211 let mut ctx = DataContext::new();
212 ctx.set("weight", 157i64);
213 assert_eq!(ctx.resolve("{weight%10}"), "7");
214 }
215
216 #[test]
217 fn test_unknown_var_placeholder() {
218 let ctx = DataContext::new();
219 assert_eq!(ctx.resolve("{unknown}"), "?");
220 }
221
222 #[test]
223 fn test_plain_text_no_vars() {
224 let ctx = DataContext::new();
225 assert_eq!(ctx.resolve("Hello World"), "Hello World");
226 }
227
228 #[test]
229 fn test_mixed_text_and_vars() {
230 let mut ctx = DataContext::new();
231 ctx.set("item", "POTION");
232 ctx.set("count", 3i64);
233 assert_eq!(ctx.resolve("{item} x{count}"), "POTION x3");
234 }
235
236 #[test]
237 fn test_bool_value() {
238 let mut ctx = DataContext::new();
239 ctx.set("flag", true);
240 assert_eq!(ctx.resolve("{flag}"), "true");
241
242 ctx.set("flag", false);
243 assert_eq!(ctx.resolve("{flag}"), "false");
244 }
245
246 #[test]
247 fn test_is_truthy() {
248 let mut ctx = DataContext::new();
249
250 assert!(!ctx.is_truthy("missing"));
251
252 ctx.set("active", true);
253 assert!(ctx.is_truthy("active"));
254
255 ctx.set("active", false);
256 assert!(!ctx.is_truthy("active"));
257
258 ctx.set("count", 5i64);
259 assert!(ctx.is_truthy("count"));
260
261 ctx.set("count", 0i64);
262 assert!(!ctx.is_truthy("count"));
263
264 ctx.set("name", "test");
265 assert!(ctx.is_truthy("name"));
266 }
267
268 #[test]
269 fn test_empty_braces_passthrough() {
270 let ctx = DataContext::new();
271 assert_eq!(ctx.resolve("before{}after"), "before{}after");
272 }
273
274 #[test]
275 fn test_malformed_brace_passthrough() {
276 let ctx = DataContext::new();
277 assert_eq!(ctx.resolve("Hello {unclosed"), "Hello {unclosed");
278 }
279
280 #[test]
281 fn test_get_value() {
282 let mut ctx = DataContext::new();
283 ctx.set("key", "value");
284 assert_eq!(ctx.get("key"), Some(&DataValue::Str("value".to_string())));
285 assert_eq!(ctx.get("missing"), None);
286 }
287
288 #[test]
289 fn test_zero_pad_large_number() {
290 let mut ctx = DataContext::new();
291 ctx.set("num", 42i64);
292 assert_eq!(ctx.resolve("{num:05}"), "00042");
293 }
294
295 #[test]
296 fn test_zero_pad_exact_width() {
297 let mut ctx = DataContext::new();
298 ctx.set("num", 999i64);
299 assert_eq!(ctx.resolve("{num:03}"), "999");
300 }
301
302 #[test]
303 fn test_tile_id_value() {
304 let mut ctx = DataContext::new();
305 ctx.set("tile", 42u16);
306 assert_eq!(ctx.resolve("Tile:{tile}"), "Tile:42");
307 }
308
309 #[test]
310 fn test_float_value() {
311 let mut ctx = DataContext::new();
312 ctx.set("ratio", DataValue::Float(1.5));
313 assert_eq!(ctx.resolve("{ratio}"), "1.5");
314 }
315
316 #[test]
317 fn test_list_value() {
318 let mut ctx = DataContext::new();
319 ctx.set(
320 "items",
321 DataValue::List(vec![
322 DataValue::Str("A".into()),
323 DataValue::Str("B".into()),
324 DataValue::Str("C".into()),
325 ]),
326 );
327 assert_eq!(ctx.resolve("{items}"), "A, B, C");
328 }
329}