reifydb_core/value/frame/
render.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4use std::fmt::{self, Write};
5
6use reifydb_type::util::unicode::UnicodeWidthStr;
7
8use crate::value::frame::{Frame, FrameColumn};
9
10/// Frame renderer with various rendering options
11pub struct FrameRenderer;
12
13impl FrameRenderer {
14	/// Render the frame with all features enabled (including encoded numbers if present)
15	pub fn render_full(frame: &Frame) -> Result<String, fmt::Error> {
16		let mut output = String::new();
17		Self::render_full_to(frame, &mut output)?;
18		Ok(output)
19	}
20
21	/// Render the frame without encoded numbers
22	pub fn render_without_row_numbers(frame: &Frame) -> Result<String, fmt::Error> {
23		let mut output = String::new();
24		Self::render_without_row_numbers_to(frame, &mut output)?;
25		Ok(output)
26	}
27
28	/// Render the frame with all features to the given formatter
29	pub fn render_full_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
30		Self::render_internal(frame, f, true)
31	}
32
33	/// Render the frame without encoded numbers to the given formatter
34	pub fn render_without_row_numbers_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
35		Self::render_internal(frame, f, false)
36	}
37
38	/// Internal rendering implementation
39	fn render_internal(frame: &Frame, f: &mut dyn Write, include_row_numbers: bool) -> fmt::Result {
40		let row_count = frame.first().map_or(0, |c| c.data.len());
41		let has_row_numbers = include_row_numbers && !frame.row_numbers.is_empty();
42		let col_count = frame.len()
43			+ if has_row_numbers {
44				1
45			} else {
46				0
47			};
48
49		// Get the display order for regular columns
50		let column_order = Self::get_column_display_order(frame);
51
52		let mut col_widths = vec![0; col_count];
53
54		// If we have encoded numbers, calculate width for encoded number column
55		let row_num_col_idx = if has_row_numbers {
56			// Row number column is always first
57			let row_num_header = "__ROW__NUMBER__";
58			col_widths[0] = Self::display_width(row_num_header);
59
60			// Calculate max width needed for encoded numbers
61			for row_num in &frame.row_numbers {
62				let s = row_num.to_string();
63				col_widths[0] = col_widths[0].max(Self::display_width(&s));
64			}
65			1 // Start regular columns at index 1
66		} else {
67			0 // Start regular columns at index 0
68		};
69
70		for (display_idx, &col_idx) in column_order.iter().enumerate() {
71			let col = &frame[col_idx];
72			let display_name = Self::escape_control_chars(&col.qualified_name());
73			col_widths[row_num_col_idx + display_idx] = Self::display_width(&display_name);
74		}
75
76		for row_numberx in 0..row_count {
77			for (display_idx, &col_idx) in column_order.iter().enumerate() {
78				let col = &frame[col_idx];
79				let s = Self::extract_string_value(col, row_numberx);
80				col_widths[row_num_col_idx + display_idx] =
81					col_widths[row_num_col_idx + display_idx].max(Self::display_width(&s));
82			}
83		}
84
85		// Add padding
86		for w in &mut col_widths {
87			*w += 2;
88		}
89
90		let sep = format!("+{}+", col_widths.iter().map(|w| "-".repeat(*w + 2)).collect::<Vec<_>>().join("+"));
91		writeln!(f, "{}", sep)?;
92
93		let mut header = Vec::new();
94
95		// Add encoded number header if present
96		if has_row_numbers {
97			let w = col_widths[0];
98			let name = "__ROW__NUMBER__";
99			let pad = w - Self::display_width(name);
100			let l = pad / 2;
101			let r = pad - l;
102			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
103		}
104
105		// Add regular column headers
106		for (display_idx, &col_idx) in column_order.iter().enumerate() {
107			let col = &frame[col_idx];
108			let w = col_widths[row_num_col_idx + display_idx];
109			let name = Self::escape_control_chars(&col.qualified_name());
110			let pad = w - Self::display_width(&name);
111			let l = pad / 2;
112			let r = pad - l;
113			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
114		}
115
116		writeln!(f, "|{}|", header.join("|"))?;
117
118		writeln!(f, "{}", sep)?;
119
120		for row_numberx in 0..row_count {
121			let mut row = Vec::new();
122
123			// Add encoded number value if present
124			if has_row_numbers {
125				let w = col_widths[0];
126				let s = if row_numberx < frame.row_numbers.len() {
127					frame.row_numbers[row_numberx].to_string()
128				} else {
129					"Undefined".to_string()
130				};
131				let pad = w - Self::display_width(&s);
132				let l = pad / 2;
133				let r = pad - l;
134				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
135			}
136
137			// Add regular column values
138			for (display_idx, &col_idx) in column_order.iter().enumerate() {
139				let col = &frame[col_idx];
140				let w = col_widths[row_num_col_idx + display_idx];
141				let s = Self::extract_string_value(col, row_numberx);
142				let pad = w - Self::display_width(&s);
143				let l = pad / 2;
144				let r = pad - l;
145				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
146			}
147
148			writeln!(f, "|{}|", row.join("|"))?;
149		}
150
151		writeln!(f, "{}", sep)
152	}
153
154	/// Calculate the display width of a string, handling newlines properly.
155	/// For strings with newlines, returns the width of the longest line.
156	/// For strings without newlines, returns the unicode display width.
157	fn display_width(s: &str) -> usize {
158		if s.contains('\n') {
159			s.lines().map(|line| line.width()).max().unwrap_or(0)
160		} else {
161			s.width()
162		}
163	}
164
165	/// Escape newlines and tabs in a string for single-line display.
166	/// Replaces '\n' with "\\n" and '\t' with "\\t".
167	fn escape_control_chars(s: &str) -> String {
168		s.replace('\n', "\\n").replace('\t', "\\t")
169	}
170
171	/// Create a column display order (no special handling needed since encoded numbers are separate)
172	fn get_column_display_order(frame: &Frame) -> Vec<usize> {
173		(0..frame.len()).collect()
174	}
175
176	/// Extract string value from column at given encoded index, with proper escaping
177	fn extract_string_value(col: &FrameColumn, row_numberx: usize) -> String {
178		let s = col.data.as_string(row_numberx);
179		Self::escape_control_chars(&s)
180	}
181}