1#[macro_export]
105macro_rules! tabby {
106 ($($rest:tt)+) => {{
107 $crate::_tabby_internal!(@start $($rest)+)
108 }};
109}
110
111pub mod tabled {
113 pub use ::tabled::*;
114}
115
116pub mod paste {
118 pub use ::paste::*;
119}
120
121mod internal {
123 #[macro_export]
124 #[doc(hidden)]
125 macro_rules! _tabby_internal {
126 (@start $($rest:tt)+) => {{
127 $crate::_tabby_internal!(@style $($rest)+)
128 }};
129
130 (@style {$($style:ident: $value:ident),+$(,)?} $($rest:tt)+) => {{
131 $crate::_tabby_internal!(@table {$($style: $value),+}; $($rest)+)
132 }};
133
134 (@style $($rest:tt)+) => {{
135 $crate::_tabby_internal!(@table {}; $($rest)+)
136 }};
137
138 (@table {$($style:ident: $value:ident),*}; $vec:expr) => {{
140 use $crate::tabled;
141
142 let mut rows = $vec;
143 let (max_columns, spans) = $crate::_process_rows!(rows);
144 let mut table = $crate::_table_convert!(max_columns, rows, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
145 $crate::_apply_table_styling!({$($style: $value),*}, table, spans, max_columns)
146 }};
147
148 (@table {$($style:ident: $value:ident),*}; $([ $($cells:expr),* $(,)? ]),+ $(,)?) => {{
150 use $crate::tabled;
151
152 let mut rows = vec![
153 $({
154 vec![$( $cells.to_string() ),*]
155 }),+
156 ];
157
158 let (max_columns, spans) = $crate::_process_rows!(rows);
159 let mut table = $crate::_table_convert!(max_columns, rows, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
160 $crate::_apply_table_styling!({$($style: $value),*}, table, spans, max_columns)
161 }};
162 }
163
164 #[macro_export]
165 #[doc(hidden)]
166 macro_rules! _table_convert {
167 ($max_columns:ident, $rows:ident, $($n:literal),+) => {
168 match $max_columns {
169 $(
170 $n => {
171 let data: Vec<[String; $n]> = $rows.drain(..)
172 .map(|mut r| {
173 r.resize($n, String::new());
174 r.try_into().unwrap_or_else(|v: Vec<String>| {
175 panic!("Expected vec of length {}, got {}", $n, v.len())
176 })
177 })
178 .collect();
179 tabled::Table::new(data)
180 }
181 )+,
182 _ => panic!("Table exceeds maximum supported columns (20)")
183 }
184 };
185 }
186
187 #[macro_export]
188 #[doc(hidden)]
189 macro_rules! _table_style {
190 (_default) => {
191 tabled::settings::Style::modern_rounded()
192 };
193 ($style:ident) => {
194 tabled::settings::Style::$style()
195 };
196 }
197
198 #[macro_export]
199 #[doc(hidden)]
200 macro_rules! _table_border {
201 (_default) => {
202 tabled::settings::Color::FG_BRIGHT_BLACK
203 };
204 ($color:ident) => {
205 $crate::paste::paste! {
206 tabled::settings::Color::[<FG_ $color:upper>]
207 }
208 };
209 }
210
211 #[macro_export]
212 #[doc(hidden)]
213 macro_rules! _table_labels {
214 (_default) => {
215 tabled::settings::Color::FG_WHITE
216 };
217 ($color:ident) => {
218 $crate::paste::paste! {
219 tabled::settings::Color::[<FG_ $color:upper>]
220 }
221 };
222 }
223
224 #[macro_export]
225 #[doc(hidden)]
226 macro_rules! _table_header {
227 (_default) => {
228 tabled::settings::Color::FG_WHITE
229 };
230 ($color:ident) => {
231 $crate::paste::paste! {
232 tabled::settings::Color::[<FG_ $color:upper>]
233 }
234 };
235 }
236
237 #[macro_export]
238 #[doc(hidden)]
239 macro_rules! _table_color {
240 (_default) => {
241 tabled::settings::Color::FG_WHITE
242 };
243 ($color:ident) => {
244 $crate::paste::paste! {
245 tabled::settings::Color::[<FG_ $color:upper>]
246 }
247 };
248 }
249
250 #[macro_export]
251 #[doc(hidden)]
252 macro_rules! _table_bold {
253 (_default) => {
254 false
255 };
256 ($bold:ident) => {
257 $bold
258 };
259 }
260
261 #[macro_export]
262 #[doc(hidden)]
263 macro_rules! _extract_field {
264 ($macro:ident; $field:ident; $($style:ident : $value:ident),* $(,)?) => {
265 $crate::_extract_field_inner!($macro; $field; $($style : $value),*; _default)
266 };
267 ($macro:ident; $field:ident;) => {
268 $crate::$macro!(_default)
269 };
270 }
271
272 #[macro_export]
273 #[doc(hidden)]
274 macro_rules! _extract_field_inner {
275 ($macro:ident; $field:ident; $style:ident : $value:ident $(, $rest_style:ident : $rest_value:ident)*; $default:ident) => {{
276 macro_rules! _check_field {
277 ($field : $v:ident) => {
278 $crate::$macro!($v)
279 };
280 ($other:ident : $v:ident) => {
281 $crate::_extract_field_inner!($macro; $field; $($rest_style : $rest_value),*; $default)
282 };
283 }
284 _check_field!($style : $value)
285 }};
286 ($macro:ident; $field:ident; ; $default:ident) => {
287 $crate::$macro!($default)
288 };
289 }
290
291 #[macro_export]
292 #[doc(hidden)]
293 macro_rules! _apply_table_styling {
294 ({$($style:ident: $value:ident),*}, $table:expr, $spans:expr, $max_columns:expr) => {{
295 for (row, &width) in $spans.iter().enumerate() {
297 if width > 0 && width < $max_columns {
298 $table.modify(
299 (row, width - 1),
300 tabled::settings::Span::column(isize::MAX)
301 );
302 }
303 }
304
305 $table
306 .with(tabled::settings::Remove::row(tabled::settings::object::Rows::first()))
308 .with($crate::_extract_field!(_table_style; style; $($style: $value),*))
309 .with(tabled::settings::themes::BorderCorrection::span())
310 .modify(
311 tabled::settings::object::Columns::new(0..),
312 tabled::settings::style::BorderColor::filled($crate::_extract_field!(_table_border; border; $($style: $value),*))
313 )
314 .modify(
315 tabled::settings::object::Columns::new(0..),
316 $crate::_extract_field!(_table_color; color; $($style: $value),*)
317 )
318 .modify(
319 tabled::settings::object::Columns::first(),
320 $crate::_extract_field!(_table_labels; labels; $($style: $value),*)
321 )
322 .modify(
323 tabled::settings::object::Rows::first(),
324 if $crate::_extract_field!(_table_bold; bold; $($style: $value),*) {
325 $crate::_extract_field!(_table_header; header; $($style: $value),*) | tabled::settings::Color::BOLD
326 } else {
327 $crate::_extract_field!(_table_header; header; $($style: $value),*)
328 }
329 );
330
331 $table
332 }};
333 }
334
335 #[macro_export]
336 #[doc(hidden)]
337 macro_rules! _process_rows {
338 ($rows:expr) => {{
339 let mut max_columns = 0;
340 let mut spans = Vec::new();
341
342 for row in $rows.iter() {
343 spans.push(row.len());
344 max_columns = std::cmp::max(max_columns, row.len());
345 }
346
347 (max_columns, spans)
348 }};
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
358 fn test_literal_syntax() {
359 let t = tabby![["a", "b", "c"], ["c", "d"], ["e", "f", "g"]];
360
361 let output = t.to_string();
362 assert!(output.contains("a"));
363 assert!(output.contains("b"));
364 assert!(output.contains("c"));
365 assert!(output.contains("d"));
366 assert!(output.contains("e"));
367 assert!(output.contains("f"));
368 assert!(output.contains("g"));
369 }
370
371 #[test]
373 fn test_vec_syntax() {
374 let data = vec![
375 vec!["Header1".to_string(), "Header2".to_string()],
376 vec!["Row1".to_string()],
377 vec!["Row2Col1".to_string(), "Row2Col2".to_string()],
378 ];
379
380 let t = tabby!(data);
381 let output = t.to_string();
382 assert!(output.contains("Header1"));
383 assert!(output.contains("Header2"));
384 assert!(output.contains("Row1"));
385 assert!(output.contains("Row2Col1"));
386 assert!(output.contains("Row2Col2"));
387 }
388
389 #[test]
391 fn test_styling_options() {
392 let t = tabby![
393 {style: modern_rounded, header: green, labels: yellow, color: blue, border: red, bold: true}
394 ["Header 1", "Header 2", "Header 3"],
395 ["Short", "Row"],
396 ["Full", "Width", "Row"]
397 ];
398
399 let output = t.to_string();
400 assert!(output.contains("\x1b[31m")); assert!(output.contains("\x1b[32m")); assert!(output.contains("\x1b[33m")); assert!(output.contains("\x1b[34m")); assert!(output.contains("Header 1"));
406 assert!(output.contains("Short"));
407 }
408
409 #[test]
411 fn test_empty_rows() {
412 let t = tabby![["Header 1", "Header 2"], [], ["Row 2 Col 1"]];
413
414 let output = t.to_string();
415 assert!(output.contains("Header 1"));
416 assert!(output.contains("Header 2"));
417 assert!(output.contains("Row 2 Col 1"));
418 }
419
420 #[test]
422 fn test_single_column() {
423 let t = tabby![["Header"], ["Row 1"], ["Row 2"]];
424
425 let output = t.to_string();
426 assert!(output.contains("Header"));
427 assert!(output.contains("Row 1"));
428 assert!(output.contains("Row 2"));
429 }
430 #[test]
432 fn test_max_columns() {
433 let row1: Vec<String> = (0..20).map(|i| format!("Col{}", i)).collect();
434 let row2: Vec<String> = vec!["Row 1".to_string()];
435 let t = tabby![vec!(row1, row2)];
436 let output = t.to_string();
437 assert!(output.contains("Col0"));
438 assert!(output.contains("Col19"));
439 assert!(output.contains("Row 1"));
440 }
441}