1use std::collections::HashMap;
49
50use regex::Regex;
51use serde_json::Value;
52
53#[cfg(test)]
54use super::value_to_string;
55use super::{is_truthy, parse_literal, resolve_var_expr, ViewConfig, ViewData, ViewError};
56
57pub fn render_control_flow<F>(
72 content: &str,
73 data: &ViewData,
74 config: &ViewConfig,
75 render_inner: F,
76) -> Result<String, ViewError>
77where
78 F: Fn(&str, &ViewData) -> Result<String, ViewError>,
79{
80 let renderer = CxRenderer {
81 config,
82 render_inner,
83 };
84 renderer.render(content, data)
85}
86
87struct CxRenderer<'a, F>
93where
94 F: Fn(&str, &ViewData) -> Result<String, ViewError>,
95{
96 config: &'a ViewConfig,
97 render_inner: F,
98}
99
100impl<'a, F> CxRenderer<'a, F>
101where
102 F: Fn(&str, &ViewData) -> Result<String, ViewError>,
103{
104 fn render(&self, content: &str, data: &ViewData) -> Result<String, ViewError> {
106 let begin = &self.config.tpl_begin;
107 let begin_esc = regex::escape(begin);
108
109 let pattern = format!(r"{}\s*(if|foreach|volist|switch|for)(\s|\()", begin_esc);
111 let re = Regex::new(&pattern).map_err(|e| ViewError::SyntaxError(e.to_string()))?;
112
113 let mut result = String::with_capacity(content.len());
114 let mut last_end = 0;
115
116 for caps in re.captures_iter(content) {
117 let m = caps
118 .get(0)
119 .ok_or_else(|| ViewError::SyntaxError("正则捕获组 0(整体匹配)缺失".into()))?;
120 if m.start() < last_end {
122 continue;
123 }
124 let tag_type = caps
125 .get(1)
126 .ok_or_else(|| ViewError::SyntaxError("正则捕获组 1(标签类型)缺失".into()))?
127 .as_str();
128 let separator = caps
129 .get(2)
130 .ok_or_else(|| ViewError::SyntaxError("正则捕获组 2(分隔符)缺失".into()))?
131 .as_str(); let after_name = m.start() + m.len() - separator.len();
135 let end_str = &self.config.tpl_end;
136 let tag_end_pos = match content[after_name..].find(end_str.as_str()) {
137 Some(pos) => after_name + pos + end_str.len(),
138 None => continue, };
140
141 result.push_str(&content[last_end..m.start()]);
142
143 let tag_content_start = m.start() + begin.len() + tag_type.len();
145 let tag_inner = &content[tag_content_start..tag_end_pos - end_str.len()];
147
148 match self.find_matching_close(content, tag_end_pos, tag_type) {
150 Ok((block_content, block_end)) => {
151 let rendered = match tag_type {
152 "if" => self.render_if(tag_inner, &block_content, data)?,
153 "foreach" => self.render_foreach(tag_inner, &block_content, data)?,
154 "volist" => self.render_volist(tag_inner, &block_content, data)?,
155 "switch" => self.render_switch(tag_inner, &block_content, data)?,
156 "for" => self.render_for(tag_inner, &block_content, data)?,
157 _ => block_content.to_string(),
158 };
159 result.push_str(&rendered);
160 last_end = block_end;
161 }
162 Err(e) => {
163 return Err(e);
165 }
166 }
167 }
168
169 result.push_str(&content[last_end..]);
170 Ok(result)
171 }
172
173 fn find_matching_close(
178 &self,
179 content: &str,
180 start: usize,
181 tag_type: &str,
182 ) -> Result<(String, usize), ViewError> {
183 let begin = &self.config.tpl_begin;
184 let end = &self.config.tpl_end;
185
186 let open_prefix = format!("{}{}", begin, tag_type);
187 let close_tag = format!("{}{}{}{}", begin, "/", tag_type, end);
188
189 let mut depth: i32 = 1;
190 let mut pos = start;
191
192 while pos < content.len() {
193 let remaining = &content[pos..];
194
195 let next_close = remaining.find(&close_tag);
197
198 let next_open = remaining.find(&open_prefix).and_then(|p| {
200 let after = pos + p + open_prefix.len();
201 if after < content.len() {
202 let next_char = content.as_bytes()[after];
203 if next_char == b' '
205 || next_char == b'\t'
206 || next_char == b'\n'
207 || next_char == b'\r'
208 || next_char == b'('
209 || content[after..].starts_with(end.as_str())
210 {
211 return Some(p);
212 }
213 }
214 None
215 });
216
217 match (next_open, next_close) {
218 (_, None) => {
219 return Err(ViewError::SyntaxError(format!(
220 "未闭合的 {{{}}} 标签",
221 tag_type
222 )));
223 }
224 (Some(open_pos), Some(close_pos)) if open_pos < close_pos => {
225 depth += 1;
226 pos += open_pos + open_prefix.len();
227 }
228 (_, Some(close_pos)) => {
229 depth -= 1;
230 if depth == 0 {
231 let block_content = content[start..pos + close_pos].to_string();
232 let block_end = pos + close_pos + close_tag.len();
233 return Ok((block_content, block_end));
234 }
235 pos += close_pos + close_tag.len();
236 }
237 }
238 }
239
240 Err(ViewError::SyntaxError(format!(
241 "未闭合的 {{{}}} 标签",
242 tag_type
243 )))
244 }
245
246 fn render_if(
259 &self,
260 tag_inner: &str,
261 block_content: &str,
262 data: &ViewData,
263 ) -> Result<String, ViewError> {
264 let attrs = parse_attributes(tag_inner);
265 let condition = if let Some(cond) = attrs.get("condition") {
266 cond.clone()
267 } else {
268 tag_inner
271 .trim()
272 .trim_start_matches('(')
273 .trim_end_matches(')')
274 .trim()
275 .to_string()
276 };
277
278 if evaluate_condition(&condition, data)? {
280 let if_body = self.split_if_branches(block_content).0;
282 return (self.render_inner)(&if_body, data);
283 }
284
285 let branches = self.split_if_branches(block_content);
287 let branches_ref = &branches.1;
288
289 for (elseif_cond, elseif_body) in branches_ref {
290 if evaluate_condition(elseif_cond, data)? {
291 return (self.render_inner)(elseif_body, data);
292 }
293 }
294
295 if let Some(else_body) = &branches.2 {
297 return (self.render_inner)(else_body, data);
298 }
299
300 Ok(String::new())
301 }
302
303 fn split_if_branches(&self, content: &str) -> (String, Vec<(String, String)>, Option<String>) {
308 let begin = &self.config.tpl_begin;
309 let end = &self.config.tpl_end;
310
311 let elseif_pattern = format!(
314 "{}\\s*elseif\\s+([^{{}}]*?)(?:\\s*/)?{}",
315 regex::escape(begin),
316 regex::escape(end)
317 );
318 let else_pattern = format!(
320 r"{}\s*else\s*/?{}",
321 regex::escape(begin),
322 regex::escape(end)
323 );
324
325 let elseif_re = Regex::new(&elseif_pattern).unwrap_or_else(|e| panic!("elseif regex: {e}"));
326 let else_re = Regex::new(&else_pattern).unwrap_or_else(|e| panic!("else regex: {e}"));
327
328 let first_elseif = elseif_re.find(content).map(|m| m.start());
330 let first_else = else_re.find(content).map(|m| m.start());
331
332 let first_split = match (first_elseif, first_else) {
333 (Some(ei), Some(el)) => ei.min(el),
334 (Some(ei), None) => ei,
335 (None, Some(el)) => el,
336 (None, None) => return (content.to_string(), Vec::new(), None),
337 };
338
339 let if_body = content[..first_split].to_string();
340 let rest = &content[first_split..];
341
342 let mut elseif_branches = Vec::new();
343 let mut else_body = None;
344 let mut remaining = rest;
345
346 loop {
347 if let Some(m) = elseif_re.find(remaining) {
349 if m.start() == 0 {
350 let attrs_str = elseif_re
351 .captures(remaining)
352 .expect("elseif_re 已通过 find 匹配且 start==0,captures 必成功")
353 .get(1)
354 .expect("elseif 正则含捕获组 1,匹配成功时必存在")
355 .as_str();
356 let attrs = parse_attributes(attrs_str);
357 let cond = attrs
358 .get("condition")
359 .cloned()
360 .unwrap_or_else(|| attrs_str.trim().to_string());
361 let after = &remaining[m.end()..];
362
363 let next_split = self.find_next_branch(after);
365
366 let body = match next_split {
367 Some(pos) => {
368 let b = after[..pos].to_string();
369 remaining = &after[pos..];
370 b
371 }
372 None => {
373 let b = after.to_string();
374 remaining = "";
375 b
376 }
377 };
378 elseif_branches.push((cond, body));
379 if remaining.is_empty() {
380 break;
381 }
382 continue;
383 }
384 }
385
386 if let Some(m) = else_re.find(remaining) {
388 if m.start() == 0 {
389 else_body = Some(remaining[m.end()..].to_string());
390 break;
391 }
392 }
393
394 break;
395 }
396
397 (if_body, elseif_branches, else_body)
398 }
399
400 fn find_next_branch(&self, content: &str) -> Option<usize> {
402 let begin = &self.config.tpl_begin;
403 let end = &self.config.tpl_end;
404
405 let elseif_pattern = format!(
406 "{}\\s*elseif\\s+[^{{}}]*?{}",
407 regex::escape(begin),
408 regex::escape(end)
409 );
410 let else_pattern = format!(
411 "{}\\s*else\\s*/?{}",
412 regex::escape(begin),
413 regex::escape(end)
414 );
415
416 let elseif_re = Regex::new(&elseif_pattern).ok()?;
417 let else_re = Regex::new(&else_pattern).ok()?;
418
419 let ei = elseif_re.find(content).map(|m| m.start());
420 let el = else_re.find(content).map(|m| m.start());
421
422 match (ei, el) {
423 (Some(a), Some(b)) => Some(a.min(b)),
424 (Some(a), None) => Some(a),
425 (None, Some(b)) => Some(b),
426 (None, None) => None,
427 }
428 }
429
430 fn render_foreach(
449 &self,
450 tag_inner: &str,
451 block_content: &str,
452 data: &ViewData,
453 ) -> Result<String, ViewError> {
454 let attrs = parse_attributes(tag_inner);
455
456 if attrs.is_empty() && tag_inner.trim().starts_with('(') {
459 return self.render_foreach_expression(tag_inner.trim(), block_content, data);
460 }
461 if let Some(expr) = attrs.get("expression") {
462 return self.render_foreach_expression(expr, block_content, data);
463 }
464
465 let name = attrs.get("name").cloned().unwrap_or_default();
466 let item_name = attrs
467 .get("id")
468 .or_else(|| attrs.get("item"))
469 .cloned()
470 .unwrap_or_else(|| "item".to_string());
471 let key_name = attrs
472 .get("key")
473 .cloned()
474 .unwrap_or_else(|| "key".to_string());
475 let index_name = attrs.get("index").cloned();
476 let empty_msg = attrs.get("empty").cloned().unwrap_or_default();
477 let offset: usize = attrs
478 .get("offset")
479 .and_then(|v| v.parse().ok())
480 .unwrap_or(0);
481 let length: Option<usize> = attrs.get("length").and_then(|v| v.parse().ok());
482
483 let array_val = resolve_name(&name, data);
485
486 let items = match &array_val {
487 Value::Array(arr) => arr.clone(),
488 Value::Object(map) => {
489 map.iter()
491 .map(|(k, v)| serde_json::json!({"key": k, "value": v.clone()}))
492 .collect()
493 }
494 _ => Vec::new(),
495 };
496
497 let start = offset.min(items.len());
499 let end = match length {
500 Some(len) => (start + len).min(items.len()),
501 None => items.len(),
502 };
503 let slice = &items[start..end];
504
505 if slice.is_empty() && !empty_msg.is_empty() {
506 return Ok(empty_msg);
507 }
508
509 let mut result = String::new();
510 for (idx, item) in slice.iter().enumerate() {
511 let mut child_data = data.clone();
512
513 if array_val.is_array() {
515 child_data.insert(item_name.clone(), item.clone());
516 } else {
517 if let Some(v) = item.get("value") {
519 child_data.insert(item_name.clone(), v.clone());
520 }
521 if let Some(k) = item.get("key").and_then(|k| k.as_str()) {
522 child_data.insert(key_name.clone(), Value::String(k.to_string()));
523 }
524 }
525
526 if array_val.is_array() {
528 if let Value::Array(arr) = &array_val {
529 let actual_idx = start + idx;
530 if let Some(actual_item) = arr.get(actual_idx) {
531 child_data.insert(item_name.clone(), actual_item.clone());
532 }
533 child_data.insert(key_name.clone(), Value::Number((actual_idx).into()));
534 }
535 }
536
537 if let Some(ref index) = index_name {
539 child_data.insert(index.clone(), Value::Number((idx).into()));
540 }
541
542 let rendered = (self.render_inner)(block_content, &child_data)?;
543 result.push_str(&rendered);
544 }
545
546 Ok(result)
547 }
548
549 fn render_foreach_expression(
551 &self,
552 expr: &str,
553 block_content: &str,
554 data: &ViewData,
555 ) -> Result<String, ViewError> {
556 let expr = expr
558 .trim()
559 .trim_start_matches('(')
560 .trim_end_matches(')')
561 .trim();
562
563 let as_pos = expr
565 .find(" as ")
566 .ok_or_else(|| ViewError::SyntaxError("foreach 表达式缺少 'as'".to_string()))?;
567
568 let name_part = expr[..as_pos].trim();
569 let after_as = expr[as_pos + 4..].trim();
570
571 let name = name_part.trim_start_matches('$');
573 let array_val = resolve_var_expr(name, data);
574
575 let (key_name, item_name) = if let Some(arrow_pos) = after_as.find("=>") {
576 let k = after_as[..arrow_pos].trim().trim_start_matches('$');
577 let v = after_as[arrow_pos + 2..].trim().trim_start_matches('$');
578 (k.to_string(), v.to_string())
579 } else {
580 (
581 "key".to_string(),
582 after_as.trim_start_matches('$').to_string(),
583 )
584 };
585
586 let mut result = String::new();
587
588 match &array_val {
589 Value::Array(arr) => {
590 for (idx, item) in arr.iter().enumerate() {
591 let mut child_data = data.clone();
592 child_data.insert(item_name.clone(), item.clone());
593 child_data.insert(key_name.clone(), Value::Number((idx).into()));
594 let rendered = (self.render_inner)(block_content, &child_data)?;
595 result.push_str(&rendered);
596 }
597 }
598 Value::Object(map) => {
599 for (k, v) in map.iter() {
600 let mut child_data = data.clone();
601 child_data.insert(item_name.clone(), v.clone());
602 child_data.insert(key_name.clone(), Value::String(k.clone()));
603 let rendered = (self.render_inner)(block_content, &child_data)?;
604 result.push_str(&rendered);
605 }
606 }
607 _ => {}
608 }
609
610 Ok(result)
611 }
612
613 fn render_volist(
633 &self,
634 tag_inner: &str,
635 block_content: &str,
636 data: &ViewData,
637 ) -> Result<String, ViewError> {
638 let attrs = parse_attributes(tag_inner);
639
640 let name = attrs.get("name").cloned().unwrap_or_default();
641 let id_name = attrs
642 .get("id")
643 .cloned()
644 .unwrap_or_else(|| "item".to_string());
645 let key_name = attrs.get("key").cloned().unwrap_or_else(|| "i".to_string());
646 let empty_msg = attrs.get("empty").cloned().unwrap_or_default();
647 let offset: usize = attrs
648 .get("offset")
649 .and_then(|v| v.parse().ok())
650 .unwrap_or(0);
651 let length: Option<usize> = attrs.get("length").and_then(|v| v.parse().ok());
652
653 let array_val = resolve_name(&name, data);
655
656 let pairs: Vec<(Value, Value)> = match &array_val {
658 Value::Array(arr) => arr
659 .iter()
660 .enumerate()
661 .map(|(i, v)| (Value::Number(i.into()), v.clone()))
662 .collect(),
663 Value::Object(map) => map
664 .iter()
665 .map(|(k, v)| (Value::String(k.clone()), v.clone()))
666 .collect(),
667 _ => Vec::new(),
668 };
669
670 let start = offset.min(pairs.len());
672 let end = match length {
673 Some(len) => (start + len).min(pairs.len()),
674 None => pairs.len(),
675 };
676
677 if start >= end && !empty_msg.is_empty() {
678 return Ok(empty_msg);
679 }
680
681 let mut result = String::new();
682 for (view_idx, (key_val, item_val)) in pairs[start..end].iter().enumerate() {
683 let mut child_data = data.clone();
684
685 child_data.insert("key".to_string(), key_val.clone());
687 child_data.insert(key_name.clone(), Value::Number((view_idx + 1).into()));
688 child_data.insert(id_name.clone(), item_val.clone());
689
690 let rendered = (self.render_inner)(block_content, &child_data)?;
691 result.push_str(&rendered);
692 }
693
694 Ok(result)
695 }
696
697 fn render_switch(
711 &self,
712 tag_inner: &str,
713 block_content: &str,
714 data: &ViewData,
715 ) -> Result<String, ViewError> {
716 let attrs = parse_attributes(tag_inner);
717 let name = attrs.get("name").cloned().unwrap_or_default();
718 let switch_val = resolve_name(&name, data);
719
720 let cases = self.parse_switch_cases(block_content);
722
723 let mut matched = false;
724 let mut result = String::new();
725 let mut fall_through = false;
726
727 for case in &cases {
728 if fall_through {
729 let rendered = (self.render_inner)(&case.content, data)?;
731 result.push_str(&rendered);
732 if case.should_break() {
733 break;
734 }
735 continue;
736 }
737
738 if matched {
739 break;
740 }
741
742 match &case.kind {
743 CaseKind::Default => {
744 matched = true;
745 let rendered = (self.render_inner)(&case.content, data)?;
746 result.push_str(&rendered);
747 if case.should_break() {
748 break;
749 }
750 fall_through = true;
751 }
752 CaseKind::Values(values) => {
753 for val in values {
754 let case_val = resolve_case_value(val, data);
755 if loose_equal(&switch_val, &case_val) {
756 matched = true;
757 let rendered = (self.render_inner)(&case.content, data)?;
758 result.push_str(&rendered);
759 if case.should_break() {
760 break;
761 }
762 fall_through = true;
763 break;
764 }
765 }
766 }
767 }
768 }
769
770 Ok(result)
771 }
772
773 fn parse_switch_cases(&self, content: &str) -> Vec<SwitchCase> {
775 let begin = &self.config.tpl_begin;
776 let end = &self.config.tpl_end;
777
778 let case_pattern = format!(
780 "{}\\s*case\\s+([^{{}}]*?)(?:\\s*/)?{}",
781 regex::escape(begin),
782 regex::escape(end)
783 );
784 let default_pattern = format!(
786 "{}\\s*default\\s*/?{}",
787 regex::escape(begin),
788 regex::escape(end)
789 );
790
791 let case_re = Regex::new(&case_pattern).unwrap_or_else(|e| panic!("case regex: {e}"));
792 let default_re =
793 Regex::new(&default_pattern).unwrap_or_else(|e| panic!("default regex: {e}"));
794
795 let mut cases = Vec::new();
796 let mut pos = 0;
797
798 while pos < content.len() {
799 let next_case = case_re
801 .find(&content[pos..])
802 .map(|m| (pos + m.start(), m.end() - m.start()));
803 let next_default = default_re
804 .find(&content[pos..])
805 .map(|m| (pos + m.start(), m.end() - m.start()));
806
807 let (tag_start, tag_len, is_default) = match (next_case, next_default) {
808 (Some((cs, cl)), Some((ds, dl))) => {
809 if cs <= ds {
810 (cs, cl, false)
811 } else {
812 (ds, dl, true)
813 }
814 }
815 (Some((cs, cl)), None) => (cs, cl, false),
816 (None, Some((ds, dl))) => (ds, dl, true),
817 (None, None) => break,
818 };
819
820 let tag_str = &content[tag_start..tag_start + tag_len];
822
823 if is_default {
824 let body_start = tag_start + tag_len;
825 let (body_end, is_close_case) = self
827 .find_next_case_or_default(&content[body_start..])
828 .map(|(p, is_cc)| (body_start + p, is_cc))
829 .unwrap_or((content.len(), false));
830 let body = content[body_start..body_end].to_string();
831 cases.push(SwitchCase {
832 kind: CaseKind::Default,
833 content: body,
834 break_attr: None,
835 });
836 let close_case_tag = format!("{}/case{}", begin, end);
838 if is_close_case {
839 pos = body_end + close_case_tag.len();
840 } else {
841 pos = body_end;
842 }
843 } else {
844 let caps = case_re
845 .captures(tag_str)
846 .expect("else 分支仅处理 case 标签,case_re 必匹配");
847 let attrs_str = caps
848 .get(1)
849 .expect("case 正则含捕获组 1,匹配成功时必存在")
850 .as_str();
851 let attrs = parse_attributes(attrs_str);
852 let value = attrs.get("value").cloned().unwrap_or_default();
853 let break_attr = attrs.get("break").cloned();
854
855 let body_start = tag_start + tag_len;
856 let (body_end, is_close_case) = self
857 .find_next_case_or_default(&content[body_start..])
858 .map(|(p, is_cc)| (body_start + p, is_cc))
859 .unwrap_or((content.len(), false));
860 let body = content[body_start..body_end].to_string();
861
862 let values: Vec<String> = value.split('|').map(|s| s.trim().to_string()).collect();
864
865 cases.push(SwitchCase {
866 kind: CaseKind::Values(values),
867 content: body,
868 break_attr,
869 });
870 let close_case_tag = format!("{}/case{}", begin, end);
872 if is_close_case {
873 pos = body_end + close_case_tag.len();
874 } else {
875 pos = body_end;
876 }
877 }
878 }
879
880 cases
881 }
882
883 fn find_next_case_or_default(&self, content: &str) -> Option<(usize, bool)> {
887 let begin = &self.config.tpl_begin;
888 let end = &self.config.tpl_end;
889
890 let case_pattern = format!(
891 "{}\\s*case\\s+[^{{}}]*?{}",
892 regex::escape(begin),
893 regex::escape(end)
894 );
895 let default_pattern = format!(
896 "{}\\s*default\\s*/?{}",
897 regex::escape(begin),
898 regex::escape(end)
899 );
900 let close_case_pattern = format!(
902 "{}\\s*/case\\s*{}",
903 regex::escape(begin),
904 regex::escape(end)
905 );
906
907 let case_re = Regex::new(&case_pattern).ok()?;
908 let default_re = Regex::new(&default_pattern).ok()?;
909 let close_case_re = Regex::new(&close_case_pattern).ok()?;
910
911 let cs = case_re.find(content).map(|m| m.start());
912 let ds = default_re.find(content).map(|m| m.start());
913 let ccs = close_case_re.find(content).map(|m| m.start());
914
915 let mut candidates: Vec<(usize, bool)> = Vec::new();
917 if let Some(p) = cs {
918 candidates.push((p, false));
919 }
920 if let Some(p) = ds {
921 candidates.push((p, false));
922 }
923 if let Some(p) = ccs {
924 candidates.push((p, true));
925 }
926
927 candidates.into_iter().min_by_key(|(p, _)| *p)
928 }
929
930 fn render_for(
946 &self,
947 tag_inner: &str,
948 block_content: &str,
949 data: &ViewData,
950 ) -> Result<String, ViewError> {
951 let attrs = parse_attributes(tag_inner);
952
953 let start: f64 = attrs
954 .get("start")
955 .and_then(|v| parse_operand_value(v, data).as_f64())
956 .unwrap_or(0.0);
957 let end: f64 = attrs
958 .get("end")
959 .and_then(|v| parse_operand_value(v, data).as_f64())
960 .unwrap_or(0.0);
961 let step: f64 = attrs
962 .get("step")
963 .and_then(|v| parse_operand_value(v, data).as_f64())
964 .unwrap_or(1.0);
965 let comparison = attrs
966 .get("comparison")
967 .cloned()
968 .unwrap_or_else(|| "lt".to_string());
969 let name = attrs
970 .get("name")
971 .cloned()
972 .unwrap_or_else(|| "i".to_string());
973
974 if step == 0.0 {
976 return Err(ViewError::SyntaxError("{for} step 不能为 0".to_string()));
977 }
978
979 let mut result = String::new();
980 let mut current = start;
981
982 loop {
983 let should_continue = match comparison.as_str() {
985 "lt" | "<" => current < end,
986 "elt" | "<=" => current <= end,
987 "gt" | ">" => current > end,
988 "egt" | ">=" => current >= end,
989 "eq" | "==" => current == end,
990 "neq" | "!=" => current != end,
991 "heq" | "===" => current == end,
992 "nheq" | "!==" => current != end,
993 _ => current < end, };
995
996 if !should_continue {
997 break;
998 }
999
1000 let mut child_data = data.clone();
1002 child_data.insert(
1003 name.clone(),
1004 serde_json::Number::from_f64(current)
1005 .map(Value::Number)
1006 .unwrap_or(Value::Null),
1007 );
1008
1009 let rendered = (self.render_inner)(block_content, &child_data)?;
1010 result.push_str(&rendered);
1011
1012 current += step;
1013
1014 if result.len() > 10_000_000 {
1016 return Err(ViewError::RenderError(
1017 "{for} 循环输出超过 10MB 限制".to_string(),
1018 ));
1019 }
1020 }
1021
1022 Ok(result)
1023 }
1024}
1025
1026#[derive(Debug, Clone)]
1032enum CaseKind {
1033 Values(Vec<String>),
1035 Default,
1037}
1038
1039#[derive(Debug, Clone)]
1041struct SwitchCase {
1042 kind: CaseKind,
1043 content: String,
1044 break_attr: Option<String>,
1045}
1046
1047impl SwitchCase {
1048 fn should_break(&self) -> bool {
1060 match &self.break_attr {
1061 None => true, Some(v) => is_truthy(&Value::String(v.clone())),
1063 }
1064 }
1065}
1066
1067pub fn evaluate_condition(condition: &str, data: &ViewData) -> Result<bool, ViewError> {
1076 let condition = replace_condition_shorthands(condition.trim());
1077 evaluate_or(&condition, data)
1078}
1079
1080fn evaluate_or(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1082 for part in split_top_level(expr, "||") {
1083 if evaluate_and(part.trim(), data)? {
1084 return Ok(true);
1085 }
1086 }
1087 Ok(false)
1088}
1089
1090fn evaluate_and(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1092 for part in split_top_level(expr, "&&") {
1093 if !evaluate_not(part.trim(), data)? {
1094 return Ok(false);
1095 }
1096 }
1097 Ok(true)
1098}
1099
1100fn evaluate_not(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1102 let expr = expr.trim();
1103 if let Some(rest) = expr.strip_prefix('!') {
1104 return Ok(!evaluate_not(rest.trim(), data)?);
1105 }
1106 evaluate_comparison(expr, data)
1107}
1108
1109fn evaluate_comparison(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1113 let expr = expr.trim();
1114
1115 if expr.starts_with('(') && expr.ends_with(')') && is_balanced_parens(&expr[1..expr.len() - 1])
1117 {
1118 return evaluate_or(&expr[1..expr.len() - 1], data);
1119 }
1120
1121 if let Some((op, pos)) = find_comparison_operator(expr) {
1123 let left = expr[..pos].trim();
1124 let right = expr[pos + op.len()..].trim();
1125 let left_val = parse_operand_value(left, data);
1126 let right_val = parse_operand_value(right, data);
1127 return Ok(compare_values(&left_val, op, &right_val));
1128 }
1129
1130 let val = parse_operand_value(expr, data);
1132 Ok(is_truthy(&val))
1133}
1134
1135fn find_comparison_operator(expr: &str) -> Option<(&'static str, usize)> {
1137 let bytes = expr.as_bytes();
1138 let mut depth = 0;
1139 let mut in_single = false;
1140 let mut in_double = false;
1141
1142 let operators: &[&'static str] = &["===", "!==", "==", "!=", ">=", "<=", ">", "<"];
1143
1144 let mut i = 0;
1145 while i < bytes.len() {
1146 match bytes[i] {
1147 b'(' if !in_single && !in_double => {
1148 depth += 1;
1149 }
1150 b')' if !in_single && !in_double => {
1151 depth -= 1;
1152 }
1153 b'\'' if !in_double => {
1154 in_single = !in_single;
1155 }
1156 b'"' if !in_single => {
1157 in_double = !in_double;
1158 }
1159 _ if depth == 0 && !in_single && !in_double => {
1160 let rest = &expr[i..];
1161 for op in operators {
1162 if rest.starts_with(op) {
1163 return Some((op, i));
1164 }
1165 }
1166 }
1167 _ => {}
1168 }
1169 i += 1;
1170 }
1171 None
1172}
1173
1174fn is_balanced_parens(expr: &str) -> bool {
1176 let mut depth = 0;
1177 for c in expr.chars() {
1178 match c {
1179 '(' => depth += 1,
1180 ')' => {
1181 depth -= 1;
1182 if depth < 0 {
1183 return false;
1184 }
1185 }
1186 _ => {}
1187 }
1188 }
1189 depth == 0
1190}
1191
1192fn split_top_level<'a>(expr: &'a str, sep: &str) -> Vec<&'a str> {
1194 let mut parts = Vec::new();
1195 let mut depth = 0;
1196 let mut in_single = false;
1197 let mut in_double = false;
1198 let mut last = 0;
1199
1200 let bytes = expr.as_bytes();
1201 let sep_bytes = sep.as_bytes();
1202 let sep_len = sep_bytes.len();
1203
1204 let mut i = 0;
1205 while i < bytes.len() {
1206 match bytes[i] {
1207 b'(' if !in_single && !in_double => depth += 1,
1208 b')' if !in_single && !in_double => depth -= 1,
1209 b'\'' if !in_double => in_single = !in_single,
1210 b'"' if !in_single => in_double = !in_double,
1211 _ if depth == 0
1212 && !in_single
1213 && !in_double
1214 && i + sep_len <= bytes.len()
1215 && &bytes[i..i + sep_len] == sep_bytes =>
1216 {
1217 parts.push(&expr[last..i]);
1218 last = i + sep_len;
1219 i += sep_len;
1220 continue;
1221 }
1222 _ => {}
1223 }
1224 i += 1;
1225 }
1226 parts.push(&expr[last..]);
1227 parts
1228}
1229
1230fn parse_operand_value(expr: &str, data: &ViewData) -> Value {
1244 let expr = expr.trim();
1245 if expr.is_empty() {
1246 return Value::Null;
1247 }
1248
1249 if let Some(rest) = expr.strip_prefix('$') {
1251 return resolve_var_expr(rest, data);
1252 }
1253
1254 if expr.starts_with(':') {
1256 return Value::Null;
1258 }
1259
1260 if (expr.starts_with('\'') && expr.ends_with('\'') && expr.len() >= 2)
1262 || (expr.starts_with('"') && expr.ends_with('"') && expr.len() >= 2)
1263 {
1264 return Value::String(expr[1..expr.len() - 1].to_string());
1265 }
1266
1267 if let Ok(n) = expr.parse::<i64>() {
1269 return Value::Number(n.into());
1270 }
1271 if let Ok(f) = expr.parse::<f64>() {
1272 if let Some(n) = serde_json::Number::from_f64(f) {
1273 return Value::Number(n);
1274 }
1275 }
1276
1277 match expr {
1279 "true" => return Value::Bool(true),
1280 "false" => return Value::Bool(false),
1281 "null" => return Value::Null,
1282 _ => {}
1283 }
1284
1285 match expr.chars().next() {
1289 Some(first) if first.is_alphabetic() || first == '_' => {
1290 return resolve_var_expr(expr, data);
1291 }
1292 _ => {}
1293 }
1294
1295 Value::Null
1296}
1297
1298fn compare_values(left: &Value, op: &str, right: &Value) -> bool {
1300 match op {
1301 "==" => loose_equal(left, right),
1302 "!=" => !loose_equal(left, right),
1303 "===" => strict_equal(left, right),
1304 "!==" => !strict_equal(left, right),
1305 ">" => value_to_f64(left) > value_to_f64(right),
1306 "<" => value_to_f64(left) < value_to_f64(right),
1307 ">=" => value_to_f64(left) >= value_to_f64(right),
1308 "<=" => value_to_f64(left) <= value_to_f64(right),
1309 _ => false,
1310 }
1311}
1312
1313fn loose_equal(left: &Value, right: &Value) -> bool {
1320 match (left, right) {
1321 (Value::Null, Value::Null) => true,
1322 (Value::Bool(a), Value::Bool(b)) => a == b,
1323 (Value::Number(a), Value::Number(b)) => a == b,
1324 (Value::String(a), Value::String(b)) => a == b,
1325 (Value::Number(a), Value::String(b)) => {
1326 if let Ok(n) = b.parse::<f64>() {
1327 a.as_f64() == Some(n)
1328 } else {
1329 a.to_string() == *b
1330 }
1331 }
1332 (Value::String(a), Value::Number(b)) => {
1333 if let Ok(n) = a.parse::<f64>() {
1334 Some(n) == b.as_f64()
1335 } else {
1336 *a == b.to_string()
1337 }
1338 }
1339 (Value::Bool(a), Value::Number(b)) => *a == (b.as_f64().unwrap_or(0.0) != 0.0),
1340 (Value::Number(a), Value::Bool(b)) => (a.as_f64().unwrap_or(0.0) != 0.0) == *b,
1341 (Value::Bool(a), Value::String(b)) => *a == is_truthy(&Value::String(b.clone())),
1342 (Value::String(a), Value::Bool(b)) => is_truthy(&Value::String(a.clone())) == *b,
1343 _ => left == right,
1344 }
1345}
1346
1347fn strict_equal(left: &Value, right: &Value) -> bool {
1349 left == right
1350}
1351
1352fn value_to_f64(value: &Value) -> f64 {
1354 match value {
1355 Value::Number(n) => n.as_f64().unwrap_or(0.0),
1356 Value::String(s) => s.parse::<f64>().unwrap_or(0.0),
1357 Value::Bool(b) => {
1358 if *b {
1359 1.0
1360 } else {
1361 0.0
1362 }
1363 }
1364 Value::Null => 0.0,
1365 Value::Array(a) => a.len() as f64,
1366 Value::Object(o) => o.len() as f64,
1367 }
1368}
1369
1370fn parse_attributes(tag: &str) -> HashMap<String, String> {
1378 let mut attrs = HashMap::new();
1379 let re = Regex::new(r#"(\w+)=["']([^"']*)["']"#).unwrap_or_else(|e| panic!("attr regex: {e}"));
1380 for cap in re.captures_iter(tag) {
1381 attrs.insert(cap[1].to_string(), cap[2].to_string());
1382 }
1383 attrs
1384}
1385
1386fn replace_condition_shorthands(condition: &str) -> String {
1398 let mut result = condition.to_string();
1400
1401 let replacements: &[(&str, &str)] = &[
1403 (" nheq ", " !== "),
1404 (" heq ", " === "),
1405 (" neq ", " != "),
1406 (" egt ", " >= "),
1407 (" elt ", " <= "),
1408 (" eq ", " == "),
1409 (" gt ", " > "),
1410 (" lt ", " < "),
1411 ];
1412
1413 for (from, to) in replacements {
1414 let mut search_pos = 0;
1415 loop {
1416 let lower = result.to_lowercase();
1418 match lower[search_pos..].find(from) {
1419 Some(pos) => {
1420 let abs_pos = search_pos + pos;
1421 result.replace_range(abs_pos..abs_pos + from.len(), to);
1422 search_pos = abs_pos + to.len();
1423 }
1424 None => break,
1425 }
1426 }
1427 }
1428
1429 result
1430}
1431
1432fn resolve_name(name: &str, data: &ViewData) -> Value {
1438 let name = name.trim();
1439 if name.is_empty() {
1440 return Value::Null;
1441 }
1442
1443 if name.starts_with(':') {
1445 return Value::Null;
1447 }
1448
1449 let var_name = name.strip_prefix('$').unwrap_or(name);
1451 resolve_var_expr(var_name, data)
1452}
1453
1454fn resolve_case_value(value: &str, data: &ViewData) -> Value {
1460 let value = value.trim();
1461 if value.is_empty() {
1462 return Value::Null;
1463 }
1464
1465 if let Some(rest) = value.strip_prefix('$') {
1466 return resolve_var_expr(rest, data);
1467 }
1468
1469 if value.starts_with(':') {
1470 return Value::Null;
1471 }
1472
1473 parse_literal(value)
1474}
1475
1476#[cfg(test)]
1481mod tests {
1482 use super::*;
1483 use serde_json::json;
1484 use std::collections::HashMap;
1485
1486 fn make_config() -> ViewConfig {
1488 ViewConfig::default()
1489 }
1490
1491 fn make_data() -> ViewData {
1493 let mut data = HashMap::new();
1494 data.insert("a".to_string(), json!(1));
1495 data.insert("b".to_string(), json!(2));
1496 data.insert("name".to_string(), json!("Alice"));
1497 data.insert("list".to_string(), json!([1, 2, 3]));
1498 data.insert("empty_list".to_string(), json!([]));
1499 data.insert("obj".to_string(), json!({"x": 10, "y": 20}));
1500 data.insert("var".to_string(), json!(2));
1501 data.insert("flag".to_string(), json!(true));
1502 data
1503 }
1504
1505 fn render(template: &str, data: &ViewData) -> String {
1509 let config = make_config();
1510
1511 fn render_recursive(
1512 content: &str,
1513 d: &ViewData,
1514 config: &ViewConfig,
1515 ) -> Result<String, ViewError> {
1516 let content =
1518 render_control_flow(content, d, config, |c, d2| render_recursive(c, d2, config))?;
1519 let begin = &config.tpl_begin;
1521 let end = &config.tpl_end;
1522 let pattern = format!("{}(.*?){}", regex::escape(begin), regex::escape(end));
1523 let re = Regex::new(&pattern).unwrap();
1524 let mut result = String::new();
1525 let mut last = 0;
1526 for caps in re.captures_iter(&content) {
1527 let m = caps.get(0).unwrap();
1528 let tag = caps.get(1).unwrap().as_str().trim();
1529 result.push_str(&content[last..m.start()]);
1530 if let Some(rest) = tag.strip_prefix('$') {
1531 let val = resolve_var_expr(rest, d);
1532 result.push_str(&value_to_string(&val));
1533 } else {
1534 result.push_str(&content[m.start()..m.end()]);
1535 }
1536 last = m.end();
1537 }
1538 result.push_str(&content[last..]);
1539 Ok(result)
1540 }
1541
1542 render_recursive(template, data, &config).unwrap()
1543 }
1544
1545 #[test]
1550 fn test_if_true() {
1551 let data = make_data();
1552 let result = render("{if condition=\"$a eq 1\"}yes{/if}", &data);
1553 assert_eq!(result, "yes");
1554 }
1555
1556 #[test]
1557 fn test_if_false() {
1558 let data = make_data();
1559 let result = render("{if condition=\"$a eq 2\"}yes{/if}", &data);
1560 assert_eq!(result, "");
1561 }
1562
1563 #[test]
1564 fn test_if_else_true() {
1565 let data = make_data();
1566 let result = render("{if condition=\"$a eq 1\"}yes{else /}no{/if}", &data);
1567 assert_eq!(result, "yes");
1568 }
1569
1570 #[test]
1571 fn test_if_else_false() {
1572 let data = make_data();
1573 let result = render("{if condition=\"$a eq 2\"}yes{else /}no{/if}", &data);
1574 assert_eq!(result, "no");
1575 }
1576
1577 #[test]
1578 fn test_if_elseif_else() {
1579 let data = make_data();
1580 let result = render(
1581 "{if condition=\"$a eq 2\"}two{elseif condition=\"$a eq 1\" /}one{else /}other{/if}",
1582 &data,
1583 );
1584 assert_eq!(result, "one");
1585 }
1586
1587 #[test]
1588 fn test_if_elseif_else_default() {
1589 let data = make_data();
1590 let result = render(
1591 "{if condition=\"$a eq 5\"}five{elseif condition=\"$a eq 6\" /}six{else /}other{/if}",
1592 &data,
1593 );
1594 assert_eq!(result, "other");
1595 }
1596
1597 #[test]
1598 fn test_if_with_neq() {
1599 let data = make_data();
1600 let result = render("{if condition=\"$a neq 2\"}not_two{/if}", &data);
1601 assert_eq!(result, "not_two");
1602 }
1603
1604 #[test]
1605 fn test_if_with_gt_lt() {
1606 let data = make_data();
1607 assert_eq!(render("{if condition=\"$b gt 1\"}big{/if}", &data), "big");
1608 assert_eq!(
1609 render("{if condition=\"$a lt 2\"}small{/if}", &data),
1610 "small"
1611 );
1612 }
1613
1614 #[test]
1615 fn test_if_with_egt_elt() {
1616 let data = make_data();
1617 assert_eq!(render("{if condition=\"$a egt 1\"}ge1{/if}", &data), "ge1");
1618 assert_eq!(render("{if condition=\"$b elt 2\"}le2{/if}", &data), "le2");
1619 }
1620
1621 #[test]
1622 fn test_if_with_heq_nheq() {
1623 let data = make_data();
1624 assert_eq!(
1625 render("{if condition=\"$a heq 1\"}identical{/if}", &data),
1626 "identical"
1627 );
1628 assert_eq!(
1629 render("{if condition=\"$a nheq 2\"}not_identical{/if}", &data),
1630 "not_identical"
1631 );
1632 }
1633
1634 #[test]
1635 fn test_if_expression_form() {
1636 let data = make_data();
1637 let result = render("{if ($a == 1)}match{/if}", &data);
1638 assert_eq!(result, "match");
1639 }
1640
1641 #[test]
1642 fn test_if_with_and() {
1643 let data = make_data();
1644 let result = render("{if condition=\"$a gt 0 && $b lt 5\"}in_range{/if}", &data);
1645 assert_eq!(result, "in_range");
1646 }
1647
1648 #[test]
1649 fn test_if_with_or() {
1650 let data = make_data();
1651 let result = render("{if condition=\"$a eq 5 || $b eq 2\"}match{/if}", &data);
1652 assert_eq!(result, "match");
1653 }
1654
1655 #[test]
1656 fn test_if_with_not() {
1657 let data = make_data();
1658 let result = render("{if condition=\"!$flag\"}not_flag{/if}", &data);
1659 assert_eq!(result, "");
1660 }
1661
1662 #[test]
1663 fn test_if_with_parens() {
1664 let data = make_data();
1665 let result = render("{if condition=\"($a eq 1) && ($b eq 2)\"}both{/if}", &data);
1666 assert_eq!(result, "both");
1667 }
1668
1669 #[test]
1670 fn test_if_case_insensitive_shorthand() {
1671 let data = make_data();
1672 let result = render("{if condition=\"$a EQ 1\"}yes{/if}", &data);
1674 assert_eq!(result, "yes");
1675 }
1676
1677 #[test]
1682 fn test_foreach_basic() {
1683 let data = make_data();
1684 let result = render(
1685 "{foreach name=\"list\" id=\"item\"}{$item},{/foreach}",
1686 &data,
1687 );
1688 assert_eq!(result, "1,2,3,");
1689 }
1690
1691 #[test]
1692 fn test_foreach_with_key() {
1693 let data = make_data();
1694 let result = render(
1695 "{foreach name=\"list\" id=\"item\" key=\"k\"}{$k}={$item},{/foreach}",
1696 &data,
1697 );
1698 assert_eq!(result, "0=1,1=2,2=3,");
1699 }
1700
1701 #[test]
1702 fn test_foreach_with_index() {
1703 let data = make_data();
1704 let result = render(
1705 "{foreach name=\"list\" id=\"item\" index=\"idx\"}{$idx}:{$item},{/foreach}",
1706 &data,
1707 );
1708 assert_eq!(result, "0:1,1:2,2:3,");
1709 }
1710
1711 #[test]
1712 fn test_foreach_object() {
1713 let data = make_data();
1714 let result = render(
1715 "{foreach name=\"obj\" id=\"v\" key=\"k\"}{$k}={$v},{/foreach}",
1716 &data,
1717 );
1718 assert_eq!(result, "x=10,y=20,");
1719 }
1720
1721 #[test]
1722 fn test_foreach_empty() {
1723 let data = make_data();
1724 let result = render(
1725 "{foreach name=\"empty_list\" id=\"item\" empty=\"no data\"}{$item}{/foreach}",
1726 &data,
1727 );
1728 assert_eq!(result, "no data");
1729 }
1730
1731 #[test]
1732 fn test_foreach_expression_form() {
1733 let data = make_data();
1734 let result = render("{foreach ($list as $k=>$v)}{$k}={$v},{/foreach}", &data);
1735 assert_eq!(result, "0=1,1=2,2=3,");
1736 }
1737
1738 #[test]
1739 fn test_foreach_expression_form_no_key() {
1740 let data = make_data();
1741 let result = render("{foreach ($list as $v)}{$v},{/foreach}", &data);
1742 assert_eq!(result, "1,2,3,");
1743 }
1744
1745 #[test]
1746 fn test_foreach_with_offset() {
1747 let data = make_data();
1748 let result = render(
1749 "{foreach name=\"list\" id=\"item\" offset=\"1\"}{$item},{/foreach}",
1750 &data,
1751 );
1752 assert_eq!(result, "2,3,");
1753 }
1754
1755 #[test]
1756 fn test_foreach_with_length() {
1757 let data = make_data();
1758 let result = render(
1759 "{foreach name=\"list\" id=\"item\" length=\"2\"}{$item},{/foreach}",
1760 &data,
1761 );
1762 assert_eq!(result, "1,2,");
1763 }
1764
1765 #[test]
1770 fn test_volist_basic() {
1771 let data = make_data();
1772 let result = render("{volist name=\"list\" id=\"item\"}{$item},{/volist}", &data);
1773 assert_eq!(result, "1,2,3,");
1774 }
1775
1776 #[test]
1777 fn test_volist_key_is_counter() {
1778 let data = make_data();
1780 let result = render(
1781 "{volist name=\"list\" id=\"item\" key=\"i\"}{$i}:{$item},{/volist}",
1782 &data,
1783 );
1784 assert_eq!(result, "1:1,2:2,3:3,");
1785 }
1786
1787 #[test]
1788 fn test_volist_key_default_is_i() {
1789 let data = make_data();
1790 let result = render(
1791 "{volist name=\"list\" id=\"item\" key=\"counter\"}{$counter}:{$item},{/volist}",
1792 &data,
1793 );
1794 assert_eq!(result, "1:1,2:2,3:3,");
1795 }
1796
1797 #[test]
1798 fn test_volist_array_key_variable() {
1799 let data = make_data();
1801 let result = render(
1802 "{volist name=\"list\" id=\"item\"}key={$key},item={$item};{/volist}",
1803 &data,
1804 );
1805 assert_eq!(result, "key=0,item=1;key=1,item=2;key=2,item=3;");
1806 }
1807
1808 #[test]
1809 fn test_volist_empty() {
1810 let data = make_data();
1811 let result = render(
1812 "{volist name=\"empty_list\" id=\"item\" empty=\"no data\"}{$item}{/volist}",
1813 &data,
1814 );
1815 assert_eq!(result, "no data");
1816 }
1817
1818 #[test]
1819 fn test_volist_with_offset() {
1820 let data = make_data();
1821 let result = render(
1822 "{volist name=\"list\" id=\"item\" offset=\"1\"}{$item},{/volist}",
1823 &data,
1824 );
1825 assert_eq!(result, "2,3,");
1826 }
1827
1828 #[test]
1829 fn test_volist_with_length() {
1830 let data = make_data();
1831 let result = render(
1832 "{volist name=\"list\" id=\"item\" length=\"2\"}{$item},{/volist}",
1833 &data,
1834 );
1835 assert_eq!(result, "1,2,");
1836 }
1837
1838 #[test]
1843 fn test_switch_case_match() {
1844 let data = make_data();
1845 let result = render(
1846 "{switch name=\"var\"}{case value=\"1\"}one{/case}{case value=\"2\"}two{/case}{default /}other{/switch}",
1847 &data,
1848 );
1849 assert_eq!(result, "two");
1850 }
1851
1852 #[test]
1853 fn test_switch_default() {
1854 let data = make_data();
1855 let result = render(
1856 "{switch name=\"var\"}{case value=\"5\"}five{/case}{default /}other{/switch}",
1857 &data,
1858 );
1859 assert_eq!(result, "other");
1860 }
1861
1862 #[test]
1863 fn test_switch_case_multi_value() {
1864 let data = make_data();
1865 let result = render(
1866 "{switch name=\"var\"}{case value=\"1|2|3\"}low{/case}{default /}high{/switch}",
1867 &data,
1868 );
1869 assert_eq!(result, "low");
1870 }
1871
1872 #[test]
1873 fn test_switch_case_no_close_tag() {
1874 let data = make_data();
1876 let result = render(
1877 "{switch name=\"var\"}{case value=\"1\"}one{case value=\"2\"}two{default /}other{/switch}",
1878 &data,
1879 );
1880 assert_eq!(result, "two");
1881 }
1882
1883 #[test]
1884 fn test_switch_case_break_false_still_breaks() {
1885 let data = make_data();
1887 let result = render(
1888 "{switch name=\"var\"}{case value=\"2\" break=\"false\"}two{/case}{case value=\"2\"}also_two{/case}{default /}other{/switch}",
1889 &data,
1890 );
1891 assert_eq!(result, "two");
1892 }
1893
1894 #[test]
1895 fn test_switch_case_break_zero_fall_through() {
1896 let data = make_data();
1898 let result = render(
1899 "{switch name=\"var\"}{case value=\"2\" break=\"0\"}two_{/case}{case value=\"2\"}also_two{/case}{default /}other{/switch}",
1900 &data,
1901 );
1902 assert_eq!(result, "two_also_two");
1903 }
1904
1905 #[test]
1910 fn test_for_basic() {
1911 let data = make_data();
1912 let result = render("{for start=\"0\" end=\"3\" name=\"i\"}{$i},{/for}", &data);
1913 assert_eq!(result, "0,1,2,");
1914 }
1915
1916 #[test]
1917 fn test_for_with_step() {
1918 let data = make_data();
1919 let result = render(
1920 "{for start=\"0\" end=\"10\" step=\"3\" name=\"i\"}{$i},{/for}",
1921 &data,
1922 );
1923 assert_eq!(result, "0,3,6,9,");
1924 }
1925
1926 #[test]
1927 fn test_for_default_name() {
1928 let data = make_data();
1929 let result = render("{for start=\"1\" end=\"3\"}{$i},{/for}", &data);
1930 assert_eq!(result, "1,2,");
1931 }
1932
1933 #[test]
1934 fn test_for_comparison_elt() {
1935 let data = make_data();
1936 let result = render(
1937 "{for start=\"0\" end=\"3\" comparison=\"elt\" name=\"i\"}{$i},{/for}",
1938 &data,
1939 );
1940 assert_eq!(result, "0,1,2,3,");
1941 }
1942
1943 #[test]
1944 fn test_for_comparison_gt() {
1945 let data = make_data();
1946 let result = render(
1947 "{for start=\"5\" end=\"0\" step=\"-1\" comparison=\"gt\" name=\"i\"}{$i},{/for}",
1948 &data,
1949 );
1950 assert_eq!(result, "5,4,3,2,1,");
1951 }
1952
1953 #[test]
1958 fn test_nested_if_in_foreach() {
1959 let data = make_data();
1960 let result = render(
1961 "{foreach name=\"list\" id=\"item\"}{if condition=\"$item gt 1\"}big{else /}small{/if},{/foreach}",
1962 &data,
1963 );
1964 assert_eq!(result, "small,big,big,");
1965 }
1966
1967 #[test]
1968 fn test_nested_foreach() {
1969 let mut data = make_data();
1970 data.insert("matrix".to_string(), json!([[1, 2], [3, 4]]));
1971 let result = render(
1972 "{foreach name=\"matrix\" id=\"row\"}{foreach name=\"row\" id=\"col\"}{$col} {/foreach}| {/foreach}",
1973 &data,
1974 );
1975 assert_eq!(result, "1 2 | 3 4 | ");
1976 }
1977
1978 #[test]
1979 fn test_nested_if() {
1980 let data = make_data();
1981 let result = render(
1982 "{if condition=\"$a eq 1\"}{if condition=\"$b eq 2\"}both{/if}{/if}",
1983 &data,
1984 );
1985 assert_eq!(result, "both");
1986 }
1987
1988 #[test]
1989 fn test_switch_in_foreach() {
1990 let data = make_data();
1991 let result = render(
1992 "{foreach name=\"list\" id=\"item\"}{switch name=\"item\"}{case value=\"1\"}one{/case}{case value=\"2\"}two{/case}{default /}other{/switch},{/foreach}",
1993 &data,
1994 );
1995 assert_eq!(result, "one,two,other,");
1996 }
1997
1998 #[test]
2003 fn test_evaluate_condition_eq() {
2004 let data = make_data();
2005 assert!(evaluate_condition("$a eq 1", &data).unwrap());
2006 assert!(!evaluate_condition("$a eq 2", &data).unwrap());
2007 }
2008
2009 #[test]
2010 fn test_evaluate_condition_neq() {
2011 let data = make_data();
2012 assert!(evaluate_condition("$a neq 2", &data).unwrap());
2013 }
2014
2015 #[test]
2016 fn test_evaluate_condition_and() {
2017 let data = make_data();
2018 assert!(evaluate_condition("$a eq 1 && $b eq 2", &data).unwrap());
2019 assert!(!evaluate_condition("$a eq 1 && $b eq 3", &data).unwrap());
2020 }
2021
2022 #[test]
2023 fn test_evaluate_condition_or() {
2024 let data = make_data();
2025 assert!(evaluate_condition("$a eq 5 || $b eq 2", &data).unwrap());
2026 assert!(!evaluate_condition("$a eq 5 || $b eq 5", &data).unwrap());
2027 }
2028
2029 #[test]
2030 fn test_evaluate_condition_not() {
2031 let data = make_data();
2032 assert!(!evaluate_condition("!$flag", &data).unwrap());
2033 assert!(!evaluate_condition("!$a", &data).unwrap()); }
2035
2036 #[test]
2037 fn test_evaluate_condition_parens() {
2038 let data = make_data();
2039 assert!(evaluate_condition("($a eq 1) && ($b eq 2)", &data).unwrap());
2040 }
2041
2042 #[test]
2043 fn test_evaluate_condition_truthy_var() {
2044 let data = make_data();
2045 assert!(evaluate_condition("$flag", &data).unwrap());
2046 assert!(evaluate_condition("$name", &data).unwrap());
2047 }
2048
2049 #[test]
2050 fn test_evaluate_condition_strict_equal() {
2051 let data = make_data();
2052 assert!(evaluate_condition("$a heq 1", &data).unwrap());
2053 assert!(!evaluate_condition("$a heq '1'", &data).unwrap());
2054 }
2055
2056 #[test]
2061 fn test_replace_shorthands_basic() {
2062 assert_eq!(replace_condition_shorthands("$a eq 1"), "$a == 1");
2063 assert_eq!(replace_condition_shorthands("$a neq 1"), "$a != 1");
2064 assert_eq!(replace_condition_shorthands("$a gt 1"), "$a > 1");
2065 assert_eq!(replace_condition_shorthands("$a lt 1"), "$a < 1");
2066 assert_eq!(replace_condition_shorthands("$a egt 1"), "$a >= 1");
2067 assert_eq!(replace_condition_shorthands("$a elt 1"), "$a <= 1");
2068 assert_eq!(replace_condition_shorthands("$a heq 1"), "$a === 1");
2069 assert_eq!(replace_condition_shorthands("$a nheq 1"), "$a !== 1");
2070 }
2071
2072 #[test]
2073 fn test_replace_shorthands_case_insensitive() {
2074 assert_eq!(replace_condition_shorthands("$a EQ 1"), "$a == 1");
2075 assert_eq!(replace_condition_shorthands("$a NEQ 1"), "$a != 1");
2076 }
2077
2078 #[test]
2079 fn test_replace_shorthands_multiple() {
2080 assert_eq!(
2081 replace_condition_shorthands("$a eq 1 && $b neq 2"),
2082 "$a == 1 && $b != 2"
2083 );
2084 }
2085
2086 #[test]
2087 fn test_parse_attributes() {
2088 let attrs = parse_attributes("name=\"list\" id=\"item\" key=\"k\"");
2089 assert_eq!(attrs.get("name"), Some(&"list".to_string()));
2090 assert_eq!(attrs.get("id"), Some(&"item".to_string()));
2091 assert_eq!(attrs.get("key"), Some(&"k".to_string()));
2092 }
2093
2094 #[test]
2095 fn test_parse_attributes_single_quote() {
2096 let attrs = parse_attributes("name='list' id='item'");
2097 assert_eq!(attrs.get("name"), Some(&"list".to_string()));
2098 assert_eq!(attrs.get("id"), Some(&"item".to_string()));
2099 }
2100
2101 #[test]
2102 fn test_parse_attributes_empty() {
2103 let attrs = parse_attributes("");
2104 assert!(attrs.is_empty());
2105 }
2106
2107 #[test]
2108 fn test_loose_equal_numbers() {
2109 assert!(loose_equal(&json!(1), &json!(1)));
2110 assert!(!loose_equal(&json!(1), &json!(2)));
2111 }
2112
2113 #[test]
2114 fn test_loose_equal_number_string() {
2115 assert!(loose_equal(&json!(1), &json!("1")));
2116 assert!(loose_equal(&json!("1"), &json!(1)));
2117 }
2118
2119 #[test]
2120 fn test_loose_equal_strings() {
2121 assert!(loose_equal(&json!("hello"), &json!("hello")));
2122 assert!(!loose_equal(&json!("hello"), &json!("world")));
2123 }
2124
2125 #[test]
2126 fn test_strict_equal() {
2127 assert!(strict_equal(&json!(1), &json!(1)));
2128 assert!(!strict_equal(&json!(1), &json!("1")));
2129 assert!(!strict_equal(&json!(1), &json!(true)));
2130 }
2131
2132 #[test]
2133 fn test_split_top_level_and() {
2134 let parts = split_top_level("$a eq 1 && $b eq 2", "&&");
2135 assert_eq!(parts.len(), 2);
2136 assert_eq!(parts[0].trim(), "$a eq 1");
2137 assert_eq!(parts[1].trim(), "$b eq 2");
2138 }
2139
2140 #[test]
2141 fn test_split_top_level_or() {
2142 let parts = split_top_level("$a eq 1 || $b eq 2", "||");
2143 assert_eq!(parts.len(), 2);
2144 }
2145
2146 #[test]
2147 fn test_split_top_level_with_parens() {
2148 let parts = split_top_level("($a eq 1 || $b eq 2) && $c eq 3", "&&");
2149 assert_eq!(parts.len(), 2);
2150 assert_eq!(parts[0].trim(), "($a eq 1 || $b eq 2)");
2151 assert_eq!(parts[1].trim(), "$c eq 3");
2152 }
2153
2154 #[test]
2155 fn test_find_comparison_operator() {
2156 assert_eq!(find_comparison_operator("$a == 1"), Some(("==", 3)));
2157 assert_eq!(find_comparison_operator("$a === 1"), Some(("===", 3)));
2158 assert_eq!(find_comparison_operator("$a >= 1"), Some((">=", 3)));
2159 }
2160
2161 #[test]
2162 fn test_find_comparison_operator_in_parens() {
2163 assert!(find_comparison_operator("($a == 1)").is_none());
2165 }
2166
2167 #[test]
2172 fn test_should_break_default() {
2173 let case = SwitchCase {
2174 kind: CaseKind::Default,
2175 content: String::new(),
2176 break_attr: None,
2177 };
2178 assert!(case.should_break()); }
2180
2181 #[test]
2182 fn test_should_break_false_string() {
2183 let case = SwitchCase {
2185 kind: CaseKind::Values(vec!["1".to_string()]),
2186 content: String::new(),
2187 break_attr: Some("false".to_string()),
2188 };
2189 assert!(case.should_break()); }
2191
2192 #[test]
2193 fn test_should_break_zero_string() {
2194 let case = SwitchCase {
2196 kind: CaseKind::Values(vec!["1".to_string()]),
2197 content: String::new(),
2198 break_attr: Some("0".to_string()),
2199 };
2200 assert!(!case.should_break()); }
2202
2203 #[test]
2204 fn test_should_break_empty_string() {
2205 let case = SwitchCase {
2207 kind: CaseKind::Values(vec!["1".to_string()]),
2208 content: String::new(),
2209 break_attr: Some("".to_string()),
2210 };
2211 assert!(!case.should_break()); }
2215
2216 #[test]
2221 fn test_no_control_flow_tags() {
2222 let data = make_data();
2223 let result = render("hello world", &data);
2224 assert_eq!(result, "hello world");
2225 }
2226
2227 #[test]
2228 fn test_control_flow_with_static_text() {
2229 let data = make_data();
2230 let result = render("before {if condition=\"$a eq 1\"}mid{/if} after", &data);
2231 assert_eq!(result, "before mid after");
2232 }
2233
2234 #[test]
2235 fn test_multiple_control_flow_blocks() {
2236 let data = make_data();
2237 let result = render(
2238 "{if condition=\"$a eq 1\"}A{/if}{if condition=\"$b eq 2\"}B{/if}",
2239 &data,
2240 );
2241 assert_eq!(result, "AB");
2242 }
2243
2244 #[test]
2245 fn test_for_zero_iterations() {
2246 let data = make_data();
2247 let result = render("{for start=\"5\" end=\"3\" name=\"i\"}{$i}{/for}", &data);
2248 assert_eq!(result, "");
2249 }
2250
2251 #[test]
2252 fn test_foreach_empty_no_empty_attr() {
2253 let data = make_data();
2254 let result = render(
2255 "{foreach name=\"empty_list\" id=\"item\"}{$item}{/foreach}",
2256 &data,
2257 );
2258 assert_eq!(result, "");
2259 }
2260}