tabulate_rs/
format.rs

1use std::{borrow::Cow, collections::BTreeMap};
2
3use once_cell::sync::Lazy;
4
5use crate::{alignment::Alignment, width::visible_width};
6
7/// Horizontal line descriptor.
8#[derive(Clone, Debug)]
9pub struct Line {
10    /// Characters printed before the first column.
11    pub begin: Cow<'static, str>,
12    /// Characters used to fill the gap above/below a column.
13    pub fill: Cow<'static, str>,
14    /// Characters placed between columns.
15    pub separator: Cow<'static, str>,
16    /// Characters printed after the last column.
17    pub end: Cow<'static, str>,
18}
19
20impl Line {
21    fn borrowed(
22        begin: &'static str,
23        fill: &'static str,
24        separator: &'static str,
25        end: &'static str,
26    ) -> Self {
27        Self {
28            begin: Cow::Borrowed(begin),
29            fill: Cow::Borrowed(fill),
30            separator: Cow::Borrowed(separator),
31            end: Cow::Borrowed(end),
32        }
33    }
34}
35
36/// Row descriptor.
37#[derive(Clone, Debug)]
38pub struct DataRow {
39    /// Characters printed before the first cell.
40    pub begin: Cow<'static, str>,
41    /// Characters printed in-between adjacent cells.
42    pub separator: Cow<'static, str>,
43    /// Characters appended after the last cell.
44    pub end: Cow<'static, str>,
45}
46
47impl DataRow {
48    fn borrowed(begin: &'static str, separator: &'static str, end: &'static str) -> Self {
49        Self {
50            begin: Cow::Borrowed(begin),
51            separator: Cow::Borrowed(separator),
52            end: Cow::Borrowed(end),
53        }
54    }
55}
56
57/// A function that generates a horizontal rule dynamically.
58pub type LineFn = fn(col_widths: &[usize], col_aligns: &[Alignment]) -> String;
59/// A function that renders a single row dynamically.
60pub type RowFn =
61    fn(cell_values: &[String], col_widths: &[usize], col_aligns: &[Alignment]) -> String;
62
63/// Representation of the different ways a table component can be rendered.
64#[derive(Clone, Debug, Default)]
65pub enum LineFormat {
66    /// Do not render the line.
67    #[default]
68    None,
69    /// Render a static [`Line`].
70    Static(Line),
71    /// Render an inline string as-is.
72    Text(Cow<'static, str>),
73    /// Render with a dynamic callback.
74    Dynamic(LineFn),
75}
76
77/// Representation of the different ways a table row can be rendered.
78#[derive(Clone, Debug, Default)]
79pub enum RowFormat {
80    /// Do not render the row.
81    #[default]
82    None,
83    /// Render a static [`DataRow`].
84    Static(DataRow),
85    /// Render via a dynamic callback.
86    Dynamic(RowFn),
87}
88
89/// Table format definition mirroring the Python implementation.
90#[derive(Clone, Debug)]
91pub struct TableFormat {
92    /// Line printed above the table.
93    pub line_above: LineFormat,
94    /// Line between headers and data rows.
95    pub line_below_header: LineFormat,
96    /// Line between consecutive data rows.
97    pub line_between_rows: LineFormat,
98    /// Line printed after the final row.
99    pub line_below: LineFormat,
100    /// Header row template.
101    pub header_row: RowFormat,
102    /// Data row template.
103    pub data_row: RowFormat,
104    /// Extra padding applied around each cell.
105    pub padding: usize,
106    /// Table components that should be hidden when headers are present.
107    pub with_header_hide: &'static [&'static str],
108}
109
110impl TableFormat {
111    /// A helper that returns `true` if the format should hide the requested component.
112    pub fn hides(&self, component: &str) -> bool {
113        self.with_header_hide.contains(&component)
114    }
115}
116
117fn line(
118    begin: &'static str,
119    fill: &'static str,
120    separator: &'static str,
121    end: &'static str,
122) -> Line {
123    Line::borrowed(begin, fill, separator, end)
124}
125
126fn row(begin: &'static str, separator: &'static str, end: &'static str) -> DataRow {
127    DataRow::borrowed(begin, separator, end)
128}
129
130fn default_align(aligns: &[Alignment], index: usize) -> Alignment {
131    aligns.get(index).copied().unwrap_or(Alignment::Left)
132}
133
134fn pipe_segment_with_colons(align: Alignment, width: usize) -> String {
135    let width = width.max(1);
136    match align {
137        Alignment::Right | Alignment::Decimal => {
138            if width == 1 {
139                ":".to_string()
140            } else {
141                format!("{:-<w$}:", "-", w = width - 1)
142            }
143        }
144        Alignment::Center => {
145            if width <= 2 {
146                "::".chars().take(width).collect()
147            } else {
148                format!(":{}:", "-".repeat(width - 2))
149            }
150        }
151        Alignment::Left => {
152            if width == 1 {
153                ":".to_string()
154            } else {
155                format!(":{:-<w$}", "-", w = width - 1)
156            }
157        }
158    }
159}
160
161fn pipe_line_with_colons(col_widths: &[usize], col_aligns: &[Alignment]) -> String {
162    if col_aligns.is_empty() {
163        let line = col_widths
164            .iter()
165            .map(|w| "-".repeat(*w))
166            .collect::<Vec<_>>()
167            .join("|");
168        return format!("|{}|", line);
169    }
170
171    let segments = col_widths
172        .iter()
173        .enumerate()
174        .map(|(idx, width)| pipe_segment_with_colons(default_align(col_aligns, idx), *width))
175        .collect::<Vec<_>>();
176    format!("|{}|", segments.join("|"))
177}
178
179fn mediawiki_row_with_attrs(
180    separator: &str,
181    cell_values: &[String],
182    col_aligns: &[Alignment],
183) -> String {
184    let mut values = Vec::with_capacity(cell_values.len());
185    for (idx, cell) in cell_values.iter().enumerate() {
186        let align = match default_align(col_aligns, idx) {
187            Alignment::Right | Alignment::Decimal => r#" align="right"| "#,
188            Alignment::Center => r#" align="center"| "#,
189            Alignment::Left => "",
190        };
191        let mut value = String::new();
192        if align.is_empty() {
193            value.push(' ');
194            value.push_str(cell);
195            value.push(' ');
196        } else {
197            value.push_str(align);
198            value.push_str(cell);
199        }
200        values.push(value);
201    }
202    let mut result = String::new();
203    let colsep = separator.repeat(2);
204    result.push_str(separator);
205    result.push_str(&values.join(&colsep));
206    while result.ends_with(char::is_whitespace) {
207        result.pop();
208    }
209    result
210}
211
212fn mediawiki_header_row(
213    cell_values: &[String],
214    col_widths: &[usize],
215    col_aligns: &[Alignment],
216) -> String {
217    let _ = col_widths;
218    mediawiki_row_with_attrs("!", cell_values, col_aligns)
219}
220
221fn mediawiki_data_row(
222    cell_values: &[String],
223    col_widths: &[usize],
224    col_aligns: &[Alignment],
225) -> String {
226    let _ = col_widths;
227    mediawiki_row_with_attrs("|", cell_values, col_aligns)
228}
229
230fn moin_row_with_attrs(
231    celltag: &str,
232    header: Option<&str>,
233    cell_values: &[String],
234    col_widths: &[usize],
235    col_aligns: &[Alignment],
236) -> String {
237    let mut out = String::new();
238    for (idx, value) in cell_values.iter().enumerate() {
239        let alignment = default_align(col_aligns, idx);
240        let align_attr = match alignment {
241            Alignment::Right | Alignment::Decimal => r#"<style="text-align: right;">"#,
242            Alignment::Center => r#"<style="text-align: center;">"#,
243            Alignment::Left => "",
244        };
245        let total_width = col_widths.get(idx).copied().unwrap_or(value.len());
246        let inner_width = total_width.saturating_sub(2);
247        let mut core = value.clone();
248        let current_width = visible_width(&core, false);
249        if current_width < inner_width {
250            let padding = inner_width - current_width;
251            let (left_pad, right_pad) = match alignment {
252                Alignment::Right | Alignment::Decimal => (padding, 0),
253                Alignment::Center => (padding / 2, padding - (padding / 2)),
254                Alignment::Left => (0, padding),
255            };
256            core = format!("{}{}{}", " ".repeat(left_pad), core, " ".repeat(right_pad));
257        }
258        let mut base = String::new();
259        if let Some(marker) = header {
260            base.push_str(marker);
261        }
262        base.push_str(&core);
263        if let Some(marker) = header {
264            base.push_str(marker);
265        }
266        out.push_str(celltag);
267        out.push_str(align_attr);
268        out.push(' ');
269        out.push_str(&base);
270        out.push(' ');
271    }
272    out.push_str("||");
273    out
274}
275
276fn moin_header_row(
277    cell_values: &[String],
278    col_widths: &[usize],
279    col_aligns: &[Alignment],
280) -> String {
281    moin_row_with_attrs("||", Some("'''"), cell_values, col_widths, col_aligns)
282}
283
284fn moin_data_row(cell_values: &[String], col_widths: &[usize], col_aligns: &[Alignment]) -> String {
285    moin_row_with_attrs("||", None, cell_values, col_widths, col_aligns)
286}
287
288fn html_begin_table_without_header(_col_widths: &[usize], _col_aligns: &[Alignment]) -> String {
289    "<table>\n<tbody>".to_string()
290}
291
292fn html_escape(input: &str) -> String {
293    input
294        .chars()
295        .map(|c| match c {
296            '&' => "&amp;".to_string(),
297            '<' => "&lt;".to_string(),
298            '>' => "&gt;".to_string(),
299            '"' => "&quot;".to_string(),
300            '\'' => "&#39;".to_string(),
301            _ => c.to_string(),
302        })
303        .collect()
304}
305
306fn html_row_with_attrs(
307    celltag: &str,
308    unsafe_mode: bool,
309    cell_values: &[String],
310    col_widths: &[usize],
311    col_aligns: &[Alignment],
312) -> String {
313    let _ = col_widths;
314    let mut values = Vec::with_capacity(cell_values.len());
315    for (idx, cell) in cell_values.iter().enumerate() {
316        let align = match default_align(col_aligns, idx) {
317            Alignment::Right | Alignment::Decimal => r#" style="text-align: right;""#,
318            Alignment::Center => r#" style="text-align: center;""#,
319            Alignment::Left => "",
320        };
321        let content = if unsafe_mode {
322            cell.clone()
323        } else {
324            html_escape(cell)
325        };
326        let value = format!("<{celltag}{align}>{content}</{celltag}>");
327        values.push(value);
328    }
329    let mut rowhtml = format!("<tr>{}</tr>", values.join(""));
330    rowhtml.truncate(rowhtml.trim_end().len());
331    if celltag == "th" {
332        format!("<table>\n<thead>\n{rowhtml}\n</thead>\n<tbody>")
333    } else {
334        rowhtml
335    }
336}
337
338fn html_header_row_safe(
339    cell_values: &[String],
340    col_widths: &[usize],
341    col_aligns: &[Alignment],
342) -> String {
343    html_row_with_attrs("th", false, cell_values, col_widths, col_aligns)
344}
345
346fn html_data_row_safe(
347    cell_values: &[String],
348    col_widths: &[usize],
349    col_aligns: &[Alignment],
350) -> String {
351    html_row_with_attrs("td", false, cell_values, col_widths, col_aligns)
352}
353
354fn html_header_row_unsafe(
355    cell_values: &[String],
356    col_widths: &[usize],
357    col_aligns: &[Alignment],
358) -> String {
359    html_row_with_attrs("th", true, cell_values, col_widths, col_aligns)
360}
361
362fn html_data_row_unsafe(
363    cell_values: &[String],
364    col_widths: &[usize],
365    col_aligns: &[Alignment],
366) -> String {
367    html_row_with_attrs("td", true, cell_values, col_widths, col_aligns)
368}
369
370fn latex_line_begin_tabular(_col_widths: &[usize], col_aligns: &[Alignment]) -> String {
371    latex_line_begin_tabular_internal(col_aligns, false, false)
372}
373
374fn latex_line_begin_tabular_booktabs(_col_widths: &[usize], col_aligns: &[Alignment]) -> String {
375    latex_line_begin_tabular_internal(col_aligns, true, false)
376}
377
378fn latex_line_begin_tabular_longtable(_col_widths: &[usize], col_aligns: &[Alignment]) -> String {
379    latex_line_begin_tabular_internal(col_aligns, false, true)
380}
381
382fn latex_line_begin_tabular_internal(
383    col_aligns: &[Alignment],
384    booktabs: bool,
385    longtable: bool,
386) -> String {
387    let columns: String = col_aligns
388        .iter()
389        .map(|align| match align {
390            Alignment::Right | Alignment::Decimal => 'r',
391            Alignment::Center => 'c',
392            Alignment::Left => 'l',
393        })
394        .collect();
395    let begin = if longtable {
396        "\\begin{longtable}{"
397    } else {
398        "\\begin{tabular}{"
399    };
400    let mut result = String::new();
401    result.push_str(begin);
402    result.push_str(&columns);
403    result.push_str("}\n");
404    result.push_str(if booktabs { "\\toprule" } else { "\\hline" });
405    result
406}
407
408const LATEX_ESCAPE_RULES: &[(char, &str)] = &[
409    ('&', r"\&"),
410    ('%', r"\%"),
411    ('$', r"\$"),
412    ('#', r"\#"),
413    ('_', r"\_"),
414    ('^', r"\^{}"),
415    ('{', r"\{"),
416    ('}', r"\}"),
417    ('~', r"\textasciitilde{}"),
418    ('\\', r"\textbackslash{}"),
419    ('<', r"\ensuremath{<}"),
420    ('>', r"\ensuremath{>}"),
421];
422
423fn latex_escape(value: &str, escape: bool) -> String {
424    if !escape {
425        return value.to_string();
426    }
427    let mut out = String::with_capacity(value.len());
428    for ch in value.chars() {
429        if let Some((_, replacement)) = LATEX_ESCAPE_RULES
430            .iter()
431            .find(|(candidate, _)| *candidate == ch)
432        {
433            out.push_str(replacement);
434        } else {
435            out.push(ch);
436        }
437    }
438    out
439}
440
441fn latex_row(cell_values: &[String], col_widths: &[usize], col_aligns: &[Alignment]) -> String {
442    latex_row_internal(cell_values, col_widths, col_aligns, true)
443}
444
445fn latex_row_raw(cell_values: &[String], col_widths: &[usize], col_aligns: &[Alignment]) -> String {
446    latex_row_internal(cell_values, col_widths, col_aligns, false)
447}
448
449fn latex_row_internal(
450    cell_values: &[String],
451    _col_widths: &[usize],
452    _col_aligns: &[Alignment],
453    escape: bool,
454) -> String {
455    let escaped = cell_values
456        .iter()
457        .map(|cell| latex_escape(cell, escape))
458        .collect::<Vec<_>>();
459    format!("{}\\\\", escaped.join("&"))
460}
461
462fn textile_row_with_attrs(
463    cell_values: &[String],
464    col_widths: &[usize],
465    col_aligns: &[Alignment],
466) -> String {
467    let _ = col_widths;
468    if cell_values.is_empty() {
469        return String::from("||");
470    }
471    let mut values = cell_values.to_vec();
472    if let Some(first) = values.first_mut() {
473        first.push(' ');
474    }
475    let mut parts = Vec::with_capacity(values.len());
476    for (idx, value) in values.into_iter().enumerate() {
477        let align_prefix = match default_align(col_aligns, idx) {
478            Alignment::Left => "<.",
479            Alignment::Right | Alignment::Decimal => ">.",
480            Alignment::Center => "=.",
481        };
482        parts.push(format!("{}{}", align_prefix, value));
483    }
484    format!("|{}|", parts.join("|"))
485}
486
487fn asciidoc_alignment_code(align: Alignment) -> char {
488    match align {
489        Alignment::Left => '<',
490        Alignment::Right | Alignment::Decimal => '>',
491        Alignment::Center => '^',
492    }
493}
494
495fn asciidoc_make_header_line(
496    is_header: bool,
497    col_widths: &[usize],
498    col_aligns: &[Alignment],
499) -> String {
500    let mut column_specifiers = Vec::new();
501    for (idx, width) in col_widths.iter().enumerate() {
502        let align_char = asciidoc_alignment_code(default_align(col_aligns, idx));
503        column_specifiers.push(format!("{width}{align_char}"));
504    }
505    let mut header_entries = vec![format!("cols=\"{}\"", column_specifiers.join(","))];
506    if is_header {
507        header_entries.push("options=\"header\"".to_string());
508    }
509    format!("[{}]\n|====", header_entries.join(","))
510}
511
512fn asciidoc_line_above(col_widths: &[usize], col_aligns: &[Alignment]) -> String {
513    asciidoc_make_header_line(false, col_widths, col_aligns)
514}
515
516fn asciidoc_header_row(
517    cell_values: &[String],
518    col_widths: &[usize],
519    col_aligns: &[Alignment],
520) -> String {
521    let header = asciidoc_make_header_line(true, col_widths, col_aligns);
522    let data_line = format!("|{}", cell_values.join("|"));
523    format!("{header}\n{data_line}")
524}
525
526fn asciidoc_data_row(
527    cell_values: &[String],
528    _col_widths: &[usize],
529    _col_aligns: &[Alignment],
530) -> String {
531    format!("|{}", cell_values.join("|"))
532}
533
534fn build_formats() -> BTreeMap<&'static str, TableFormat> {
535    let mut formats = BTreeMap::new();
536    formats.insert(
537        "asciidoc",
538        TableFormat {
539            line_above: LineFormat::Dynamic(asciidoc_line_above),
540            line_below_header: LineFormat::None,
541            line_between_rows: LineFormat::None,
542            line_below: LineFormat::Static(line("|====", "", "", "")),
543            header_row: RowFormat::Dynamic(asciidoc_header_row),
544            data_row: RowFormat::Dynamic(asciidoc_data_row),
545            padding: 1,
546            with_header_hide: &["lineabove"],
547        },
548    );
549    formats.insert(
550        "colon_grid",
551        TableFormat {
552            line_above: LineFormat::Static(line("", "-", "  ", "")),
553            line_below_header: LineFormat::Static(line("", "-", "  ", "")),
554            line_between_rows: LineFormat::None,
555            line_below: LineFormat::Static(line("", "-", "  ", "")),
556            header_row: RowFormat::Static(row("", "  ", "")),
557            data_row: RowFormat::Static(row("", "  ", "")),
558            padding: 0,
559            with_header_hide: &["lineabove", "linebelow"],
560        },
561    );
562    formats.insert(
563        "double_grid",
564        TableFormat {
565            line_above: LineFormat::Static(line("\u{2554}", "\u{2550}", "\u{2566}", "\u{2557}")),
566            line_below_header: LineFormat::Static(line(
567                "\u{2560}", "\u{2550}", "\u{256c}", "\u{2563}",
568            )),
569            line_between_rows: LineFormat::Static(line(
570                "\u{2560}", "\u{2550}", "\u{256c}", "\u{2563}",
571            )),
572            line_below: LineFormat::Static(line("\u{255a}", "\u{2550}", "\u{2569}", "\u{255d}")),
573            header_row: RowFormat::Static(row("\u{2551}", "\u{2551}", "\u{2551}")),
574            data_row: RowFormat::Static(row("\u{2551}", "\u{2551}", "\u{2551}")),
575            padding: 1,
576            with_header_hide: &[],
577        },
578    );
579    formats.insert(
580        "double_outline",
581        TableFormat {
582            line_above: LineFormat::Static(line("\u{2554}", "\u{2550}", "\u{2566}", "\u{2557}")),
583            line_below_header: LineFormat::Static(line(
584                "\u{2560}", "\u{2550}", "\u{256c}", "\u{2563}",
585            )),
586            line_between_rows: LineFormat::None,
587            line_below: LineFormat::Static(line("\u{255a}", "\u{2550}", "\u{2569}", "\u{255d}")),
588            header_row: RowFormat::Static(row("\u{2551}", "\u{2551}", "\u{2551}")),
589            data_row: RowFormat::Static(row("\u{2551}", "\u{2551}", "\u{2551}")),
590            padding: 1,
591            with_header_hide: &[],
592        },
593    );
594    formats.insert(
595        "fancy_grid",
596        TableFormat {
597            line_above: LineFormat::Static(line("\u{2552}", "\u{2550}", "\u{2564}", "\u{2555}")),
598            line_below_header: LineFormat::Static(line(
599                "\u{255e}", "\u{2550}", "\u{256a}", "\u{2561}",
600            )),
601            line_between_rows: LineFormat::Static(line(
602                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
603            )),
604            line_below: LineFormat::Static(line("\u{2558}", "\u{2550}", "\u{2567}", "\u{255b}")),
605            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
606            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
607            padding: 1,
608            with_header_hide: &[],
609        },
610    );
611    formats.insert(
612        "fancy_outline",
613        TableFormat {
614            line_above: LineFormat::Static(line("\u{2552}", "\u{2550}", "\u{2564}", "\u{2555}")),
615            line_below_header: LineFormat::Static(line(
616                "\u{255e}", "\u{2550}", "\u{256a}", "\u{2561}",
617            )),
618            line_between_rows: LineFormat::None,
619            line_below: LineFormat::Static(line("\u{2558}", "\u{2550}", "\u{2567}", "\u{255b}")),
620            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
621            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
622            padding: 1,
623            with_header_hide: &[],
624        },
625    );
626    formats.insert(
627        "github",
628        TableFormat {
629            line_above: LineFormat::Static(line("|", "-", "|", "|")),
630            line_below_header: LineFormat::Static(line("|", "-", "|", "|")),
631            line_between_rows: LineFormat::None,
632            line_below: LineFormat::None,
633            header_row: RowFormat::Static(row("|", "|", "|")),
634            data_row: RowFormat::Static(row("|", "|", "|")),
635            padding: 1,
636            with_header_hide: &["lineabove"],
637        },
638    );
639    formats.insert(
640        "grid",
641        TableFormat {
642            line_above: LineFormat::Static(line("+", "-", "+", "+")),
643            line_below_header: LineFormat::Static(line("+", "=", "+", "+")),
644            line_between_rows: LineFormat::Static(line("+", "-", "+", "+")),
645            line_below: LineFormat::Static(line("+", "-", "+", "+")),
646            header_row: RowFormat::Static(row("|", "|", "|")),
647            data_row: RowFormat::Static(row("|", "|", "|")),
648            padding: 1,
649            with_header_hide: &[],
650        },
651    );
652    formats.insert(
653        "heavy_grid",
654        TableFormat {
655            line_above: LineFormat::Static(line("\u{250f}", "\u{2501}", "\u{2533}", "\u{2513}")),
656            line_below_header: LineFormat::Static(line(
657                "\u{2523}", "\u{2501}", "\u{254b}", "\u{252b}",
658            )),
659            line_between_rows: LineFormat::Static(line(
660                "\u{2523}", "\u{2501}", "\u{254b}", "\u{252b}",
661            )),
662            line_below: LineFormat::Static(line("\u{2517}", "\u{2501}", "\u{253b}", "\u{251b}")),
663            header_row: RowFormat::Static(row("\u{2503}", "\u{2503}", "\u{2503}")),
664            data_row: RowFormat::Static(row("\u{2503}", "\u{2503}", "\u{2503}")),
665            padding: 1,
666            with_header_hide: &[],
667        },
668    );
669    formats.insert(
670        "heavy_outline",
671        TableFormat {
672            line_above: LineFormat::Static(line("\u{250f}", "\u{2501}", "\u{2533}", "\u{2513}")),
673            line_below_header: LineFormat::Static(line(
674                "\u{2523}", "\u{2501}", "\u{254b}", "\u{252b}",
675            )),
676            line_between_rows: LineFormat::None,
677            line_below: LineFormat::Static(line("\u{2517}", "\u{2501}", "\u{253b}", "\u{251b}")),
678            header_row: RowFormat::Static(row("\u{2503}", "\u{2503}", "\u{2503}")),
679            data_row: RowFormat::Static(row("\u{2503}", "\u{2503}", "\u{2503}")),
680            padding: 1,
681            with_header_hide: &[],
682        },
683    );
684    formats.insert(
685        "html",
686        TableFormat {
687            line_above: LineFormat::Dynamic(html_begin_table_without_header),
688            line_below_header: LineFormat::Text(Cow::Borrowed("")),
689            line_between_rows: LineFormat::None,
690            line_below: LineFormat::Static(line("</tbody>\n</table>", "", "", "")),
691            header_row: RowFormat::Dynamic(html_header_row_safe),
692            data_row: RowFormat::Dynamic(html_data_row_safe),
693            padding: 0,
694            with_header_hide: &["lineabove"],
695        },
696    );
697    formats.insert(
698        "jira",
699        TableFormat {
700            line_above: LineFormat::None,
701            line_below_header: LineFormat::None,
702            line_between_rows: LineFormat::None,
703            line_below: LineFormat::None,
704            header_row: RowFormat::Static(row("||", "||", "||")),
705            data_row: RowFormat::Static(row("|", "|", "|")),
706            padding: 1,
707            with_header_hide: &[],
708        },
709    );
710    formats.insert(
711        "latex",
712        TableFormat {
713            line_above: LineFormat::Dynamic(latex_line_begin_tabular),
714            line_below_header: LineFormat::Static(line("\\hline", "", "", "")),
715            line_between_rows: LineFormat::None,
716            line_below: LineFormat::Static(line("\\hline\n\\end{tabular}", "", "", "")),
717            header_row: RowFormat::Dynamic(latex_row),
718            data_row: RowFormat::Dynamic(latex_row),
719            padding: 1,
720            with_header_hide: &[],
721        },
722    );
723    formats.insert(
724        "latex_booktabs",
725        TableFormat {
726            line_above: LineFormat::Dynamic(latex_line_begin_tabular_booktabs),
727            line_below_header: LineFormat::Static(line("\\midrule", "", "", "")),
728            line_between_rows: LineFormat::None,
729            line_below: LineFormat::Static(line("\\bottomrule\n\\end{tabular}", "", "", "")),
730            header_row: RowFormat::Dynamic(latex_row),
731            data_row: RowFormat::Dynamic(latex_row),
732            padding: 1,
733            with_header_hide: &[],
734        },
735    );
736    formats.insert(
737        "latex_longtable",
738        TableFormat {
739            line_above: LineFormat::Dynamic(latex_line_begin_tabular_longtable),
740            line_below_header: LineFormat::Static(line("\\hline\n\\endhead", "", "", "")),
741            line_between_rows: LineFormat::None,
742            line_below: LineFormat::Static(line("\\hline\n\\end{longtable}", "", "", "")),
743            header_row: RowFormat::Dynamic(latex_row),
744            data_row: RowFormat::Dynamic(latex_row),
745            padding: 1,
746            with_header_hide: &[],
747        },
748    );
749    formats.insert(
750        "latex_raw",
751        TableFormat {
752            line_above: LineFormat::Dynamic(latex_line_begin_tabular),
753            line_below_header: LineFormat::Static(line("\\hline", "", "", "")),
754            line_between_rows: LineFormat::None,
755            line_below: LineFormat::Static(line("\\hline\n\\end{tabular}", "", "", "")),
756            header_row: RowFormat::Dynamic(latex_row_raw),
757            data_row: RowFormat::Dynamic(latex_row_raw),
758            padding: 1,
759            with_header_hide: &[],
760        },
761    );
762    formats.insert(
763        "mediawiki",
764        TableFormat {
765            line_above: LineFormat::Static(line(
766                "{| class=\"wikitable\" style=\"text-align: left;\"",
767                "",
768                "",
769                "\n|+ <!-- caption -->\n|-",
770            )),
771            line_below_header: LineFormat::Static(line("|-", "", "", "")),
772            line_between_rows: LineFormat::Static(line("|-", "", "", "")),
773            line_below: LineFormat::Static(line("|}", "", "", "")),
774            header_row: RowFormat::Dynamic(mediawiki_header_row),
775            data_row: RowFormat::Dynamic(mediawiki_data_row),
776            padding: 0,
777            with_header_hide: &[],
778        },
779    );
780    formats.insert(
781        "mixed_grid",
782        TableFormat {
783            line_above: LineFormat::Static(line("\u{250d}", "\u{2501}", "\u{252f}", "\u{2511}")),
784            line_below_header: LineFormat::Static(line(
785                "\u{251d}", "\u{2501}", "\u{253f}", "\u{2525}",
786            )),
787            line_between_rows: LineFormat::Static(line(
788                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
789            )),
790            line_below: LineFormat::Static(line("\u{2515}", "\u{2501}", "\u{2537}", "\u{2519}")),
791            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
792            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
793            padding: 1,
794            with_header_hide: &[],
795        },
796    );
797    formats.insert(
798        "mixed_outline",
799        TableFormat {
800            line_above: LineFormat::Static(line("\u{250d}", "\u{2501}", "\u{252f}", "\u{2511}")),
801            line_below_header: LineFormat::Static(line(
802                "\u{251d}", "\u{2501}", "\u{253f}", "\u{2525}",
803            )),
804            line_between_rows: LineFormat::None,
805            line_below: LineFormat::Static(line("\u{2515}", "\u{2501}", "\u{2537}", "\u{2519}")),
806            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
807            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
808            padding: 1,
809            with_header_hide: &[],
810        },
811    );
812    formats.insert(
813        "moinmoin",
814        TableFormat {
815            line_above: LineFormat::None,
816            line_below_header: LineFormat::None,
817            line_between_rows: LineFormat::None,
818            line_below: LineFormat::None,
819            header_row: RowFormat::Dynamic(moin_header_row),
820            data_row: RowFormat::Dynamic(moin_data_row),
821            padding: 1,
822            with_header_hide: &[],
823        },
824    );
825    formats.insert(
826        "orgtbl",
827        TableFormat {
828            line_above: LineFormat::None,
829            line_below_header: LineFormat::Static(line("|", "-", "+", "|")),
830            line_between_rows: LineFormat::None,
831            line_below: LineFormat::None,
832            header_row: RowFormat::Static(row("|", "|", "|")),
833            data_row: RowFormat::Static(row("|", "|", "|")),
834            padding: 1,
835            with_header_hide: &[],
836        },
837    );
838    formats.insert(
839        "outline",
840        TableFormat {
841            line_above: LineFormat::Static(line("+", "-", "+", "+")),
842            line_below_header: LineFormat::Static(line("+", "=", "+", "+")),
843            line_between_rows: LineFormat::None,
844            line_below: LineFormat::Static(line("+", "-", "+", "+")),
845            header_row: RowFormat::Static(row("|", "|", "|")),
846            data_row: RowFormat::Static(row("|", "|", "|")),
847            padding: 1,
848            with_header_hide: &[],
849        },
850    );
851    formats.insert(
852        "pipe",
853        TableFormat {
854            line_above: LineFormat::Dynamic(pipe_line_with_colons),
855            line_below_header: LineFormat::Dynamic(pipe_line_with_colons),
856            line_between_rows: LineFormat::None,
857            line_below: LineFormat::None,
858            header_row: RowFormat::Static(row("|", "|", "|")),
859            data_row: RowFormat::Static(row("|", "|", "|")),
860            padding: 1,
861            with_header_hide: &["lineabove"],
862        },
863    );
864    formats.insert(
865        "plain",
866        TableFormat {
867            line_above: LineFormat::None,
868            line_below_header: LineFormat::None,
869            line_between_rows: LineFormat::None,
870            line_below: LineFormat::None,
871            header_row: RowFormat::Static(row("", "  ", "")),
872            data_row: RowFormat::Static(row("", "  ", "")),
873            padding: 0,
874            with_header_hide: &[],
875        },
876    );
877    formats.insert(
878        "presto",
879        TableFormat {
880            line_above: LineFormat::None,
881            line_below_header: LineFormat::Static(line("", "-", "+", "")),
882            line_between_rows: LineFormat::None,
883            line_below: LineFormat::None,
884            header_row: RowFormat::Static(row("", "|", "")),
885            data_row: RowFormat::Static(row("", "|", "")),
886            padding: 1,
887            with_header_hide: &[],
888        },
889    );
890    formats.insert(
891        "pretty",
892        TableFormat {
893            line_above: LineFormat::Static(line("+", "-", "+", "+")),
894            line_below_header: LineFormat::Static(line("+", "-", "+", "+")),
895            line_between_rows: LineFormat::None,
896            line_below: LineFormat::Static(line("+", "-", "+", "+")),
897            header_row: RowFormat::Static(row("|", "|", "|")),
898            data_row: RowFormat::Static(row("|", "|", "|")),
899            padding: 1,
900            with_header_hide: &[],
901        },
902    );
903    formats.insert(
904        "psql",
905        TableFormat {
906            line_above: LineFormat::Static(line("+", "-", "+", "+")),
907            line_below_header: LineFormat::Static(line("|", "-", "+", "|")),
908            line_between_rows: LineFormat::None,
909            line_below: LineFormat::Static(line("+", "-", "+", "+")),
910            header_row: RowFormat::Static(row("|", "|", "|")),
911            data_row: RowFormat::Static(row("|", "|", "|")),
912            padding: 1,
913            with_header_hide: &[],
914        },
915    );
916    formats.insert(
917        "rounded_grid",
918        TableFormat {
919            line_above: LineFormat::Static(line("\u{256d}", "\u{2500}", "\u{252c}", "\u{256e}")),
920            line_below_header: LineFormat::Static(line(
921                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
922            )),
923            line_between_rows: LineFormat::Static(line(
924                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
925            )),
926            line_below: LineFormat::Static(line("\u{2570}", "\u{2500}", "\u{2534}", "\u{256f}")),
927            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
928            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
929            padding: 1,
930            with_header_hide: &[],
931        },
932    );
933    formats.insert(
934        "rounded_outline",
935        TableFormat {
936            line_above: LineFormat::Static(line("\u{256d}", "\u{2500}", "\u{252c}", "\u{256e}")),
937            line_below_header: LineFormat::Static(line(
938                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
939            )),
940            line_between_rows: LineFormat::None,
941            line_below: LineFormat::Static(line("\u{2570}", "\u{2500}", "\u{2534}", "\u{256f}")),
942            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
943            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
944            padding: 1,
945            with_header_hide: &[],
946        },
947    );
948    formats.insert(
949        "rst",
950        TableFormat {
951            line_above: LineFormat::Static(line("", "=", "  ", "")),
952            line_below_header: LineFormat::Static(line("", "=", "  ", "")),
953            line_between_rows: LineFormat::None,
954            line_below: LineFormat::Static(line("", "=", "  ", "")),
955            header_row: RowFormat::Static(row("", "  ", "")),
956            data_row: RowFormat::Static(row("", "  ", "")),
957            padding: 0,
958            with_header_hide: &[],
959        },
960    );
961    formats.insert(
962        "simple",
963        TableFormat {
964            line_above: LineFormat::Static(line("", "-", "  ", "")),
965            line_below_header: LineFormat::Static(line("", "-", "  ", "")),
966            line_between_rows: LineFormat::None,
967            line_below: LineFormat::Static(line("", "-", "  ", "")),
968            header_row: RowFormat::Static(row("", "  ", "")),
969            data_row: RowFormat::Static(row("", "  ", "")),
970            padding: 0,
971            with_header_hide: &["lineabove", "linebelow"],
972        },
973    );
974    formats.insert(
975        "simple_grid",
976        TableFormat {
977            line_above: LineFormat::Static(line("\u{250c}", "\u{2500}", "\u{252c}", "\u{2510}")),
978            line_below_header: LineFormat::Static(line(
979                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
980            )),
981            line_between_rows: LineFormat::Static(line(
982                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
983            )),
984            line_below: LineFormat::Static(line("\u{2514}", "\u{2500}", "\u{2534}", "\u{2518}")),
985            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
986            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
987            padding: 1,
988            with_header_hide: &[],
989        },
990    );
991    formats.insert(
992        "simple_outline",
993        TableFormat {
994            line_above: LineFormat::Static(line("\u{250c}", "\u{2500}", "\u{252c}", "\u{2510}")),
995            line_below_header: LineFormat::Static(line(
996                "\u{251c}", "\u{2500}", "\u{253c}", "\u{2524}",
997            )),
998            line_between_rows: LineFormat::None,
999            line_below: LineFormat::Static(line("\u{2514}", "\u{2500}", "\u{2534}", "\u{2518}")),
1000            header_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
1001            data_row: RowFormat::Static(row("\u{2502}", "\u{2502}", "\u{2502}")),
1002            padding: 1,
1003            with_header_hide: &[],
1004        },
1005    );
1006    formats.insert(
1007        "textile",
1008        TableFormat {
1009            line_above: LineFormat::None,
1010            line_below_header: LineFormat::None,
1011            line_between_rows: LineFormat::None,
1012            line_below: LineFormat::None,
1013            header_row: RowFormat::Static(row("|_. ", "|_.", "|")),
1014            data_row: RowFormat::Dynamic(textile_row_with_attrs),
1015            padding: 1,
1016            with_header_hide: &[],
1017        },
1018    );
1019    formats.insert(
1020        "tsv",
1021        TableFormat {
1022            line_above: LineFormat::None,
1023            line_below_header: LineFormat::None,
1024            line_between_rows: LineFormat::None,
1025            line_below: LineFormat::None,
1026            header_row: RowFormat::Static(row("", "\t", "")),
1027            data_row: RowFormat::Static(row("", "\t", "")),
1028            padding: 0,
1029            with_header_hide: &[],
1030        },
1031    );
1032    formats.insert(
1033        "unsafehtml",
1034        TableFormat {
1035            line_above: LineFormat::Dynamic(html_begin_table_without_header),
1036            line_below_header: LineFormat::Text(Cow::Borrowed("")),
1037            line_between_rows: LineFormat::None,
1038            line_below: LineFormat::Static(line("</tbody>\n</table>", "", "", "")),
1039            header_row: RowFormat::Dynamic(html_header_row_unsafe),
1040            data_row: RowFormat::Dynamic(html_data_row_unsafe),
1041            padding: 0,
1042            with_header_hide: &["lineabove"],
1043        },
1044    );
1045    formats.insert(
1046        "youtrack",
1047        TableFormat {
1048            line_above: LineFormat::None,
1049            line_below_header: LineFormat::None,
1050            line_between_rows: LineFormat::None,
1051            line_below: LineFormat::None,
1052            header_row: RowFormat::Static(row("|| ", " || ", " || ")),
1053            data_row: RowFormat::Static(row("| ", " | ", " |")),
1054            padding: 1,
1055            with_header_hide: &[],
1056        },
1057    );
1058    formats
1059}
1060
1061static TABLE_FORMATS: Lazy<BTreeMap<&'static str, TableFormat>> = Lazy::new(build_formats);
1062
1063static TABLE_FORMAT_NAMES: Lazy<Vec<&'static str>> =
1064    Lazy::new(|| TABLE_FORMATS.keys().copied().collect());
1065
1066/// Retrieve a table format by name.
1067pub fn table_format(name: &str) -> Option<&'static TableFormat> {
1068    TABLE_FORMATS.get(name)
1069}
1070
1071/// Return the list of available table format identifiers.
1072pub fn tabulate_formats() -> &'static [&'static str] {
1073    TABLE_FORMAT_NAMES.as_slice()
1074}
1075
1076/// Construct a simple column-separated [`TableFormat`].
1077pub fn simple_separated_format<S: Into<String>>(separator: S) -> TableFormat {
1078    let separator: Cow<'static, str> = Cow::Owned(separator.into());
1079    TableFormat {
1080        line_above: LineFormat::None,
1081        line_below_header: LineFormat::None,
1082        line_between_rows: LineFormat::None,
1083        line_below: LineFormat::None,
1084        header_row: RowFormat::Static(DataRow {
1085            begin: Cow::Borrowed(""),
1086            separator: separator.clone(),
1087            end: Cow::Borrowed(""),
1088        }),
1089        data_row: RowFormat::Static(DataRow {
1090            begin: Cow::Borrowed(""),
1091            separator,
1092            end: Cow::Borrowed(""),
1093        }),
1094        padding: 0,
1095        with_header_hide: &[],
1096    }
1097}