Skip to main content

standout_render/tabular/
formatter.rs

1//! Table row formatter.
2//!
3//! This module provides the `TabularFormatter` type that formats data rows
4//! according to a table specification, producing aligned output.
5//!
6//! # Template Integration
7//!
8//! `TabularFormatter` implements `minijinja::value::Object`, allowing it to be
9//! used in templates when injected via context:
10//!
11//! ```jinja
12//! {% for item in items %}
13//! {{ table.row([item.name, item.value, item.status]) }}
14//! {% endfor %}
15//! ```
16//!
17//! Available methods in templates:
18//! - `row(values)`: Format a row with the given values (array)
19//! - `num_columns`: Get the number of columns
20//!
21//! # Example with Context Injection
22//!
23//! ```rust,ignore
24//! use standout::tabular::{TabularSpec, Column, Width, TabularFormatter};
25//! use standout::context::ContextRegistry;
26//!
27//! let spec = TabularSpec::builder()
28//!     .column(Column::new(Width::Fixed(10)))
29//!     .column(Column::new(Width::Fill))
30//!     .separator("  ")
31//!     .build();
32//!
33//! let mut registry = ContextRegistry::new();
34//! registry.add_provider("table", |ctx| {
35//!     let formatter = TabularFormatter::new(&spec, ctx.terminal_width.unwrap_or(80));
36//!     minijinja::Value::from_object(formatter)
37//! });
38//! ```
39
40use 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::{Align, Anchor, Column, FlatDataSpec, Overflow, TabularSpec, TruncateAt};
48use super::util::{
49    display_width, pad_center, pad_left, pad_right, truncate_end, truncate_middle, truncate_start,
50    wrap_indent,
51};
52
53/// Formats table rows according to a specification.
54///
55/// The formatter holds resolved column widths and produces formatted rows.
56/// It supports row-by-row formatting for interleaved output patterns.
57///
58/// # Example
59///
60/// ```rust
61/// use standout::tabular::{FlatDataSpec, Column, Width, TabularFormatter};
62///
63/// let spec = FlatDataSpec::builder()
64///     .column(Column::new(Width::Fixed(8)))
65///     .column(Column::new(Width::Fill))
66///     .column(Column::new(Width::Fixed(10)))
67///     .separator("  ")
68///     .build();
69///
70/// let formatter = TabularFormatter::new(&spec, 80);
71///
72/// // Format rows one at a time (enables interleaved output)
73/// let row1 = formatter.format_row(&["abc123", "path/to/file.rs", "pending"]);
74/// println!("{}", row1);
75/// println!("  └─ Note: needs review");  // Interleaved content
76/// let row2 = formatter.format_row(&["def456", "src/lib.rs", "done"]);
77/// println!("{}", row2);
78/// ```
79#[derive(Clone, Debug)]
80pub struct TabularFormatter {
81    /// Column specifications.
82    columns: Vec<Column>,
83    /// Resolved widths for each column.
84    widths: Vec<usize>,
85    /// Column separator string.
86    separator: String,
87    /// Row prefix string.
88    prefix: String,
89    /// Row suffix string.
90    suffix: String,
91    /// Total target width for anchor calculations.
92    total_width: usize,
93}
94
95impl TabularFormatter {
96    /// Create a new formatter by resolving widths from the spec.
97    ///
98    /// # Arguments
99    ///
100    /// * `spec` - Table specification
101    /// * `total_width` - Total available width including decorations
102    pub fn new(spec: &FlatDataSpec, total_width: usize) -> Self {
103        let resolved = spec.resolve_widths(total_width);
104        Self::from_resolved_with_width(spec, resolved, total_width)
105    }
106
107    /// Create a formatter with pre-resolved widths.
108    ///
109    /// Use this when you've already calculated widths (e.g., from data).
110    pub fn from_resolved(spec: &FlatDataSpec, resolved: ResolvedWidths) -> Self {
111        // Calculate total width from resolved widths + overhead
112        let content_width: usize = resolved.widths.iter().sum();
113        let overhead = spec.decorations.overhead(resolved.widths.len());
114        let total_width = content_width + overhead;
115        Self::from_resolved_with_width(spec, resolved, total_width)
116    }
117
118    /// Create a formatter with pre-resolved widths and explicit total width.
119    pub fn from_resolved_with_width(
120        spec: &FlatDataSpec,
121        resolved: ResolvedWidths,
122        total_width: usize,
123    ) -> Self {
124        TabularFormatter {
125            columns: spec.columns.clone(),
126            widths: resolved.widths,
127            separator: spec.decorations.column_sep.clone(),
128            prefix: spec.decorations.row_prefix.clone(),
129            suffix: spec.decorations.row_suffix.clone(),
130            total_width,
131        }
132    }
133
134    /// Create a formatter from explicit widths and columns.
135    ///
136    /// This is useful for direct construction without a full FlatDataSpec.
137    pub fn with_widths(columns: Vec<Column>, widths: Vec<usize>) -> Self {
138        let total_width = widths.iter().sum();
139        TabularFormatter {
140            columns,
141            widths,
142            separator: String::new(),
143            prefix: String::new(),
144            suffix: String::new(),
145            total_width,
146        }
147    }
148
149    /// Create a formatter from a type that implements `Tabular`.
150    ///
151    /// This constructor uses the `TabularSpec` generated by the `#[derive(Tabular)]`
152    /// macro to configure the formatter.
153    ///
154    /// # Example
155    ///
156    /// ```rust,ignore
157    /// use standout::tabular::{Tabular, TabularFormatter};
158    /// use serde::Serialize;
159    ///
160    /// #[derive(Serialize, Tabular)]
161    /// struct Task {
162    ///     #[col(width = 8)]
163    ///     id: String,
164    ///     #[col(width = "fill")]
165    ///     title: String,
166    /// }
167    ///
168    /// let formatter = TabularFormatter::from_type::<Task>(80);
169    /// ```
170    pub fn from_type<T: super::traits::Tabular>(total_width: usize) -> Self {
171        let spec: TabularSpec = T::tabular_spec();
172        Self::new(&spec, total_width)
173    }
174
175    /// Set the total target width (for anchor gap calculations).
176    pub fn total_width(mut self, width: usize) -> Self {
177        self.total_width = width;
178        self
179    }
180
181    /// Set the column separator.
182    pub fn separator(mut self, sep: impl Into<String>) -> Self {
183        self.separator = sep.into();
184        self
185    }
186
187    /// Set the row prefix.
188    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
189        self.prefix = prefix.into();
190        self
191    }
192
193    /// Set the row suffix.
194    pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
195        self.suffix = suffix.into();
196        self
197    }
198
199    /// Format a single row of values.
200    ///
201    /// Values are truncated/padded according to the column specifications.
202    /// Missing values use the column's null representation.
203    ///
204    /// # Arguments
205    ///
206    /// * `values` - Slice of cell values (strings)
207    ///
208    /// # Example
209    ///
210    /// ```rust
211    /// use standout::tabular::{FlatDataSpec, Column, Width, TabularFormatter};
212    ///
213    /// let spec = FlatDataSpec::builder()
214    ///     .column(Column::new(Width::Fixed(10)))
215    ///     .column(Column::new(Width::Fixed(8)))
216    ///     .separator(" | ")
217    ///     .build();
218    ///
219    /// let formatter = TabularFormatter::new(&spec, 80);
220    /// let output = formatter.format_row(&["Hello", "World"]);
221    /// assert_eq!(output, "Hello      | World   ");
222    /// ```
223    pub fn format_row<S: AsRef<str>>(&self, values: &[S]) -> String {
224        let mut result = String::new();
225        result.push_str(&self.prefix);
226
227        // Find anchor transition point and calculate gap
228        let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
229
230        for (i, col) in self.columns.iter().enumerate() {
231            // Insert separator (or anchor gap at transition point)
232            if i > 0 {
233                if anchor_gap > 0 && i == anchor_transition {
234                    // Insert anchor gap instead of separator
235                    result.push_str(&" ".repeat(anchor_gap));
236                } else {
237                    result.push_str(&self.separator);
238                }
239            }
240
241            let width = self.widths.get(i).copied().unwrap_or(0);
242            let value = values.get(i).map(|s| s.as_ref()).unwrap_or(&col.null_repr);
243
244            let formatted = format_cell(value, width, col);
245            result.push_str(&formatted);
246        }
247
248        result.push_str(&self.suffix);
249        result
250    }
251
252    /// Calculate the anchor gap size and transition point.
253    ///
254    /// Returns (gap_size, transition_index) where:
255    /// - gap_size is the number of spaces to insert between left and right groups
256    /// - transition_index is the column index where right-anchored columns start
257    fn calculate_anchor_gap(&self) -> (usize, usize) {
258        // Find first right-anchored column
259        let transition = self
260            .columns
261            .iter()
262            .position(|c| c.anchor == Anchor::Right)
263            .unwrap_or(self.columns.len());
264
265        // If no right-anchored columns or all columns are right-anchored, no gap
266        if transition == 0 || transition == self.columns.len() {
267            return (0, transition);
268        }
269
270        // Calculate current content width
271        let prefix_width = display_width(&self.prefix);
272        let suffix_width = display_width(&self.suffix);
273        let sep_width = display_width(&self.separator);
274        let content_width: usize = self.widths.iter().sum();
275        let num_seps = self.columns.len().saturating_sub(1);
276        let current_total = prefix_width + content_width + (num_seps * sep_width) + suffix_width;
277
278        // Calculate gap - the extra space available to push right columns to the right
279        if current_total >= self.total_width {
280            // No room for a gap
281            (0, transition)
282        } else {
283            // Gap = extra space, minus one separator (which we replace with the gap)
284            let extra = self.total_width - current_total;
285            // The gap replaces one separator, so add sep_width back to the gap
286            (extra + sep_width, transition)
287        }
288    }
289
290    /// Format multiple rows.
291    ///
292    /// Returns a vector of formatted row strings.
293    pub fn format_rows<S: AsRef<str>>(&self, rows: &[Vec<S>]) -> Vec<String> {
294        rows.iter().map(|row| self.format_row(row)).collect()
295    }
296
297    /// Format a row that may produce multiple output lines (due to wrapping).
298    ///
299    /// If any cell wraps to multiple lines, the output contains multiple lines
300    /// with proper vertical alignment. Cells are top-aligned.
301    ///
302    /// # Example
303    ///
304    /// ```rust
305    /// use standout::tabular::{FlatDataSpec, Column, Width, Overflow, TabularFormatter};
306    ///
307    /// let spec = FlatDataSpec::builder()
308    ///     .column(Column::new(Width::Fixed(10)).wrap())
309    ///     .column(Column::new(Width::Fixed(8)))
310    ///     .separator("  ")
311    ///     .build();
312    ///
313    /// let formatter = TabularFormatter::new(&spec, 80);
314    /// let lines = formatter.format_row_lines(&["This is a long text", "Short"]);
315    /// // Returns multiple lines if the first column wraps
316    /// ```
317    pub fn format_row_lines<S: AsRef<str>>(&self, values: &[S]) -> Vec<String> {
318        // Format each cell
319        let cell_outputs: Vec<CellOutput> = self
320            .columns
321            .iter()
322            .enumerate()
323            .map(|(i, col)| {
324                let width = self.widths.get(i).copied().unwrap_or(0);
325                let value = values.get(i).map(|s| s.as_ref()).unwrap_or(&col.null_repr);
326                format_cell_lines(value, width, col)
327            })
328            .collect();
329
330        // Find max lines needed
331        let max_lines = cell_outputs
332            .iter()
333            .map(|c| c.line_count())
334            .max()
335            .unwrap_or(1);
336
337        // If only single line, use simple path
338        if max_lines == 1 {
339            return vec![self.format_row(values)];
340        }
341
342        // Build output lines with anchor support
343        let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
344        let mut output = Vec::with_capacity(max_lines);
345
346        for line_idx in 0..max_lines {
347            let mut row = String::new();
348            row.push_str(&self.prefix);
349
350            for (i, (cell, col)) in cell_outputs.iter().zip(self.columns.iter()).enumerate() {
351                if i > 0 {
352                    if anchor_gap > 0 && i == anchor_transition {
353                        row.push_str(&" ".repeat(anchor_gap));
354                    } else {
355                        row.push_str(&self.separator);
356                    }
357                }
358
359                let width = self.widths.get(i).copied().unwrap_or(0);
360                let line = cell.line(line_idx, width, col.align);
361                row.push_str(&line);
362            }
363
364            row.push_str(&self.suffix);
365            output.push(row);
366        }
367
368        output
369    }
370
371    /// Get the resolved width for a column by index.
372    pub fn column_width(&self, index: usize) -> Option<usize> {
373        self.widths.get(index).copied()
374    }
375
376    /// Get all resolved column widths.
377    pub fn widths(&self) -> &[usize] {
378        &self.widths
379    }
380
381    /// Get the number of columns.
382    pub fn num_columns(&self) -> usize {
383        self.columns.len()
384    }
385
386    /// Extract headers from column specifications.
387    ///
388    /// For each column, uses (in order of preference):
389    /// 1. The `header` field if set
390    /// 2. The `key` field if set
391    /// 3. The `name` field if set
392    /// 4. Empty string
393    ///
394    /// This is useful for `Table::header_from_columns()`.
395    pub fn extract_headers(&self) -> Vec<String> {
396        self.columns
397            .iter()
398            .map(|col| {
399                col.header
400                    .as_deref()
401                    .or(col.key.as_deref())
402                    .or(col.name.as_deref())
403                    .unwrap_or("")
404                    .to_string()
405            })
406            .collect()
407    }
408
409    /// Format a row by extracting values from a serializable struct.
410    ///
411    /// This method extracts field values from the struct based on each column's
412    /// `key` or `name` field. Supports dot notation for nested field access
413    /// (e.g., "user.email").
414    ///
415    /// # Arguments
416    ///
417    /// * `value` - Any serializable value to extract fields from
418    ///
419    /// # Example
420    ///
421    /// ```rust
422    /// use standout::tabular::{FlatDataSpec, Column, Width, TabularFormatter};
423    /// use serde::Serialize;
424    ///
425    /// #[derive(Serialize)]
426    /// struct Record {
427    ///     name: String,
428    ///     status: String,
429    ///     count: u32,
430    /// }
431    ///
432    /// let spec = FlatDataSpec::builder()
433    ///     .column(Column::new(Width::Fixed(20)).key("name"))
434    ///     .column(Column::new(Width::Fixed(10)).key("status"))
435    ///     .column(Column::new(Width::Fixed(5)).key("count"))
436    ///     .separator("  ")
437    ///     .build();
438    ///
439    /// let formatter = TabularFormatter::new(&spec, 80);
440    /// let record = Record {
441    ///     name: "example".to_string(),
442    ///     status: "active".to_string(),
443    ///     count: 42,
444    /// };
445    ///
446    /// let row = formatter.row_from(&record);
447    /// assert!(row.contains("example"));
448    /// assert!(row.contains("active"));
449    /// assert!(row.contains("42"));
450    /// ```
451    pub fn row_from<T: Serialize>(&self, value: &T) -> String {
452        let values = self.extract_values(value);
453        let string_refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
454        self.format_row(&string_refs)
455    }
456
457    /// Format a row with potential multi-line output from a serializable struct.
458    ///
459    /// Same as `row_from` but handles word-wrap columns that may produce
460    /// multiple output lines.
461    pub fn row_lines_from<T: Serialize>(&self, value: &T) -> Vec<String> {
462        let values = self.extract_values(value);
463        let string_refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
464        self.format_row_lines(&string_refs)
465    }
466
467    /// Format a row using the `TabularRow` trait.
468    ///
469    /// This method uses the optimized `to_row()` implementation generated by
470    /// `#[derive(TabularRow)]`, avoiding JSON serialization overhead.
471    ///
472    /// # Example
473    ///
474    /// ```rust,ignore
475    /// use standout::tabular::{TabularRow, TabularFormatter};
476    ///
477    /// #[derive(TabularRow)]
478    /// struct Task {
479    ///     id: String,
480    ///     title: String,
481    /// }
482    ///
483    /// let task = Task {
484    ///     id: "TSK-001".to_string(),
485    ///     title: "Implement feature".to_string(),
486    /// };
487    ///
488    /// let formatter = TabularFormatter::from_type::<Task>(80);
489    /// let row = formatter.row_from_trait(&task);
490    /// ```
491    pub fn row_from_trait<T: TabularRow>(&self, value: &T) -> String {
492        let values = value.to_row();
493        self.format_row(&values)
494    }
495
496    /// Format a row with potential multi-line output using the `TabularRow` trait.
497    ///
498    /// Same as `row_from_trait` but handles word-wrap columns that may produce
499    /// multiple output lines.
500    pub fn row_lines_from_trait<T: TabularRow>(&self, value: &T) -> Vec<String> {
501        let values = value.to_row();
502        self.format_row_lines(&values)
503    }
504
505    /// Extract values from a serializable struct based on column keys.
506    fn extract_values<T: Serialize>(&self, value: &T) -> Vec<String> {
507        // Convert to JSON for field access
508        let json = match serde_json::to_value(value) {
509            Ok(v) => v,
510            Err(_) => return vec![String::new(); self.columns.len()],
511        };
512
513        self.columns
514            .iter()
515            .map(|col| {
516                // Use key first, fall back to name
517                let key = col.key.as_ref().or(col.name.as_ref());
518
519                match key {
520                    Some(k) => extract_field(&json, k),
521                    None => col.null_repr.clone(),
522                }
523            })
524            .collect()
525    }
526}
527
528/// Extract a field value from JSON using dot notation.
529///
530/// Supports paths like "user.email" or "items.0.name".
531fn extract_field(value: &JsonValue, path: &str) -> String {
532    let mut current = value;
533
534    for part in path.split('.') {
535        match current {
536            JsonValue::Object(map) => {
537                current = match map.get(part) {
538                    Some(v) => v,
539                    None => return String::new(),
540                };
541            }
542            JsonValue::Array(arr) => {
543                // Try to parse as index
544                if let Ok(idx) = part.parse::<usize>() {
545                    current = match arr.get(idx) {
546                        Some(v) => v,
547                        None => return String::new(),
548                    };
549                } else {
550                    return String::new();
551                }
552            }
553            _ => return String::new(),
554        }
555    }
556
557    // Convert final value to string
558    match current {
559        JsonValue::String(s) => s.clone(),
560        JsonValue::Number(n) => n.to_string(),
561        JsonValue::Bool(b) => b.to_string(),
562        JsonValue::Null => String::new(),
563        // For arrays/objects, use JSON representation
564        _ => current.to_string(),
565    }
566}
567
568// ============================================================================
569// MiniJinja Object Implementation
570// ============================================================================
571
572impl Object for TabularFormatter {
573    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
574        match key.as_str()? {
575            "num_columns" => Some(Value::from(self.num_columns())),
576            "widths" => {
577                let widths: Vec<Value> = self.widths.iter().map(|&w| Value::from(w)).collect();
578                Some(Value::from(widths))
579            }
580            "separator" => Some(Value::from(self.separator.clone())),
581            _ => None,
582        }
583    }
584
585    fn enumerate(self: &Arc<Self>) -> Enumerator {
586        Enumerator::Str(&["num_columns", "widths", "separator"])
587    }
588
589    fn call_method(
590        self: &Arc<Self>,
591        _state: &minijinja::State,
592        name: &str,
593        args: &[Value],
594    ) -> Result<Value, minijinja::Error> {
595        match name {
596            "row" => {
597                // row([value1, value2, ...]) - format a row
598                if args.is_empty() {
599                    return Err(minijinja::Error::new(
600                        minijinja::ErrorKind::MissingArgument,
601                        "row() requires an array of values",
602                    ));
603                }
604
605                let values_arg = &args[0];
606
607                // Handle both array and non-array arguments
608                let values: Vec<String> = match values_arg.try_iter() {
609                    Ok(iter) => iter.map(|v| v.to_string()).collect(),
610                    Err(_) => {
611                        // Single value - wrap in vec
612                        vec![values_arg.to_string()]
613                    }
614                };
615
616                let formatted = self.format_row(&values);
617                Ok(Value::from(formatted))
618            }
619            "column_width" => {
620                // column_width(index) - get width of a specific column
621                if args.is_empty() {
622                    return Err(minijinja::Error::new(
623                        minijinja::ErrorKind::MissingArgument,
624                        "column_width() requires an index argument",
625                    ));
626                }
627
628                let index = args[0].as_usize().ok_or_else(|| {
629                    minijinja::Error::new(
630                        minijinja::ErrorKind::InvalidOperation,
631                        "column_width() index must be a number",
632                    )
633                })?;
634
635                match self.column_width(index) {
636                    Some(w) => Ok(Value::from(w)),
637                    None => Ok(Value::from(())),
638                }
639            }
640            _ => Err(minijinja::Error::new(
641                minijinja::ErrorKind::UnknownMethod,
642                format!("TabularFormatter has no method '{}'", name),
643            )),
644        }
645    }
646}
647
648/// Format a single cell value according to column spec.
649fn format_cell(value: &str, width: usize, col: &Column) -> String {
650    // If style_from_value is set, use the value as the style
651    let style_override = if col.style_from_value {
652        Some(value)
653    } else {
654        None
655    };
656    format_cell_styled(value, width, col, style_override)
657}
658
659/// Format a single cell with optional style override.
660///
661/// If `style_override` is Some, it takes precedence over column's style.
662/// This is useful for dynamic styling based on cell content.
663fn format_cell_styled(
664    value: &str,
665    width: usize,
666    col: &Column,
667    style_override: Option<&str>,
668) -> String {
669    if width == 0 {
670        return String::new();
671    }
672
673    let current_width = display_width(value);
674
675    // Handle overflow
676    let processed = if current_width > width {
677        match &col.overflow {
678            Overflow::Truncate { at, marker } => match at {
679                TruncateAt::End => truncate_end(value, width, marker),
680                TruncateAt::Start => truncate_start(value, width, marker),
681                TruncateAt::Middle => truncate_middle(value, width, marker),
682            },
683            Overflow::Clip => {
684                // Hard cut with no marker
685                truncate_end(value, width, "")
686            }
687            Overflow::Expand => {
688                // Don't truncate, let it overflow
689                value.to_string()
690            }
691            Overflow::Wrap { .. } => {
692                // For single-line format_cell, truncate as fallback
693                // Multi-line wrapping is handled by format_cell_lines
694                truncate_end(value, width, "…")
695            }
696        }
697    } else {
698        value.to_string()
699    };
700
701    // Pad to width (skip if Expand mode overflowed)
702    let padded = if matches!(col.overflow, Overflow::Expand) && current_width > width {
703        processed
704    } else {
705        match col.align {
706            Align::Left => pad_right(&processed, width),
707            Align::Right => pad_left(&processed, width),
708            Align::Center => pad_center(&processed, width),
709        }
710    };
711
712    // Apply style wrapping
713    let style = style_override.or(col.style.as_deref());
714    match style {
715        Some(s) if !s.is_empty() => format!("[{}]{}[/{}]", s, padded, s),
716        _ => padded,
717    }
718}
719
720/// Result of formatting a cell, which may be single or multi-line.
721#[derive(Clone, Debug, PartialEq, Eq)]
722pub enum CellOutput {
723    /// Single line of formatted text.
724    Single(String),
725    /// Multiple lines (from word-wrap).
726    Multi(Vec<String>),
727}
728
729impl CellOutput {
730    /// Returns true if this is a single-line output.
731    pub fn is_single(&self) -> bool {
732        matches!(self, CellOutput::Single(_))
733    }
734
735    /// Returns the number of lines.
736    pub fn line_count(&self) -> usize {
737        match self {
738            CellOutput::Single(_) => 1,
739            CellOutput::Multi(lines) => lines.len().max(1),
740        }
741    }
742
743    /// Get a specific line, padding to width if needed.
744    pub fn line(&self, index: usize, width: usize, align: Align) -> String {
745        let content = match self {
746            CellOutput::Single(s) if index == 0 => s.as_str(),
747            CellOutput::Multi(lines) => lines.get(index).map(|s| s.as_str()).unwrap_or(""),
748            _ => "",
749        };
750
751        // Pad to width
752        match align {
753            Align::Left => pad_right(content, width),
754            Align::Right => pad_left(content, width),
755            Align::Center => pad_center(content, width),
756        }
757    }
758
759    /// Convert to a single string (first line for Multi).
760    pub fn to_single(&self) -> String {
761        match self {
762            CellOutput::Single(s) => s.clone(),
763            CellOutput::Multi(lines) => lines.first().cloned().unwrap_or_default(),
764        }
765    }
766}
767
768/// Wrap content with style tags if a style is specified.
769fn apply_style(content: &str, style: Option<&str>) -> String {
770    match style {
771        Some(s) if !s.is_empty() => format!("[{}]{}[/{}]", s, content, s),
772        _ => content.to_string(),
773    }
774}
775
776/// Format a cell with potential multi-line output (for Wrap mode).
777fn format_cell_lines(value: &str, width: usize, col: &Column) -> CellOutput {
778    if width == 0 {
779        return CellOutput::Single(String::new());
780    }
781
782    let current_width = display_width(value);
783
784    // Determine style: style_from_value takes precedence
785    let style = if col.style_from_value {
786        Some(value)
787    } else {
788        col.style.as_deref()
789    };
790
791    match &col.overflow {
792        Overflow::Wrap { indent } => {
793            if current_width <= width {
794                // Fits on one line
795                let padded = match col.align {
796                    Align::Left => pad_right(value, width),
797                    Align::Right => pad_left(value, width),
798                    Align::Center => pad_center(value, width),
799                };
800                CellOutput::Single(apply_style(&padded, style))
801            } else {
802                // Wrap to multiple lines
803                let wrapped = wrap_indent(value, width, *indent);
804                let padded: Vec<String> = wrapped
805                    .into_iter()
806                    .map(|line| {
807                        let padded_line = match col.align {
808                            Align::Left => pad_right(&line, width),
809                            Align::Right => pad_left(&line, width),
810                            Align::Center => pad_center(&line, width),
811                        };
812                        apply_style(&padded_line, style)
813                    })
814                    .collect();
815                if padded.len() == 1 {
816                    CellOutput::Single(padded.into_iter().next().unwrap())
817                } else {
818                    CellOutput::Multi(padded)
819                }
820            }
821        }
822        // All other modes are single-line
823        _ => CellOutput::Single(format_cell(value, width, col)),
824    }
825}
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830    use crate::tabular::{TabularSpec, Width};
831
832    fn simple_spec() -> FlatDataSpec {
833        FlatDataSpec::builder()
834            .column(Column::new(Width::Fixed(10)))
835            .column(Column::new(Width::Fixed(8)))
836            .separator(" | ")
837            .build()
838    }
839
840    #[test]
841    fn format_basic_row() {
842        let formatter = TabularFormatter::new(&simple_spec(), 80);
843        let output = formatter.format_row(&["Hello", "World"]);
844        assert_eq!(output, "Hello      | World   ");
845    }
846
847    #[test]
848    fn format_row_with_truncation() {
849        let spec = FlatDataSpec::builder()
850            .column(Column::new(Width::Fixed(8)))
851            .build();
852        let formatter = TabularFormatter::new(&spec, 80);
853
854        let output = formatter.format_row(&["Hello World"]);
855        assert_eq!(output, "Hello W…");
856    }
857
858    #[test]
859    fn format_row_right_align() {
860        let spec = FlatDataSpec::builder()
861            .column(Column::new(Width::Fixed(10)).align(Align::Right))
862            .build();
863        let formatter = TabularFormatter::new(&spec, 80);
864
865        let output = formatter.format_row(&["42"]);
866        assert_eq!(output, "        42");
867    }
868
869    #[test]
870    fn format_row_center_align() {
871        let spec = FlatDataSpec::builder()
872            .column(Column::new(Width::Fixed(10)).align(Align::Center))
873            .build();
874        let formatter = TabularFormatter::new(&spec, 80);
875
876        let output = formatter.format_row(&["hi"]);
877        assert_eq!(output, "    hi    ");
878    }
879
880    #[test]
881    fn format_row_truncate_start() {
882        let spec = FlatDataSpec::builder()
883            .column(Column::new(Width::Fixed(10)).truncate(TruncateAt::Start))
884            .build();
885        let formatter = TabularFormatter::new(&spec, 80);
886
887        let output = formatter.format_row(&["/path/to/file.rs"]);
888        assert_eq!(display_width(&output), 10);
889        assert!(output.starts_with("…"));
890    }
891
892    #[test]
893    fn format_row_truncate_middle() {
894        let spec = FlatDataSpec::builder()
895            .column(Column::new(Width::Fixed(10)).truncate(TruncateAt::Middle))
896            .build();
897        let formatter = TabularFormatter::new(&spec, 80);
898
899        let output = formatter.format_row(&["abcdefghijklmno"]);
900        assert_eq!(display_width(&output), 10);
901        assert!(output.contains("…"));
902    }
903
904    #[test]
905    fn format_row_with_null() {
906        let spec = FlatDataSpec::builder()
907            .column(Column::new(Width::Fixed(10)))
908            .column(Column::new(Width::Fixed(8)).null_repr("N/A"))
909            .separator("  ")
910            .build();
911        let formatter = TabularFormatter::new(&spec, 80);
912
913        // Only provide first value - second uses null_repr
914        let output = formatter.format_row(&["value"]);
915        assert!(output.contains("N/A"));
916    }
917
918    #[test]
919    fn format_row_with_decorations() {
920        let spec = FlatDataSpec::builder()
921            .column(Column::new(Width::Fixed(10)))
922            .column(Column::new(Width::Fixed(8)))
923            .separator(" │ ")
924            .prefix("│ ")
925            .suffix(" │")
926            .build();
927        let formatter = TabularFormatter::new(&spec, 80);
928
929        let output = formatter.format_row(&["Hello", "World"]);
930        assert!(output.starts_with("│ "));
931        assert!(output.ends_with(" │"));
932        assert!(output.contains(" │ "));
933    }
934
935    #[test]
936    fn format_multiple_rows() {
937        let formatter = TabularFormatter::new(&simple_spec(), 80);
938        let rows = vec![vec!["a", "1"], vec!["b", "2"], vec!["c", "3"]];
939
940        let output = formatter.format_rows(&rows);
941        assert_eq!(output.len(), 3);
942    }
943
944    #[test]
945    fn format_row_fill_column() {
946        let spec = FlatDataSpec::builder()
947            .column(Column::new(Width::Fixed(5)))
948            .column(Column::new(Width::Fill))
949            .column(Column::new(Width::Fixed(5)))
950            .separator("  ")
951            .build();
952
953        // Total: 30, overhead: 4 (2 separators), fixed: 10, fill: 16
954        let formatter = TabularFormatter::new(&spec, 30);
955        let _output = formatter.format_row(&["abc", "middle", "xyz"]);
956
957        // Check that widths are as expected
958        assert_eq!(formatter.widths(), &[5, 16, 5]);
959    }
960
961    #[test]
962    fn formatter_accessors() {
963        let spec = FlatDataSpec::builder()
964            .column(Column::new(Width::Fixed(10)))
965            .column(Column::new(Width::Fixed(8)))
966            .build();
967        let formatter = TabularFormatter::new(&spec, 80);
968
969        assert_eq!(formatter.num_columns(), 2);
970        assert_eq!(formatter.column_width(0), Some(10));
971        assert_eq!(formatter.column_width(1), Some(8));
972        assert_eq!(formatter.column_width(2), None);
973    }
974
975    #[test]
976    fn format_empty_spec() {
977        let spec = FlatDataSpec::builder().build();
978        let formatter = TabularFormatter::new(&spec, 80);
979
980        let output = formatter.format_row::<&str>(&[]);
981        assert_eq!(output, "");
982    }
983
984    #[test]
985    fn format_with_ansi() {
986        let spec = FlatDataSpec::builder()
987            .column(Column::new(Width::Fixed(10)))
988            .build();
989        let formatter = TabularFormatter::new(&spec, 80);
990
991        let styled = "\x1b[31mred\x1b[0m";
992        let output = formatter.format_row(&[styled]);
993
994        // ANSI codes should be preserved, display width should be 10
995        assert!(output.contains("\x1b[31m"));
996        assert_eq!(display_width(&output), 10);
997    }
998
999    #[test]
1000    fn format_with_explicit_widths() {
1001        let columns = vec![Column::new(Width::Fixed(5)), Column::new(Width::Fixed(10))];
1002        let formatter = TabularFormatter::with_widths(columns, vec![5, 10]).separator(" - ");
1003
1004        let output = formatter.format_row(&["hi", "there"]);
1005        assert_eq!(output, "hi    - there     ");
1006    }
1007
1008    // ============================================================================
1009    // Object Trait Tests
1010    // ============================================================================
1011
1012    #[test]
1013    fn object_get_num_columns() {
1014        let formatter = Arc::new(TabularFormatter::new(&simple_spec(), 80));
1015        let value = formatter.get_value(&Value::from("num_columns"));
1016        assert_eq!(value, Some(Value::from(2)));
1017    }
1018
1019    #[test]
1020    fn object_get_widths() {
1021        let spec = TabularSpec::builder()
1022            .column(Column::new(Width::Fixed(10)))
1023            .column(Column::new(Width::Fixed(8)))
1024            .build();
1025        let formatter = Arc::new(TabularFormatter::new(&spec, 80));
1026
1027        let value = formatter.get_value(&Value::from("widths"));
1028        assert!(value.is_some());
1029        let widths = value.unwrap();
1030        // Check we can iterate over the widths
1031        assert!(widths.try_iter().is_ok());
1032    }
1033
1034    #[test]
1035    fn object_get_separator() {
1036        let spec = TabularSpec::builder()
1037            .column(Column::new(Width::Fixed(10)))
1038            .separator(" | ")
1039            .build();
1040        let formatter = Arc::new(TabularFormatter::new(&spec, 80));
1041
1042        let value = formatter.get_value(&Value::from("separator"));
1043        assert_eq!(value, Some(Value::from(" | ")));
1044    }
1045
1046    #[test]
1047    fn object_get_unknown_returns_none() {
1048        let formatter = Arc::new(TabularFormatter::new(&simple_spec(), 80));
1049        let value = formatter.get_value(&Value::from("unknown"));
1050        assert_eq!(value, None);
1051    }
1052
1053    #[test]
1054    fn object_row_method_via_template() {
1055        use minijinja::Environment;
1056
1057        let spec = TabularSpec::builder()
1058            .column(Column::new(Width::Fixed(10)))
1059            .column(Column::new(Width::Fixed(8)))
1060            .separator(" | ")
1061            .build();
1062        let formatter = TabularFormatter::new(&spec, 80);
1063
1064        let mut env = Environment::new();
1065        env.add_template("test", "{{ table.row(['Hello', 'World']) }}")
1066            .unwrap();
1067
1068        let tmpl = env.get_template("test").unwrap();
1069        let output = tmpl
1070            .render(minijinja::context! { table => Value::from_object(formatter) })
1071            .unwrap();
1072
1073        assert_eq!(output, "Hello      | World   ");
1074    }
1075
1076    #[test]
1077    fn object_row_method_in_loop() {
1078        use minijinja::Environment;
1079
1080        let spec = TabularSpec::builder()
1081            .column(Column::new(Width::Fixed(8)))
1082            .column(Column::new(Width::Fixed(6)))
1083            .separator("  ")
1084            .build();
1085        let formatter = TabularFormatter::new(&spec, 80);
1086
1087        let mut env = Environment::new();
1088        env.add_template(
1089            "test",
1090            "{% for item in items %}{{ table.row([item.name, item.value]) }}\n{% endfor %}",
1091        )
1092        .unwrap();
1093
1094        let tmpl = env.get_template("test").unwrap();
1095        let output = tmpl
1096            .render(minijinja::context! {
1097                table => Value::from_object(formatter),
1098                items => vec![
1099                    minijinja::context! { name => "Alice", value => "100" },
1100                    minijinja::context! { name => "Bob", value => "200" },
1101                ]
1102            })
1103            .unwrap();
1104
1105        assert!(output.contains("Alice"));
1106        assert!(output.contains("Bob"));
1107    }
1108
1109    #[test]
1110    fn object_column_width_method_via_template() {
1111        use minijinja::Environment;
1112
1113        let spec = TabularSpec::builder()
1114            .column(Column::new(Width::Fixed(10)))
1115            .column(Column::new(Width::Fixed(8)))
1116            .build();
1117        let formatter = TabularFormatter::new(&spec, 80);
1118
1119        let mut env = Environment::new();
1120        env.add_template(
1121            "test",
1122            "{{ table.column_width(0) }}-{{ table.column_width(1) }}",
1123        )
1124        .unwrap();
1125
1126        let tmpl = env.get_template("test").unwrap();
1127        let output = tmpl
1128            .render(minijinja::context! { table => Value::from_object(formatter) })
1129            .unwrap();
1130
1131        assert_eq!(output, "10-8");
1132    }
1133
1134    #[test]
1135    fn object_attribute_access_via_template() {
1136        use minijinja::Environment;
1137
1138        let spec = TabularSpec::builder()
1139            .column(Column::new(Width::Fixed(10)))
1140            .column(Column::new(Width::Fixed(8)))
1141            .separator(" | ")
1142            .build();
1143        let formatter = TabularFormatter::new(&spec, 80);
1144
1145        let mut env = Environment::new();
1146        env.add_template(
1147            "test",
1148            "cols={{ table.num_columns }}, sep='{{ table.separator }}'",
1149        )
1150        .unwrap();
1151
1152        let tmpl = env.get_template("test").unwrap();
1153        let output = tmpl
1154            .render(minijinja::context! { table => Value::from_object(formatter) })
1155            .unwrap();
1156
1157        assert_eq!(output, "cols=2, sep=' | '");
1158    }
1159
1160    // ============================================================================
1161    // Overflow Mode Tests (Phase 4)
1162    // ============================================================================
1163
1164    #[test]
1165    fn format_cell_clip_no_marker() {
1166        let spec = FlatDataSpec::builder()
1167            .column(Column::new(Width::Fixed(5)).clip())
1168            .build();
1169        let formatter = TabularFormatter::new(&spec, 80);
1170
1171        let output = formatter.format_row(&["Hello World"]);
1172        // Clip truncates without marker
1173        assert_eq!(display_width(&output), 5);
1174        assert!(!output.contains("…"));
1175        assert!(output.starts_with("Hello"));
1176    }
1177
1178    #[test]
1179    fn format_cell_expand_overflows() {
1180        // Expand mode lets content overflow
1181        let col = Column::new(Width::Fixed(5)).overflow(Overflow::Expand);
1182        let output = format_cell("Hello World", 5, &col);
1183
1184        // Should NOT be truncated
1185        assert_eq!(output, "Hello World");
1186        assert_eq!(display_width(&output), 11); // Full width
1187    }
1188
1189    #[test]
1190    fn format_cell_expand_pads_when_short() {
1191        let col = Column::new(Width::Fixed(10)).overflow(Overflow::Expand);
1192        let output = format_cell("Hi", 10, &col);
1193
1194        // Should be padded to width
1195        assert_eq!(output, "Hi        ");
1196        assert_eq!(display_width(&output), 10);
1197    }
1198
1199    #[test]
1200    fn format_cell_wrap_single_line() {
1201        // Content fits, no wrapping needed
1202        let col = Column::new(Width::Fixed(20)).wrap();
1203        let output = format_cell_lines("Short text", 20, &col);
1204
1205        assert!(output.is_single());
1206        assert_eq!(output.line_count(), 1);
1207        assert_eq!(display_width(&output.to_single()), 20);
1208    }
1209
1210    #[test]
1211    fn format_cell_wrap_multi_line() {
1212        let col = Column::new(Width::Fixed(10)).wrap();
1213        let output = format_cell_lines("This is a longer text that wraps", 10, &col);
1214
1215        assert!(!output.is_single());
1216        assert!(output.line_count() > 1);
1217
1218        // Each line should be padded to width
1219        if let CellOutput::Multi(lines) = &output {
1220            for line in lines {
1221                assert_eq!(display_width(line), 10);
1222            }
1223        }
1224    }
1225
1226    #[test]
1227    fn format_cell_wrap_with_indent() {
1228        let col = Column::new(Width::Fixed(15)).overflow(Overflow::Wrap { indent: 2 });
1229        let output = format_cell_lines("First line then continuation", 15, &col);
1230
1231        if let CellOutput::Multi(lines) = output {
1232            // First line should start normally
1233            assert!(lines[0].starts_with("First"));
1234            // Subsequent lines should be indented
1235            if lines.len() > 1 {
1236                // The line content should start with spaces due to indent
1237                let second_trimmed = lines[1].trim_start();
1238                assert!(lines[1].len() > second_trimmed.len()); // Has leading spaces
1239            }
1240        }
1241    }
1242
1243    #[test]
1244    fn format_row_lines_single_line() {
1245        let spec = FlatDataSpec::builder()
1246            .column(Column::new(Width::Fixed(10)))
1247            .column(Column::new(Width::Fixed(8)))
1248            .separator("  ")
1249            .build();
1250        let formatter = TabularFormatter::new(&spec, 80);
1251
1252        let lines = formatter.format_row_lines(&["Hello", "World"]);
1253        assert_eq!(lines.len(), 1);
1254        assert_eq!(lines[0], formatter.format_row(&["Hello", "World"]));
1255    }
1256
1257    #[test]
1258    fn format_row_lines_multi_line() {
1259        let spec = FlatDataSpec::builder()
1260            .column(Column::new(Width::Fixed(8)).wrap())
1261            .column(Column::new(Width::Fixed(6)))
1262            .separator("  ")
1263            .build();
1264        let formatter = TabularFormatter::new(&spec, 80);
1265
1266        let lines = formatter.format_row_lines(&["This is long", "Short"]);
1267
1268        // Should have multiple lines due to wrapping
1269        assert!(!lines.is_empty());
1270
1271        // Each line should have consistent width
1272        let expected_width = display_width(&lines[0]);
1273        for line in &lines {
1274            assert_eq!(display_width(line), expected_width);
1275        }
1276    }
1277
1278    #[test]
1279    fn format_row_lines_mixed_columns() {
1280        // One column wraps, others don't
1281        let spec = FlatDataSpec::builder()
1282            .column(Column::new(Width::Fixed(6))) // truncates
1283            .column(Column::new(Width::Fixed(10)).wrap()) // wraps
1284            .column(Column::new(Width::Fixed(4))) // truncates
1285            .separator(" ")
1286            .build();
1287        let formatter = TabularFormatter::new(&spec, 80);
1288
1289        let lines = formatter.format_row_lines(&["aaaaa", "this text wraps here", "bbbb"]);
1290
1291        // Multiple lines due to middle column wrapping
1292        assert!(!lines.is_empty());
1293    }
1294
1295    // ============================================================================
1296    // CellOutput Tests
1297    // ============================================================================
1298
1299    #[test]
1300    fn cell_output_single_accessors() {
1301        let cell = CellOutput::Single("Hello".to_string());
1302
1303        assert!(cell.is_single());
1304        assert_eq!(cell.line_count(), 1);
1305        assert_eq!(cell.to_single(), "Hello");
1306    }
1307
1308    #[test]
1309    fn cell_output_multi_accessors() {
1310        let cell = CellOutput::Multi(vec!["Line 1".to_string(), "Line 2".to_string()]);
1311
1312        assert!(!cell.is_single());
1313        assert_eq!(cell.line_count(), 2);
1314        assert_eq!(cell.to_single(), "Line 1");
1315    }
1316
1317    #[test]
1318    fn cell_output_line_accessor() {
1319        let cell = CellOutput::Multi(vec!["First".to_string(), "Second".to_string()]);
1320
1321        // Get first line, padded to 10
1322        let line0 = cell.line(0, 10, Align::Left);
1323        assert_eq!(line0, "First     ");
1324        assert_eq!(display_width(&line0), 10);
1325
1326        // Get second line
1327        let line1 = cell.line(1, 10, Align::Right);
1328        assert_eq!(line1, "    Second");
1329
1330        // Out of bounds returns empty padded
1331        let line2 = cell.line(2, 10, Align::Left);
1332        assert_eq!(line2, "          ");
1333    }
1334
1335    // ============================================================================
1336    // Anchor Tests (Phase 5)
1337    // ============================================================================
1338
1339    #[test]
1340    fn format_row_all_left_anchor_no_gap() {
1341        // All columns left-anchored - no gap inserted
1342        let spec = FlatDataSpec::builder()
1343            .column(Column::new(Width::Fixed(5)))
1344            .column(Column::new(Width::Fixed(5)))
1345            .separator(" ")
1346            .build();
1347        let formatter = TabularFormatter::new(&spec, 50);
1348
1349        let output = formatter.format_row(&["A", "B"]);
1350        // Total content: 5 + 1 + 5 = 11, no gap
1351        assert_eq!(output, "A     B    ");
1352        assert_eq!(display_width(&output), 11);
1353    }
1354
1355    #[test]
1356    fn format_row_with_right_anchor() {
1357        // Left column + right column with gap
1358        let spec = FlatDataSpec::builder()
1359            .column(Column::new(Width::Fixed(5))) // left-anchored
1360            .column(Column::new(Width::Fixed(5)).anchor_right()) // right-anchored
1361            .separator(" ")
1362            .build();
1363
1364        // Total: 30, content: 5 + 5 = 10, sep: 1, overhead: 11
1365        // Gap: 30 - 11 + 1 = 20 (replaces separator)
1366        let formatter = TabularFormatter::new(&spec, 30);
1367
1368        let output = formatter.format_row(&["L", "R"]);
1369        assert_eq!(display_width(&output), 30);
1370        // Left content at start, right content at end
1371        assert!(output.starts_with("L    "));
1372        assert!(output.ends_with("R    "));
1373    }
1374
1375    #[test]
1376    fn format_row_with_right_anchor_exact_fit() {
1377        // When total_width equals content width, no gap
1378        let spec = FlatDataSpec::builder()
1379            .column(Column::new(Width::Fixed(10)))
1380            .column(Column::new(Width::Fixed(10)).anchor_right())
1381            .separator("  ")
1382            .build();
1383
1384        // Total: 22 (10 + 2 + 10), no extra space
1385        let formatter = TabularFormatter::new(&spec, 22);
1386
1387        let output = formatter.format_row(&["Left", "Right"]);
1388        assert_eq!(display_width(&output), 22);
1389        // Normal separator, no gap
1390        assert!(output.contains("  ")); // Original separator preserved
1391    }
1392
1393    #[test]
1394    fn format_row_all_right_anchor_no_gap() {
1395        // All columns right-anchored - no gap needed
1396        let spec = FlatDataSpec::builder()
1397            .column(Column::new(Width::Fixed(5)).anchor_right())
1398            .column(Column::new(Width::Fixed(5)).anchor_right())
1399            .separator(" ")
1400            .build();
1401        let formatter = TabularFormatter::new(&spec, 50);
1402
1403        let output = formatter.format_row(&["A", "B"]);
1404        // No transition from left to right, so no gap
1405        assert_eq!(output, "A     B    ");
1406    }
1407
1408    #[test]
1409    fn format_row_multiple_anchors() {
1410        // Two left, two right
1411        let spec = FlatDataSpec::builder()
1412            .column(Column::new(Width::Fixed(4))) // L1
1413            .column(Column::new(Width::Fixed(4))) // L2
1414            .column(Column::new(Width::Fixed(4)).anchor_right()) // R1
1415            .column(Column::new(Width::Fixed(4)).anchor_right()) // R2
1416            .separator(" ")
1417            .build();
1418
1419        // Content: 4*4 = 16, seps: 3, overhead: 19
1420        // Total: 40, gap: 40 - 19 + 1 = 22
1421        let formatter = TabularFormatter::new(&spec, 40);
1422
1423        let output = formatter.format_row(&["A", "B", "C", "D"]);
1424        assert_eq!(display_width(&output), 40);
1425        // Left group at start, right group at end
1426        assert!(output.starts_with("A    B   "));
1427    }
1428
1429    #[test]
1430    fn calculate_anchor_gap_no_transition() {
1431        let spec = FlatDataSpec::builder()
1432            .column(Column::new(Width::Fixed(10)))
1433            .column(Column::new(Width::Fixed(10)))
1434            .build();
1435        let formatter = TabularFormatter::new(&spec, 50);
1436
1437        let (gap, transition) = formatter.calculate_anchor_gap();
1438        assert_eq!(transition, 2); // No right-anchored columns
1439        assert_eq!(gap, 0);
1440    }
1441
1442    #[test]
1443    fn calculate_anchor_gap_with_transition() {
1444        let spec = FlatDataSpec::builder()
1445            .column(Column::new(Width::Fixed(10)))
1446            .column(Column::new(Width::Fixed(10)).anchor_right())
1447            .separator(" ")
1448            .build();
1449        let formatter = TabularFormatter::new(&spec, 50);
1450
1451        let (gap, transition) = formatter.calculate_anchor_gap();
1452        assert_eq!(transition, 1); // Second column is right-anchored
1453        assert!(gap > 0);
1454    }
1455
1456    #[test]
1457    fn format_row_lines_with_anchor() {
1458        // Multi-line output should also respect anchors
1459        let spec = FlatDataSpec::builder()
1460            .column(Column::new(Width::Fixed(8)).wrap())
1461            .column(Column::new(Width::Fixed(6)).anchor_right())
1462            .separator(" ")
1463            .build();
1464        let formatter = TabularFormatter::new(&spec, 40);
1465
1466        let lines = formatter.format_row_lines(&["This is text", "Right"]);
1467
1468        // All lines should have consistent width due to anchor
1469        for line in &lines {
1470            assert_eq!(display_width(line), 40);
1471        }
1472    }
1473
1474    // ============================================================================
1475    // Struct Extraction Tests (Phase 6)
1476    // ============================================================================
1477
1478    #[test]
1479    fn row_from_simple_struct() {
1480        #[derive(Serialize)]
1481        struct Record {
1482            name: String,
1483            value: i32,
1484        }
1485
1486        let spec = FlatDataSpec::builder()
1487            .column(Column::new(Width::Fixed(10)).key("name"))
1488            .column(Column::new(Width::Fixed(5)).key("value"))
1489            .separator("  ")
1490            .build();
1491        let formatter = TabularFormatter::new(&spec, 80);
1492
1493        let record = Record {
1494            name: "Test".to_string(),
1495            value: 42,
1496        };
1497
1498        let row = formatter.row_from(&record);
1499        assert!(row.contains("Test"));
1500        assert!(row.contains("42"));
1501    }
1502
1503    #[test]
1504    fn row_from_uses_name_as_fallback() {
1505        #[derive(Serialize)]
1506        struct Item {
1507            title: String,
1508        }
1509
1510        let spec = FlatDataSpec::builder()
1511            .column(Column::new(Width::Fixed(15)).named("title"))
1512            .build();
1513        let formatter = TabularFormatter::new(&spec, 80);
1514
1515        let item = Item {
1516            title: "Hello".to_string(),
1517        };
1518
1519        let row = formatter.row_from(&item);
1520        assert!(row.contains("Hello"));
1521    }
1522
1523    #[test]
1524    fn row_from_nested_field() {
1525        #[derive(Serialize)]
1526        struct User {
1527            email: String,
1528        }
1529
1530        #[derive(Serialize)]
1531        struct Record {
1532            user: User,
1533            status: String,
1534        }
1535
1536        let spec = FlatDataSpec::builder()
1537            .column(Column::new(Width::Fixed(20)).key("user.email"))
1538            .column(Column::new(Width::Fixed(10)).key("status"))
1539            .separator("  ")
1540            .build();
1541        let formatter = TabularFormatter::new(&spec, 80);
1542
1543        let record = Record {
1544            user: User {
1545                email: "test@example.com".to_string(),
1546            },
1547            status: "active".to_string(),
1548        };
1549
1550        let row = formatter.row_from(&record);
1551        assert!(row.contains("test@example.com"));
1552        assert!(row.contains("active"));
1553    }
1554
1555    #[test]
1556    fn row_from_array_index() {
1557        #[derive(Serialize)]
1558        struct Record {
1559            items: Vec<String>,
1560        }
1561
1562        let spec = FlatDataSpec::builder()
1563            .column(Column::new(Width::Fixed(10)).key("items.0"))
1564            .column(Column::new(Width::Fixed(10)).key("items.1"))
1565            .build();
1566        let formatter = TabularFormatter::new(&spec, 80);
1567
1568        let record = Record {
1569            items: vec!["First".to_string(), "Second".to_string()],
1570        };
1571
1572        let row = formatter.row_from(&record);
1573        assert!(row.contains("First"));
1574        assert!(row.contains("Second"));
1575    }
1576
1577    #[test]
1578    fn row_from_missing_field_uses_null_repr() {
1579        #[derive(Serialize)]
1580        struct Record {
1581            present: String,
1582        }
1583
1584        let spec = FlatDataSpec::builder()
1585            .column(Column::new(Width::Fixed(10)).key("present"))
1586            .column(Column::new(Width::Fixed(10)).key("missing").null_repr("-"))
1587            .build();
1588        let formatter = TabularFormatter::new(&spec, 80);
1589
1590        let record = Record {
1591            present: "value".to_string(),
1592        };
1593
1594        let row = formatter.row_from(&record);
1595        assert!(row.contains("value"));
1596        // Missing field should show empty (extract_field returns empty string)
1597    }
1598
1599    #[test]
1600    fn row_from_no_key_uses_null_repr() {
1601        #[derive(Serialize)]
1602        struct Record {
1603            value: String,
1604        }
1605
1606        let spec = FlatDataSpec::builder()
1607            .column(Column::new(Width::Fixed(10)).null_repr("N/A"))
1608            .build();
1609        let formatter = TabularFormatter::new(&spec, 80);
1610
1611        let record = Record {
1612            value: "test".to_string(),
1613        };
1614
1615        let row = formatter.row_from(&record);
1616        assert!(row.contains("N/A"));
1617    }
1618
1619    #[test]
1620    fn row_from_various_types() {
1621        #[derive(Serialize)]
1622        struct Record {
1623            string_val: String,
1624            int_val: i64,
1625            float_val: f64,
1626            bool_val: bool,
1627        }
1628
1629        let spec = FlatDataSpec::builder()
1630            .column(Column::new(Width::Fixed(10)).key("string_val"))
1631            .column(Column::new(Width::Fixed(10)).key("int_val"))
1632            .column(Column::new(Width::Fixed(10)).key("float_val"))
1633            .column(Column::new(Width::Fixed(10)).key("bool_val"))
1634            .build();
1635        let formatter = TabularFormatter::new(&spec, 80);
1636
1637        let record = Record {
1638            string_val: "text".to_string(),
1639            int_val: 123,
1640            float_val: 9.87,
1641            bool_val: true,
1642        };
1643
1644        let row = formatter.row_from(&record);
1645        assert!(row.contains("text"));
1646        assert!(row.contains("123"));
1647        assert!(row.contains("9.87"));
1648        assert!(row.contains("true"));
1649    }
1650
1651    #[test]
1652    fn extract_field_simple() {
1653        let json = serde_json::json!({
1654            "name": "Alice",
1655            "age": 30
1656        });
1657
1658        assert_eq!(extract_field(&json, "name"), "Alice");
1659        assert_eq!(extract_field(&json, "age"), "30");
1660        assert_eq!(extract_field(&json, "missing"), "");
1661    }
1662
1663    #[test]
1664    fn extract_field_nested() {
1665        let json = serde_json::json!({
1666            "user": {
1667                "profile": {
1668                    "email": "test@example.com"
1669                }
1670            }
1671        });
1672
1673        assert_eq!(
1674            extract_field(&json, "user.profile.email"),
1675            "test@example.com"
1676        );
1677        assert_eq!(extract_field(&json, "user.missing"), "");
1678    }
1679
1680    #[test]
1681    fn extract_field_array() {
1682        let json = serde_json::json!({
1683            "items": ["a", "b", "c"]
1684        });
1685
1686        assert_eq!(extract_field(&json, "items.0"), "a");
1687        assert_eq!(extract_field(&json, "items.1"), "b");
1688        assert_eq!(extract_field(&json, "items.10"), ""); // Out of bounds
1689    }
1690
1691    #[test]
1692    fn row_lines_from_struct() {
1693        #[derive(Serialize)]
1694        struct Record {
1695            description: String,
1696            status: String,
1697        }
1698
1699        let spec = FlatDataSpec::builder()
1700            .column(Column::new(Width::Fixed(10)).key("description").wrap())
1701            .column(Column::new(Width::Fixed(6)).key("status"))
1702            .separator("  ")
1703            .build();
1704        let formatter = TabularFormatter::new(&spec, 80);
1705
1706        let record = Record {
1707            description: "A longer description that wraps".to_string(),
1708            status: "OK".to_string(),
1709        };
1710
1711        let lines = formatter.row_lines_from(&record);
1712        // Should have multiple lines due to wrapping
1713        assert!(!lines.is_empty());
1714    }
1715
1716    // ============================================================================
1717    // Style Tests (Phase 7)
1718    // ============================================================================
1719
1720    #[test]
1721    fn format_cell_with_style() {
1722        let spec = FlatDataSpec::builder()
1723            .column(Column::new(Width::Fixed(10)).style("header"))
1724            .build();
1725        let formatter = TabularFormatter::new(&spec, 80);
1726
1727        let output = formatter.format_row(&["Hello"]);
1728        // Should wrap in style tags
1729        assert!(output.starts_with("[header]"));
1730        assert!(output.ends_with("[/header]"));
1731        assert!(output.contains("Hello"));
1732    }
1733
1734    #[test]
1735    fn format_cell_style_from_value() {
1736        let spec = FlatDataSpec::builder()
1737            .column(Column::new(Width::Fixed(10)).style_from_value())
1738            .build();
1739        let formatter = TabularFormatter::new(&spec, 80);
1740
1741        let output = formatter.format_row(&["error"]);
1742        // Value "error" becomes the style
1743        assert!(output.contains("[error]"));
1744        assert!(output.contains("[/error]"));
1745    }
1746
1747    #[test]
1748    fn format_cell_no_style() {
1749        let spec = FlatDataSpec::builder()
1750            .column(Column::new(Width::Fixed(10)))
1751            .build();
1752        let formatter = TabularFormatter::new(&spec, 80);
1753
1754        let output = formatter.format_row(&["Hello"]);
1755        // No style tags
1756        assert!(!output.contains("["));
1757        assert!(!output.contains("]"));
1758        assert!(output.contains("Hello"));
1759    }
1760
1761    #[test]
1762    fn format_cell_style_overrides_style_from_value() {
1763        // When both style and style_from_value are set, style_from_value wins
1764        let mut col = Column::new(Width::Fixed(10));
1765        col.style = Some("default".to_string());
1766        col.style_from_value = true;
1767
1768        let spec = FlatDataSpec::builder().column(col).build();
1769        let formatter = TabularFormatter::new(&spec, 80);
1770
1771        let output = formatter.format_row(&["custom"]);
1772        // style_from_value takes precedence
1773        assert!(output.contains("[custom]"));
1774        assert!(output.contains("[/custom]"));
1775    }
1776
1777    #[test]
1778    fn format_row_multiple_styled_columns() {
1779        let spec = FlatDataSpec::builder()
1780            .column(Column::new(Width::Fixed(8)).style("name"))
1781            .column(Column::new(Width::Fixed(8)).style("status"))
1782            .separator("  ")
1783            .build();
1784        let formatter = TabularFormatter::new(&spec, 80);
1785
1786        let output = formatter.format_row(&["Alice", "Active"]);
1787        assert!(output.contains("[name]"));
1788        assert!(output.contains("[status]"));
1789    }
1790
1791    #[test]
1792    fn format_cell_lines_with_style() {
1793        let spec = FlatDataSpec::builder()
1794            .column(Column::new(Width::Fixed(10)).wrap().style("text"))
1795            .build();
1796        let formatter = TabularFormatter::new(&spec, 80);
1797
1798        let lines = formatter.format_row_lines(&["This is a long text that wraps"]);
1799
1800        // Each line should have style tags
1801        for line in &lines {
1802            assert!(line.contains("[text]"));
1803            assert!(line.contains("[/text]"));
1804        }
1805    }
1806
1807    // ============================================================================
1808    // Extract Headers Tests
1809    // ============================================================================
1810
1811    #[test]
1812    fn extract_headers_from_header_field() {
1813        let spec = FlatDataSpec::builder()
1814            .column(Column::new(Width::Fixed(10)).header("Name"))
1815            .column(Column::new(Width::Fixed(8)).header("Status"))
1816            .build();
1817        let formatter = TabularFormatter::new(&spec, 80);
1818
1819        let headers = formatter.extract_headers();
1820        assert_eq!(headers, vec!["Name", "Status"]);
1821    }
1822
1823    #[test]
1824    fn extract_headers_fallback_to_key() {
1825        let spec = FlatDataSpec::builder()
1826            .column(Column::new(Width::Fixed(10)).key("user_name"))
1827            .column(Column::new(Width::Fixed(8)).key("status"))
1828            .build();
1829        let formatter = TabularFormatter::new(&spec, 80);
1830
1831        let headers = formatter.extract_headers();
1832        assert_eq!(headers, vec!["user_name", "status"]);
1833    }
1834
1835    #[test]
1836    fn extract_headers_fallback_to_name() {
1837        let spec = FlatDataSpec::builder()
1838            .column(Column::new(Width::Fixed(10)).named("col1"))
1839            .column(Column::new(Width::Fixed(8)).named("col2"))
1840            .build();
1841        let formatter = TabularFormatter::new(&spec, 80);
1842
1843        let headers = formatter.extract_headers();
1844        assert_eq!(headers, vec!["col1", "col2"]);
1845    }
1846
1847    #[test]
1848    fn extract_headers_priority_order() {
1849        // header > key > name > ""
1850        let spec = FlatDataSpec::builder()
1851            .column(
1852                Column::new(Width::Fixed(10))
1853                    .header("Header")
1854                    .key("key")
1855                    .named("name"),
1856            )
1857            .column(
1858                Column::new(Width::Fixed(10))
1859                    .key("key_only")
1860                    .named("name_only"),
1861            )
1862            .column(Column::new(Width::Fixed(10)).named("name_only"))
1863            .column(Column::new(Width::Fixed(10))) // No header, key, or name
1864            .build();
1865        let formatter = TabularFormatter::new(&spec, 80);
1866
1867        let headers = formatter.extract_headers();
1868        assert_eq!(headers, vec!["Header", "key_only", "name_only", ""]);
1869    }
1870
1871    #[test]
1872    fn extract_headers_empty_spec() {
1873        let spec = FlatDataSpec::builder().build();
1874        let formatter = TabularFormatter::new(&spec, 80);
1875
1876        let headers = formatter.extract_headers();
1877        assert!(headers.is_empty());
1878    }
1879}