1use std::collections::HashMap;
44use std::path::PathBuf;
45use std::sync::Arc;
46
47use parking_lot::RwLock;
48use regex::Regex;
49use serde_json::Value;
50
51pub mod template;
53
54pub mod layout;
56
57pub mod inheritance;
59
60#[derive(Debug, thiserror::Error)]
66pub enum ViewError {
67 #[error("模板文件未找到: {0}")]
69 TemplateNotFound(String),
70
71 #[error("模板语法错误: {0}")]
73 SyntaxError(String),
74
75 #[error("模板渲染错误: {0}")]
77 RenderError(String),
78
79 #[error("IO 错误: {0}")]
81 IoError(#[from] std::io::Error),
82}
83
84pub type ViewData = HashMap<String, Value>;
90
91pub type ContentFilter = Arc<dyn Fn(&str) -> String + Send + Sync>;
93
94pub type TemplateFn = Arc<dyn Fn(&[Value]) -> Result<Value, ViewError> + Send + Sync>;
96
97#[derive(Debug, Clone)]
103pub struct ViewConfig {
104 pub view_path: PathBuf,
106
107 pub view_suffix: String,
109
110 pub view_depr: String,
112
113 pub tpl_begin: String,
115
116 pub tpl_end: String,
118
119 pub taglib_begin: String,
121
122 pub taglib_end: String,
124
125 pub default_filter: String,
127
128 pub layout_on: bool,
130
131 pub layout_name: String,
133
134 pub layout_item: String,
136
137 pub tpl_var_identify: String,
139}
140
141impl Default for ViewConfig {
142 fn default() -> Self {
143 Self {
144 view_path: PathBuf::from("view"),
145 view_suffix: "html".to_string(),
146 view_depr: "/".to_string(),
147 tpl_begin: "{".to_string(),
148 tpl_end: "}".to_string(),
149 taglib_begin: "{".to_string(),
150 taglib_end: "}".to_string(),
151 default_filter: "htmlentities".to_string(),
152 layout_on: false,
153 layout_name: "layout".to_string(),
154 layout_item: "{__CONTENT__}".to_string(),
155 tpl_var_identify: "array".to_string(),
156 }
157 }
158}
159
160pub trait TemplateEngine: Send + Sync {
173 fn exists(&self, template: &str) -> bool;
175
176 fn fetch(&self, template: &str, data: &ViewData) -> Result<String, ViewError>;
178
179 fn display(&self, content: &str, data: &ViewData) -> Result<String, ViewError>;
181
182 fn set_config(&mut self, config: ViewConfig);
184
185 fn get_config(&self, name: &str) -> Option<Value>;
187
188 fn as_any(&self) -> &dyn std::any::Any;
190}
191
192pub struct SimpleTemplateEngine {
214 config: RwLock<ViewConfig>,
215 functions: RwLock<HashMap<String, TemplateFn>>,
216}
217
218impl SimpleTemplateEngine {
219 pub fn new(config: ViewConfig) -> Self {
221 let mut functions = HashMap::new();
222 register_builtin_functions(&mut functions);
223 Self {
224 config: RwLock::new(config),
225 functions: RwLock::new(functions),
226 }
227 }
228
229 pub fn register_function(&self, name: &str, func: TemplateFn) {
231 self.functions.write().insert(name.to_string(), func);
232 }
233
234 pub fn parse_template_path(&self, template: &str) -> PathBuf {
241 let config = self.config.read();
242 let view_path = &config.view_path;
243 let suffix = &config.view_suffix;
244
245 if template.is_empty() {
246 return view_path.join(format!("index.{}", suffix));
247 }
248
249 if let Some(stripped) = template.strip_prefix('/') {
251 let mut path = PathBuf::from(stripped);
252 if path.extension().is_none() {
253 path = path.with_extension(suffix);
254 }
255 return path;
256 }
257
258 if let Some(at_pos) = template.find('@') {
260 let app = &template[..at_pos];
261 let tpl = &template[at_pos + 1..];
262 let mut path = PathBuf::from(app);
263 path.push("view");
264 path.push(tpl);
265 if path.extension().is_none() {
266 path = path.with_extension(suffix);
267 }
268 return path;
269 }
270
271 let mut path = view_path.join(template);
273 if path.extension().is_none() {
274 path = path.with_extension(suffix);
275 }
276 path
277 }
278
279 fn render_content(&self, content: &str, data: &ViewData) -> Result<String, ViewError> {
287 let (content, literals) = self.extract_literals(content);
289
290 let config = self.config.read().clone();
292 let content = template::render_control_flow(&content, data, &config, |c, d| {
293 self.render_content(c, d)
294 })?;
295
296 let content = self.parse_tags(&content, data)?;
298
299 let content = self.restore_literals(&content, &literals);
301
302 Ok(content)
303 }
304
305 fn extract_literals(&self, content: &str) -> (String, Vec<String>) {
307 let config = self.config.read();
308 let begin = &config.tpl_begin;
309 let end = &config.tpl_end;
310 let literal_open = format!("{}literal{}", begin, end);
311 let literal_close = format!("{}/literal{}", begin, end);
312
313 let mut result = String::with_capacity(content.len());
314 let mut literals = Vec::new();
315 let mut remaining = content;
316
317 loop {
318 if let Some(open_pos) = remaining.find(&literal_open) {
319 result.push_str(&remaining[..open_pos]);
320 let after_open = &remaining[open_pos + literal_open.len()..];
321 if let Some(close_pos) = after_open.find(&literal_close) {
322 let literal_content = &after_open[..close_pos];
323 let placeholder = format!("<!--###LITERAL{}###-->", literals.len());
324 literals.push(literal_content.to_string());
325 result.push_str(&placeholder);
326 remaining = &after_open[close_pos + literal_close.len()..];
327 } else {
328 result.push_str(&remaining[open_pos..]);
330 break;
331 }
332 } else {
333 result.push_str(remaining);
334 break;
335 }
336 }
337
338 (result, literals)
339 }
340
341 fn restore_literals(&self, content: &str, literals: &[String]) -> String {
343 let mut result = content.to_string();
344 for (i, literal) in literals.iter().enumerate() {
345 let placeholder = format!("<!--###LITERAL{}###-->", i);
346 result = result.replace(&placeholder, literal);
347 }
348 result
349 }
350
351 fn parse_tags(&self, content: &str, data: &ViewData) -> Result<String, ViewError> {
360 let config = self.config.read();
361 let begin = regex::escape(&config.tpl_begin);
362 let end = regex::escape(&config.tpl_end);
363
364 let pattern = format!("{}(.*?){}", begin, end);
367 let re = Regex::new(&pattern).map_err(|e| ViewError::SyntaxError(e.to_string()))?;
368
369 let mut result = String::with_capacity(content.len());
370 let mut last_end = 0;
371
372 for caps in re.captures_iter(content) {
373 let full_match = caps.get(0).expect("正则捕获组 0 必定存在");
374 let tag_content = caps.get(1).expect("正则捕获组 1 必定存在").as_str();
375
376 result.push_str(&content[last_end..full_match.start()]);
377
378 let rendered = self.render_tag(tag_content, data)?;
379 result.push_str(&rendered);
380
381 last_end = full_match.end();
382 }
383 result.push_str(&content[last_end..]);
384
385 Ok(result)
386 }
387
388 fn render_tag(&self, tag: &str, data: &ViewData) -> Result<String, ViewError> {
390 let tag = tag.trim();
391
392 if tag.is_empty() {
393 return Ok(String::new());
394 }
395
396 let first_char = tag.chars().next().expect("已检查 tag 非空");
398
399 match first_char {
400 '$' => self.render_var_tag(tag, data),
401 ':' => self.render_func_tag(tag, data, false),
402 '~' => self.render_func_tag(tag, data, true),
403 '/' => {
404 Ok(String::new())
406 }
407 _ => {
408 let config = self.config.read();
410 Ok(format!("{}{}{}", config.tpl_begin, tag, config.tpl_end))
411 }
412 }
413 }
414
415 fn render_var_tag(&self, tag: &str, data: &ViewData) -> Result<String, ViewError> {
426 let expr = &tag[1..];
428
429 let (var_expr, filters, ternary) = self.split_var_expr(expr);
432
433 let value = self.resolve_var(&var_expr, data);
435
436 let value = if let Some(ternary_expr) = &ternary {
438 self.apply_ternary(&value, ternary_expr)?
439 } else {
440 value
441 };
442
443 let value = self.apply_filters(value, &filters)?;
445
446 Ok(value_to_string(&value))
448 }
449
450 fn split_var_expr(&self, expr: &str) -> (String, Vec<String>, Option<String>) {
456 if let Some(pos) = expr.find("??") {
459 let var = expr[..pos].trim().to_string();
460 let ternary = expr[pos..].trim().to_string();
461 return (var, Vec::new(), Some(ternary));
462 }
463
464 let parts: Vec<&str> = expr.split('|').collect();
468 let var_expr = parts[0].trim().to_string();
469
470 if let Some(pos) = var_expr.find('?') {
472 let var = var_expr[..pos].trim().to_string();
473 let ternary = var_expr[pos..].trim().to_string();
474 let filters: Vec<String> = parts[1..]
475 .iter()
476 .map(|s| s.trim().to_string())
477 .filter(|s| !s.is_empty())
478 .collect();
479 return (var, filters, Some(ternary));
480 }
481
482 let filters: Vec<String> = parts[1..]
483 .iter()
484 .map(|s| s.trim().to_string())
485 .filter(|s| !s.is_empty())
486 .collect();
487
488 (var_expr, filters, None)
489 }
490
491 fn resolve_var(&self, expr: &str, data: &ViewData) -> Value {
498 resolve_var_expr(expr, data)
499 }
500
501 fn apply_ternary(&self, value: &Value, ternary: &str) -> Result<Value, ViewError> {
509 if let Some(default) = ternary.strip_prefix("??") {
511 if value.is_null() {
512 return Ok(parse_literal(default.trim()));
513 }
514 return Ok(value.clone());
515 }
516
517 if let Some(default) = ternary.strip_prefix("?:") {
519 if is_truthy(value) {
520 return Ok(value.clone());
521 }
522 return Ok(parse_literal(default.trim()));
523 }
524
525 if let Some(output) = ternary.strip_prefix("?=") {
527 if is_truthy(value) {
528 return Ok(parse_literal(output.trim()));
529 }
530 return Ok(Value::Null);
531 }
532
533 if let Some(rest) = ternary.strip_prefix('?') {
535 if let Some(colon_pos) = rest.find(':') {
536 let true_val = rest[..colon_pos].trim();
537 let false_val = rest[colon_pos + 1..].trim();
538 if is_truthy(value) {
539 return Ok(parse_literal(true_val));
540 }
541 return Ok(parse_literal(false_val));
542 }
543 if is_truthy(value) {
545 return Ok(parse_literal(rest.trim()));
546 }
547 return Ok(Value::Null);
548 }
549
550 Ok(value.clone())
551 }
552
553 fn apply_filters(&self, mut value: Value, filters: &[String]) -> Result<Value, ViewError> {
562 let config = self.config.read();
563 let default_filter = &config.default_filter;
564
565 let has_raw = filters.iter().any(|f| f.starts_with("raw"));
567 if !has_raw && !default_filter.is_empty() && default_filter != "raw" {
568 value = apply_builtin_filter(value, default_filter, None)?;
569 }
570
571 for filter in filters {
573 if filter.starts_with("raw") {
574 continue;
575 }
576
577 let (filter_name, filter_arg) = if let Some(eq_pos) = filter.find('=') {
579 (&filter[..eq_pos], Some(filter[eq_pos + 1..].to_string()))
580 } else if let Some(paren_pos) = filter.find('(') {
581 (
582 &filter[..paren_pos],
583 Some(filter[paren_pos + 1..].trim_end_matches(')').to_string()),
584 )
585 } else {
586 (filter.as_str(), None)
587 };
588
589 value = apply_builtin_filter(value, filter_name.trim(), filter_arg)?;
590 }
591
592 Ok(value)
593 }
594
595 fn render_func_tag(
601 &self,
602 tag: &str,
603 _data: &ViewData,
604 suppress_output: bool,
605 ) -> Result<String, ViewError> {
606 let expr = &tag[1..];
608
609 let (func_name, args) = parse_func_call(expr)?;
611
612 let functions = self.functions.read();
614 let func = functions
615 .get(&func_name)
616 .ok_or_else(|| ViewError::RenderError(format!("未注册的模板函数: {}", func_name)))?;
617
618 let result = func(&args)?;
620
621 if suppress_output {
623 return Ok(String::new());
624 }
625
626 Ok(value_to_string(&result))
627 }
628}
629
630impl TemplateEngine for SimpleTemplateEngine {
631 fn exists(&self, template: &str) -> bool {
632 let path = self.parse_template_path(template);
633 path.is_file()
634 }
635
636 fn fetch(&self, template: &str, data: &ViewData) -> Result<String, ViewError> {
637 let path = self.parse_template_path(template);
638
639 if !path.is_file() {
640 return Err(ViewError::TemplateNotFound(format!(
641 "{} (解析路径: {})",
642 template,
643 path.display()
644 )));
645 }
646
647 let content = std::fs::read_to_string(&path)?;
648 let config = self.config.read().clone();
649 let content = inheritance::apply_inheritance(&content, &config)?;
652 let content = layout::apply_layout(&content, &config)?;
654 self.render_content(&content, data)
655 }
656
657 fn display(&self, content: &str, data: &ViewData) -> Result<String, ViewError> {
658 let config = self.config.read().clone();
659 let content = inheritance::apply_inheritance(content, &config)?;
662 let content = layout::apply_layout(&content, &config)?;
664 self.render_content(&content, data)
665 }
666
667 fn set_config(&mut self, config: ViewConfig) {
668 *self.config.write() = config;
669 }
670
671 fn get_config(&self, name: &str) -> Option<Value> {
672 let config = self.config.read();
673 match name {
674 "view_path" => Some(Value::String(config.view_path.to_string_lossy().into())),
675 "view_suffix" => Some(Value::String(config.view_suffix.clone())),
676 "view_depr" => Some(Value::String(config.view_depr.clone())),
677 "tpl_begin" => Some(Value::String(config.tpl_begin.clone())),
678 "tpl_end" => Some(Value::String(config.tpl_end.clone())),
679 "taglib_begin" => Some(Value::String(config.taglib_begin.clone())),
680 "taglib_end" => Some(Value::String(config.taglib_end.clone())),
681 "default_filter" => Some(Value::String(config.default_filter.clone())),
682 "layout_on" => Some(Value::Bool(config.layout_on)),
683 "layout_name" => Some(Value::String(config.layout_name.clone())),
684 "layout_item" => Some(Value::String(config.layout_item.clone())),
685 "tpl_var_identify" => Some(Value::String(config.tpl_var_identify.clone())),
686 _ => None,
687 }
688 }
689
690 fn as_any(&self) -> &dyn std::any::Any {
691 self
692 }
693}
694
695pub struct View {
717 data: RwLock<ViewData>,
719
720 filter: RwLock<Option<ContentFilter>>,
722
723 engine: RwLock<Box<dyn TemplateEngine>>,
725}
726
727impl View {
728 pub fn new(engine: Box<dyn TemplateEngine>) -> Self {
730 Self {
731 data: RwLock::new(HashMap::new()),
732 filter: RwLock::new(None),
733 engine: RwLock::new(engine),
734 }
735 }
736
737 pub fn with_default_engine() -> Self {
739 Self::new(Box::new(SimpleTemplateEngine::new(ViewConfig::default())))
740 }
741
742 pub fn with_config(config: ViewConfig) -> Self {
744 Self::new(Box::new(SimpleTemplateEngine::new(config)))
745 }
746
747 pub fn assign(&self, name: &str, value: Value) -> &Self {
751 self.data.write().insert(name.to_string(), value);
752 self
753 }
754
755 pub fn assign_many(&self, vars: ViewData) -> &Self {
757 self.data.write().extend(vars);
758 self
759 }
760
761 pub fn set_filter(&self, filter: ContentFilter) -> &Self {
765 *self.filter.write() = Some(filter);
766 self
767 }
768
769 pub fn clear_filter(&self) -> &Self {
771 *self.filter.write() = None;
772 self
773 }
774
775 pub fn fetch(&self, template: &str, vars: Option<ViewData>) -> Result<String, ViewError> {
781 let mut data = self.data.read().clone();
782 if let Some(vars) = vars {
783 data.extend(vars);
784 }
785
786 let content = self.engine.read().fetch(template, &data)?;
787 self.apply_filter(content)
788 }
789
790 pub fn display(&self, content: &str, vars: Option<ViewData>) -> Result<String, ViewError> {
794 let mut data = self.data.read().clone();
795 if let Some(vars) = vars {
796 data.extend(vars);
797 }
798
799 let rendered = self.engine.read().display(content, &data)?;
800 self.apply_filter(rendered)
801 }
802
803 pub fn exists(&self, template: &str) -> bool {
805 self.engine.read().exists(template)
806 }
807
808 pub fn get_var(&self, name: &str) -> Option<Value> {
810 self.data.read().get(name).cloned()
811 }
812
813 pub fn has_var(&self, name: &str) -> bool {
815 self.data.read().contains_key(name)
816 }
817
818 pub fn clear_vars(&self) -> &Self {
820 self.data.write().clear();
821 self
822 }
823
824 pub fn engine(&self) -> parking_lot::RwLockReadGuard<'_, Box<dyn TemplateEngine>> {
826 self.engine.read()
827 }
828
829 pub fn set_engine(&self, engine: Box<dyn TemplateEngine>) -> &Self {
831 *self.engine.write() = engine;
832 self
833 }
834
835 fn apply_filter(&self, content: String) -> Result<String, ViewError> {
837 if let Some(filter) = self.filter.read().as_ref() {
838 Ok(filter(&content))
839 } else {
840 Ok(content)
841 }
842 }
843}
844
845pub(super) fn resolve_var_expr(expr: &str, data: &ViewData) -> Value {
853 let parts: Vec<&str> = expr.split('.').collect();
854 let mut current = data.get(parts[0]).cloned().unwrap_or(Value::Null);
855
856 for part in &parts[1..] {
857 current = match ¤t {
858 Value::Object(map) => map.get(*part).cloned().unwrap_or(Value::Null),
859 Value::Array(arr) => {
860 if let Ok(idx) = part.parse::<usize>() {
862 arr.get(idx).cloned().unwrap_or(Value::Null)
863 } else {
864 Value::Null
865 }
866 }
867 _ => Value::Null,
868 };
869 }
870
871 current
872}
873
874fn htmlentities(s: &str) -> String {
876 let mut result = String::with_capacity(s.len());
877 for c in s.chars() {
878 match c {
879 '&' => result.push_str("&"),
880 '<' => result.push_str("<"),
881 '>' => result.push_str(">"),
882 '"' => result.push_str("""),
883 '\'' => result.push_str("'"),
884 _ => result.push(c),
885 }
886 }
887 result
888}
889
890pub(super) fn is_truthy(value: &Value) -> bool {
896 match value {
897 Value::Null => false,
898 Value::Bool(b) => *b,
899 Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
900 Value::String(s) => !s.is_empty() && s != "0",
901 Value::Array(a) => !a.is_empty(),
902 Value::Object(o) => !o.is_empty(),
903 }
904}
905
906pub(super) fn value_to_string(value: &Value) -> String {
908 match value {
909 Value::Null => String::new(),
910 Value::Bool(b) => if *b { "1" } else { "" }.to_string(),
911 Value::Number(n) => {
912 if let Some(i) = n.as_i64() {
913 i.to_string()
914 } else if let Some(f) = n.as_f64() {
915 if f == f.trunc() {
916 format!("{}", f as i64)
917 } else {
918 format!("{}", f)
919 }
920 } else {
921 n.to_string()
922 }
923 }
924 Value::String(s) => s.clone(),
925 Value::Array(a) => serde_json::to_string(a).unwrap_or_default(),
926 Value::Object(o) => serde_json::to_string(o).unwrap_or_default(),
927 }
928}
929
930pub(super) fn parse_literal(s: &str) -> Value {
932 let s = s.trim();
933
934 if (s.starts_with('\'') && s.ends_with('\'') && s.len() >= 2)
936 || (s.starts_with('"') && s.ends_with('"') && s.len() >= 2)
937 {
938 return Value::String(s[1..s.len() - 1].to_string());
939 }
940
941 if let Ok(i) = s.parse::<i64>() {
943 return Value::Number(i.into());
944 }
945 if let Ok(f) = s.parse::<f64>() {
946 if let Some(n) = serde_json::Number::from_f64(f) {
947 return Value::Number(n);
948 }
949 }
950
951 match s {
953 "true" => return Value::Bool(true),
954 "false" => return Value::Bool(false),
955 "null" => return Value::Null,
956 _ => {}
957 }
958
959 Value::String(s.to_string())
961}
962
963fn parse_func_call(expr: &str) -> Result<(String, Vec<Value>), ViewError> {
967 let expr = expr.trim();
968
969 if let Some(paren_pos) = expr.find('(') {
970 let func_name = expr[..paren_pos].trim().to_string();
971 let args_str = expr[paren_pos + 1..].trim_end_matches(')');
972
973 let mut args = Vec::new();
974 if !args_str.trim().is_empty() {
975 for arg in split_args(args_str) {
976 args.push(parse_literal(arg.trim()));
977 }
978 }
979
980 Ok((func_name, args))
981 } else {
982 Ok((expr.to_string(), Vec::new()))
984 }
985}
986
987fn split_args(s: &str) -> Vec<String> {
989 let mut args = Vec::new();
990 let mut current = String::new();
991 let mut in_single_quote = false;
992 let mut in_double_quote = false;
993
994 for c in s.chars() {
995 match c {
996 '\'' if !in_double_quote => {
997 in_single_quote = !in_single_quote;
998 current.push(c);
999 }
1000 '"' if !in_single_quote => {
1001 in_double_quote = !in_double_quote;
1002 current.push(c);
1003 }
1004 ',' if !in_single_quote && !in_double_quote => {
1005 args.push(current.trim().to_string());
1006 current.clear();
1007 }
1008 _ => current.push(c),
1009 }
1010 }
1011
1012 if !current.trim().is_empty() {
1013 args.push(current.trim().to_string());
1014 }
1015
1016 args
1017}
1018
1019fn apply_builtin_filter(
1021 value: Value,
1022 filter_name: &str,
1023 arg: Option<String>,
1024) -> Result<Value, ViewError> {
1025 match filter_name {
1026 "raw" => Ok(value),
1027 "htmlentities" | "htmlspecialchars" => {
1028 Ok(Value::String(htmlentities(&value_to_string(&value))))
1029 }
1030 "upper" | "strtoupper" => Ok(Value::String(value_to_string(&value).to_uppercase())),
1031 "lower" | "strtolower" => Ok(Value::String(value_to_string(&value).to_lowercase())),
1032 "default" => {
1033 if is_truthy(&value) {
1034 Ok(value)
1035 } else {
1036 let default_val = arg.unwrap_or_default();
1037 Ok(parse_literal(&default_val))
1038 }
1039 }
1040 "first" => {
1041 if let Value::Array(arr) = &value {
1042 Ok(arr.first().cloned().unwrap_or(Value::Null))
1043 } else {
1044 Ok(Value::Null)
1045 }
1046 }
1047 "last" => {
1048 if let Value::Array(arr) = &value {
1049 Ok(arr.last().cloned().unwrap_or(Value::Null))
1050 } else {
1051 Ok(Value::Null)
1052 }
1053 }
1054 _ => Err(ViewError::RenderError(format!(
1055 "未知的模板过滤器: {}",
1056 filter_name
1057 ))),
1058 }
1059}
1060
1061fn register_builtin_functions(functions: &mut HashMap<String, TemplateFn>) {
1063 functions.insert(
1065 "date".to_string(),
1066 Arc::new(|args: &[Value]| -> Result<Value, ViewError> {
1067 let format = args
1068 .first()
1069 .and_then(|v| v.as_str())
1070 .unwrap_or("Y-m-d H:i:s");
1071 let now = chrono::Local::now();
1072 let php_format = php_date_to_chrono(format);
1073 Ok(Value::String(now.format(&php_format).to_string()))
1074 }),
1075 );
1076
1077 functions.insert(
1079 "strtoupper".to_string(),
1080 Arc::new(|args: &[Value]| -> Result<Value, ViewError> {
1081 let s = args.first().map(value_to_string).unwrap_or_default();
1082 Ok(Value::String(s.to_uppercase()))
1083 }),
1084 );
1085
1086 functions.insert(
1088 "strtolower".to_string(),
1089 Arc::new(|args: &[Value]| -> Result<Value, ViewError> {
1090 let s = args.first().map(value_to_string).unwrap_or_default();
1091 Ok(Value::String(s.to_lowercase()))
1092 }),
1093 );
1094}
1095
1096fn php_date_to_chrono(php_format: &str) -> String {
1098 let mut result = String::with_capacity(php_format.len() * 2);
1099 let chars = php_format.chars();
1100 for c in chars {
1101 match c {
1102 'Y' => result.push_str("%Y"),
1103 'y' => result.push_str("%y"),
1104 'm' => result.push_str("%m"),
1105 'n' => result.push_str("%-m"),
1106 'd' => result.push_str("%d"),
1107 'j' => result.push_str("%-d"),
1108 'H' => result.push_str("%H"),
1109 'G' => result.push_str("%-H"),
1110 'i' => result.push_str("%M"),
1111 's' => result.push_str("%S"),
1112 'D' => result.push_str("%a"),
1113 'l' => result.push_str("%A"),
1114 'M' => result.push_str("%b"),
1115 'F' => result.push_str("%B"),
1116 'a' => result.push_str("%p"),
1117 'A' => result.push_str("%p"),
1118 'U' => result.push_str("%s"),
1119 _ => {
1120 result.push(c);
1121 }
1122 }
1123 }
1124 result
1125}
1126
1127use axum::response::Response;
1161
1162pub struct ViewFallback {
1184 view: View,
1186}
1187
1188impl ViewFallback {
1189 pub fn new(view: View) -> Self {
1191 Self { view }
1192 }
1193
1194 pub fn with_default_engine() -> Self {
1196 Self::new(View::with_default_engine())
1197 }
1198
1199 pub fn with_config(config: ViewConfig) -> Self {
1201 Self::new(View::with_config(config))
1202 }
1203
1204 pub fn render_template(
1219 &self,
1220 template: &str,
1221 vars: Option<ViewData>,
1222 ) -> Result<Response, ViewError> {
1223 let content = self.view.fetch(template, vars)?;
1224 Ok(sz_rust_http_facade::response::respond_html(content))
1225 }
1226
1227 pub fn render_display(
1241 &self,
1242 content: &str,
1243 vars: Option<ViewData>,
1244 ) -> Result<Response, ViewError> {
1245 let rendered = self.view.display(content, vars)?;
1246 Ok(sz_rust_http_facade::response::respond_html(rendered))
1247 }
1248
1249 pub fn render_to_string(
1264 &self,
1265 template: &str,
1266 vars: Option<ViewData>,
1267 ) -> Result<String, ViewError> {
1268 self.view.fetch(template, vars)
1269 }
1270
1271 pub fn display_to_string(
1283 &self,
1284 content: &str,
1285 vars: Option<ViewData>,
1286 ) -> Result<String, ViewError> {
1287 self.view.display(content, vars)
1288 }
1289
1290 pub fn view(&self) -> &View {
1292 &self.view
1293 }
1294
1295 pub fn assign(&self, name: &str, value: Value) -> &Self {
1297 self.view.assign(name, value);
1298 self
1299 }
1300
1301 pub fn assign_many(&self, vars: ViewData) -> &Self {
1303 self.view.assign_many(vars);
1304 self
1305 }
1306
1307 pub fn clear_vars(&self) -> &Self {
1309 self.view.clear_vars();
1310 self
1311 }
1312}
1313
1314pub fn render_template_response(
1330 view: &View,
1331 template: &str,
1332 vars: Option<ViewData>,
1333) -> Result<Response, ViewError> {
1334 let content = view.fetch(template, vars)?;
1335 Ok(sz_rust_http_facade::response::respond_html(content))
1336}
1337
1338pub fn render_display_response(
1353 view: &View,
1354 content: &str,
1355 vars: Option<ViewData>,
1356) -> Result<Response, ViewError> {
1357 let rendered = view.display(content, vars)?;
1358 Ok(sz_rust_http_facade::response::respond_html(rendered))
1359}
1360
1361#[cfg(test)]
1366mod tests {
1367 use super::*;
1368 use serde_json::json;
1369 use std::path::Path;
1370
1371 fn make_view() -> View {
1377 View::with_default_engine()
1378 }
1379
1380 fn make_view_with_path(path: &Path) -> View {
1382 let config = ViewConfig {
1383 view_path: path.to_path_buf(),
1384 ..Default::default()
1385 };
1386 View::with_config(config)
1387 }
1388
1389 fn make_temp_dir() -> PathBuf {
1391 let dir = std::env::temp_dir().join(format!(
1392 "sz_rust_view_test_{}",
1393 std::time::SystemTime::now()
1394 .duration_since(std::time::UNIX_EPOCH)
1395 .unwrap_or_default()
1396 .as_nanos()
1397 ));
1398 std::fs::create_dir_all(&dir).unwrap();
1399 dir
1400 }
1401
1402 fn write_template(dir: &Path, name: &str, content: &str) {
1404 let path = dir.join(format!("{}.html", name));
1405 std::fs::write(&path, content).unwrap();
1406 }
1407
1408 fn cleanup_dir(dir: &Path) {
1410 let _ = std::fs::remove_dir_all(dir);
1411 }
1412
1413 #[test]
1418 fn test_assign_single_var() {
1419 let view = make_view();
1421 view.assign("foo", json!("bar"));
1422 assert_eq!(view.get_var("foo"), Some(json!("bar")));
1423 }
1424
1425 #[test]
1426 fn test_assign_multiple_vars() {
1427 let view = make_view();
1428 view.assign("foo", json!("bar"))
1429 .assign("baz", json!("boom"));
1430 assert_eq!(view.get_var("foo"), Some(json!("bar")));
1431 assert_eq!(view.get_var("baz"), Some(json!("boom")));
1432 }
1433
1434 #[test]
1435 fn test_assign_overwrite() {
1436 let view = make_view();
1437 view.assign("foo", json!("bar"));
1438 view.assign("foo", json!("new"));
1439 assert_eq!(view.get_var("foo"), Some(json!("new")));
1440 }
1441
1442 #[test]
1443 fn test_has_var() {
1444 let view = make_view();
1445 assert!(!view.has_var("foo"));
1446 view.assign("foo", json!("bar"));
1447 assert!(view.has_var("foo"));
1448 }
1449
1450 #[test]
1451 fn test_clear_vars() {
1452 let view = make_view();
1453 view.assign("foo", json!("bar"));
1454 view.clear_vars();
1455 assert!(!view.has_var("foo"));
1456 }
1457
1458 #[test]
1459 fn test_assign_many() {
1460 let view = make_view();
1461 let mut vars = ViewData::new();
1462 vars.insert("a".to_string(), json!(1));
1463 vars.insert("b".to_string(), json!(2));
1464 view.assign_many(vars);
1465 assert_eq!(view.get_var("a"), Some(json!(1)));
1466 assert_eq!(view.get_var("b"), Some(json!(2)));
1467 }
1468
1469 #[test]
1474 fn test_display_string_var() {
1475 let view = make_view();
1477 let result = view
1478 .display(
1479 "Hello {$name}!",
1480 Some(ViewData::from([("name".to_string(), json!("World"))])),
1481 )
1482 .unwrap();
1483 assert_eq!(result, "Hello World!");
1484 }
1485
1486 #[test]
1487 fn test_display_with_assign() {
1488 let view = make_view();
1490 view.assign("name", json!("World"));
1491 let result = view.display("Hello {$name}!", None).unwrap();
1492 assert_eq!(result, "Hello World!");
1493 }
1494
1495 #[test]
1496 fn test_display_vars_override_assign() {
1497 let view = make_view();
1499 view.assign("name", json!("Default"));
1500 let result = view
1501 .display(
1502 "Hello {$name}!",
1503 Some(ViewData::from([("name".to_string(), json!("Override"))])),
1504 )
1505 .unwrap();
1506 assert_eq!(result, "Hello Override!");
1507 }
1508
1509 #[test]
1510 fn test_display_no_vars() {
1511 let view = make_view();
1512 let result = view.display("Hello World!", None).unwrap();
1513 assert_eq!(result, "Hello World!");
1514 }
1515
1516 #[test]
1517 fn test_display_missing_var() {
1518 let view = make_view();
1520 let result = view.display("Hello {$name}!", None).unwrap();
1521 assert_eq!(result, "Hello !");
1522 }
1523
1524 #[test]
1525 fn test_display_multiple_vars() {
1526 let view = make_view();
1527 let result = view
1528 .display(
1529 "{$greeting}, {$name}!",
1530 Some(ViewData::from([
1531 ("greeting".to_string(), json!("Hello")),
1532 ("name".to_string(), json!("World")),
1533 ])),
1534 )
1535 .unwrap();
1536 assert_eq!(result, "Hello, World!");
1537 }
1538
1539 #[test]
1544 fn test_display_nested_object() {
1545 let view = make_view();
1547 let result = view
1548 .display(
1549 "Name: {$user.name}",
1550 Some(ViewData::from([(
1551 "user".to_string(),
1552 json!({"name": "Alice", "age": 30}),
1553 )])),
1554 )
1555 .unwrap();
1556 assert_eq!(result, "Name: Alice");
1557 }
1558
1559 #[test]
1560 fn test_display_deep_nested() {
1561 let view = make_view();
1562 let result = view
1563 .display(
1564 "{$a.b.c}",
1565 Some(ViewData::from([(
1566 "a".to_string(),
1567 json!({"b": {"c": "deep"}}),
1568 )])),
1569 )
1570 .unwrap();
1571 assert_eq!(result, "deep");
1572 }
1573
1574 #[test]
1575 fn test_display_array_index() {
1576 let view = make_view();
1578 let result = view
1579 .display(
1580 "{$arr.0}",
1581 Some(ViewData::from([(
1582 "arr".to_string(),
1583 json!(["first", "second"]),
1584 )])),
1585 )
1586 .unwrap();
1587 assert_eq!(result, "first");
1588 }
1589
1590 #[test]
1591 fn test_display_nested_missing() {
1592 let view = make_view();
1593 let result = view
1594 .display(
1595 "{$user.name}",
1596 Some(ViewData::from([("user".to_string(), json!({}))])),
1597 )
1598 .unwrap();
1599 assert_eq!(result, "");
1600 }
1601
1602 #[test]
1607 fn test_filter_upper() {
1608 let view = make_view();
1610 let result = view
1611 .display(
1612 "{$name|upper}",
1613 Some(ViewData::from([("name".to_string(), json!("hello"))])),
1614 )
1615 .unwrap();
1616 assert_eq!(result, "HELLO");
1617 }
1618
1619 #[test]
1620 fn test_filter_lower() {
1621 let view = make_view();
1622 let result = view
1623 .display(
1624 "{$name|lower}",
1625 Some(ViewData::from([("name".to_string(), json!("HELLO"))])),
1626 )
1627 .unwrap();
1628 assert_eq!(result, "hello");
1629 }
1630
1631 #[test]
1632 fn test_filter_default_with_value() {
1633 let view = make_view();
1635 let result = view
1636 .display(
1637 "{$name|default='N/A'}",
1638 Some(ViewData::from([("name".to_string(), json!("Alice"))])),
1639 )
1640 .unwrap();
1641 assert_eq!(result, "Alice");
1642 }
1643
1644 #[test]
1645 fn test_filter_default_without_value() {
1646 let view = make_view();
1648 let result = view.display("{$name|default='N/A'}", None).unwrap();
1649 assert_eq!(result, "N/A");
1650 }
1651
1652 #[test]
1653 fn test_filter_raw() {
1654 let view = make_view();
1656 let result = view
1657 .display(
1658 "{$name|raw}",
1659 Some(ViewData::from([("name".to_string(), json!("<b>bold</b>"))])),
1660 )
1661 .unwrap();
1662 assert_eq!(result, "<b>bold</b>");
1663 }
1664
1665 #[test]
1666 fn test_filter_default_htmlentities() {
1667 let view = make_view();
1669 let result = view
1670 .display(
1671 "{$name}",
1672 Some(ViewData::from([("name".to_string(), json!("<b>bold</b>"))])),
1673 )
1674 .unwrap();
1675 assert_eq!(result, "<b>bold</b>");
1676 }
1677
1678 #[test]
1679 fn test_filter_chained() {
1680 let view = make_view();
1682 let result = view
1683 .display(
1684 "{$name|upper|lower}",
1685 Some(ViewData::from([("name".to_string(), json!("Hello"))])),
1686 )
1687 .unwrap();
1688 assert_eq!(result, "hello");
1689 }
1690
1691 #[test]
1696 fn test_ternary_null_coalescing() {
1697 let view = make_view();
1699 let result = view.display("{$name??'default'}", None).unwrap();
1700 assert_eq!(result, "default");
1701 }
1702
1703 #[test]
1704 fn test_ternary_null_coalescing_with_value() {
1705 let view = make_view();
1706 let result = view
1707 .display(
1708 "{$name??'default'}",
1709 Some(ViewData::from([("name".to_string(), json!("Alice"))])),
1710 )
1711 .unwrap();
1712 assert_eq!(result, "Alice");
1713 }
1714
1715 #[test]
1716 fn test_ternary_falsy_default() {
1717 let view = make_view();
1719 let result = view.display("{$name?:'default'}", None).unwrap();
1720 assert_eq!(result, "default");
1721 }
1722
1723 #[test]
1724 fn test_ternary_truthy_output() {
1725 let view = make_view();
1727 let result = view
1728 .display(
1729 "{$name?='yes'}",
1730 Some(ViewData::from([("name".to_string(), json!("Alice"))])),
1731 )
1732 .unwrap();
1733 assert_eq!(result, "yes");
1734 }
1735
1736 #[test]
1741 fn test_func_date() {
1742 let view = make_view();
1744 let result = view.display("{:date('Y')}", None).unwrap();
1745 let year: u32 = result.parse().unwrap();
1746 assert!((2000..=2100).contains(&year));
1747 }
1748
1749 #[test]
1750 fn test_func_strtoupper() {
1751 let view = make_view();
1752 let result = view.display("{:strtoupper('hello')}", None).unwrap();
1753 assert_eq!(result, "HELLO");
1754 }
1755
1756 #[test]
1757 fn test_func_no_args() {
1758 let view = make_view();
1760 let result = view.display("{:date()}", None).unwrap();
1761 assert!(!result.is_empty());
1763 }
1764
1765 #[test]
1766 fn test_func_suppress_output() {
1767 let view = make_view();
1769 let result = view.display("{~strtoupper('hello')}", None).unwrap();
1770 assert_eq!(result, "");
1771 }
1772
1773 #[test]
1774 fn test_func_unknown() {
1775 let view = make_view();
1776 let result = view.display("{:unknown_func()}", None);
1777 assert!(result.is_err());
1778 }
1779
1780 #[test]
1785 fn test_single_line_comment() {
1786 let view = make_view();
1788 let result = view.display("Hello{//这是注释}World", None).unwrap();
1789 assert_eq!(result, "HelloWorld");
1790 }
1791
1792 #[test]
1793 fn test_block_comment() {
1794 let view = make_view();
1796 let result = view.display("Hello{/*块注释*/}World", None).unwrap();
1797 assert_eq!(result, "HelloWorld");
1798 }
1799
1800 #[test]
1805 fn test_literal_preserves_tags() {
1806 let view = make_view();
1808 let result = view
1809 .display(
1810 "{literal}{$name}{/literal}",
1811 Some(ViewData::from([("name".to_string(), json!("World"))])),
1812 )
1813 .unwrap();
1814 assert_eq!(result, "{$name}");
1815 }
1816
1817 #[test]
1818 fn test_literal_mixed() {
1819 let view = make_view();
1820 let result = view
1821 .display(
1822 "Hello {$name}! {literal}{$raw}{/literal} Bye",
1823 Some(ViewData::from([("name".to_string(), json!("World"))])),
1824 )
1825 .unwrap();
1826 assert_eq!(result, "Hello World! {$raw} Bye");
1827 }
1828
1829 #[test]
1830 fn test_literal_multiple() {
1831 let view = make_view();
1832 let result = view
1833 .display(
1834 "{literal}A{/literal} {$name} {literal}B{/literal}",
1835 Some(ViewData::from([("name".to_string(), json!("X"))])),
1836 )
1837 .unwrap();
1838 assert_eq!(result, "A X B");
1839 }
1840
1841 #[test]
1846 fn test_fetch_template_file() {
1847 let dir = make_temp_dir();
1848 write_template(&dir, "index", "<h1>{$title}</h1>");
1849 let view = make_view_with_path(&dir);
1850 let result = view
1851 .fetch(
1852 "index",
1853 Some(ViewData::from([("title".to_string(), json!("Hello"))])),
1854 )
1855 .unwrap();
1856 assert_eq!(result, "<h1>Hello</h1>");
1857 cleanup_dir(&dir);
1858 }
1859
1860 #[test]
1861 fn test_fetch_not_found() {
1862 let dir = make_temp_dir();
1863 let view = make_view_with_path(&dir);
1864 let result = view.fetch("nonexistent", None);
1865 assert!(matches!(result, Err(ViewError::TemplateNotFound(_))));
1866 cleanup_dir(&dir);
1867 }
1868
1869 #[test]
1870 fn test_exists() {
1871 let dir = make_temp_dir();
1872 write_template(&dir, "index", "content");
1873 let view = make_view_with_path(&dir);
1874 assert!(view.exists("index"));
1875 assert!(!view.exists("nonexistent"));
1876 cleanup_dir(&dir);
1877 }
1878
1879 #[test]
1880 fn test_fetch_with_assign() {
1881 let dir = make_temp_dir();
1882 write_template(&dir, "index", "Name: {$name}");
1883 let view = make_view_with_path(&dir);
1884 view.assign("name", json!("Alice"));
1885 let result = view.fetch("index", None).unwrap();
1886 assert_eq!(result, "Name: Alice");
1887 cleanup_dir(&dir);
1888 }
1889
1890 #[test]
1895 fn test_content_filter() {
1896 let view = make_view();
1898 view.set_filter(Arc::new(|content: &str| content.to_uppercase()));
1899 let result = view.display("hello world", None).unwrap();
1900 assert_eq!(result, "HELLO WORLD");
1901 }
1902
1903 #[test]
1904 fn test_clear_filter() {
1905 let view = make_view();
1906 view.set_filter(Arc::new(|content: &str| content.to_uppercase()));
1907 view.clear_filter();
1908 let result = view.display("hello world", None).unwrap();
1909 assert_eq!(result, "hello world");
1910 }
1911
1912 #[test]
1917 fn test_integer_var() {
1918 let view = make_view();
1919 let result = view
1920 .display(
1921 "Count: {$count}",
1922 Some(ViewData::from([("count".to_string(), json!(42))])),
1923 )
1924 .unwrap();
1925 assert_eq!(result, "Count: 42");
1926 }
1927
1928 #[test]
1929 fn test_boolean_true() {
1930 let view = make_view();
1932 let result = view
1933 .display(
1934 "Flag: {$flag}",
1935 Some(ViewData::from([("flag".to_string(), json!(true))])),
1936 )
1937 .unwrap();
1938 assert_eq!(result, "Flag: 1");
1939 }
1940
1941 #[test]
1942 fn test_boolean_false() {
1943 let view = make_view();
1945 let result = view
1946 .display(
1947 "Flag: {$flag}",
1948 Some(ViewData::from([("flag".to_string(), json!(false))])),
1949 )
1950 .unwrap();
1951 assert_eq!(result, "Flag: ");
1952 }
1953
1954 #[test]
1955 fn test_float_var() {
1956 let view = make_view();
1957 let result = view
1958 .display(
1959 "Float: {$f}",
1960 Some(ViewData::from([("f".to_string(), json!(2.5))])),
1961 )
1962 .unwrap();
1963 assert_eq!(result, "Float: 2.5");
1964 }
1965
1966 #[test]
1967 fn test_float_integer_value() {
1968 let view = make_view();
1970 let result = view
1971 .display(
1972 "Num: {$num}",
1973 Some(ViewData::from([("num".to_string(), json!(3.0))])),
1974 )
1975 .unwrap();
1976 assert_eq!(result, "Num: 3");
1977 }
1978
1979 #[test]
1984 fn test_config_default() {
1985 let config = ViewConfig::default();
1986 assert_eq!(config.view_suffix, "html");
1987 assert_eq!(config.tpl_begin, "{");
1988 assert_eq!(config.tpl_end, "}");
1989 assert_eq!(config.default_filter, "htmlentities");
1990 assert_eq!(config.tpl_var_identify, "array");
1991 assert!(!config.layout_on);
1992 }
1993
1994 #[test]
1995 fn test_config_get_config() {
1996 let engine = SimpleTemplateEngine::new(ViewConfig::default());
1997 assert_eq!(
1998 engine.get_config("view_suffix"),
1999 Some(Value::String("html".to_string()))
2000 );
2001 assert_eq!(
2002 engine.get_config("tpl_begin"),
2003 Some(Value::String("{".to_string()))
2004 );
2005 assert_eq!(engine.get_config("nonexistent"), None);
2006 }
2007
2008 #[test]
2013 fn test_register_custom_function() {
2014 let view = make_view();
2015 if let Some(engine) = view
2017 .engine()
2018 .as_any()
2019 .downcast_ref::<SimpleTemplateEngine>()
2020 {
2021 engine.register_function(
2022 "greet",
2023 Arc::new(|args: &[Value]| {
2024 let name = args.first().and_then(|v| v.as_str()).unwrap_or("World");
2025 Ok(Value::String(format!("Hello, {}!", name)))
2026 }),
2027 );
2028 }
2029 let result = view.display("{:greet('Alice')}", None).unwrap();
2030 assert_eq!(result, "Hello, Alice!");
2031 }
2032
2033 #[test]
2038 fn test_parse_template_path_relative() {
2039 let engine = SimpleTemplateEngine::new(ViewConfig::default());
2040 let path = engine.parse_template_path("index");
2041 assert_eq!(path, PathBuf::from("view/index.html"));
2042 }
2043
2044 #[test]
2045 fn test_parse_template_path_with_extension() {
2046 let engine = SimpleTemplateEngine::new(ViewConfig::default());
2047 let path = engine.parse_template_path("index.html");
2048 assert_eq!(path, PathBuf::from("view/index.html"));
2049 }
2050
2051 #[test]
2052 fn test_parse_template_path_absolute() {
2053 let engine = SimpleTemplateEngine::new(ViewConfig::default());
2054 let path = engine.parse_template_path("/absolute/path");
2055 assert_eq!(path, PathBuf::from("absolute/path.html"));
2056 }
2057
2058 #[test]
2059 fn test_parse_template_path_cross_app() {
2060 let engine = SimpleTemplateEngine::new(ViewConfig::default());
2062 let path = engine.parse_template_path("admin@dashboard");
2063 assert_eq!(path, PathBuf::from("admin/view/dashboard.html"));
2064 }
2065
2066 #[test]
2067 fn test_parse_template_path_empty() {
2068 let engine = SimpleTemplateEngine::new(ViewConfig::default());
2069 let path = engine.parse_template_path("");
2070 assert_eq!(path, PathBuf::from("view/index.html"));
2071 }
2072
2073 #[test]
2078 fn test_htmlentities_basic() {
2079 assert_eq!(htmlentities("<b>"), "<b>");
2080 assert_eq!(htmlentities("\"quote\""), ""quote"");
2081 assert_eq!(htmlentities("'apos'"), "'apos'");
2082 assert_eq!(htmlentities("&"), "&amp;");
2083 }
2084
2085 #[test]
2086 fn test_is_truthy() {
2087 assert!(!is_truthy(&Value::Null));
2088 assert!(!is_truthy(&Value::Bool(false)));
2089 assert!(is_truthy(&Value::Bool(true)));
2090 assert!(!is_truthy(&json!(0)));
2091 assert!(is_truthy(&json!(1)));
2092 assert!(!is_truthy(&json!("")));
2093 assert!(!is_truthy(&json!("0")));
2094 assert!(is_truthy(&json!("hello")));
2095 assert!(!is_truthy(&json!([])));
2096 assert!(is_truthy(&json!([1, 2])));
2097 assert!(!is_truthy(&json!({})));
2098 assert!(is_truthy(&json!({"a": 1})));
2099 }
2100
2101 #[test]
2102 fn test_value_to_string() {
2103 assert_eq!(value_to_string(&Value::Null), "");
2104 assert_eq!(value_to_string(&Value::Bool(true)), "1");
2105 assert_eq!(value_to_string(&Value::Bool(false)), "");
2106 assert_eq!(value_to_string(&json!(42)), "42");
2107 assert_eq!(value_to_string(&json!(2.5)), "2.5");
2108 assert_eq!(value_to_string(&json!(3.0)), "3");
2109 assert_eq!(value_to_string(&json!("hello")), "hello");
2110 }
2111
2112 #[test]
2113 fn test_parse_literal() {
2114 assert_eq!(parse_literal("'string'"), Value::String("string".into()));
2115 assert_eq!(parse_literal("\"double\""), Value::String("double".into()));
2116 assert_eq!(parse_literal("42"), json!(42));
2117 assert_eq!(parse_literal("2.5"), json!(2.5));
2118 assert_eq!(parse_literal("true"), Value::Bool(true));
2119 assert_eq!(parse_literal("false"), Value::Bool(false));
2120 assert_eq!(parse_literal("null"), Value::Null);
2121 }
2122
2123 #[test]
2124 fn test_split_args() {
2125 assert_eq!(split_args("a, b, c"), vec!["a", "b", "c"]);
2126 assert_eq!(split_args("'a,b', c"), vec!["'a,b'", "c"]);
2127 assert_eq!(split_args("\"a,b\", c"), vec!["\"a,b\"", "c"]);
2128 assert_eq!(split_args(""), Vec::<String>::new());
2129 }
2130
2131 #[test]
2132 fn test_parse_func_call() {
2133 let (name, args) = parse_func_call("date('Y')").unwrap();
2134 assert_eq!(name, "date");
2135 assert_eq!(args, vec![Value::String("Y".into())]);
2136
2137 let (name, args) = parse_func_call("now()").unwrap();
2138 assert_eq!(name, "now");
2139 assert!(args.is_empty());
2140
2141 let (name, _args) = parse_func_call("noargs").unwrap();
2142 assert_eq!(name, "noargs");
2143 }
2144
2145 async fn extract_body_string(resp: axum::response::Response) -> String {
2151 use http_body_util::BodyExt;
2152 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
2153 String::from_utf8(bytes.to_vec()).unwrap()
2154 }
2155
2156 #[test]
2157 fn test_view_fallback_new() {
2158 let view = View::with_default_engine();
2160 let fallback = ViewFallback::new(view);
2161 assert!(!fallback.view().has_var("any"));
2162 }
2163
2164 #[test]
2165 fn test_view_fallback_with_default_engine() {
2166 let fallback = ViewFallback::with_default_engine();
2168 assert!(!fallback.view().has_var("any"));
2169 }
2170
2171 #[test]
2172 fn test_view_fallback_with_config() {
2173 let config = ViewConfig {
2175 view_suffix: "tpl".to_string(),
2176 ..Default::default()
2177 };
2178 let fallback = ViewFallback::with_config(config);
2179 let view = fallback.view();
2180 let engine = view.engine();
2182 assert_eq!(
2183 engine.get_config("view_suffix"),
2184 Some(Value::String("tpl".to_string()))
2185 );
2186 }
2187
2188 #[test]
2189 fn test_view_fallback_assign() {
2190 let fallback = ViewFallback::with_default_engine();
2192 fallback.assign("name", json!("Alice"));
2193 assert_eq!(fallback.view().get_var("name"), Some(json!("Alice")));
2194 }
2195
2196 #[test]
2197 fn test_view_fallback_assign_many() {
2198 let fallback = ViewFallback::with_default_engine();
2200 let mut vars = ViewData::new();
2201 vars.insert("a".to_string(), json!(1));
2202 vars.insert("b".to_string(), json!(2));
2203 fallback.assign_many(vars);
2204 assert_eq!(fallback.view().get_var("a"), Some(json!(1)));
2205 assert_eq!(fallback.view().get_var("b"), Some(json!(2)));
2206 }
2207
2208 #[test]
2209 fn test_view_fallback_assign_chain() {
2210 let fallback = ViewFallback::with_default_engine();
2212 fallback
2213 .assign("a", json!(1))
2214 .assign("b", json!(2))
2215 .assign("c", json!(3));
2216 assert_eq!(fallback.view().get_var("a"), Some(json!(1)));
2217 assert_eq!(fallback.view().get_var("b"), Some(json!(2)));
2218 assert_eq!(fallback.view().get_var("c"), Some(json!(3)));
2219 }
2220
2221 #[test]
2222 fn test_view_fallback_clear_vars() {
2223 let fallback = ViewFallback::with_default_engine();
2225 fallback.assign("name", json!("Alice"));
2226 assert!(fallback.view().has_var("name"));
2227 fallback.clear_vars();
2228 assert!(!fallback.view().has_var("name"));
2229 }
2230
2231 #[test]
2232 fn test_view_fallback_display_to_string() {
2233 let fallback = ViewFallback::with_default_engine();
2236 let result = fallback
2237 .display_to_string(
2238 "Hello {$name}!",
2239 Some(ViewData::from([("name".to_string(), json!("World"))])),
2240 )
2241 .unwrap();
2242 assert_eq!(result, "Hello World!");
2243 }
2244
2245 #[test]
2246 fn test_view_fallback_display_to_string_with_assign() {
2247 let fallback = ViewFallback::with_default_engine();
2249 fallback.assign("name", json!("Alice"));
2250 let result = fallback.display_to_string("Hello {$name}!", None).unwrap();
2251 assert_eq!(result, "Hello Alice!");
2252 }
2253
2254 #[tokio::test]
2255 async fn test_view_fallback_render_display_response() {
2256 let fallback = ViewFallback::with_default_engine();
2259 let resp = fallback
2260 .render_display(
2261 "<h1>Hello {$name}!</h1>",
2262 Some(ViewData::from([("name".to_string(), json!("World"))])),
2263 )
2264 .unwrap();
2265
2266 assert_eq!(resp.status(), axum::http::StatusCode::OK);
2268
2269 let content_type = resp
2271 .headers()
2272 .get(axum::http::header::CONTENT_TYPE)
2273 .unwrap()
2274 .to_str()
2275 .unwrap()
2276 .to_string();
2277 assert_eq!(content_type, "text/html; charset=utf-8");
2278
2279 let body = extract_body_string(resp).await;
2281 assert_eq!(body, "<h1>Hello World!</h1>");
2282 }
2283
2284 #[tokio::test]
2285 async fn test_view_fallback_render_display_with_assign() {
2286 let fallback = ViewFallback::with_default_engine();
2288 fallback.assign("title", json!("Report"));
2289 let resp = fallback
2290 .render_display("<title>{$title}</title>", None)
2291 .unwrap();
2292 let body = extract_body_string(resp).await;
2293 assert_eq!(body, "<title>Report</title>");
2294 }
2295
2296 #[tokio::test]
2297 async fn test_view_fallback_render_template_file() {
2298 let dir = make_temp_dir();
2301 write_template(&dir, "pdf_template", "<pdf>{$content}</pdf>");
2302 let config = ViewConfig {
2303 view_path: dir.clone(),
2304 ..Default::default()
2305 };
2306 let fallback = ViewFallback::with_config(config);
2307
2308 let resp = fallback
2309 .render_template(
2310 "pdf_template",
2311 Some(ViewData::from([(
2312 "content".to_string(),
2313 json!("Hello PDF"),
2314 )])),
2315 )
2316 .unwrap();
2317
2318 assert_eq!(resp.status(), axum::http::StatusCode::OK);
2319 let content_type = resp
2320 .headers()
2321 .get(axum::http::header::CONTENT_TYPE)
2322 .unwrap()
2323 .to_str()
2324 .unwrap()
2325 .to_string();
2326 assert_eq!(content_type, "text/html; charset=utf-8");
2327 let body = extract_body_string(resp).await;
2328 assert_eq!(body, "<pdf>Hello PDF</pdf>");
2329
2330 cleanup_dir(&dir);
2331 }
2332
2333 #[test]
2334 fn test_view_fallback_render_template_not_found() {
2335 let dir = make_temp_dir();
2338 let config = ViewConfig {
2339 view_path: dir.clone(),
2340 ..Default::default()
2341 };
2342 let fallback = ViewFallback::with_config(config);
2343
2344 let result = fallback.render_template("nonexistent", None);
2345 assert!(result.is_err());
2346 match result {
2347 Err(ViewError::TemplateNotFound(_)) => {}
2348 Err(e) => panic!("Expected TemplateNotFound, got: {:?}", e),
2349 Ok(_) => panic!("Expected error, got Ok"),
2350 }
2351
2352 cleanup_dir(&dir);
2353 }
2354
2355 #[test]
2356 fn test_view_fallback_render_to_string() {
2357 let dir = make_temp_dir();
2360 write_template(
2361 &dir,
2362 "excel_template",
2363 "<table><tr><td>{$value}</td></tr></table>",
2364 );
2365 let config = ViewConfig {
2366 view_path: dir.clone(),
2367 ..Default::default()
2368 };
2369 let fallback = ViewFallback::with_config(config);
2370
2371 let html = fallback
2372 .render_to_string(
2373 "excel_template",
2374 Some(ViewData::from([("value".to_string(), json!(42))])),
2375 )
2376 .unwrap();
2377 assert_eq!(html, "<table><tr><td>42</td></tr></table>");
2378
2379 cleanup_dir(&dir);
2380 }
2381
2382 #[test]
2383 fn test_view_fallback_render_to_string_not_found() {
2384 let dir = make_temp_dir();
2386 let config = ViewConfig {
2387 view_path: dir.clone(),
2388 ..Default::default()
2389 };
2390 let fallback = ViewFallback::with_config(config);
2391
2392 let result = fallback.render_to_string("nonexistent", None);
2393 assert!(result.is_err());
2394
2395 cleanup_dir(&dir);
2396 }
2397
2398 #[tokio::test]
2399 async fn test_render_template_response_free_function() {
2400 let dir = make_temp_dir();
2402 write_template(&dir, "report", "<report>{$title}</report>");
2403 let config = ViewConfig {
2404 view_path: dir.clone(),
2405 ..Default::default()
2406 };
2407 let view = View::with_config(config);
2408
2409 let resp = render_template_response(
2410 &view,
2411 "report",
2412 Some(ViewData::from([("title".to_string(), json!("Monthly"))])),
2413 )
2414 .unwrap();
2415
2416 assert_eq!(resp.status(), axum::http::StatusCode::OK);
2417 let content_type = resp
2418 .headers()
2419 .get(axum::http::header::CONTENT_TYPE)
2420 .unwrap()
2421 .to_str()
2422 .unwrap()
2423 .to_string();
2424 assert_eq!(content_type, "text/html; charset=utf-8");
2425 let body = extract_body_string(resp).await;
2426 assert_eq!(body, "<report>Monthly</report>");
2427
2428 cleanup_dir(&dir);
2429 }
2430
2431 #[tokio::test]
2432 async fn test_render_display_response_free_function() {
2433 let view = View::with_default_engine();
2435 let resp = render_display_response(
2436 &view,
2437 "<p>{$msg}</p>",
2438 Some(ViewData::from([("msg".to_string(), json!("Hello"))])),
2439 )
2440 .unwrap();
2441
2442 assert_eq!(resp.status(), axum::http::StatusCode::OK);
2443 let content_type = resp
2444 .headers()
2445 .get(axum::http::header::CONTENT_TYPE)
2446 .unwrap()
2447 .to_str()
2448 .unwrap()
2449 .to_string();
2450 assert_eq!(content_type, "text/html; charset=utf-8");
2451 let body = extract_body_string(resp).await;
2452 assert_eq!(body, "<p>Hello</p>");
2453 }
2454
2455 #[test]
2456 fn test_render_template_response_not_found() {
2457 let dir = make_temp_dir();
2459 let config = ViewConfig {
2460 view_path: dir.clone(),
2461 ..Default::default()
2462 };
2463 let view = View::with_config(config);
2464
2465 let result = render_template_response(&view, "nonexistent", None);
2466 assert!(result.is_err());
2467
2468 cleanup_dir(&dir);
2469 }
2470
2471 #[test]
2472 fn test_view_fallback_pdf_export_scenario() {
2473 let dir = make_temp_dir();
2476 write_template(
2477 &dir,
2478 "payment_pdf",
2479 r#"<html><body><h1>付款单 {$payment_id}</h1><p>金额: {$amount}</p></body></html>"#,
2480 );
2481 let config = ViewConfig {
2482 view_path: dir.clone(),
2483 ..Default::default()
2484 };
2485 let fallback = ViewFallback::with_config(config);
2486
2487 let html = fallback
2489 .render_to_string(
2490 "payment_pdf",
2491 Some(ViewData::from([
2492 ("payment_id".to_string(), json!("PAY-001")),
2493 ("amount".to_string(), json!("¥1,234.56")),
2494 ])),
2495 )
2496 .unwrap();
2497
2498 assert_eq!(
2499 html,
2500 r#"<html><body><h1>付款单 PAY-001</h1><p>金额: ¥1,234.56</p></body></html>"#
2501 );
2502
2503 cleanup_dir(&dir);
2504 }
2505
2506 #[tokio::test]
2507 async fn test_view_fallback_excel_export_scenario() {
2508 let dir = make_temp_dir();
2511 write_template(
2512 &dir,
2513 "order_excel",
2514 r#"<table><tr><th>订单号</th><th>金额</th></tr><tr><td>{$order_no}</td><td>{$amount}</td></tr></table>"#,
2515 );
2516 let config = ViewConfig {
2517 view_path: dir.clone(),
2518 ..Default::default()
2519 };
2520 let fallback = ViewFallback::with_config(config);
2521
2522 let resp = fallback
2524 .render_template(
2525 "order_excel",
2526 Some(ViewData::from([
2527 ("order_no".to_string(), json!("ORD-2026-001")),
2528 ("amount".to_string(), json!(99.50)),
2529 ])),
2530 )
2531 .unwrap();
2532
2533 let body = extract_body_string(resp).await;
2534 assert!(body.contains("<th>订单号</th>"));
2535 assert!(body.contains("<td>ORD-2026-001</td>"));
2536 assert!(body.contains("<td>99.5</td>"));
2537
2538 cleanup_dir(&dir);
2539 }
2540
2541 #[tokio::test]
2542 async fn test_view_fallback_email_scenario() {
2543 let fallback = ViewFallback::with_default_engine();
2545 let resp = fallback
2546 .render_display(
2547 r#"<html><body><h2>Dear {$name}</h2><p>Your order #{$order_id} has been shipped.</p></body></html>"#,
2548 Some(ViewData::from([
2549 ("name".to_string(), json!("Alice")),
2550 ("order_id".to_string(), json!(12345)),
2551 ])),
2552 )
2553 .unwrap();
2554
2555 let body = extract_body_string(resp).await;
2556 assert!(body.contains("Dear Alice"));
2557 assert!(body.contains("#12345"));
2558 assert!(body.contains("has been shipped"));
2559 }
2560
2561 #[test]
2562 fn test_view_fallback_content_type_header() {
2563 let fallback = ViewFallback::with_default_engine();
2566 let resp = fallback.render_display("<p>test</p>", None).unwrap();
2567
2568 let content_type = resp
2569 .headers()
2570 .get(axum::http::header::CONTENT_TYPE)
2571 .unwrap()
2572 .to_str()
2573 .unwrap();
2574 assert!(content_type.starts_with("text/html"));
2576 assert!(content_type.contains("charset=utf-8"));
2577 }
2578
2579 #[test]
2580 fn test_view_fallback_http_status() {
2581 let fallback = ViewFallback::with_default_engine();
2584 let resp = fallback.render_display("<html></html>", None).unwrap();
2585 assert_eq!(resp.status(), axum::http::StatusCode::OK);
2586 }
2587
2588 #[test]
2589 fn test_view_fallback_with_layout() {
2590 let dir = make_temp_dir();
2593 write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
2594 write_template(&dir, "page", "<p>{$content}</p>");
2595 let config = ViewConfig {
2596 view_path: dir.clone(),
2597 layout_on: true,
2598 layout_name: "layout".to_string(),
2599 ..Default::default()
2600 };
2601 let fallback = ViewFallback::with_config(config);
2602
2603 let html = fallback
2604 .render_to_string(
2605 "page",
2606 Some(ViewData::from([("content".to_string(), json!("Hello"))])),
2607 )
2608 .unwrap();
2609 assert_eq!(html, "<html><body><p>Hello</p></body></html>");
2610
2611 cleanup_dir(&dir);
2612 }
2613
2614 #[test]
2615 fn test_view_fallback_with_inheritance() {
2616 let dir = make_temp_dir();
2619 write_template(
2620 &dir,
2621 "base",
2622 "<html>{block name='content'}default{/block}</html>",
2623 );
2624 write_template(
2625 &dir,
2626 "child",
2627 "{extend name='base'}{block name='content'}{$msg}{/block}",
2628 );
2629 let config = ViewConfig {
2630 view_path: dir.clone(),
2631 ..Default::default()
2632 };
2633 let fallback = ViewFallback::with_config(config);
2634
2635 let html = fallback
2636 .render_to_string(
2637 "child",
2638 Some(ViewData::from([(
2639 "msg".to_string(),
2640 json!("Hello Inheritance"),
2641 )])),
2642 )
2643 .unwrap();
2644 assert_eq!(html, "<html>Hello Inheritance</html>");
2645
2646 cleanup_dir(&dir);
2647 }
2648
2649 #[tokio::test]
2650 async fn test_view_fallback_complex_template() {
2651 let fallback = ViewFallback::with_default_engine();
2653 let template = r#"<div class="user">
2654 <span>{$name|upper}</span>
2655 <span>{$email|default='N/A'}</span>
2656 <span>{$active?='启用':'禁用'}</span>
2657</div>"#;
2658 let resp = fallback
2659 .render_display(
2660 template,
2661 Some(ViewData::from([
2662 ("name".to_string(), json!("alice")),
2663 ("active".to_string(), json!(true)),
2664 ])),
2665 )
2666 .unwrap();
2667 let body = extract_body_string(resp).await;
2668 assert!(body.contains("ALICE"));
2669 assert!(body.contains("N/A"));
2670 assert!(body.contains("启用"));
2671 }
2672}