1use serde::Serialize;
75use standout_bbparser::{BBParser, TagTransform, UnknownTagBehavior};
76use std::collections::HashMap;
77
78use super::engine::{MiniJinjaEngine, TemplateEngine};
79use crate::context::{ContextRegistry, RenderContext};
80use crate::error::RenderError;
81use crate::output::OutputMode;
82use crate::style::Styles;
83use crate::tabular::FlatDataSpec;
84use crate::theme::{detect_color_mode, detect_icon_mode, ColorMode, Theme};
85
86fn output_mode_to_transform(mode: OutputMode) -> TagTransform {
88 match mode {
89 OutputMode::Auto => {
90 if mode.should_use_color() {
91 TagTransform::Apply
92 } else {
93 TagTransform::Remove
94 }
95 }
96 OutputMode::Term => TagTransform::Apply,
97 OutputMode::Text => TagTransform::Remove,
98 OutputMode::TermDebug => TagTransform::Keep,
99 OutputMode::Json | OutputMode::Yaml | OutputMode::Xml | OutputMode::Csv => {
101 TagTransform::Remove
102 }
103 }
104}
105
106pub fn apply_style_tags(output: &str, styles: &Styles, mode: OutputMode) -> String {
110 let transform = output_mode_to_transform(mode);
111 let resolved_styles = styles.to_resolved_map();
112 let parser =
113 BBParser::new(resolved_styles, transform).unknown_behavior(UnknownTagBehavior::Passthrough);
114 parser.parse(output)
115}
116
117#[derive(Debug, Clone)]
123pub struct RenderResult {
124 pub formatted: String,
126 pub raw: String,
130}
131
132impl RenderResult {
133 pub fn new(formatted: String, raw: String) -> Self {
135 Self { formatted, raw }
136 }
137
138 pub fn plain(text: String) -> Self {
142 Self {
143 formatted: text.clone(),
144 raw: text,
145 }
146 }
147}
148
149pub fn validate_template<T: Serialize>(
195 template: &str,
196 data: &T,
197 theme: &Theme,
198) -> Result<(), Box<dyn std::error::Error>> {
199 let color_mode = detect_color_mode();
200 let styles = theme.resolve_styles(Some(color_mode));
201
202 let engine = MiniJinjaEngine::new();
204 let data_value = serde_json::to_value(data)?;
205 let minijinja_output = engine.render_template(template, &data_value)?;
206
207 let resolved_styles = styles.to_resolved_map();
209 let parser = BBParser::new(resolved_styles, TagTransform::Remove);
210 parser.validate(&minijinja_output)?;
211
212 Ok(())
213}
214
215pub fn render<T: Serialize>(
245 template: &str,
246 data: &T,
247 theme: &Theme,
248) -> Result<String, RenderError> {
249 render_with_output(template, data, theme, OutputMode::Auto)
250}
251
252pub fn render_with_output<T: Serialize>(
296 template: &str,
297 data: &T,
298 theme: &Theme,
299 mode: OutputMode,
300) -> Result<String, RenderError> {
301 let color_mode = detect_color_mode();
303 render_with_mode(template, data, theme, mode, color_mode)
304}
305
306pub fn render_with_mode<T: Serialize>(
356 template: &str,
357 data: &T,
358 theme: &Theme,
359 output_mode: OutputMode,
360 color_mode: ColorMode,
361) -> Result<String, RenderError> {
362 theme
364 .validate()
365 .map_err(|e| RenderError::StyleError(e.to_string()))?;
366
367 let styles = theme.resolve_styles(Some(color_mode));
369
370 let engine = MiniJinjaEngine::new();
372 let data_value = serde_json::to_value(data)?;
373 let icon_context = build_icon_context(theme);
374 let template_output = if icon_context.is_empty() {
375 engine.render_template(template, &data_value)?
376 } else {
377 engine.render_with_context(template, &data_value, icon_context)?
378 };
379
380 let final_output = apply_style_tags(&template_output, &styles, output_mode);
382
383 Ok(final_output)
384}
385
386pub fn render_with_vars<T, K, V, I>(
427 template: &str,
428 data: &T,
429 theme: &Theme,
430 mode: OutputMode,
431 vars: I,
432) -> Result<String, RenderError>
433where
434 T: Serialize,
435 K: AsRef<str>,
436 V: Into<serde_json::Value>,
437 I: IntoIterator<Item = (K, V)>,
438{
439 let color_mode = detect_color_mode();
440 let styles = theme.resolve_styles(Some(color_mode));
441
442 styles
444 .validate()
445 .map_err(|e| RenderError::StyleError(e.to_string()))?;
446
447 let mut context: HashMap<String, serde_json::Value> = build_icon_context(theme);
449 for (key, value) in vars {
450 context.insert(key.as_ref().to_string(), value.into());
451 }
452
453 let engine = MiniJinjaEngine::new();
455 let data_value = serde_json::to_value(data)?;
456 let template_output = engine.render_with_context(template, &data_value, context)?;
457
458 let final_output = apply_style_tags(&template_output, &styles, mode);
460
461 Ok(final_output)
462}
463
464pub fn render_auto<T: Serialize>(
511 template: &str,
512 data: &T,
513 theme: &Theme,
514 mode: OutputMode,
515) -> Result<String, RenderError> {
516 if mode.is_structured() {
517 match mode {
518 OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
519 OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
520 OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
521 OutputMode::Csv => {
522 let value = serde_json::to_value(data)?;
523 let (headers, rows) = crate::util::flatten_json_for_csv(&value);
524
525 let mut wtr = csv::Writer::from_writer(Vec::new());
526 wtr.write_record(&headers)?;
527 for row in rows {
528 wtr.write_record(&row)?;
529 }
530 let bytes = wtr.into_inner()?;
531 Ok(String::from_utf8(bytes)?)
532 }
533 _ => unreachable!("is_structured() returned true for non-structured mode"),
534 }
535 } else {
536 render_with_output(template, data, theme, mode)
537 }
538}
539
540pub fn render_auto_with_spec<T: Serialize>(
554 template: &str,
555 data: &T,
556 theme: &Theme,
557 mode: OutputMode,
558 spec: Option<&FlatDataSpec>,
559) -> Result<String, RenderError> {
560 if mode.is_structured() {
561 match mode {
562 OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
563 OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
564 OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
565 OutputMode::Csv => {
566 let value = serde_json::to_value(data)?;
567
568 let (headers, rows) = if let Some(s) = spec {
569 let headers = s.extract_header();
571 let rows: Vec<Vec<String>> = match value {
572 serde_json::Value::Array(items) => {
573 items.iter().map(|item| s.extract_row(item)).collect()
574 }
575 _ => vec![s.extract_row(&value)],
576 };
577 (headers, rows)
578 } else {
579 crate::util::flatten_json_for_csv(&value)
581 };
582
583 let mut wtr = csv::Writer::from_writer(Vec::new());
584 wtr.write_record(&headers)?;
585 for row in rows {
586 wtr.write_record(&row)?;
587 }
588 let bytes = wtr.into_inner()?;
589 Ok(String::from_utf8(bytes)?)
590 }
591 _ => unreachable!("is_structured() returned true for non-structured mode"),
592 }
593 } else {
594 render_with_output(template, data, theme, mode)
595 }
596}
597
598pub fn render_with_context<T: Serialize>(
663 template: &str,
664 data: &T,
665 theme: &Theme,
666 mode: OutputMode,
667 context_registry: &ContextRegistry,
668 render_context: &RenderContext,
669 template_registry: Option<&super::TemplateRegistry>,
670) -> Result<String, RenderError> {
671 let color_mode = detect_color_mode();
672 let styles = theme.resolve_styles(Some(color_mode));
673
674 styles
676 .validate()
677 .map_err(|e| RenderError::StyleError(e.to_string()))?;
678
679 let mut engine = MiniJinjaEngine::new();
680
681 let template_content = if let Some(registry) = template_registry {
685 if let Ok(content) = registry.get_content(template) {
686 content
687 } else {
688 template.to_string()
689 }
690 } else {
691 template.to_string()
692 };
693
694 if let Some(registry) = template_registry {
696 for name in registry.names() {
697 if let Ok(content) = registry.get_content(name) {
698 engine.add_template(name, &content)?;
699 }
700 }
701 }
702
703 let icon_context = build_icon_context(theme);
705 let context = build_combined_context(data, context_registry, render_context, icon_context)?;
706
707 let data_value = serde_json::to_value(data)?;
709 let template_output = engine.render_with_context(&template_content, &data_value, context)?;
710
711 let final_output = apply_style_tags(&template_output, &styles, mode);
713
714 Ok(final_output)
715}
716
717pub fn render_auto_with_context<T: Serialize>(
784 template: &str,
785 data: &T,
786 theme: &Theme,
787 mode: OutputMode,
788 context_registry: &ContextRegistry,
789 render_context: &RenderContext,
790 template_registry: Option<&super::TemplateRegistry>,
791) -> Result<String, RenderError> {
792 if mode.is_structured() {
793 match mode {
794 OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
795 OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
796 OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
797 OutputMode::Csv => {
798 let value = serde_json::to_value(data)?;
799 let (headers, rows) = crate::util::flatten_json_for_csv(&value);
800
801 let mut wtr = csv::Writer::from_writer(Vec::new());
802 wtr.write_record(&headers)?;
803 for row in rows {
804 wtr.write_record(&row)?;
805 }
806 let bytes = wtr.into_inner()?;
807 Ok(String::from_utf8(bytes)?)
808 }
809 _ => unreachable!("is_structured() returned true for non-structured mode"),
810 }
811 } else {
812 render_with_context(
813 template,
814 data,
815 theme,
816 mode,
817 context_registry,
818 render_context,
819 template_registry,
820 )
821 }
822}
823
824fn build_icon_context(theme: &Theme) -> HashMap<String, serde_json::Value> {
829 if theme.icons().is_empty() {
830 return HashMap::new();
831 }
832 let icon_mode = detect_icon_mode();
833 let resolved = theme.resolve_icons(icon_mode);
834 let mut ctx = HashMap::new();
835 ctx.insert("icons".to_string(), serde_json::to_value(resolved).unwrap());
836 ctx
837}
838
839fn build_combined_context<T: Serialize>(
843 data: &T,
844 context_registry: &ContextRegistry,
845 render_context: &RenderContext,
846 icon_context: HashMap<String, serde_json::Value>,
847) -> Result<HashMap<String, serde_json::Value>, RenderError> {
848 let context_values = context_registry.resolve(render_context);
850
851 let data_value = serde_json::to_value(data)?;
853
854 let mut combined: HashMap<String, serde_json::Value> = icon_context;
856
857 for (key, value) in context_values {
859 let json_val =
863 serde_json::to_value(value).map_err(|e| RenderError::ContextError(e.to_string()))?;
864 combined.insert(key, json_val);
865 }
866
867 if let Some(obj) = data_value.as_object() {
869 for (key, value) in obj {
870 combined.insert(key.clone(), value.clone());
871 }
872 }
873
874 Ok(combined)
875}
876
877pub fn render_auto_with_engine(
882 engine: &dyn super::TemplateEngine,
883 template: &str,
884 data: &serde_json::Value,
885 theme: &Theme,
886 mode: OutputMode,
887 context_registry: &ContextRegistry,
888 render_context: &RenderContext,
889) -> Result<String, RenderError> {
890 if mode.is_structured() {
891 match mode {
892 OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
893 OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
894 OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
895 OutputMode::Csv => {
896 let (headers, rows) = crate::util::flatten_json_for_csv(data);
897
898 let mut wtr = csv::Writer::from_writer(Vec::new());
899 wtr.write_record(&headers)?;
900 for row in rows {
901 wtr.write_record(&row)?;
902 }
903 let bytes = wtr.into_inner()?;
904 Ok(String::from_utf8(bytes)?)
905 }
906 _ => unreachable!("is_structured() returned true for non-structured mode"),
907 }
908 } else {
909 let color_mode = detect_color_mode();
910 let styles = theme.resolve_styles(Some(color_mode));
911
912 styles
914 .validate()
915 .map_err(|e| RenderError::StyleError(e.to_string()))?;
916
917 let icon_context = build_icon_context(theme);
919 let context_map =
920 build_combined_context(data, context_registry, render_context, icon_context)?;
921
922 let combined_value = serde_json::Value::Object(context_map.into_iter().collect());
924
925 let template_output = if engine.has_template(template) {
927 engine.render_named(template, &combined_value)?
928 } else {
929 engine.render_template(template, &combined_value)?
930 };
931
932 let final_output = apply_style_tags(&template_output, &styles, mode);
934
935 Ok(final_output)
936 }
937}
938
939pub fn render_auto_with_engine_split(
949 engine: &dyn super::TemplateEngine,
950 template: &str,
951 data: &serde_json::Value,
952 theme: &Theme,
953 mode: OutputMode,
954 context_registry: &ContextRegistry,
955 render_context: &RenderContext,
956) -> Result<RenderResult, RenderError> {
957 if mode.is_structured() {
958 let output = match mode {
960 OutputMode::Json => serde_json::to_string_pretty(data)?,
961 OutputMode::Yaml => serde_yaml::to_string(data)?,
962 OutputMode::Xml => crate::util::serialize_to_xml(data)?,
963 OutputMode::Csv => {
964 let (headers, rows) = crate::util::flatten_json_for_csv(data);
965
966 let mut wtr = csv::Writer::from_writer(Vec::new());
967 wtr.write_record(&headers)?;
968 for row in rows {
969 wtr.write_record(&row)?;
970 }
971 let bytes = wtr.into_inner()?;
972 String::from_utf8(bytes)?
973 }
974 _ => unreachable!("is_structured() returned true for non-structured mode"),
975 };
976 Ok(RenderResult::plain(output))
977 } else {
978 let color_mode = detect_color_mode();
979 let styles = theme.resolve_styles(Some(color_mode));
980
981 styles
983 .validate()
984 .map_err(|e| RenderError::StyleError(e.to_string()))?;
985
986 let icon_context = build_icon_context(theme);
988 let context_map =
989 build_combined_context(data, context_registry, render_context, icon_context)?;
990
991 let combined_value = serde_json::Value::Object(context_map.into_iter().collect());
993
994 let raw_output = if engine.has_template(template) {
996 engine.render_named(template, &combined_value)?
997 } else {
998 engine.render_template(template, &combined_value)?
999 };
1000
1001 let formatted_output = apply_style_tags(&raw_output, &styles, mode);
1003
1004 let stripped_output = apply_style_tags(&raw_output, &styles, OutputMode::Text);
1006
1007 Ok(RenderResult::new(formatted_output, stripped_output))
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use crate::tabular::{Column, FlatDataSpec, Width};
1015 use crate::Theme;
1016 use console::Style;
1017 use minijinja::Value;
1018 use serde::Serialize;
1019 use serde_json::json;
1020
1021 #[derive(Serialize)]
1022 struct SimpleData {
1023 message: String,
1024 }
1025
1026 #[derive(Serialize)]
1027 struct ListData {
1028 items: Vec<String>,
1029 count: usize,
1030 }
1031
1032 #[test]
1033 fn test_render_with_output_text_no_ansi() {
1034 let theme = Theme::new().add("red", Style::new().red());
1035 let data = SimpleData {
1036 message: "test".into(),
1037 };
1038
1039 let output = render_with_output(
1040 r#"[red]{{ message }}[/red]"#,
1041 &data,
1042 &theme,
1043 OutputMode::Text,
1044 )
1045 .unwrap();
1046
1047 assert_eq!(output, "test");
1048 assert!(!output.contains("\x1b["));
1049 }
1050
1051 #[test]
1052 fn test_render_with_output_term_has_ansi() {
1053 let theme = Theme::new().add("green", Style::new().green().force_styling(true));
1054 let data = SimpleData {
1055 message: "success".into(),
1056 };
1057
1058 let output = render_with_output(
1059 r#"[green]{{ message }}[/green]"#,
1060 &data,
1061 &theme,
1062 OutputMode::Term,
1063 )
1064 .unwrap();
1065
1066 assert!(output.contains("success"));
1067 assert!(output.contains("\x1b["));
1068 }
1069
1070 #[test]
1071 fn test_render_unknown_style_shows_indicator() {
1072 let theme = Theme::new();
1073 let data = SimpleData {
1074 message: "hello".into(),
1075 };
1076
1077 let output = render_with_output(
1078 r#"[unknown]{{ message }}[/unknown]"#,
1079 &data,
1080 &theme,
1081 OutputMode::Term,
1082 )
1083 .unwrap();
1084
1085 assert_eq!(output, "[unknown?]hello[/unknown?]");
1087 }
1088
1089 #[test]
1090 fn test_render_unknown_style_stripped_in_text_mode() {
1091 let theme = Theme::new();
1092 let data = SimpleData {
1093 message: "hello".into(),
1094 };
1095
1096 let output = render_with_output(
1097 r#"[unknown]{{ message }}[/unknown]"#,
1098 &data,
1099 &theme,
1100 OutputMode::Text,
1101 )
1102 .unwrap();
1103
1104 assert_eq!(output, "hello");
1106 }
1107
1108 #[test]
1109 fn test_render_template_with_loop() {
1110 let theme = Theme::new().add("item", Style::new().cyan());
1111 let data = ListData {
1112 items: vec!["one".into(), "two".into()],
1113 count: 2,
1114 };
1115
1116 let template = r#"{% for item in items %}[item]{{ item }}[/item]
1117{% endfor %}"#;
1118
1119 let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1120 assert_eq!(output, "one\ntwo\n");
1121 }
1122
1123 #[test]
1124 fn test_render_mixed_styled_and_plain() {
1125 let theme = Theme::new().add("count", Style::new().bold());
1126 let data = ListData {
1127 items: vec![],
1128 count: 42,
1129 };
1130
1131 let template = r#"Total: [count]{{ count }}[/count] items"#;
1132 let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1133
1134 assert_eq!(output, "Total: 42 items");
1135 }
1136
1137 #[test]
1138 fn test_render_literal_string_styled() {
1139 let theme = Theme::new().add("header", Style::new().bold());
1140
1141 #[derive(Serialize)]
1142 struct Empty {}
1143
1144 let output = render_with_output(
1145 r#"[header]Header[/header]"#,
1146 &Empty {},
1147 &theme,
1148 OutputMode::Text,
1149 )
1150 .unwrap();
1151
1152 assert_eq!(output, "Header");
1153 }
1154
1155 #[test]
1156 fn test_empty_template() {
1157 let theme = Theme::new();
1158
1159 #[derive(Serialize)]
1160 struct Empty {}
1161
1162 let output = render_with_output("", &Empty {}, &theme, OutputMode::Text).unwrap();
1163 assert_eq!(output, "");
1164 }
1165
1166 #[test]
1167 fn test_template_syntax_error() {
1168 let theme = Theme::new();
1169
1170 #[derive(Serialize)]
1171 struct Empty {}
1172
1173 let result = render_with_output("{{ unclosed", &Empty {}, &theme, OutputMode::Text);
1174 assert!(result.is_err());
1175 }
1176
1177 #[test]
1178 fn test_style_tag_with_nested_data() {
1179 #[derive(Serialize)]
1180 struct Item {
1181 name: String,
1182 value: i32,
1183 }
1184
1185 #[derive(Serialize)]
1186 struct Container {
1187 items: Vec<Item>,
1188 }
1189
1190 let theme = Theme::new().add("name", Style::new().bold());
1191 let data = Container {
1192 items: vec![
1193 Item {
1194 name: "foo".into(),
1195 value: 1,
1196 },
1197 Item {
1198 name: "bar".into(),
1199 value: 2,
1200 },
1201 ],
1202 };
1203
1204 let template = r#"{% for item in items %}[name]{{ item.name }}[/name]={{ item.value }}
1205{% endfor %}"#;
1206
1207 let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1208 assert_eq!(output, "foo=1\nbar=2\n");
1209 }
1210
1211 #[test]
1212 fn test_render_with_output_term_debug() {
1213 let theme = Theme::new()
1214 .add("title", Style::new().bold())
1215 .add("count", Style::new().cyan());
1216
1217 #[derive(Serialize)]
1218 struct Data {
1219 name: String,
1220 value: usize,
1221 }
1222
1223 let data = Data {
1224 name: "Test".into(),
1225 value: 42,
1226 };
1227
1228 let output = render_with_output(
1229 r#"[title]{{ name }}[/title]: [count]{{ value }}[/count]"#,
1230 &data,
1231 &theme,
1232 OutputMode::TermDebug,
1233 )
1234 .unwrap();
1235
1236 assert_eq!(output, "[title]Test[/title]: [count]42[/count]");
1237 }
1238
1239 #[test]
1240 fn test_render_with_output_term_debug_preserves_tags() {
1241 let theme = Theme::new().add("known", Style::new().bold());
1242
1243 #[derive(Serialize)]
1244 struct Data {
1245 message: String,
1246 }
1247
1248 let data = Data {
1249 message: "hello".into(),
1250 };
1251
1252 let output = render_with_output(
1254 r#"[unknown]{{ message }}[/unknown]"#,
1255 &data,
1256 &theme,
1257 OutputMode::TermDebug,
1258 )
1259 .unwrap();
1260
1261 assert_eq!(output, "[unknown]hello[/unknown]");
1262
1263 let output = render_with_output(
1265 r#"[known]{{ message }}[/known]"#,
1266 &data,
1267 &theme,
1268 OutputMode::TermDebug,
1269 )
1270 .unwrap();
1271
1272 assert_eq!(output, "[known]hello[/known]");
1273 }
1274
1275 #[test]
1276 fn test_render_auto_json_mode() {
1277 use serde_json::json;
1278
1279 let theme = Theme::new();
1280 let data = json!({"name": "test", "count": 42});
1281
1282 let output = render_auto("unused template", &data, &theme, OutputMode::Json).unwrap();
1283
1284 assert!(output.contains("\"name\": \"test\""));
1285 assert!(output.contains("\"count\": 42"));
1286 }
1287
1288 #[test]
1289 fn test_render_auto_text_mode_uses_template() {
1290 use serde_json::json;
1291
1292 let theme = Theme::new();
1293 let data = json!({"name": "test"});
1294
1295 let output = render_auto("Name: {{ name }}", &data, &theme, OutputMode::Text).unwrap();
1296
1297 assert_eq!(output, "Name: test");
1298 }
1299
1300 #[test]
1301 fn test_render_auto_term_mode_uses_template() {
1302 use serde_json::json;
1303
1304 let theme = Theme::new().add("bold", Style::new().bold().force_styling(true));
1305 let data = json!({"name": "test"});
1306
1307 let output = render_auto(
1308 r#"[bold]{{ name }}[/bold]"#,
1309 &data,
1310 &theme,
1311 OutputMode::Term,
1312 )
1313 .unwrap();
1314
1315 assert!(output.contains("\x1b[1m"));
1316 assert!(output.contains("test"));
1317 }
1318
1319 #[test]
1320 fn test_render_auto_json_with_struct() {
1321 #[derive(Serialize)]
1322 struct Report {
1323 title: String,
1324 items: Vec<String>,
1325 }
1326
1327 let theme = Theme::new();
1328 let data = Report {
1329 title: "Summary".into(),
1330 items: vec!["one".into(), "two".into()],
1331 };
1332
1333 let output = render_auto("unused", &data, &theme, OutputMode::Json).unwrap();
1334
1335 assert!(output.contains("\"title\": \"Summary\""));
1336 assert!(output.contains("\"items\""));
1337 assert!(output.contains("\"one\""));
1338 }
1339
1340 #[test]
1341 fn test_render_with_alias() {
1342 let theme = Theme::new()
1343 .add("base", Style::new().bold())
1344 .add("alias", "base");
1345
1346 let output = render_with_output(
1347 r#"[alias]text[/alias]"#,
1348 &serde_json::json!({}),
1349 &theme,
1350 OutputMode::Text,
1351 )
1352 .unwrap();
1353
1354 assert_eq!(output, "text");
1355 }
1356
1357 #[test]
1358 fn test_render_with_alias_chain() {
1359 let theme = Theme::new()
1360 .add("muted", Style::new().dim())
1361 .add("disabled", "muted")
1362 .add("timestamp", "disabled");
1363
1364 let output = render_with_output(
1365 r#"[timestamp]12:00[/timestamp]"#,
1366 &serde_json::json!({}),
1367 &theme,
1368 OutputMode::Text,
1369 )
1370 .unwrap();
1371
1372 assert_eq!(output, "12:00");
1373 }
1374
1375 #[test]
1376 fn test_render_fails_with_dangling_alias() {
1377 let theme = Theme::new().add("orphan", "missing");
1378
1379 let result = render_with_output(
1380 r#"[orphan]text[/orphan]"#,
1381 &serde_json::json!({}),
1382 &theme,
1383 OutputMode::Text,
1384 );
1385
1386 assert!(result.is_err());
1387 let err = result.unwrap_err();
1388 assert!(err.to_string().contains("orphan"));
1389 assert!(err.to_string().contains("missing"));
1390 }
1391
1392 #[test]
1393 fn test_render_fails_with_cycle() {
1394 let theme = Theme::new().add("a", "b").add("b", "a");
1395
1396 let result = render_with_output(
1397 r#"[a]text[/a]"#,
1398 &serde_json::json!({}),
1399 &theme,
1400 OutputMode::Text,
1401 );
1402
1403 assert!(result.is_err());
1404 assert!(result.unwrap_err().to_string().contains("cycle"));
1405 }
1406
1407 #[test]
1408 fn test_three_layer_styling_pattern() {
1409 let theme = Theme::new()
1410 .add("dim_style", Style::new().dim())
1411 .add("cyan_bold", Style::new().cyan().bold())
1412 .add("yellow_bg", Style::new().on_yellow())
1413 .add("muted", "dim_style")
1414 .add("accent", "cyan_bold")
1415 .add("highlighted", "yellow_bg")
1416 .add("timestamp", "muted")
1417 .add("title", "accent")
1418 .add("selected_item", "highlighted");
1419
1420 assert!(theme.validate().is_ok());
1421
1422 let output = render_with_output(
1423 r#"[timestamp]{{ time }}[/timestamp] - [title]{{ name }}[/title]"#,
1424 &serde_json::json!({"time": "12:00", "name": "Report"}),
1425 &theme,
1426 OutputMode::Text,
1427 )
1428 .unwrap();
1429
1430 assert_eq!(output, "12:00 - Report");
1431 }
1432
1433 #[test]
1438 fn test_render_auto_yaml_mode() {
1439 use serde_json::json;
1440
1441 let theme = Theme::new();
1442 let data = json!({"name": "test", "count": 42});
1443
1444 let output = render_auto("unused template", &data, &theme, OutputMode::Yaml).unwrap();
1445
1446 assert!(output.contains("name: test"));
1447 assert!(output.contains("count: 42"));
1448 }
1449
1450 #[test]
1451 fn test_render_auto_xml_mode_named_struct() {
1452 let theme = Theme::new();
1453
1454 #[derive(Serialize)]
1455 #[serde(rename = "root")]
1456 struct Data {
1457 name: String,
1458 count: usize,
1459 }
1460
1461 let data = Data {
1462 name: "test".into(),
1463 count: 42,
1464 };
1465
1466 let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1467
1468 assert!(output.contains("<root>"));
1469 assert!(output.contains("<name>test</name>"));
1470 }
1471
1472 #[test]
1473 fn test_render_auto_xml_mode_json_map() {
1474 use serde_json::json;
1475
1476 let theme = Theme::new();
1477 let data = json!({"name": "test", "count": 42});
1478
1479 let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1480
1481 assert!(output.contains("<data>"));
1482 assert!(output.contains("<name>test</name>"));
1483 assert!(output.contains("<count>42</count>"));
1484 }
1485
1486 #[test]
1487 fn test_render_auto_xml_mode_nested_map() {
1488 use serde_json::json;
1489
1490 let theme = Theme::new();
1491 let data = json!({"user": {"name": "Alice", "age": 30}});
1492
1493 let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1494
1495 assert!(output.contains("<data>"));
1496 assert!(output.contains("<user>"));
1497 assert!(output.contains("<name>Alice</name>"));
1498 }
1499
1500 #[test]
1501 fn test_render_auto_xml_mode_with_array() {
1502 use serde_json::json;
1503
1504 let theme = Theme::new();
1505 let data = json!({"items": ["a", "b", "c"]});
1506
1507 let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1508
1509 assert!(output.contains("<data>"));
1510 assert!(output.contains("<items>a</items>"));
1511 }
1512
1513 #[test]
1514 fn test_render_auto_csv_mode_auto_flatten() {
1515 use serde_json::json;
1516
1517 let theme = Theme::new();
1518 let data = json!([
1519 {"name": "Alice", "stats": {"score": 10}},
1520 {"name": "Bob", "stats": {"score": 20}}
1521 ]);
1522
1523 let output = render_auto("unused", &data, &theme, OutputMode::Csv).unwrap();
1524
1525 assert!(output.contains("name,stats.score"));
1526 assert!(output.contains("Alice,10"));
1527 assert!(output.contains("Bob,20"));
1528 }
1529
1530 #[test]
1531 fn test_render_auto_csv_mode_with_spec() {
1532 let theme = Theme::new();
1533 let data = json!([
1534 {"name": "Alice", "meta": {"age": 30, "role": "admin"}},
1535 {"name": "Bob", "meta": {"age": 25, "role": "user"}}
1536 ]);
1537
1538 let spec = FlatDataSpec::builder()
1539 .column(Column::new(Width::Fixed(10)).key("name"))
1540 .column(
1541 Column::new(Width::Fixed(10))
1542 .key("meta.role")
1543 .header("Role"),
1544 )
1545 .build();
1546
1547 let output =
1548 render_auto_with_spec("unused", &data, &theme, OutputMode::Csv, Some(&spec)).unwrap();
1549
1550 let lines: Vec<&str> = output.lines().collect();
1551 assert_eq!(lines[0], "name,Role");
1552 assert!(lines.contains(&"Alice,admin"));
1553 assert!(lines.contains(&"Bob,user"));
1554 assert!(!output.contains("30"));
1555 }
1556
1557 #[test]
1558 fn test_render_auto_csv_mode_with_array_field() {
1559 use serde_json::json;
1560
1561 let theme = Theme::new();
1562 let data = json!([
1563 {"name": "Alice", "tags": ["admin", "user"]},
1564 {"name": "Bob", "tags": ["user"]}
1565 ]);
1566
1567 let output = render_auto("unused", &data, &theme, OutputMode::Csv).unwrap();
1568
1569 assert!(output.contains("tags.0"));
1571 assert!(output.contains("tags.1"));
1572 assert!(output.contains("admin"));
1573 assert!(output.contains("user"));
1574 assert!(!output.contains("[\""));
1576 }
1577
1578 #[test]
1583 fn test_render_with_context_basic() {
1584 use crate::context::{ContextRegistry, RenderContext};
1585
1586 #[derive(Serialize)]
1587 struct Data {
1588 name: String,
1589 }
1590
1591 let theme = Theme::new();
1592 let data = Data {
1593 name: "Alice".into(),
1594 };
1595 let json_data = serde_json::to_value(&data).unwrap();
1596
1597 let mut registry = ContextRegistry::new();
1598 registry.add_static("version", Value::from("1.0.0"));
1599
1600 let render_ctx = RenderContext::new(OutputMode::Text, Some(80), &theme, &json_data);
1601
1602 let output = render_with_context(
1603 "{{ name }} (v{{ version }})",
1604 &data,
1605 &theme,
1606 OutputMode::Text,
1607 ®istry,
1608 &render_ctx,
1609 None,
1610 )
1611 .unwrap();
1612
1613 assert_eq!(output, "Alice (v1.0.0)");
1614 }
1615
1616 #[test]
1617 fn test_render_with_context_dynamic_provider() {
1618 use crate::context::{ContextRegistry, RenderContext};
1619
1620 #[derive(Serialize)]
1621 struct Data {
1622 message: String,
1623 }
1624
1625 let theme = Theme::new();
1626 let data = Data {
1627 message: "Hello".into(),
1628 };
1629 let json_data = serde_json::to_value(&data).unwrap();
1630
1631 let mut registry = ContextRegistry::new();
1632 registry.add_provider("terminal_width", |ctx: &RenderContext| {
1633 Value::from(ctx.terminal_width.unwrap_or(80))
1634 });
1635
1636 let render_ctx = RenderContext::new(OutputMode::Text, Some(120), &theme, &json_data);
1637
1638 let output = render_with_context(
1639 "{{ message }} (width={{ terminal_width }})",
1640 &data,
1641 &theme,
1642 OutputMode::Text,
1643 ®istry,
1644 &render_ctx,
1645 None,
1646 )
1647 .unwrap();
1648
1649 assert_eq!(output, "Hello (width=120)");
1650 }
1651
1652 #[test]
1653 fn test_render_with_context_data_takes_precedence() {
1654 use crate::context::{ContextRegistry, RenderContext};
1655
1656 #[derive(Serialize)]
1657 struct Data {
1658 value: String,
1659 }
1660
1661 let theme = Theme::new();
1662 let data = Data {
1663 value: "from_data".into(),
1664 };
1665 let json_data = serde_json::to_value(&data).unwrap();
1666
1667 let mut registry = ContextRegistry::new();
1668 registry.add_static("value", Value::from("from_context"));
1669
1670 let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1671
1672 let output = render_with_context(
1673 "{{ value }}",
1674 &data,
1675 &theme,
1676 OutputMode::Text,
1677 ®istry,
1678 &render_ctx,
1679 None,
1680 )
1681 .unwrap();
1682
1683 assert_eq!(output, "from_data");
1684 }
1685
1686 #[test]
1687 fn test_render_with_context_empty_registry() {
1688 use crate::context::{ContextRegistry, RenderContext};
1689
1690 #[derive(Serialize)]
1691 struct Data {
1692 name: String,
1693 }
1694
1695 let theme = Theme::new();
1696 let data = Data {
1697 name: "Test".into(),
1698 };
1699 let json_data = serde_json::to_value(&data).unwrap();
1700
1701 let registry = ContextRegistry::new();
1702 let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1703
1704 let output = render_with_context(
1705 "{{ name }}",
1706 &data,
1707 &theme,
1708 OutputMode::Text,
1709 ®istry,
1710 &render_ctx,
1711 None,
1712 )
1713 .unwrap();
1714
1715 assert_eq!(output, "Test");
1716 }
1717
1718 #[test]
1719 fn test_render_auto_with_context_json_mode() {
1720 use crate::context::{ContextRegistry, RenderContext};
1721
1722 #[derive(Serialize)]
1723 struct Data {
1724 count: usize,
1725 }
1726
1727 let theme = Theme::new();
1728 let data = Data { count: 42 };
1729 let json_data = serde_json::to_value(&data).unwrap();
1730
1731 let mut registry = ContextRegistry::new();
1732 registry.add_static("extra", Value::from("ignored"));
1733
1734 let render_ctx = RenderContext::new(OutputMode::Json, None, &theme, &json_data);
1735
1736 let output = render_auto_with_context(
1737 "unused template {{ extra }}",
1738 &data,
1739 &theme,
1740 OutputMode::Json,
1741 ®istry,
1742 &render_ctx,
1743 None,
1744 )
1745 .unwrap();
1746
1747 assert!(output.contains("\"count\": 42"));
1748 assert!(!output.contains("ignored"));
1749 }
1750
1751 #[test]
1752 fn test_render_auto_with_context_text_mode() {
1753 use crate::context::{ContextRegistry, RenderContext};
1754
1755 #[derive(Serialize)]
1756 struct Data {
1757 count: usize,
1758 }
1759
1760 let theme = Theme::new();
1761 let data = Data { count: 42 };
1762 let json_data = serde_json::to_value(&data).unwrap();
1763
1764 let mut registry = ContextRegistry::new();
1765 registry.add_static("label", Value::from("Items"));
1766
1767 let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1768
1769 let output = render_auto_with_context(
1770 "{{ label }}: {{ count }}",
1771 &data,
1772 &theme,
1773 OutputMode::Text,
1774 ®istry,
1775 &render_ctx,
1776 None,
1777 )
1778 .unwrap();
1779
1780 assert_eq!(output, "Items: 42");
1781 }
1782
1783 #[test]
1784 fn test_render_with_context_provider_uses_output_mode() {
1785 use crate::context::{ContextRegistry, RenderContext};
1786
1787 #[derive(Serialize)]
1788 struct Data {}
1789
1790 let theme = Theme::new();
1791 let data = Data {};
1792 let json_data = serde_json::to_value(&data).unwrap();
1793
1794 let mut registry = ContextRegistry::new();
1795 registry.add_provider("mode", |ctx: &RenderContext| {
1796 Value::from(format!("{:?}", ctx.output_mode))
1797 });
1798
1799 let render_ctx = RenderContext::new(OutputMode::Term, None, &theme, &json_data);
1800
1801 let output = render_with_context(
1802 "Mode: {{ mode }}",
1803 &data,
1804 &theme,
1805 OutputMode::Term,
1806 ®istry,
1807 &render_ctx,
1808 None,
1809 )
1810 .unwrap();
1811
1812 assert_eq!(output, "Mode: Term");
1813 }
1814
1815 #[test]
1816 fn test_render_with_context_nested_data() {
1817 use crate::context::{ContextRegistry, RenderContext};
1818
1819 #[derive(Serialize)]
1820 struct Item {
1821 name: String,
1822 }
1823
1824 #[derive(Serialize)]
1825 struct Data {
1826 items: Vec<Item>,
1827 }
1828
1829 let theme = Theme::new();
1830 let data = Data {
1831 items: vec![Item { name: "one".into() }, Item { name: "two".into() }],
1832 };
1833 let json_data = serde_json::to_value(&data).unwrap();
1834
1835 let mut registry = ContextRegistry::new();
1836 registry.add_static("prefix", Value::from("- "));
1837
1838 let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1839
1840 let output = render_with_context(
1841 "{% for item in items %}{{ prefix }}{{ item.name }}\n{% endfor %}",
1842 &data,
1843 &theme,
1844 OutputMode::Text,
1845 ®istry,
1846 &render_ctx,
1847 None,
1848 )
1849 .unwrap();
1850
1851 assert_eq!(output, "- one\n- two\n");
1852 }
1853
1854 #[test]
1855 fn test_render_with_mode_forces_color_mode() {
1856 use console::Style;
1857
1858 #[derive(Serialize)]
1859 struct Data {
1860 status: String,
1861 }
1862
1863 let theme = Theme::new().add_adaptive(
1866 "status",
1867 Style::new(), Some(Style::new().black().force_styling(true)), Some(Style::new().white().force_styling(true)), );
1871
1872 let data = Data {
1873 status: "test".into(),
1874 };
1875
1876 let dark_output = render_with_mode(
1878 r#"[status]{{ status }}[/status]"#,
1879 &data,
1880 &theme,
1881 OutputMode::Term,
1882 ColorMode::Dark,
1883 )
1884 .unwrap();
1885
1886 let light_output = render_with_mode(
1888 r#"[status]{{ status }}[/status]"#,
1889 &data,
1890 &theme,
1891 OutputMode::Term,
1892 ColorMode::Light,
1893 )
1894 .unwrap();
1895
1896 assert_ne!(dark_output, light_output);
1898
1899 assert!(
1901 dark_output.contains("\x1b[37"),
1902 "Expected white (37) in dark mode"
1903 );
1904
1905 assert!(
1907 light_output.contains("\x1b[30"),
1908 "Expected black (30) in light mode"
1909 );
1910 }
1911
1912 #[test]
1917 fn test_tag_syntax_text_mode() {
1918 let theme = Theme::new().add("title", Style::new().bold());
1919
1920 #[derive(Serialize)]
1921 struct Data {
1922 name: String,
1923 }
1924
1925 let output = render_with_output(
1926 "[title]{{ name }}[/title]",
1927 &Data {
1928 name: "Hello".into(),
1929 },
1930 &theme,
1931 OutputMode::Text,
1932 )
1933 .unwrap();
1934
1935 assert_eq!(output, "Hello");
1937 }
1938
1939 #[test]
1940 fn test_tag_syntax_term_mode() {
1941 let theme = Theme::new().add("bold", Style::new().bold().force_styling(true));
1942
1943 #[derive(Serialize)]
1944 struct Data {
1945 name: String,
1946 }
1947
1948 let output = render_with_output(
1949 "[bold]{{ name }}[/bold]",
1950 &Data {
1951 name: "Hello".into(),
1952 },
1953 &theme,
1954 OutputMode::Term,
1955 )
1956 .unwrap();
1957
1958 assert!(output.contains("\x1b[1m"));
1960 assert!(output.contains("Hello"));
1961 }
1962
1963 #[test]
1964 fn test_tag_syntax_debug_mode() {
1965 let theme = Theme::new().add("title", Style::new().bold());
1966
1967 #[derive(Serialize)]
1968 struct Data {
1969 name: String,
1970 }
1971
1972 let output = render_with_output(
1973 "[title]{{ name }}[/title]",
1974 &Data {
1975 name: "Hello".into(),
1976 },
1977 &theme,
1978 OutputMode::TermDebug,
1979 )
1980 .unwrap();
1981
1982 assert_eq!(output, "[title]Hello[/title]");
1984 }
1985
1986 #[test]
1987 fn test_tag_syntax_unknown_tag_passthrough() {
1988 let theme = Theme::new().add("known", Style::new().bold());
1990
1991 #[derive(Serialize)]
1992 struct Data {
1993 name: String,
1994 }
1995
1996 let output = render_with_output(
1998 "[unknown]{{ name }}[/unknown]",
1999 &Data {
2000 name: "Hello".into(),
2001 },
2002 &theme,
2003 OutputMode::Term,
2004 )
2005 .unwrap();
2006
2007 assert!(output.contains("[unknown?]"));
2009 assert!(output.contains("[/unknown?]"));
2010 assert!(output.contains("Hello"));
2011
2012 let text_output = render_with_output(
2014 "[unknown]{{ name }}[/unknown]",
2015 &Data {
2016 name: "Hello".into(),
2017 },
2018 &theme,
2019 OutputMode::Text,
2020 )
2021 .unwrap();
2022
2023 assert_eq!(text_output, "Hello");
2025 }
2026
2027 #[test]
2028 fn test_tag_syntax_nested() {
2029 let theme = Theme::new()
2030 .add("bold", Style::new().bold().force_styling(true))
2031 .add("red", Style::new().red().force_styling(true));
2032
2033 #[derive(Serialize)]
2034 struct Data {
2035 word: String,
2036 }
2037
2038 let output = render_with_output(
2039 "[bold][red]{{ word }}[/red][/bold]",
2040 &Data {
2041 word: "test".into(),
2042 },
2043 &theme,
2044 OutputMode::Term,
2045 )
2046 .unwrap();
2047
2048 assert!(output.contains("\x1b[1m")); assert!(output.contains("\x1b[31m")); assert!(output.contains("test"));
2052 }
2053
2054 #[test]
2055 fn test_tag_syntax_multiple_styles() {
2056 let theme = Theme::new()
2057 .add("title", Style::new().bold())
2058 .add("count", Style::new().cyan());
2059
2060 #[derive(Serialize)]
2061 struct Data {
2062 name: String,
2063 num: usize,
2064 }
2065
2066 let output = render_with_output(
2067 r#"[title]{{ name }}[/title]: [count]{{ num }}[/count]"#,
2068 &Data {
2069 name: "Items".into(),
2070 num: 42,
2071 },
2072 &theme,
2073 OutputMode::Text,
2074 )
2075 .unwrap();
2076
2077 assert_eq!(output, "Items: 42");
2078 }
2079
2080 #[test]
2081 fn test_tag_syntax_in_loop() {
2082 let theme = Theme::new().add("item", Style::new().cyan());
2083
2084 #[derive(Serialize)]
2085 struct Data {
2086 items: Vec<String>,
2087 }
2088
2089 let output = render_with_output(
2090 "{% for item in items %}[item]{{ item }}[/item]\n{% endfor %}",
2091 &Data {
2092 items: vec!["one".into(), "two".into()],
2093 },
2094 &theme,
2095 OutputMode::Text,
2096 )
2097 .unwrap();
2098
2099 assert_eq!(output, "one\ntwo\n");
2100 }
2101
2102 #[test]
2103 fn test_tag_syntax_literal_brackets() {
2104 let theme = Theme::new();
2106
2107 #[derive(Serialize)]
2108 struct Data {
2109 msg: String,
2110 }
2111
2112 let output = render_with_output(
2113 "Array: [1, 2, 3] and {{ msg }}",
2114 &Data { msg: "done".into() },
2115 &theme,
2116 OutputMode::Text,
2117 )
2118 .unwrap();
2119
2120 assert_eq!(output, "Array: [1, 2, 3] and done");
2122 }
2123
2124 #[test]
2129 fn test_validate_template_all_known_tags() {
2130 let theme = Theme::new()
2131 .add("title", Style::new().bold())
2132 .add("count", Style::new().cyan());
2133
2134 #[derive(Serialize)]
2135 struct Data {
2136 name: String,
2137 }
2138
2139 let result = validate_template(
2140 "[title]{{ name }}[/title]",
2141 &Data {
2142 name: "Hello".into(),
2143 },
2144 &theme,
2145 );
2146
2147 assert!(result.is_ok());
2148 }
2149
2150 #[test]
2151 fn test_validate_template_unknown_tag_fails() {
2152 let theme = Theme::new().add("known", Style::new().bold());
2153
2154 #[derive(Serialize)]
2155 struct Data {
2156 name: String,
2157 }
2158
2159 let result = validate_template(
2160 "[unknown]{{ name }}[/unknown]",
2161 &Data {
2162 name: "Hello".into(),
2163 },
2164 &theme,
2165 );
2166
2167 assert!(result.is_err());
2168 let err = result.unwrap_err();
2169 let errors = err
2170 .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2171 .expect("Expected UnknownTagErrors");
2172 assert_eq!(errors.len(), 2); }
2174
2175 #[test]
2176 fn test_validate_template_multiple_unknown_tags() {
2177 let theme = Theme::new().add("known", Style::new().bold());
2178
2179 #[derive(Serialize)]
2180 struct Data {
2181 a: String,
2182 b: String,
2183 }
2184
2185 let result = validate_template(
2186 "[foo]{{ a }}[/foo] and [bar]{{ b }}[/bar]",
2187 &Data {
2188 a: "x".into(),
2189 b: "y".into(),
2190 },
2191 &theme,
2192 );
2193
2194 assert!(result.is_err());
2195 let err = result.unwrap_err();
2196 let errors = err
2197 .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2198 .expect("Expected UnknownTagErrors");
2199 assert_eq!(errors.len(), 4); }
2201
2202 #[test]
2203 fn test_validate_template_plain_text_passes() {
2204 let theme = Theme::new();
2205
2206 #[derive(Serialize)]
2207 struct Data {
2208 msg: String,
2209 }
2210
2211 let result = validate_template("Just plain {{ msg }}", &Data { msg: "hi".into() }, &theme);
2212
2213 assert!(result.is_ok());
2214 }
2215
2216 #[test]
2217 fn test_validate_template_mixed_known_and_unknown() {
2218 let theme = Theme::new().add("known", Style::new().bold());
2219
2220 #[derive(Serialize)]
2221 struct Data {
2222 a: String,
2223 b: String,
2224 }
2225
2226 let result = validate_template(
2227 "[known]{{ a }}[/known] [unknown]{{ b }}[/unknown]",
2228 &Data {
2229 a: "x".into(),
2230 b: "y".into(),
2231 },
2232 &theme,
2233 );
2234
2235 assert!(result.is_err());
2236 let err = result.unwrap_err();
2237 let errors = err
2238 .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2239 .expect("Expected UnknownTagErrors");
2240 assert_eq!(errors.len(), 2);
2242 assert!(errors.errors.iter().any(|e| e.tag == "unknown"));
2243 }
2244
2245 #[test]
2246 fn test_validate_template_syntax_error_fails() {
2247 let theme = Theme::new();
2248 #[derive(Serialize)]
2249 struct Data {}
2250
2251 let result = validate_template("{{ unclosed", &Data {}, &theme);
2253 assert!(result.is_err());
2254
2255 let err = result.unwrap_err();
2256 assert!(err
2258 .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2259 .is_none());
2260 let msg = err.to_string();
2262 assert!(
2263 msg.contains("syntax error") || msg.contains("unexpected"),
2264 "Got: {}",
2265 msg
2266 );
2267 }
2268
2269 #[test]
2270 fn test_render_auto_with_context_yaml_mode() {
2271 use crate::context::{ContextRegistry, RenderContext};
2272 use serde_json::json;
2273
2274 let theme = Theme::new();
2275 let data = json!({"name": "test", "count": 42});
2276
2277 let registry = ContextRegistry::new();
2279 let render_ctx = RenderContext::new(OutputMode::Yaml, Some(80), &theme, &data);
2280
2281 let output = render_auto_with_context(
2283 "unused template",
2284 &data,
2285 &theme,
2286 OutputMode::Yaml,
2287 ®istry,
2288 &render_ctx,
2289 None,
2290 )
2291 .unwrap();
2292
2293 assert!(output.contains("name: test"));
2294 assert!(output.contains("count: 42"));
2295 }
2296
2297 #[test]
2302 #[serial_test::serial]
2303 fn test_render_with_icons_classic() {
2304 use crate::{set_icon_detector, IconDefinition, IconMode};
2305
2306 set_icon_detector(|| IconMode::Classic);
2307
2308 let theme = Theme::new()
2309 .add_icon(
2310 "check",
2311 IconDefinition::new("[ok]").with_nerdfont("\u{f00c}"),
2312 )
2313 .add_icon("arrow", IconDefinition::new(">>"));
2314
2315 let data = SimpleData {
2316 message: "done".into(),
2317 };
2318
2319 let output = render_with_output(
2320 "{{ icons.check }} {{ message }} {{ icons.arrow }}",
2321 &data,
2322 &theme,
2323 OutputMode::Text,
2324 )
2325 .unwrap();
2326
2327 assert_eq!(output, "[ok] done >>");
2328 }
2329
2330 #[test]
2331 #[serial_test::serial]
2332 fn test_render_with_icons_nerdfont() {
2333 use crate::{set_icon_detector, IconDefinition, IconMode};
2334
2335 set_icon_detector(|| IconMode::NerdFont);
2336
2337 let theme = Theme::new().add_icon(
2338 "check",
2339 IconDefinition::new("[ok]").with_nerdfont("\u{f00c}"),
2340 );
2341
2342 let data = SimpleData {
2343 message: "done".into(),
2344 };
2345
2346 let output = render_with_output(
2347 "{{ icons.check }} {{ message }}",
2348 &data,
2349 &theme,
2350 OutputMode::Text,
2351 )
2352 .unwrap();
2353
2354 assert_eq!(output, "\u{f00c} done");
2355
2356 set_icon_detector(|| IconMode::Classic);
2358 }
2359
2360 #[test]
2361 fn test_render_without_icons_no_overhead() {
2362 let theme = Theme::new();
2363 let data = SimpleData {
2364 message: "hello".into(),
2365 };
2366
2367 let output = render_with_output("{{ message }}", &data, &theme, OutputMode::Text).unwrap();
2369
2370 assert_eq!(output, "hello");
2371 }
2372
2373 #[test]
2374 #[serial_test::serial]
2375 fn test_render_with_icons_and_styles() {
2376 use crate::{set_icon_detector, IconDefinition, IconMode};
2377
2378 set_icon_detector(|| IconMode::Classic);
2379
2380 let theme = Theme::new()
2381 .add("title", Style::new().bold())
2382 .add_icon("bullet", IconDefinition::new("-"));
2383
2384 let data = SimpleData {
2385 message: "item".into(),
2386 };
2387
2388 let output = render_with_output(
2389 "{{ icons.bullet }} [title]{{ message }}[/title]",
2390 &data,
2391 &theme,
2392 OutputMode::Text,
2393 )
2394 .unwrap();
2395
2396 assert_eq!(output, "- item");
2397 }
2398
2399 #[test]
2400 #[serial_test::serial]
2401 fn test_render_with_vars_includes_icons() {
2402 use crate::{set_icon_detector, IconDefinition, IconMode};
2403
2404 set_icon_detector(|| IconMode::Classic);
2405
2406 let theme = Theme::new().add_icon("star", IconDefinition::new("*"));
2407
2408 let data = SimpleData {
2409 message: "hello".into(),
2410 };
2411
2412 let vars = std::collections::HashMap::from([("version", "1.0")]);
2413
2414 let output = render_with_vars(
2415 "{{ icons.star }} {{ message }} v{{ version }}",
2416 &data,
2417 &theme,
2418 OutputMode::Text,
2419 vars,
2420 )
2421 .unwrap();
2422
2423 assert_eq!(output, "* hello v1.0");
2424 }
2425
2426 #[test]
2427 #[serial_test::serial]
2428 fn test_render_with_context_includes_icons() {
2429 use crate::context::{ContextRegistry, RenderContext};
2430 use crate::{set_icon_detector, IconDefinition, IconMode};
2431
2432 set_icon_detector(|| IconMode::Classic);
2433
2434 let theme = Theme::new().add_icon("dot", IconDefinition::new("."));
2435
2436 let data = SimpleData {
2437 message: "test".into(),
2438 };
2439
2440 let mut registry = ContextRegistry::new();
2441 registry.add_static("extra", Value::from("ctx"));
2442
2443 let json_data = serde_json::to_value(&data).unwrap();
2444 let render_ctx = RenderContext::new(OutputMode::Text, Some(80), &theme, &json_data);
2445
2446 let output = render_with_context(
2447 "{{ icons.dot }} {{ message }} {{ extra }}",
2448 &data,
2449 &theme,
2450 OutputMode::Text,
2451 ®istry,
2452 &render_ctx,
2453 None,
2454 )
2455 .unwrap();
2456
2457 assert_eq!(output, ". test ctx");
2458 }
2459
2460 #[test]
2461 #[serial_test::serial]
2462 fn test_render_yaml_from_theme_with_icons() {
2463 use crate::{set_icon_detector, IconDefinition, IconMode};
2464
2465 set_icon_detector(|| IconMode::Classic);
2466
2467 let theme = Theme::from_yaml(
2468 r#"
2469 title:
2470 fg: cyan
2471 bold: true
2472 icons:
2473 check:
2474 classic: "[ok]"
2475 nerdfont: "nf"
2476 "#,
2477 )
2478 .unwrap();
2479
2480 let data = SimpleData {
2481 message: "done".into(),
2482 };
2483
2484 let output = render_with_output(
2485 "{{ icons.check }} [title]{{ message }}[/title]",
2486 &data,
2487 &theme,
2488 OutputMode::Text,
2489 )
2490 .unwrap();
2491
2492 assert_eq!(output, "[ok] done");
2493 }
2494}