1use minijinja::value::{Enumerator, Object, Value};
41use serde::Serialize;
42use serde_json::Value as JsonValue;
43use std::sync::Arc;
44
45use super::resolve::ResolvedWidths;
46use super::traits::TabularRow;
47use super::types::{
48 Align, Anchor, Column, FlatDataSpec, Overflow, SubColumns, TabularSpec, TruncateAt, Width,
49};
50use super::util::{
51 display_width, pad_center, pad_left, pad_right, truncate_end, truncate_middle, truncate_start,
52 visible_width, wrap_indent,
53};
54
55#[derive(Clone, Debug)]
82pub struct TabularFormatter {
83 columns: Vec<Column>,
85 widths: Vec<usize>,
87 separator: String,
89 prefix: String,
91 suffix: String,
93 total_width: usize,
95}
96
97impl TabularFormatter {
98 pub fn new(spec: &FlatDataSpec, total_width: usize) -> Self {
105 let resolved = spec.resolve_widths(total_width);
106 Self::from_resolved_with_width(spec, resolved, total_width)
107 }
108
109 pub fn from_resolved(spec: &FlatDataSpec, resolved: ResolvedWidths) -> Self {
113 let content_width: usize = resolved.widths.iter().sum();
115 let overhead = spec.decorations.overhead(resolved.widths.len());
116 let total_width = content_width + overhead;
117 Self::from_resolved_with_width(spec, resolved, total_width)
118 }
119
120 pub fn from_resolved_with_width(
122 spec: &FlatDataSpec,
123 resolved: ResolvedWidths,
124 total_width: usize,
125 ) -> Self {
126 TabularFormatter {
127 columns: spec.columns.clone(),
128 widths: resolved.widths,
129 separator: spec.decorations.column_sep.clone(),
130 prefix: spec.decorations.row_prefix.clone(),
131 suffix: spec.decorations.row_suffix.clone(),
132 total_width,
133 }
134 }
135
136 pub fn with_widths(columns: Vec<Column>, widths: Vec<usize>) -> Self {
140 let total_width = widths.iter().sum();
141 TabularFormatter {
142 columns,
143 widths,
144 separator: String::new(),
145 prefix: String::new(),
146 suffix: String::new(),
147 total_width,
148 }
149 }
150
151 pub fn from_type<T: super::traits::Tabular>(total_width: usize) -> Self {
173 let spec: TabularSpec = T::tabular_spec();
174 Self::new(&spec, total_width)
175 }
176
177 pub fn total_width(mut self, width: usize) -> Self {
179 self.total_width = width;
180 self
181 }
182
183 pub fn separator(mut self, sep: impl Into<String>) -> Self {
185 self.separator = sep.into();
186 self
187 }
188
189 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
191 self.prefix = prefix.into();
192 self
193 }
194
195 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
197 self.suffix = suffix.into();
198 self
199 }
200
201 pub fn format_row<S: AsRef<str>>(&self, values: &[S]) -> String {
226 if self.columns.iter().any(|c| c.sub_columns.is_some()) {
229 let cell_values: Vec<CellValue<'_>> = values
230 .iter()
231 .map(|s| CellValue::Single(s.as_ref()))
232 .collect();
233 return self.format_row_cells(&cell_values);
234 }
235
236 let mut result = String::new();
237 result.push_str(&self.prefix);
238
239 let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
241
242 for (i, col) in self.columns.iter().enumerate() {
243 if i > 0 {
245 if anchor_gap > 0 && i == anchor_transition {
246 result.push_str(&" ".repeat(anchor_gap));
248 } else {
249 result.push_str(&self.separator);
250 }
251 }
252
253 let width = self.widths.get(i).copied().unwrap_or(0);
254 let value = values.get(i).map(|s| s.as_ref()).unwrap_or(&col.null_repr);
255
256 let formatted = format_cell(value, width, col);
257 result.push_str(&formatted);
258 }
259
260 result.push_str(&self.suffix);
261 result
262 }
263
264 pub fn format_row_cells(&self, values: &[CellValue<'_>]) -> String {
297 let mut result = String::new();
298 result.push_str(&self.prefix);
299
300 let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
301
302 for (i, col) in self.columns.iter().enumerate() {
303 if i > 0 {
304 if anchor_gap > 0 && i == anchor_transition {
305 result.push_str(&" ".repeat(anchor_gap));
306 } else {
307 result.push_str(&self.separator);
308 }
309 }
310
311 let width = self.widths.get(i).copied().unwrap_or(0);
312
313 if let Some(sub_cols) = &col.sub_columns {
314 let sub_values: Vec<&str> = match values.get(i) {
316 Some(CellValue::Sub(v)) => v.clone(),
317 Some(CellValue::Single(s)) => vec![s],
318 None => vec![],
319 };
320 let formatted = format_sub_cells(sub_cols, &sub_values, width);
321 result.push_str(&formatted);
322 } else {
323 let value = match values.get(i) {
325 Some(CellValue::Single(s)) => *s,
326 Some(CellValue::Sub(v)) => v.first().copied().unwrap_or(&col.null_repr),
327 None => &col.null_repr,
328 };
329 let formatted = format_cell(value, width, col);
330 result.push_str(&formatted);
331 }
332 }
333
334 result.push_str(&self.suffix);
335 result
336 }
337
338 fn calculate_anchor_gap(&self) -> (usize, usize) {
344 let transition = self
346 .columns
347 .iter()
348 .position(|c| c.anchor == Anchor::Right)
349 .unwrap_or(self.columns.len());
350
351 if transition == 0 || transition == self.columns.len() {
353 return (0, transition);
354 }
355
356 let prefix_width = display_width(&self.prefix);
358 let suffix_width = display_width(&self.suffix);
359 let sep_width = display_width(&self.separator);
360 let content_width: usize = self.widths.iter().sum();
361 let num_seps = self.columns.len().saturating_sub(1);
362 let current_total = prefix_width + content_width + (num_seps * sep_width) + suffix_width;
363
364 if current_total >= self.total_width {
366 (0, transition)
368 } else {
369 let extra = self.total_width - current_total;
371 (extra + sep_width, transition)
373 }
374 }
375
376 pub fn format_rows<S: AsRef<str>>(&self, rows: &[Vec<S>]) -> Vec<String> {
380 rows.iter().map(|row| self.format_row(row)).collect()
381 }
382
383 pub fn format_row_lines<S: AsRef<str>>(&self, values: &[S]) -> Vec<String> {
404 let cell_outputs: Vec<CellOutput> = self
406 .columns
407 .iter()
408 .enumerate()
409 .map(|(i, col)| {
410 let width = self.widths.get(i).copied().unwrap_or(0);
411 let value = values.get(i).map(|s| s.as_ref()).unwrap_or(&col.null_repr);
412 format_cell_lines(value, width, col)
413 })
414 .collect();
415
416 let max_lines = cell_outputs
418 .iter()
419 .map(|c| c.line_count())
420 .max()
421 .unwrap_or(1);
422
423 if max_lines == 1 {
425 return vec![self.format_row(values)];
426 }
427
428 let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
430 let mut output = Vec::with_capacity(max_lines);
431
432 for line_idx in 0..max_lines {
433 let mut row = String::new();
434 row.push_str(&self.prefix);
435
436 for (i, (cell, col)) in cell_outputs.iter().zip(self.columns.iter()).enumerate() {
437 if i > 0 {
438 if anchor_gap > 0 && i == anchor_transition {
439 row.push_str(&" ".repeat(anchor_gap));
440 } else {
441 row.push_str(&self.separator);
442 }
443 }
444
445 let width = self.widths.get(i).copied().unwrap_or(0);
446 let line = cell.line(line_idx, width, col.align);
447 row.push_str(&line);
448 }
449
450 row.push_str(&self.suffix);
451 output.push(row);
452 }
453
454 output
455 }
456
457 pub fn column_width(&self, index: usize) -> Option<usize> {
459 self.widths.get(index).copied()
460 }
461
462 pub fn widths(&self) -> &[usize] {
464 &self.widths
465 }
466
467 pub fn num_columns(&self) -> usize {
469 self.columns.len()
470 }
471
472 pub fn has_sub_columns(&self) -> bool {
474 self.columns.iter().any(|c| c.sub_columns.is_some())
475 }
476
477 pub fn columns(&self) -> &[Column] {
479 &self.columns
480 }
481
482 pub fn extract_headers(&self) -> Vec<String> {
492 self.columns
493 .iter()
494 .map(|col| {
495 col.header
496 .as_deref()
497 .or(col.key.as_deref())
498 .or(col.name.as_deref())
499 .unwrap_or("")
500 .to_string()
501 })
502 .collect()
503 }
504
505 pub fn row_from<T: Serialize>(&self, value: &T) -> String {
548 let values = self.extract_values(value);
549 let string_refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
550 self.format_row(&string_refs)
551 }
552
553 pub fn row_lines_from<T: Serialize>(&self, value: &T) -> Vec<String> {
558 let values = self.extract_values(value);
559 let string_refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
560 self.format_row_lines(&string_refs)
561 }
562
563 pub fn row_from_trait<T: TabularRow>(&self, value: &T) -> String {
588 let values = value.to_row();
589 self.format_row(&values)
590 }
591
592 pub fn row_lines_from_trait<T: TabularRow>(&self, value: &T) -> Vec<String> {
597 let values = value.to_row();
598 self.format_row_lines(&values)
599 }
600
601 fn extract_values<T: Serialize>(&self, value: &T) -> Vec<String> {
603 let json = match serde_json::to_value(value) {
605 Ok(v) => v,
606 Err(_) => return vec![String::new(); self.columns.len()],
607 };
608
609 self.columns
610 .iter()
611 .map(|col| {
612 let key = col.key.as_ref().or(col.name.as_ref());
614
615 match key {
616 Some(k) => extract_field(&json, k),
617 None => col.null_repr.clone(),
618 }
619 })
620 .collect()
621 }
622}
623
624fn extract_field(value: &JsonValue, path: &str) -> String {
628 let mut current = value;
629
630 for part in path.split('.') {
631 match current {
632 JsonValue::Object(map) => {
633 current = match map.get(part) {
634 Some(v) => v,
635 None => return String::new(),
636 };
637 }
638 JsonValue::Array(arr) => {
639 if let Ok(idx) = part.parse::<usize>() {
641 current = match arr.get(idx) {
642 Some(v) => v,
643 None => return String::new(),
644 };
645 } else {
646 return String::new();
647 }
648 }
649 _ => return String::new(),
650 }
651 }
652
653 match current {
655 JsonValue::String(s) => s.clone(),
656 JsonValue::Number(n) => n.to_string(),
657 JsonValue::Bool(b) => b.to_string(),
658 JsonValue::Null => String::new(),
659 _ => current.to_string(),
661 }
662}
663
664impl Object for TabularFormatter {
669 fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
670 match key.as_str()? {
671 "num_columns" => Some(Value::from(self.num_columns())),
672 "widths" => {
673 let widths: Vec<Value> = self.widths.iter().map(|&w| Value::from(w)).collect();
674 Some(Value::from(widths))
675 }
676 "separator" => Some(Value::from(self.separator.clone())),
677 _ => None,
678 }
679 }
680
681 fn enumerate(self: &Arc<Self>) -> Enumerator {
682 Enumerator::Str(&["num_columns", "widths", "separator"])
683 }
684
685 fn call_method(
686 self: &Arc<Self>,
687 _state: &minijinja::State,
688 name: &str,
689 args: &[Value],
690 ) -> Result<Value, minijinja::Error> {
691 match name {
692 "row" => {
693 if args.is_empty() {
695 return Err(minijinja::Error::new(
696 minijinja::ErrorKind::MissingArgument,
697 "row() requires an array of values",
698 ));
699 }
700
701 let values_arg = &args[0];
702 let has_sub_columns = self.columns.iter().any(|c| c.sub_columns.is_some());
703
704 if has_sub_columns {
705 let outer_iter = match values_arg.try_iter() {
707 Ok(iter) => iter,
708 Err(_) => {
709 let values = vec![values_arg.to_string()];
710 let formatted = self.format_row(&values);
711 return Ok(Value::from(formatted));
712 }
713 };
714
715 let mut owned_values: Vec<OwnedCellValue> = Vec::new();
717 for (i, v) in outer_iter.enumerate() {
718 let is_sub_col = self
719 .columns
720 .get(i)
721 .and_then(|c| c.sub_columns.as_ref())
722 .is_some();
723
724 if is_sub_col {
725 if let Ok(inner_iter) = v.try_iter() {
726 let sub_vals: Vec<String> =
727 inner_iter.map(|iv| iv.to_string()).collect();
728 owned_values.push(OwnedCellValue::Sub(sub_vals));
729 } else {
730 owned_values.push(OwnedCellValue::Single(v.to_string()));
731 }
732 } else {
733 owned_values.push(OwnedCellValue::Single(v.to_string()));
734 }
735 }
736
737 let cell_values: Vec<CellValue<'_>> = owned_values
739 .iter()
740 .map(|ov| match ov {
741 OwnedCellValue::Single(s) => CellValue::Single(s.as_str()),
742 OwnedCellValue::Sub(v) => {
743 CellValue::Sub(v.iter().map(|s| s.as_str()).collect())
744 }
745 })
746 .collect();
747
748 let formatted = self.format_row_cells(&cell_values);
749 Ok(Value::from(formatted))
750 } else {
751 let values: Vec<String> = match values_arg.try_iter() {
753 Ok(iter) => iter.map(|v| v.to_string()).collect(),
754 Err(_) => vec![values_arg.to_string()],
755 };
756
757 let formatted = self.format_row(&values);
758 Ok(Value::from(formatted))
759 }
760 }
761 "column_width" => {
762 if args.is_empty() {
764 return Err(minijinja::Error::new(
765 minijinja::ErrorKind::MissingArgument,
766 "column_width() requires an index argument",
767 ));
768 }
769
770 let index = args[0].as_usize().ok_or_else(|| {
771 minijinja::Error::new(
772 minijinja::ErrorKind::InvalidOperation,
773 "column_width() index must be a number",
774 )
775 })?;
776
777 match self.column_width(index) {
778 Some(w) => Ok(Value::from(w)),
779 None => Ok(Value::from(())),
780 }
781 }
782 _ => Err(minijinja::Error::new(
783 minijinja::ErrorKind::UnknownMethod,
784 format!("TabularFormatter has no method '{}'", name),
785 )),
786 }
787 }
788}
789
790fn format_cell(value: &str, width: usize, col: &Column) -> String {
792 let style_override = if col.style_from_value {
794 Some(value)
795 } else {
796 None
797 };
798 let style = style_override.or(col.style.as_deref());
799 format_value(value, width, col.align, &col.overflow, style)
800}
801
802fn format_value(
808 value: &str,
809 width: usize,
810 align: Align,
811 overflow: &Overflow,
812 style: Option<&str>,
813) -> String {
814 if width == 0 {
815 return String::new();
816 }
817
818 let stripped = standout_bbparser::strip_tags(value);
819 let current_width = display_width(&stripped);
820
821 if current_width > width {
822 let truncated = match overflow {
824 Overflow::Truncate { at, marker } => match at {
825 TruncateAt::End => truncate_end(&stripped, width, marker),
826 TruncateAt::Start => truncate_start(&stripped, width, marker),
827 TruncateAt::Middle => truncate_middle(&stripped, width, marker),
828 },
829 Overflow::Clip => truncate_end(&stripped, width, ""),
830 Overflow::Expand => {
831 return apply_style(value, style);
833 }
834 Overflow::Wrap { .. } => {
835 truncate_end(&stripped, width, "…")
837 }
838 };
839
840 let padded = match align {
841 Align::Left => pad_right(&truncated, width),
842 Align::Right => pad_left(&truncated, width),
843 Align::Center => pad_center(&truncated, width),
844 };
845 apply_style(&padded, style)
846 } else {
847 let padding = width - current_width;
849 let padded = match align {
850 Align::Left => format!("{}{}", value, " ".repeat(padding)),
851 Align::Right => format!("{}{}", " ".repeat(padding), value),
852 Align::Center => {
853 let left_pad = padding / 2;
854 let right_pad = padding - left_pad;
855 format!("{}{}{}", " ".repeat(left_pad), value, " ".repeat(right_pad))
856 }
857 };
858 apply_style(&padded, style)
859 }
860}
861
862#[derive(Clone, Debug)]
871pub enum CellValue<'a> {
872 Single(&'a str),
874 Sub(Vec<&'a str>),
876}
877
878impl<'a> From<&'a str> for CellValue<'a> {
879 fn from(s: &'a str) -> Self {
880 CellValue::Single(s)
881 }
882}
883
884pub(crate) enum OwnedCellValue {
887 Single(String),
888 Sub(Vec<String>),
889}
890
891fn resolve_sub_widths(sub_cols: &SubColumns, values: &[&str], parent_width: usize) -> Vec<usize> {
900 let sep_width = display_width(&sub_cols.separator);
901 let n = sub_cols.columns.len();
902 let mut widths = vec![0usize; n];
903 let mut grower_index = 0;
904
905 for (i, sub_col) in sub_cols.columns.iter().enumerate() {
907 match &sub_col.width {
908 Width::Fill => {
909 grower_index = i;
910 }
911 Width::Fixed(w) => {
912 widths[i] = *w;
913 }
914 Width::Bounded { min, max } => {
915 let content_w = values.get(i).map(|v| visible_width(v)).unwrap_or(0);
916 let min_w = min.unwrap_or(0);
917 let max_w = max.unwrap_or(usize::MAX);
918 widths[i] = content_w.max(min_w).min(max_w);
919 }
920 Width::Fraction(_) => {} }
922 }
923
924 let visible_non_growers = widths
929 .iter()
930 .enumerate()
931 .filter(|&(i, &w)| i != grower_index && w > 0)
932 .count();
933 let visible_count = visible_non_growers + 1; let sep_overhead = visible_count.saturating_sub(1) * sep_width;
935 let available = parent_width.saturating_sub(sep_overhead);
936
937 let non_grower_total: usize = widths
939 .iter()
940 .enumerate()
941 .filter(|&(i, _)| i != grower_index)
942 .map(|(_, &w)| w)
943 .sum();
944
945 if non_grower_total > available {
946 let mut excess = non_grower_total - available;
947 for i in (0..n).rev() {
949 if i == grower_index || widths[i] == 0 || excess == 0 {
950 continue;
951 }
952 let reduction = excess.min(widths[i]);
953 widths[i] -= reduction;
954 excess -= reduction;
955 }
956 }
957
958 let clamped_total: usize = widths
960 .iter()
961 .enumerate()
962 .filter(|&(i, _)| i != grower_index)
963 .map(|(_, &w)| w)
964 .sum();
965 widths[grower_index] = available.saturating_sub(clamped_total);
966
967 widths
968}
969
970fn format_sub_cells(sub_cols: &SubColumns, values: &[&str], parent_width: usize) -> String {
978 if parent_width == 0 {
979 return String::new();
980 }
981
982 let widths = resolve_sub_widths(sub_cols, values, parent_width);
983 let grower_index = sub_cols
984 .columns
985 .iter()
986 .position(|c| matches!(c.width, Width::Fill))
987 .unwrap_or(0);
988 let sep = &sub_cols.separator;
989 let mut parts: Vec<String> = Vec::new();
990
991 for (i, (sub_col, &width)) in sub_cols.columns.iter().zip(widths.iter()).enumerate() {
992 if width == 0 && i != grower_index {
994 continue;
995 }
996 if width == 0 {
997 parts.push(String::new());
999 } else {
1000 let value = values.get(i).copied().unwrap_or(&sub_col.null_repr);
1001 parts.push(format_value(
1002 value,
1003 width,
1004 sub_col.align,
1005 &sub_col.overflow,
1006 sub_col.style.as_deref(),
1007 ));
1008 }
1009 }
1010
1011 parts.join(sep)
1012}
1013
1014#[derive(Clone, Debug, PartialEq, Eq)]
1016pub enum CellOutput {
1017 Single(String),
1019 Multi(Vec<String>),
1021}
1022
1023impl CellOutput {
1024 pub fn is_single(&self) -> bool {
1026 matches!(self, CellOutput::Single(_))
1027 }
1028
1029 pub fn line_count(&self) -> usize {
1031 match self {
1032 CellOutput::Single(_) => 1,
1033 CellOutput::Multi(lines) => lines.len().max(1),
1034 }
1035 }
1036
1037 pub fn line(&self, index: usize, width: usize, align: Align) -> String {
1042 let content = match self {
1043 CellOutput::Single(s) if index == 0 => s.as_str(),
1044 CellOutput::Multi(lines) => lines.get(index).map(|s| s.as_str()).unwrap_or(""),
1045 _ => "",
1046 };
1047
1048 let content_width = visible_width(content);
1049 if content_width >= width {
1050 return content.to_string();
1051 }
1052 let padding = width - content_width;
1053 match align {
1054 Align::Left => format!("{}{}", content, " ".repeat(padding)),
1055 Align::Right => format!("{}{}", " ".repeat(padding), content),
1056 Align::Center => {
1057 let left_pad = padding / 2;
1058 let right_pad = padding - left_pad;
1059 format!(
1060 "{}{}{}",
1061 " ".repeat(left_pad),
1062 content,
1063 " ".repeat(right_pad)
1064 )
1065 }
1066 }
1067 }
1068
1069 pub fn to_single(&self) -> String {
1071 match self {
1072 CellOutput::Single(s) => s.clone(),
1073 CellOutput::Multi(lines) => lines.first().cloned().unwrap_or_default(),
1074 }
1075 }
1076}
1077
1078fn apply_style(content: &str, style: Option<&str>) -> String {
1080 match style {
1081 Some(s) if !s.is_empty() => format!("[{}]{}[/{}]", s, content, s),
1082 _ => content.to_string(),
1083 }
1084}
1085
1086fn format_cell_lines(value: &str, width: usize, col: &Column) -> CellOutput {
1088 if width == 0 {
1089 return CellOutput::Single(String::new());
1090 }
1091
1092 let stripped = standout_bbparser::strip_tags(value);
1093 let current_width = display_width(&stripped);
1094
1095 let style = if col.style_from_value {
1097 Some(value)
1098 } else {
1099 col.style.as_deref()
1100 };
1101
1102 match &col.overflow {
1103 Overflow::Wrap { indent } => {
1104 if current_width <= width {
1105 let padding = width - current_width;
1107 let padded = match col.align {
1108 Align::Left => format!("{}{}", value, " ".repeat(padding)),
1109 Align::Right => format!("{}{}", " ".repeat(padding), value),
1110 Align::Center => {
1111 let left_pad = padding / 2;
1112 let right_pad = padding - left_pad;
1113 format!("{}{}{}", " ".repeat(left_pad), value, " ".repeat(right_pad))
1114 }
1115 };
1116 CellOutput::Single(apply_style(&padded, style))
1117 } else {
1118 let wrapped = wrap_indent(&stripped, width, *indent);
1120 let padded: Vec<String> = wrapped
1121 .into_iter()
1122 .map(|line| {
1123 let padded_line = match col.align {
1124 Align::Left => pad_right(&line, width),
1125 Align::Right => pad_left(&line, width),
1126 Align::Center => pad_center(&line, width),
1127 };
1128 apply_style(&padded_line, style)
1129 })
1130 .collect();
1131 if padded.len() == 1 {
1132 CellOutput::Single(padded.into_iter().next().unwrap())
1133 } else {
1134 CellOutput::Multi(padded)
1135 }
1136 }
1137 }
1138 _ => CellOutput::Single(format_cell(value, width, col)),
1140 }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use super::*;
1146 use crate::tabular::{TabularSpec, Width};
1147
1148 fn simple_spec() -> FlatDataSpec {
1149 FlatDataSpec::builder()
1150 .column(Column::new(Width::Fixed(10)))
1151 .column(Column::new(Width::Fixed(8)))
1152 .separator(" | ")
1153 .build()
1154 }
1155
1156 #[test]
1157 fn format_basic_row() {
1158 let formatter = TabularFormatter::new(&simple_spec(), 80);
1159 let output = formatter.format_row(&["Hello", "World"]);
1160 assert_eq!(output, "Hello | World ");
1161 }
1162
1163 #[test]
1164 fn format_row_with_truncation() {
1165 let spec = FlatDataSpec::builder()
1166 .column(Column::new(Width::Fixed(8)))
1167 .build();
1168 let formatter = TabularFormatter::new(&spec, 80);
1169
1170 let output = formatter.format_row(&["Hello World"]);
1171 assert_eq!(output, "Hello W…");
1172 }
1173
1174 #[test]
1175 fn format_row_right_align() {
1176 let spec = FlatDataSpec::builder()
1177 .column(Column::new(Width::Fixed(10)).align(Align::Right))
1178 .build();
1179 let formatter = TabularFormatter::new(&spec, 80);
1180
1181 let output = formatter.format_row(&["42"]);
1182 assert_eq!(output, " 42");
1183 }
1184
1185 #[test]
1186 fn format_row_center_align() {
1187 let spec = FlatDataSpec::builder()
1188 .column(Column::new(Width::Fixed(10)).align(Align::Center))
1189 .build();
1190 let formatter = TabularFormatter::new(&spec, 80);
1191
1192 let output = formatter.format_row(&["hi"]);
1193 assert_eq!(output, " hi ");
1194 }
1195
1196 #[test]
1197 fn format_row_truncate_start() {
1198 let spec = FlatDataSpec::builder()
1199 .column(Column::new(Width::Fixed(10)).truncate(TruncateAt::Start))
1200 .build();
1201 let formatter = TabularFormatter::new(&spec, 80);
1202
1203 let output = formatter.format_row(&["/path/to/file.rs"]);
1204 assert_eq!(display_width(&output), 10);
1205 assert!(output.starts_with("…"));
1206 }
1207
1208 #[test]
1209 fn format_row_truncate_middle() {
1210 let spec = FlatDataSpec::builder()
1211 .column(Column::new(Width::Fixed(10)).truncate(TruncateAt::Middle))
1212 .build();
1213 let formatter = TabularFormatter::new(&spec, 80);
1214
1215 let output = formatter.format_row(&["abcdefghijklmno"]);
1216 assert_eq!(display_width(&output), 10);
1217 assert!(output.contains("…"));
1218 }
1219
1220 #[test]
1221 fn format_row_with_null() {
1222 let spec = FlatDataSpec::builder()
1223 .column(Column::new(Width::Fixed(10)))
1224 .column(Column::new(Width::Fixed(8)).null_repr("N/A"))
1225 .separator(" ")
1226 .build();
1227 let formatter = TabularFormatter::new(&spec, 80);
1228
1229 let output = formatter.format_row(&["value"]);
1231 assert!(output.contains("N/A"));
1232 }
1233
1234 #[test]
1235 fn format_row_with_decorations() {
1236 let spec = FlatDataSpec::builder()
1237 .column(Column::new(Width::Fixed(10)))
1238 .column(Column::new(Width::Fixed(8)))
1239 .separator(" │ ")
1240 .prefix("│ ")
1241 .suffix(" │")
1242 .build();
1243 let formatter = TabularFormatter::new(&spec, 80);
1244
1245 let output = formatter.format_row(&["Hello", "World"]);
1246 assert!(output.starts_with("│ "));
1247 assert!(output.ends_with(" │"));
1248 assert!(output.contains(" │ "));
1249 }
1250
1251 #[test]
1252 fn format_multiple_rows() {
1253 let formatter = TabularFormatter::new(&simple_spec(), 80);
1254 let rows = vec![vec!["a", "1"], vec!["b", "2"], vec!["c", "3"]];
1255
1256 let output = formatter.format_rows(&rows);
1257 assert_eq!(output.len(), 3);
1258 }
1259
1260 #[test]
1261 fn format_row_fill_column() {
1262 let spec = FlatDataSpec::builder()
1263 .column(Column::new(Width::Fixed(5)))
1264 .column(Column::new(Width::Fill))
1265 .column(Column::new(Width::Fixed(5)))
1266 .separator(" ")
1267 .build();
1268
1269 let formatter = TabularFormatter::new(&spec, 30);
1271 let _output = formatter.format_row(&["abc", "middle", "xyz"]);
1272
1273 assert_eq!(formatter.widths(), &[5, 16, 5]);
1275 }
1276
1277 #[test]
1278 fn formatter_accessors() {
1279 let spec = FlatDataSpec::builder()
1280 .column(Column::new(Width::Fixed(10)))
1281 .column(Column::new(Width::Fixed(8)))
1282 .build();
1283 let formatter = TabularFormatter::new(&spec, 80);
1284
1285 assert_eq!(formatter.num_columns(), 2);
1286 assert_eq!(formatter.column_width(0), Some(10));
1287 assert_eq!(formatter.column_width(1), Some(8));
1288 assert_eq!(formatter.column_width(2), None);
1289 }
1290
1291 #[test]
1292 fn format_empty_spec() {
1293 let spec = FlatDataSpec::builder().build();
1294 let formatter = TabularFormatter::new(&spec, 80);
1295
1296 let output = formatter.format_row::<&str>(&[]);
1297 assert_eq!(output, "");
1298 }
1299
1300 #[test]
1301 fn format_with_ansi() {
1302 let spec = FlatDataSpec::builder()
1303 .column(Column::new(Width::Fixed(10)))
1304 .build();
1305 let formatter = TabularFormatter::new(&spec, 80);
1306
1307 let styled = "\x1b[31mred\x1b[0m";
1308 let output = formatter.format_row(&[styled]);
1309
1310 assert!(output.contains("\x1b[31m"));
1312 assert_eq!(display_width(&output), 10);
1313 }
1314
1315 #[test]
1316 fn format_with_explicit_widths() {
1317 let columns = vec![Column::new(Width::Fixed(5)), Column::new(Width::Fixed(10))];
1318 let formatter = TabularFormatter::with_widths(columns, vec![5, 10]).separator(" - ");
1319
1320 let output = formatter.format_row(&["hi", "there"]);
1321 assert_eq!(output, "hi - there ");
1322 }
1323
1324 #[test]
1329 fn object_get_num_columns() {
1330 let formatter = Arc::new(TabularFormatter::new(&simple_spec(), 80));
1331 let value = formatter.get_value(&Value::from("num_columns"));
1332 assert_eq!(value, Some(Value::from(2)));
1333 }
1334
1335 #[test]
1336 fn object_get_widths() {
1337 let spec = TabularSpec::builder()
1338 .column(Column::new(Width::Fixed(10)))
1339 .column(Column::new(Width::Fixed(8)))
1340 .build();
1341 let formatter = Arc::new(TabularFormatter::new(&spec, 80));
1342
1343 let value = formatter.get_value(&Value::from("widths"));
1344 assert!(value.is_some());
1345 let widths = value.unwrap();
1346 assert!(widths.try_iter().is_ok());
1348 }
1349
1350 #[test]
1351 fn object_get_separator() {
1352 let spec = TabularSpec::builder()
1353 .column(Column::new(Width::Fixed(10)))
1354 .separator(" | ")
1355 .build();
1356 let formatter = Arc::new(TabularFormatter::new(&spec, 80));
1357
1358 let value = formatter.get_value(&Value::from("separator"));
1359 assert_eq!(value, Some(Value::from(" | ")));
1360 }
1361
1362 #[test]
1363 fn object_get_unknown_returns_none() {
1364 let formatter = Arc::new(TabularFormatter::new(&simple_spec(), 80));
1365 let value = formatter.get_value(&Value::from("unknown"));
1366 assert_eq!(value, None);
1367 }
1368
1369 #[test]
1370 fn object_row_method_via_template() {
1371 use minijinja::Environment;
1372
1373 let spec = TabularSpec::builder()
1374 .column(Column::new(Width::Fixed(10)))
1375 .column(Column::new(Width::Fixed(8)))
1376 .separator(" | ")
1377 .build();
1378 let formatter = TabularFormatter::new(&spec, 80);
1379
1380 let mut env = Environment::new();
1381 env.add_template("test", "{{ table.row(['Hello', 'World']) }}")
1382 .unwrap();
1383
1384 let tmpl = env.get_template("test").unwrap();
1385 let output = tmpl
1386 .render(minijinja::context! { table => Value::from_object(formatter) })
1387 .unwrap();
1388
1389 assert_eq!(output, "Hello | World ");
1390 }
1391
1392 #[test]
1393 fn object_row_method_in_loop() {
1394 use minijinja::Environment;
1395
1396 let spec = TabularSpec::builder()
1397 .column(Column::new(Width::Fixed(8)))
1398 .column(Column::new(Width::Fixed(6)))
1399 .separator(" ")
1400 .build();
1401 let formatter = TabularFormatter::new(&spec, 80);
1402
1403 let mut env = Environment::new();
1404 env.add_template(
1405 "test",
1406 "{% for item in items %}{{ table.row([item.name, item.value]) }}\n{% endfor %}",
1407 )
1408 .unwrap();
1409
1410 let tmpl = env.get_template("test").unwrap();
1411 let output = tmpl
1412 .render(minijinja::context! {
1413 table => Value::from_object(formatter),
1414 items => vec![
1415 minijinja::context! { name => "Alice", value => "100" },
1416 minijinja::context! { name => "Bob", value => "200" },
1417 ]
1418 })
1419 .unwrap();
1420
1421 assert!(output.contains("Alice"));
1422 assert!(output.contains("Bob"));
1423 }
1424
1425 #[test]
1426 fn object_column_width_method_via_template() {
1427 use minijinja::Environment;
1428
1429 let spec = TabularSpec::builder()
1430 .column(Column::new(Width::Fixed(10)))
1431 .column(Column::new(Width::Fixed(8)))
1432 .build();
1433 let formatter = TabularFormatter::new(&spec, 80);
1434
1435 let mut env = Environment::new();
1436 env.add_template(
1437 "test",
1438 "{{ table.column_width(0) }}-{{ table.column_width(1) }}",
1439 )
1440 .unwrap();
1441
1442 let tmpl = env.get_template("test").unwrap();
1443 let output = tmpl
1444 .render(minijinja::context! { table => Value::from_object(formatter) })
1445 .unwrap();
1446
1447 assert_eq!(output, "10-8");
1448 }
1449
1450 #[test]
1451 fn object_attribute_access_via_template() {
1452 use minijinja::Environment;
1453
1454 let spec = TabularSpec::builder()
1455 .column(Column::new(Width::Fixed(10)))
1456 .column(Column::new(Width::Fixed(8)))
1457 .separator(" | ")
1458 .build();
1459 let formatter = TabularFormatter::new(&spec, 80);
1460
1461 let mut env = Environment::new();
1462 env.add_template(
1463 "test",
1464 "cols={{ table.num_columns }}, sep='{{ table.separator }}'",
1465 )
1466 .unwrap();
1467
1468 let tmpl = env.get_template("test").unwrap();
1469 let output = tmpl
1470 .render(minijinja::context! { table => Value::from_object(formatter) })
1471 .unwrap();
1472
1473 assert_eq!(output, "cols=2, sep=' | '");
1474 }
1475
1476 #[test]
1481 fn format_cell_clip_no_marker() {
1482 let spec = FlatDataSpec::builder()
1483 .column(Column::new(Width::Fixed(5)).clip())
1484 .build();
1485 let formatter = TabularFormatter::new(&spec, 80);
1486
1487 let output = formatter.format_row(&["Hello World"]);
1488 assert_eq!(display_width(&output), 5);
1490 assert!(!output.contains("…"));
1491 assert!(output.starts_with("Hello"));
1492 }
1493
1494 #[test]
1495 fn format_cell_expand_overflows() {
1496 let col = Column::new(Width::Fixed(5)).overflow(Overflow::Expand);
1498 let output = format_cell("Hello World", 5, &col);
1499
1500 assert_eq!(output, "Hello World");
1502 assert_eq!(display_width(&output), 11); }
1504
1505 #[test]
1506 fn format_cell_expand_pads_when_short() {
1507 let col = Column::new(Width::Fixed(10)).overflow(Overflow::Expand);
1508 let output = format_cell("Hi", 10, &col);
1509
1510 assert_eq!(output, "Hi ");
1512 assert_eq!(display_width(&output), 10);
1513 }
1514
1515 #[test]
1516 fn format_cell_wrap_single_line() {
1517 let col = Column::new(Width::Fixed(20)).wrap();
1519 let output = format_cell_lines("Short text", 20, &col);
1520
1521 assert!(output.is_single());
1522 assert_eq!(output.line_count(), 1);
1523 assert_eq!(display_width(&output.to_single()), 20);
1524 }
1525
1526 #[test]
1527 fn format_cell_wrap_multi_line() {
1528 let col = Column::new(Width::Fixed(10)).wrap();
1529 let output = format_cell_lines("This is a longer text that wraps", 10, &col);
1530
1531 assert!(!output.is_single());
1532 assert!(output.line_count() > 1);
1533
1534 if let CellOutput::Multi(lines) = &output {
1536 for line in lines {
1537 assert_eq!(display_width(line), 10);
1538 }
1539 }
1540 }
1541
1542 #[test]
1543 fn format_cell_wrap_with_indent() {
1544 let col = Column::new(Width::Fixed(15)).overflow(Overflow::Wrap { indent: 2 });
1545 let output = format_cell_lines("First line then continuation", 15, &col);
1546
1547 if let CellOutput::Multi(lines) = output {
1548 assert!(lines[0].starts_with("First"));
1550 if lines.len() > 1 {
1552 let second_trimmed = lines[1].trim_start();
1554 assert!(lines[1].len() > second_trimmed.len()); }
1556 }
1557 }
1558
1559 #[test]
1560 fn format_row_lines_single_line() {
1561 let spec = FlatDataSpec::builder()
1562 .column(Column::new(Width::Fixed(10)))
1563 .column(Column::new(Width::Fixed(8)))
1564 .separator(" ")
1565 .build();
1566 let formatter = TabularFormatter::new(&spec, 80);
1567
1568 let lines = formatter.format_row_lines(&["Hello", "World"]);
1569 assert_eq!(lines.len(), 1);
1570 assert_eq!(lines[0], formatter.format_row(&["Hello", "World"]));
1571 }
1572
1573 #[test]
1574 fn format_row_lines_multi_line() {
1575 let spec = FlatDataSpec::builder()
1576 .column(Column::new(Width::Fixed(8)).wrap())
1577 .column(Column::new(Width::Fixed(6)))
1578 .separator(" ")
1579 .build();
1580 let formatter = TabularFormatter::new(&spec, 80);
1581
1582 let lines = formatter.format_row_lines(&["This is long", "Short"]);
1583
1584 assert!(!lines.is_empty());
1586
1587 let expected_width = display_width(&lines[0]);
1589 for line in &lines {
1590 assert_eq!(display_width(line), expected_width);
1591 }
1592 }
1593
1594 #[test]
1595 fn format_row_lines_mixed_columns() {
1596 let spec = FlatDataSpec::builder()
1598 .column(Column::new(Width::Fixed(6))) .column(Column::new(Width::Fixed(10)).wrap()) .column(Column::new(Width::Fixed(4))) .separator(" ")
1602 .build();
1603 let formatter = TabularFormatter::new(&spec, 80);
1604
1605 let lines = formatter.format_row_lines(&["aaaaa", "this text wraps here", "bbbb"]);
1606
1607 assert!(!lines.is_empty());
1609 }
1610
1611 #[test]
1616 fn cell_output_single_accessors() {
1617 let cell = CellOutput::Single("Hello".to_string());
1618
1619 assert!(cell.is_single());
1620 assert_eq!(cell.line_count(), 1);
1621 assert_eq!(cell.to_single(), "Hello");
1622 }
1623
1624 #[test]
1625 fn cell_output_multi_accessors() {
1626 let cell = CellOutput::Multi(vec!["Line 1".to_string(), "Line 2".to_string()]);
1627
1628 assert!(!cell.is_single());
1629 assert_eq!(cell.line_count(), 2);
1630 assert_eq!(cell.to_single(), "Line 1");
1631 }
1632
1633 #[test]
1634 fn cell_output_line_accessor() {
1635 let cell = CellOutput::Multi(vec!["First".to_string(), "Second".to_string()]);
1636
1637 let line0 = cell.line(0, 10, Align::Left);
1639 assert_eq!(line0, "First ");
1640 assert_eq!(display_width(&line0), 10);
1641
1642 let line1 = cell.line(1, 10, Align::Right);
1644 assert_eq!(line1, " Second");
1645
1646 let line2 = cell.line(2, 10, Align::Left);
1648 assert_eq!(line2, " ");
1649 }
1650
1651 #[test]
1656 fn format_row_all_left_anchor_no_gap() {
1657 let spec = FlatDataSpec::builder()
1659 .column(Column::new(Width::Fixed(5)))
1660 .column(Column::new(Width::Fixed(5)))
1661 .separator(" ")
1662 .build();
1663 let formatter = TabularFormatter::new(&spec, 50);
1664
1665 let output = formatter.format_row(&["A", "B"]);
1666 assert_eq!(output, "A B ");
1668 assert_eq!(display_width(&output), 11);
1669 }
1670
1671 #[test]
1672 fn format_row_with_right_anchor() {
1673 let spec = FlatDataSpec::builder()
1675 .column(Column::new(Width::Fixed(5))) .column(Column::new(Width::Fixed(5)).anchor_right()) .separator(" ")
1678 .build();
1679
1680 let formatter = TabularFormatter::new(&spec, 30);
1683
1684 let output = formatter.format_row(&["L", "R"]);
1685 assert_eq!(display_width(&output), 30);
1686 assert!(output.starts_with("L "));
1688 assert!(output.ends_with("R "));
1689 }
1690
1691 #[test]
1692 fn format_row_with_right_anchor_exact_fit() {
1693 let spec = FlatDataSpec::builder()
1695 .column(Column::new(Width::Fixed(10)))
1696 .column(Column::new(Width::Fixed(10)).anchor_right())
1697 .separator(" ")
1698 .build();
1699
1700 let formatter = TabularFormatter::new(&spec, 22);
1702
1703 let output = formatter.format_row(&["Left", "Right"]);
1704 assert_eq!(display_width(&output), 22);
1705 assert!(output.contains(" ")); }
1708
1709 #[test]
1710 fn format_row_all_right_anchor_no_gap() {
1711 let spec = FlatDataSpec::builder()
1713 .column(Column::new(Width::Fixed(5)).anchor_right())
1714 .column(Column::new(Width::Fixed(5)).anchor_right())
1715 .separator(" ")
1716 .build();
1717 let formatter = TabularFormatter::new(&spec, 50);
1718
1719 let output = formatter.format_row(&["A", "B"]);
1720 assert_eq!(output, "A B ");
1722 }
1723
1724 #[test]
1725 fn format_row_multiple_anchors() {
1726 let spec = FlatDataSpec::builder()
1728 .column(Column::new(Width::Fixed(4))) .column(Column::new(Width::Fixed(4))) .column(Column::new(Width::Fixed(4)).anchor_right()) .column(Column::new(Width::Fixed(4)).anchor_right()) .separator(" ")
1733 .build();
1734
1735 let formatter = TabularFormatter::new(&spec, 40);
1738
1739 let output = formatter.format_row(&["A", "B", "C", "D"]);
1740 assert_eq!(display_width(&output), 40);
1741 assert!(output.starts_with("A B "));
1743 }
1744
1745 #[test]
1746 fn calculate_anchor_gap_no_transition() {
1747 let spec = FlatDataSpec::builder()
1748 .column(Column::new(Width::Fixed(10)))
1749 .column(Column::new(Width::Fixed(10)))
1750 .build();
1751 let formatter = TabularFormatter::new(&spec, 50);
1752
1753 let (gap, transition) = formatter.calculate_anchor_gap();
1754 assert_eq!(transition, 2); assert_eq!(gap, 0);
1756 }
1757
1758 #[test]
1759 fn calculate_anchor_gap_with_transition() {
1760 let spec = FlatDataSpec::builder()
1761 .column(Column::new(Width::Fixed(10)))
1762 .column(Column::new(Width::Fixed(10)).anchor_right())
1763 .separator(" ")
1764 .build();
1765 let formatter = TabularFormatter::new(&spec, 50);
1766
1767 let (gap, transition) = formatter.calculate_anchor_gap();
1768 assert_eq!(transition, 1); assert!(gap > 0);
1770 }
1771
1772 #[test]
1773 fn format_row_lines_with_anchor() {
1774 let spec = FlatDataSpec::builder()
1776 .column(Column::new(Width::Fixed(8)).wrap())
1777 .column(Column::new(Width::Fixed(6)).anchor_right())
1778 .separator(" ")
1779 .build();
1780 let formatter = TabularFormatter::new(&spec, 40);
1781
1782 let lines = formatter.format_row_lines(&["This is text", "Right"]);
1783
1784 for line in &lines {
1786 assert_eq!(display_width(line), 40);
1787 }
1788 }
1789
1790 #[test]
1795 fn row_from_simple_struct() {
1796 #[derive(Serialize)]
1797 struct Record {
1798 name: String,
1799 value: i32,
1800 }
1801
1802 let spec = FlatDataSpec::builder()
1803 .column(Column::new(Width::Fixed(10)).key("name"))
1804 .column(Column::new(Width::Fixed(5)).key("value"))
1805 .separator(" ")
1806 .build();
1807 let formatter = TabularFormatter::new(&spec, 80);
1808
1809 let record = Record {
1810 name: "Test".to_string(),
1811 value: 42,
1812 };
1813
1814 let row = formatter.row_from(&record);
1815 assert!(row.contains("Test"));
1816 assert!(row.contains("42"));
1817 }
1818
1819 #[test]
1820 fn row_from_uses_name_as_fallback() {
1821 #[derive(Serialize)]
1822 struct Item {
1823 title: String,
1824 }
1825
1826 let spec = FlatDataSpec::builder()
1827 .column(Column::new(Width::Fixed(15)).named("title"))
1828 .build();
1829 let formatter = TabularFormatter::new(&spec, 80);
1830
1831 let item = Item {
1832 title: "Hello".to_string(),
1833 };
1834
1835 let row = formatter.row_from(&item);
1836 assert!(row.contains("Hello"));
1837 }
1838
1839 #[test]
1840 fn row_from_nested_field() {
1841 #[derive(Serialize)]
1842 struct User {
1843 email: String,
1844 }
1845
1846 #[derive(Serialize)]
1847 struct Record {
1848 user: User,
1849 status: String,
1850 }
1851
1852 let spec = FlatDataSpec::builder()
1853 .column(Column::new(Width::Fixed(20)).key("user.email"))
1854 .column(Column::new(Width::Fixed(10)).key("status"))
1855 .separator(" ")
1856 .build();
1857 let formatter = TabularFormatter::new(&spec, 80);
1858
1859 let record = Record {
1860 user: User {
1861 email: "test@example.com".to_string(),
1862 },
1863 status: "active".to_string(),
1864 };
1865
1866 let row = formatter.row_from(&record);
1867 assert!(row.contains("test@example.com"));
1868 assert!(row.contains("active"));
1869 }
1870
1871 #[test]
1872 fn row_from_array_index() {
1873 #[derive(Serialize)]
1874 struct Record {
1875 items: Vec<String>,
1876 }
1877
1878 let spec = FlatDataSpec::builder()
1879 .column(Column::new(Width::Fixed(10)).key("items.0"))
1880 .column(Column::new(Width::Fixed(10)).key("items.1"))
1881 .build();
1882 let formatter = TabularFormatter::new(&spec, 80);
1883
1884 let record = Record {
1885 items: vec!["First".to_string(), "Second".to_string()],
1886 };
1887
1888 let row = formatter.row_from(&record);
1889 assert!(row.contains("First"));
1890 assert!(row.contains("Second"));
1891 }
1892
1893 #[test]
1894 fn row_from_missing_field_uses_null_repr() {
1895 #[derive(Serialize)]
1896 struct Record {
1897 present: String,
1898 }
1899
1900 let spec = FlatDataSpec::builder()
1901 .column(Column::new(Width::Fixed(10)).key("present"))
1902 .column(Column::new(Width::Fixed(10)).key("missing").null_repr("-"))
1903 .build();
1904 let formatter = TabularFormatter::new(&spec, 80);
1905
1906 let record = Record {
1907 present: "value".to_string(),
1908 };
1909
1910 let row = formatter.row_from(&record);
1911 assert!(row.contains("value"));
1912 }
1914
1915 #[test]
1916 fn row_from_no_key_uses_null_repr() {
1917 #[derive(Serialize)]
1918 struct Record {
1919 value: String,
1920 }
1921
1922 let spec = FlatDataSpec::builder()
1923 .column(Column::new(Width::Fixed(10)).null_repr("N/A"))
1924 .build();
1925 let formatter = TabularFormatter::new(&spec, 80);
1926
1927 let record = Record {
1928 value: "test".to_string(),
1929 };
1930
1931 let row = formatter.row_from(&record);
1932 assert!(row.contains("N/A"));
1933 }
1934
1935 #[test]
1936 fn row_from_various_types() {
1937 #[derive(Serialize)]
1938 struct Record {
1939 string_val: String,
1940 int_val: i64,
1941 float_val: f64,
1942 bool_val: bool,
1943 }
1944
1945 let spec = FlatDataSpec::builder()
1946 .column(Column::new(Width::Fixed(10)).key("string_val"))
1947 .column(Column::new(Width::Fixed(10)).key("int_val"))
1948 .column(Column::new(Width::Fixed(10)).key("float_val"))
1949 .column(Column::new(Width::Fixed(10)).key("bool_val"))
1950 .build();
1951 let formatter = TabularFormatter::new(&spec, 80);
1952
1953 let record = Record {
1954 string_val: "text".to_string(),
1955 int_val: 123,
1956 float_val: 9.87,
1957 bool_val: true,
1958 };
1959
1960 let row = formatter.row_from(&record);
1961 assert!(row.contains("text"));
1962 assert!(row.contains("123"));
1963 assert!(row.contains("9.87"));
1964 assert!(row.contains("true"));
1965 }
1966
1967 #[test]
1968 fn extract_field_simple() {
1969 let json = serde_json::json!({
1970 "name": "Alice",
1971 "age": 30
1972 });
1973
1974 assert_eq!(extract_field(&json, "name"), "Alice");
1975 assert_eq!(extract_field(&json, "age"), "30");
1976 assert_eq!(extract_field(&json, "missing"), "");
1977 }
1978
1979 #[test]
1980 fn extract_field_nested() {
1981 let json = serde_json::json!({
1982 "user": {
1983 "profile": {
1984 "email": "test@example.com"
1985 }
1986 }
1987 });
1988
1989 assert_eq!(
1990 extract_field(&json, "user.profile.email"),
1991 "test@example.com"
1992 );
1993 assert_eq!(extract_field(&json, "user.missing"), "");
1994 }
1995
1996 #[test]
1997 fn extract_field_array() {
1998 let json = serde_json::json!({
1999 "items": ["a", "b", "c"]
2000 });
2001
2002 assert_eq!(extract_field(&json, "items.0"), "a");
2003 assert_eq!(extract_field(&json, "items.1"), "b");
2004 assert_eq!(extract_field(&json, "items.10"), ""); }
2006
2007 #[test]
2008 fn row_lines_from_struct() {
2009 #[derive(Serialize)]
2010 struct Record {
2011 description: String,
2012 status: String,
2013 }
2014
2015 let spec = FlatDataSpec::builder()
2016 .column(Column::new(Width::Fixed(10)).key("description").wrap())
2017 .column(Column::new(Width::Fixed(6)).key("status"))
2018 .separator(" ")
2019 .build();
2020 let formatter = TabularFormatter::new(&spec, 80);
2021
2022 let record = Record {
2023 description: "A longer description that wraps".to_string(),
2024 status: "OK".to_string(),
2025 };
2026
2027 let lines = formatter.row_lines_from(&record);
2028 assert!(!lines.is_empty());
2030 }
2031
2032 #[test]
2037 fn format_cell_with_style() {
2038 let spec = FlatDataSpec::builder()
2039 .column(Column::new(Width::Fixed(10)).style("header"))
2040 .build();
2041 let formatter = TabularFormatter::new(&spec, 80);
2042
2043 let output = formatter.format_row(&["Hello"]);
2044 assert!(output.starts_with("[header]"));
2046 assert!(output.ends_with("[/header]"));
2047 assert!(output.contains("Hello"));
2048 }
2049
2050 #[test]
2051 fn format_cell_style_from_value() {
2052 let spec = FlatDataSpec::builder()
2053 .column(Column::new(Width::Fixed(10)).style_from_value())
2054 .build();
2055 let formatter = TabularFormatter::new(&spec, 80);
2056
2057 let output = formatter.format_row(&["error"]);
2058 assert!(output.contains("[error]"));
2060 assert!(output.contains("[/error]"));
2061 }
2062
2063 #[test]
2064 fn format_cell_no_style() {
2065 let spec = FlatDataSpec::builder()
2066 .column(Column::new(Width::Fixed(10)))
2067 .build();
2068 let formatter = TabularFormatter::new(&spec, 80);
2069
2070 let output = formatter.format_row(&["Hello"]);
2071 assert!(!output.contains("["));
2073 assert!(!output.contains("]"));
2074 assert!(output.contains("Hello"));
2075 }
2076
2077 #[test]
2078 fn format_cell_style_overrides_style_from_value() {
2079 let mut col = Column::new(Width::Fixed(10));
2081 col.style = Some("default".to_string());
2082 col.style_from_value = true;
2083
2084 let spec = FlatDataSpec::builder().column(col).build();
2085 let formatter = TabularFormatter::new(&spec, 80);
2086
2087 let output = formatter.format_row(&["custom"]);
2088 assert!(output.contains("[custom]"));
2090 assert!(output.contains("[/custom]"));
2091 }
2092
2093 #[test]
2094 fn format_row_multiple_styled_columns() {
2095 let spec = FlatDataSpec::builder()
2096 .column(Column::new(Width::Fixed(8)).style("name"))
2097 .column(Column::new(Width::Fixed(8)).style("status"))
2098 .separator(" ")
2099 .build();
2100 let formatter = TabularFormatter::new(&spec, 80);
2101
2102 let output = formatter.format_row(&["Alice", "Active"]);
2103 assert!(output.contains("[name]"));
2104 assert!(output.contains("[status]"));
2105 }
2106
2107 #[test]
2108 fn format_cell_lines_with_style() {
2109 let spec = FlatDataSpec::builder()
2110 .column(Column::new(Width::Fixed(10)).wrap().style("text"))
2111 .build();
2112 let formatter = TabularFormatter::new(&spec, 80);
2113
2114 let lines = formatter.format_row_lines(&["This is a long text that wraps"]);
2115
2116 for line in &lines {
2118 assert!(line.contains("[text]"));
2119 assert!(line.contains("[/text]"));
2120 }
2121 }
2122
2123 #[test]
2128 fn extract_headers_from_header_field() {
2129 let spec = FlatDataSpec::builder()
2130 .column(Column::new(Width::Fixed(10)).header("Name"))
2131 .column(Column::new(Width::Fixed(8)).header("Status"))
2132 .build();
2133 let formatter = TabularFormatter::new(&spec, 80);
2134
2135 let headers = formatter.extract_headers();
2136 assert_eq!(headers, vec!["Name", "Status"]);
2137 }
2138
2139 #[test]
2140 fn extract_headers_fallback_to_key() {
2141 let spec = FlatDataSpec::builder()
2142 .column(Column::new(Width::Fixed(10)).key("user_name"))
2143 .column(Column::new(Width::Fixed(8)).key("status"))
2144 .build();
2145 let formatter = TabularFormatter::new(&spec, 80);
2146
2147 let headers = formatter.extract_headers();
2148 assert_eq!(headers, vec!["user_name", "status"]);
2149 }
2150
2151 #[test]
2152 fn extract_headers_fallback_to_name() {
2153 let spec = FlatDataSpec::builder()
2154 .column(Column::new(Width::Fixed(10)).named("col1"))
2155 .column(Column::new(Width::Fixed(8)).named("col2"))
2156 .build();
2157 let formatter = TabularFormatter::new(&spec, 80);
2158
2159 let headers = formatter.extract_headers();
2160 assert_eq!(headers, vec!["col1", "col2"]);
2161 }
2162
2163 #[test]
2164 fn extract_headers_priority_order() {
2165 let spec = FlatDataSpec::builder()
2167 .column(
2168 Column::new(Width::Fixed(10))
2169 .header("Header")
2170 .key("key")
2171 .named("name"),
2172 )
2173 .column(
2174 Column::new(Width::Fixed(10))
2175 .key("key_only")
2176 .named("name_only"),
2177 )
2178 .column(Column::new(Width::Fixed(10)).named("name_only"))
2179 .column(Column::new(Width::Fixed(10))) .build();
2181 let formatter = TabularFormatter::new(&spec, 80);
2182
2183 let headers = formatter.extract_headers();
2184 assert_eq!(headers, vec!["Header", "key_only", "name_only", ""]);
2185 }
2186
2187 #[test]
2188 fn extract_headers_empty_spec() {
2189 let spec = FlatDataSpec::builder().build();
2190 let formatter = TabularFormatter::new(&spec, 80);
2191
2192 let headers = formatter.extract_headers();
2193 assert!(headers.is_empty());
2194 }
2195
2196 use crate::tabular::{SubCol, SubColumns};
2201
2202 fn padz_spec() -> (FlatDataSpec, SubColumns) {
2203 let sub_cols =
2204 SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 20).right()], " ").unwrap();
2205
2206 let spec = FlatDataSpec::builder()
2207 .column(Column::new(Width::Fixed(4)))
2208 .column(Column::new(Width::Fill).sub_columns(sub_cols.clone()))
2209 .column(Column::new(Width::Fixed(6)).right())
2210 .separator(" ")
2211 .build();
2212
2213 (spec, sub_cols)
2214 }
2215
2216 #[test]
2217 fn sub_column_basic_title_and_tag() {
2218 let (spec, _) = padz_spec();
2219 let formatter = TabularFormatter::new(&spec, 60);
2220
2221 let row = formatter.format_row_cells(&[
2222 CellValue::Single("1."),
2223 CellValue::Sub(vec!["Gallery Navigation", "[feature]"]),
2224 CellValue::Single("4d"),
2225 ]);
2226
2227 assert!(row.contains("Gallery Navigation"));
2228 assert!(row.contains("[feature]"));
2229 assert!(row.contains("1."));
2230 assert!(row.contains("4d"));
2231 assert_eq!(display_width(&row), 60);
2232 }
2233
2234 #[test]
2235 fn sub_column_tag_absent() {
2236 let (spec, _) = padz_spec();
2237 let formatter = TabularFormatter::new(&spec, 60);
2238
2239 let row = formatter.format_row_cells(&[
2240 CellValue::Single("3."),
2241 CellValue::Sub(vec!["Fixing Layout of Image Nav", ""]),
2242 CellValue::Single("4d"),
2243 ]);
2244
2245 assert!(row.contains("Fixing Layout of Image Nav"));
2246 assert_eq!(display_width(&row), 60);
2248 }
2249
2250 #[test]
2251 fn sub_column_grower_gets_remaining_space() {
2252 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::fixed(10)], " ").unwrap();
2253
2254 let widths = resolve_sub_widths(&sub_cols, &["title", "fixed"], 50);
2255 assert_eq!(widths[0], 38);
2257 assert_eq!(widths[1], 10);
2258 }
2259
2260 #[test]
2261 fn sub_column_non_grower_respects_fixed() {
2262 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::fixed(15)], " ").unwrap();
2263
2264 let widths = resolve_sub_widths(&sub_cols, &["x", "y"], 40);
2265 assert_eq!(widths[1], 15); assert_eq!(widths[0], 24); }
2268
2269 #[test]
2270 fn sub_column_non_grower_respects_bounded() {
2271 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::bounded(5, 20)], " ").unwrap();
2272
2273 let widths = resolve_sub_widths(&sub_cols, &["title", "short"], 40);
2275 assert_eq!(widths[1], 5);
2276 assert_eq!(widths[0], 34); let widths2 = resolve_sub_widths(&sub_cols, &["title", "a very long tag value!"], 40);
2280 assert_eq!(widths2[1], 20);
2281 assert_eq!(widths2[0], 19); let widths3 = resolve_sub_widths(&sub_cols, &["title", ""], 40);
2285 assert_eq!(widths3[1], 5);
2286 }
2287
2288 #[test]
2289 fn sub_column_bounded_min_zero() {
2290 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 20)], " ").unwrap();
2291
2292 let widths = resolve_sub_widths(&sub_cols, &["title", ""], 40);
2294 assert_eq!(widths[1], 0);
2295 assert_eq!(widths[0], 40);
2298 }
2299
2300 #[test]
2301 fn sub_column_separator_skipped_for_zero_width() {
2302 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 20)], " ").unwrap();
2303
2304 let result1 = format_sub_cells(&sub_cols, &["Title", "tag"], 30);
2306 assert!(result1.contains(" ")); assert_eq!(display_width(&result1), 30);
2308
2309 let result2 = format_sub_cells(&sub_cols, &["Title", ""], 30);
2311 assert_eq!(display_width(&result2), 30);
2312 }
2314
2315 #[test]
2316 fn sub_column_alignment() {
2317 let sub_cols = SubColumns::new(
2318 vec![
2319 SubCol::fill(), SubCol::fixed(10).right(),
2321 ],
2322 " ",
2323 )
2324 .unwrap();
2325
2326 let result = format_sub_cells(&sub_cols, &["Left", "Right"], 30);
2327 assert!(result.starts_with("Left"));
2329 assert!(result.ends_with(" Right"));
2331 assert_eq!(display_width(&result), 30);
2332 }
2333
2334 #[test]
2335 fn sub_column_grower_truncation() {
2336 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::fixed(15)], " ").unwrap();
2337
2338 let result = format_sub_cells(
2341 &sub_cols,
2342 &["A very long title that exceeds", "fixed-col"],
2343 25,
2344 );
2345 assert_eq!(display_width(&result), 25);
2346 assert!(result.contains("…")); }
2348
2349 #[test]
2350 fn sub_column_style_application() {
2351 let sub_cols = SubColumns::new(
2352 vec![SubCol::fill(), SubCol::bounded(0, 20).right().style("tag")],
2353 " ",
2354 )
2355 .unwrap();
2356
2357 let result = format_sub_cells(&sub_cols, &["Title", "feature"], 40);
2358 assert!(result.contains("[tag]"));
2359 assert!(result.contains("[/tag]"));
2360 assert!(result.contains("feature"));
2361 }
2362
2363 #[test]
2364 fn sub_column_grower_zero_width() {
2365 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::fixed(20)], " ").unwrap();
2368
2369 let widths = resolve_sub_widths(&sub_cols, &["title", "fixed"], 20);
2370 assert_eq!(widths[0], 0); assert_eq!(widths[1], 19); let result = format_sub_cells(&sub_cols, &["title", "fixed"], 20);
2375 assert_eq!(display_width(&result), 20);
2376 }
2377
2378 #[test]
2379 fn sub_column_all_empty() {
2380 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 20)], " ").unwrap();
2381
2382 let result = format_sub_cells(&sub_cols, &["", ""], 30);
2383 assert_eq!(display_width(&result), 30);
2384 }
2385
2386 #[test]
2387 fn sub_column_plain_string_fallback() {
2388 let (spec, _) = padz_spec();
2391 let formatter = TabularFormatter::new(&spec, 60);
2392
2393 let row = formatter.format_row(&["1.", "Just a title", "4d"]);
2394 assert_eq!(display_width(&row), 60);
2396 assert!(row.contains("Just a title"));
2397 }
2398
2399 #[test]
2400 fn sub_column_format_row_cells_api() {
2401 let sub_cols =
2402 SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 15).right()], " ").unwrap();
2403
2404 let spec = FlatDataSpec::builder()
2405 .column(Column::new(Width::Fixed(3)))
2406 .column(Column::new(Width::Fill).sub_columns(sub_cols))
2407 .separator(" ")
2408 .build();
2409
2410 let formatter = TabularFormatter::new(&spec, 50);
2411
2412 let row1 = formatter.format_row_cells(&[
2414 CellValue::Single("1."),
2415 CellValue::Sub(vec!["Title", "[bug]"]),
2416 ]);
2417 assert_eq!(display_width(&row1), 50);
2418 assert!(row1.contains("Title"));
2419 assert!(row1.contains("[bug]"));
2420
2421 let row2 = formatter.format_row_cells(&[
2423 CellValue::Single("2."),
2424 CellValue::Sub(vec!["Longer Title Here", ""]),
2425 ]);
2426 assert_eq!(display_width(&row2), 50);
2427 assert!(row2.contains("Longer Title Here"));
2428 }
2429
2430 #[test]
2431 fn sub_column_via_template() {
2432 use minijinja::Environment;
2433
2434 let sub_cols =
2435 SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 15).right()], " ").unwrap();
2436
2437 let spec = TabularSpec::builder()
2438 .column(Column::new(Width::Fixed(4)))
2439 .column(Column::new(Width::Fill).sub_columns(sub_cols))
2440 .separator(" ")
2441 .build();
2442 let formatter = TabularFormatter::new(&spec, 50);
2443
2444 let mut env = Environment::new();
2445 env.add_template("test", "{{ t.row(['1.', ['My Title', '[tag]']]) }}")
2446 .unwrap();
2447
2448 let tmpl = env.get_template("test").unwrap();
2449 let output = tmpl
2450 .render(minijinja::context! { t => Value::from_object(formatter) })
2451 .unwrap();
2452
2453 assert_eq!(display_width(&output), 50);
2454 assert!(output.contains("My Title"));
2455 assert!(output.contains("[tag]"));
2456 }
2457
2458 #[test]
2459 fn sub_column_multiple_rows_alignment() {
2460 let sub_cols =
2461 SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 15).right()], " ").unwrap();
2462
2463 let spec = FlatDataSpec::builder()
2464 .column(Column::new(Width::Fixed(4)))
2465 .column(Column::new(Width::Fill).sub_columns(sub_cols))
2466 .column(Column::new(Width::Fixed(4)).right())
2467 .separator(" ")
2468 .build();
2469
2470 let formatter = TabularFormatter::new(&spec, 60);
2471
2472 let rows = vec![
2473 vec![
2474 CellValue::Single("1."),
2475 CellValue::Sub(vec!["GitHub integration", "[feature]"]),
2476 CellValue::Single("8h"),
2477 ],
2478 vec![
2479 CellValue::Single("2."),
2480 CellValue::Sub(vec!["Bug : Static", "[bug]"]),
2481 CellValue::Single("4d"),
2482 ],
2483 vec![
2484 CellValue::Single("3."),
2485 CellValue::Sub(vec!["Fixing Layout of Image Nav", ""]),
2486 CellValue::Single("4d"),
2487 ],
2488 ];
2489
2490 for (i, row) in rows.iter().enumerate() {
2491 let output = formatter.format_row_cells(row);
2492 assert_eq!(
2493 display_width(&output),
2494 60,
2495 "Row {} has wrong width: '{}'",
2496 i,
2497 output
2498 );
2499 }
2500 }
2501 #[test]
2506 fn format_value_bbcode_preserves_tags_when_fitting() {
2507 let overflow = Overflow::Truncate {
2508 at: TruncateAt::End,
2509 marker: "…".to_string(),
2510 };
2511 let result = format_value("[bold]hello[/bold]", 10, Align::Left, &overflow, None);
2513 let stripped = standout_bbparser::strip_tags(&result);
2514 assert_eq!(display_width(&stripped), 10, "visible width should be 10");
2515 assert!(
2516 result.contains("[bold]hello[/bold]"),
2517 "tags should be preserved when content fits"
2518 );
2519 }
2520
2521 #[test]
2522 fn format_value_bbcode_truncation() {
2523 let overflow = Overflow::Truncate {
2524 at: TruncateAt::End,
2525 marker: "…".to_string(),
2526 };
2527 let result = format_value("[red]hello world[/red]", 8, Align::Left, &overflow, None);
2529 let stripped = standout_bbparser::strip_tags(&result);
2530 assert_eq!(
2531 display_width(&stripped),
2532 8,
2533 "truncated output should be exactly 8 visible columns"
2534 );
2535 }
2536
2537 #[test]
2538 fn format_value_bbcode_right_align() {
2539 let overflow = Overflow::Truncate {
2540 at: TruncateAt::End,
2541 marker: "…".to_string(),
2542 };
2543 let result = format_value("[dim]hi[/dim]", 6, Align::Right, &overflow, None);
2544 let stripped = standout_bbparser::strip_tags(&result);
2545 assert_eq!(display_width(&stripped), 6);
2546 assert!(result.contains("[dim]hi[/dim]"));
2548 assert!(result.starts_with(" "));
2549 }
2550
2551 #[test]
2552 fn format_value_bbcode_with_style() {
2553 let overflow = Overflow::Truncate {
2554 at: TruncateAt::End,
2555 marker: "…".to_string(),
2556 };
2557 let result = format_value("[dim]ok[/dim]", 8, Align::Left, &overflow, Some("green"));
2559 let stripped = standout_bbparser::strip_tags(&result);
2560 assert_eq!(display_width(&stripped), 8);
2561 assert!(result.starts_with("[green]"));
2563 assert!(result.ends_with("[/green]"));
2564 }
2565
2566 #[test]
2567 fn format_cell_lines_bbcode_wrap() {
2568 let col = Column::new(Width::Fixed(10)).overflow(Overflow::Wrap { indent: 0 });
2569 let result = format_cell_lines("[bold]hello world foo[/bold]", 10, &col);
2571 match result {
2572 CellOutput::Multi(lines) => {
2573 for line in &lines {
2574 let stripped = standout_bbparser::strip_tags(line);
2575 assert!(
2576 display_width(&stripped) <= 10,
2577 "wrapped line '{}' exceeds column width (visible: {})",
2578 line,
2579 display_width(&stripped)
2580 );
2581 }
2582 }
2583 CellOutput::Single(s) => {
2584 let stripped = standout_bbparser::strip_tags(&s);
2585 assert!(display_width(&stripped) <= 10, "single line should fit");
2586 }
2587 }
2588 }
2589
2590 #[test]
2591 fn format_cell_lines_bbcode_fits_preserves_tags() {
2592 let col = Column::new(Width::Fixed(10)).overflow(Overflow::Wrap { indent: 0 });
2593 let result = format_cell_lines("[bold]hi[/bold]", 10, &col);
2595 match result {
2596 CellOutput::Single(s) => {
2597 assert!(
2598 s.contains("[bold]hi[/bold]"),
2599 "tags should be preserved when content fits"
2600 );
2601 let stripped = standout_bbparser::strip_tags(&s);
2602 assert_eq!(display_width(&stripped), 10);
2603 }
2604 _ => panic!("expected Single output"),
2605 }
2606 }
2607
2608 #[test]
2609 fn cell_output_line_bbcode_padding() {
2610 let output = CellOutput::Single("[green]ok[/green]".to_string());
2612 let line = output.line(0, 8, Align::Left);
2613 let stripped = standout_bbparser::strip_tags(&line);
2614 assert_eq!(
2615 display_width(&stripped),
2616 8,
2617 "CellOutput::line should pad to correct visible width"
2618 );
2619 }
2620
2621 #[test]
2622 fn resolve_sub_widths_bbcode() {
2623 use crate::tabular::{SubCol, SubColumns};
2624 let sub_cols =
2625 SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 30).right()], " ").unwrap();
2626 let widths = resolve_sub_widths(&sub_cols, &["Title", "[dim][tag][/dim]"], 30);
2628 assert_eq!(
2630 widths[1], 5,
2631 "bounded sub-col should use visible width, not raw string length"
2632 );
2633 assert_eq!(
2634 widths[0] + widths[1] + 1, 30,
2636 "widths + separator should equal parent width"
2637 );
2638 }
2639}
2640
2641#[cfg(test)]
2642mod proptests {
2643 use super::*;
2644 use crate::tabular::{SubCol, SubColumns};
2645 use proptest::prelude::*;
2646
2647 proptest! {
2648 #[test]
2649 fn sub_column_output_width_equals_parent(
2650 parent_width in 10usize..100,
2651 title_len in 0usize..50,
2652 tag_len in 0usize..30,
2653 bounded_max in 5usize..30,
2654 ) {
2655 let sub_cols = SubColumns::new(
2656 vec![SubCol::fill(), SubCol::bounded(0, bounded_max)],
2657 " ",
2658 ).unwrap();
2659
2660 let title: String = "x".repeat(title_len);
2661 let tag: String = "y".repeat(tag_len);
2662 let values: Vec<&str> = vec![&title, &tag];
2663
2664 let result = format_sub_cells(&sub_cols, &values, parent_width);
2665 prop_assert_eq!(
2666 display_width(&result),
2667 parent_width,
2668 "sub-cell output must exactly fill parent width. Got '{}' (dw={}), expected {}",
2669 result, display_width(&result), parent_width
2670 );
2671 }
2672
2673 #[test]
2674 fn sub_column_non_grower_respects_bounds(
2675 parent_width in 30usize..100,
2676 min_w in 0usize..10,
2677 max_w_offset in 1usize..20,
2678 content_len in 0usize..40,
2679 ) {
2680 let max_w = min_w + max_w_offset; let sub_cols = SubColumns::new(
2682 vec![SubCol::fill(), SubCol::bounded(min_w, max_w)],
2683 " ",
2684 ).unwrap();
2685
2686 let content: String = "z".repeat(content_len);
2687 let values = vec!["title", content.as_str()];
2688 let widths = resolve_sub_widths(&sub_cols, &values, parent_width);
2689
2690 let bounded_width = widths[1];
2691 prop_assert!(
2692 bounded_width >= min_w,
2693 "bounded width {} < min {}", bounded_width, min_w
2694 );
2695 prop_assert!(
2696 bounded_width <= max_w,
2697 "bounded width {} > max {}", bounded_width, max_w
2698 );
2699 }
2700
2701 #[test]
2702 fn sub_column_width_arithmetic(
2703 parent_width in 10usize..100,
2704 fixed_width in 1usize..15,
2705 title_len in 0usize..50,
2706 ) {
2707 let sub_cols = SubColumns::new(
2708 vec![SubCol::fill(), SubCol::fixed(fixed_width)],
2709 " ",
2710 ).unwrap();
2711
2712 let title: String = "t".repeat(title_len);
2713 let values = vec![title.as_str(), "fixed"];
2714 let widths = resolve_sub_widths(&sub_cols, &values, parent_width);
2715
2716 let sep_width = display_width(&sub_cols.separator);
2717 let visible_non_growers: usize = if widths[1] > 0 { 1 } else { 0 };
2721 let visible_count: usize = visible_non_growers + 1; let sep_overhead = visible_count.saturating_sub(1) * sep_width;
2723 let total: usize = widths.iter().sum::<usize>() + sep_overhead;
2724
2725 prop_assert_eq!(
2726 total, parent_width,
2727 "widths {:?} + sep_overhead {} != parent {}",
2728 widths, sep_overhead, parent_width
2729 );
2730 }
2731
2732 #[test]
2733 fn sub_column_output_three_sub_cols(
2734 parent_width in 20usize..100,
2735 prefix_len in 0usize..20,
2736 tag_len in 0usize..15,
2737 ) {
2738 let sub_cols = SubColumns::new(
2739 vec![
2740 SubCol::bounded(0, 10),
2741 SubCol::fill(),
2742 SubCol::bounded(0, 15).right(),
2743 ],
2744 " ",
2745 ).unwrap();
2746
2747 let prefix: String = "p".repeat(prefix_len);
2748 let tag: String = "t".repeat(tag_len);
2749 let values = vec![prefix.as_str(), "middle content", tag.as_str()];
2750
2751 let result = format_sub_cells(&sub_cols, &values, parent_width);
2752 prop_assert_eq!(
2753 display_width(&result),
2754 parent_width,
2755 "three sub-cols output must fill parent width"
2756 );
2757 }
2758 }
2759}