termimad/
tbl.rs

1use {
2    crate::{
3        composite::*,
4        fit::{
5            wrap,
6            TblFit,
7        },
8        line::FmtLine,
9        skin::MadSkin,
10        spacing::Spacing,
11    },
12    minimad::{
13        Alignment,
14        TableRow,
15    },
16};
17
18/// Wrap a standard table row
19#[derive(Debug)]
20pub struct FmtTableRow<'s> {
21    pub cells: Vec<FmtComposite<'s>>,
22}
23
24/// Top, Bottom, or other
25#[derive(Debug)]
26pub enum RelativePosition {
27    Top,
28    Other, // or unknown
29    Bottom,
30}
31
32/// A separator or alignment rule in a table.
33///
34/// Represent this kind of lines in tables:
35///  |----|:-:|--
36#[derive(Debug)]
37pub struct FmtTableRule {
38    pub position: RelativePosition, // position relative to the table
39    pub widths: Vec<usize>,
40    pub aligns: Vec<Alignment>,
41}
42
43impl FmtTableRule {
44    pub fn set_nbcols(&mut self, nbcols: usize) {
45        self.widths.truncate(nbcols);
46        self.aligns.truncate(nbcols);
47        for ic in 0..nbcols {
48            if ic >= self.widths.len() {
49                self.widths.push(0);
50            }
51            if ic >= self.aligns.len() {
52                self.aligns.push(Alignment::Unspecified);
53            }
54        }
55    }
56}
57
58impl<'s> FmtTableRow<'s> {
59    pub fn from(table_row: TableRow<'s>, skin: &MadSkin) -> FmtTableRow<'s> {
60        let mut table_row = table_row;
61        FmtTableRow {
62            cells: table_row
63                .cells
64                .drain(..)
65                .map(|composite| FmtComposite::from(composite, skin))
66                .collect(),
67        }
68    }
69}
70
71/// Tables are the sequences of lines whose line style is TableRow.
72///
73/// A table is just the indices, without the text
74/// This structure isn't public because the indices are invalid as
75///  soon as rows are inserted. It only serves during the formatting
76///  process.
77struct Table {
78    start: usize,
79    height: usize, // number of lines
80    nbcols: usize, // number of columns
81}
82
83#[allow(clippy::needless_range_loop)]
84impl Table {
85    pub fn fix_columns(&mut self, lines: &mut Vec<FmtLine<'_>>, width: usize, skin: &MadSkin) {
86        let mut nbcols = self.nbcols;
87        if nbcols == 0 || width == 0 {
88            return;
89        }
90        let mut cols_removed = false;
91
92        // we add the missing cells and also prepare the fitter
93        // We also add the missing cells
94        let widths = match TblFit::new(nbcols, width) {
95            Ok(mut tbl_fit) => {
96                for line in lines.iter_mut().skip(self.start).take(self.height) {
97                    if let FmtLine::TableRow(FmtTableRow { cells }) = line {
98                        for ic in 0..nbcols {
99                            if cells.len() <= ic {
100                                cells.push(FmtComposite::new());
101                            } else {
102                                tbl_fit.see_cell(ic, cells[ic].visible_length);
103                            }
104                        }
105                    } else if let FmtLine::TableRule(rule) = line {
106                        rule.set_nbcols(nbcols);
107                    } else {
108                        panic!("not a table row, should not happen");
109                    }
110                }
111                tbl_fit.fit().col_widths
112            }
113            Err(_) => {
114                // there's not enough width, we'll have to remove columns
115                nbcols = (width - 1) / 4;
116                cols_removed = true;
117                vec![3; nbcols]
118            }
119        };
120
121        // At this step, all widths are at least 3 wide
122
123        // Now we resize all cells and we insert new rows if necessary.
124        // We iterate in reverse order so that we can insert rows
125        //  without recomputing row indices.
126        for ir in (self.start..self.start + self.height).rev() {
127            let line = &mut lines[ir];
128            if let FmtLine::TableRow(FmtTableRow { cells }) = line {
129                let mut cells_to_add: Vec<Vec<FmtComposite<'_>>> = Vec::new();
130                cells.truncate(nbcols);
131                for ic in 0..nbcols {
132                    if cells.len() <= ic {
133                        //FIXME isn't this already done ?
134                        cells.push(FmtComposite::new());
135                        continue;
136                    }
137                    cells_to_add.push(Vec::new());
138                    if cells[ic].visible_length > widths[ic] {
139                        // we must wrap the cell over several lines
140                        let mut composites =
141                            wrap::hard_wrap_composite(&cells[ic], widths[ic], skin)
142                                .expect("tbl fitter guaranteed all columns to be wide enough");
143                        // the first composite replaces the cell, while the other
144                        // ones go to cells_to_add
145                        let mut drain = composites.drain(..);
146                        cells[ic] = drain.next().unwrap();
147                        for c in drain {
148                            cells_to_add[ic].push(c);
149                        }
150                    }
151                }
152                let nb_new_lines = cells_to_add.iter().fold(0, |m, cells| m.max(cells.len()));
153                for inl in (0..nb_new_lines).rev() {
154                    let mut new_cells: Vec<FmtComposite<'_>> = Vec::new();
155                    for cell in cells_to_add.iter_mut().take(nbcols) {
156                        new_cells.push(if cell.len() > inl {
157                            cell.remove(inl)
158                        } else {
159                            FmtComposite::new()
160                        });
161                    }
162                    let new_line = FmtLine::TableRow(FmtTableRow { cells: new_cells });
163                    lines.insert(ir + 1, new_line);
164                    self.height += 1;
165                }
166            }
167        }
168        // Finally we iterate in normal order to specify alignment
169        // (the alignments of a row are the ones of the last rule line)
170        let mut current_aligns: Vec<Alignment> = vec![Alignment::Center; nbcols];
171        for ir in self.start..self.start + self.height {
172            let line = &mut lines[ir];
173            match line {
174                FmtLine::TableRow(FmtTableRow { cells }) => {
175                    for ic in 0..nbcols {
176                        cells[ic].spacing = Some(Spacing {
177                            width: widths[ic],
178                            align: current_aligns[ic],
179                        });
180                    }
181                }
182                FmtLine::TableRule(rule) => {
183                    if cols_removed {
184                        rule.set_nbcols(nbcols);
185                    }
186                    if ir == self.start {
187                        rule.position = RelativePosition::Top;
188                    } else if ir == self.start + self.height - 1 {
189                        rule.position = RelativePosition::Bottom;
190                    }
191                    rule.widths[..nbcols].clone_from_slice(&widths[..nbcols]);
192                    current_aligns[..nbcols].clone_from_slice(&rule.aligns[..nbcols]);
193                }
194                _ => {
195                    panic!("It should be a table part");
196                }
197            }
198        }
199    }
200}
201
202/// find the positions of all tables
203fn find_tables(lines: &[FmtLine<'_>]) -> Vec<Table> {
204    let mut tables: Vec<Table> = Vec::new();
205    let mut current: Option<Table> = None;
206    for (idx, line) in lines.iter().enumerate() {
207        match line {
208            FmtLine::TableRule(FmtTableRule { aligns, .. }) => match current.as_mut() {
209                Some(b) => {
210                    b.height += 1;
211                    b.nbcols = b.nbcols.max(aligns.len());
212                }
213                None => {
214                    current = Some(Table {
215                        start: idx,
216                        height: 1,
217                        nbcols: aligns.len(),
218                    });
219                }
220            },
221            FmtLine::TableRow(FmtTableRow { cells }) => match current.as_mut() {
222                Some(b) => {
223                    b.height += 1;
224                    b.nbcols = b.nbcols.max(cells.len());
225                }
226                None => {
227                    current = Some(Table {
228                        start: idx,
229                        height: 1,
230                        nbcols: cells.len(),
231                    });
232                }
233            },
234            _ => {
235                if let Some(c) = current.take() {
236                    tables.push(c);
237                }
238            }
239        }
240    }
241    if let Some(c) = current.take() {
242        tables.push(c);
243    }
244    tables
245}
246
247/// Modify the rows of all tables in order to ensure it fits the widths
248/// and all cells have the widths of their column.
249///
250/// Some lines may be added to the table in the process, which means any
251///  precedent indexing might be invalid.
252pub fn fix_all_tables(lines: &mut Vec<FmtLine<'_>>, width: usize, skin: &MadSkin) {
253    for tbl in find_tables(lines).iter_mut().rev() {
254        tbl.fix_columns(lines, width, skin);
255    }
256}