1 
2 use {
3     crate::{
4         composite::*,
5         line::FmtLine,
6         skin::MadSkin,
7         spacing::Spacing,
8         fit::wrap,
9     },
10     minimad::{Alignment, TableRow},
11     std::cmp,
12 };
13 
14 
15 /// Wrap a standard table row
16 #[derive(Debug)]
17 pub struct FmtTableRow<'s> {
18     pub cells: Vec<FmtComposite<'s>>,
19 }
20 
21 /// Top, Bottom, or other
22 #[derive(Debug)]
23 pub enum RelativePosition {
24     Top,
25     Other, // or unknown
26     Bottom,
27 }
28 
29 /// A separator or alignment rule in a table.
30 ///
31 /// Represent this kind of lines in tables:
32 ///  |----|:-:|--
33 #[derive(Debug)]
34 pub struct FmtTableRule {
35     pub position: RelativePosition, // position relative to the table
36     pub widths: Vec<usize>,
37     pub aligns: Vec<Alignment>,
38 }
39 
40 impl FmtTableRule {
set_nbcols(&mut self, nbcols: usize)41     pub fn set_nbcols(&mut self, nbcols: usize) {
42         self.widths.truncate(nbcols);
43         self.aligns.truncate(nbcols);
44         for ic in 0..nbcols {
45             if ic >= self.widths.len() {
46                 self.widths.push(0);
47             }
48             if ic >= self.aligns.len() {
49                 self.aligns.push(Alignment::Unspecified);
50             }
51         }
52     }
53 }
54 
55 impl<'s> FmtTableRow<'s> {
from(table_row: TableRow<'s>, skin: &MadSkin) -> FmtTableRow<'s>56     pub fn from(table_row: TableRow<'s>, skin: &MadSkin) -> FmtTableRow<'s> {
57         let mut table_row = table_row;
58         FmtTableRow {
59             cells: table_row
60                 .cells
61                 .drain(..)
62                 .map(|composite| FmtComposite::from(composite, skin))
63                 .collect(),
64         }
65     }
66 }
67 
68 /// Tables are the sequences of lines whose line style is TableRow.
69 ///
70 /// A table is just the indices, without the text
71 /// This structure isn't public because the indices are invalid as
72 ///  soon as rows are inserted. It only serves during the formatting
73 ///  process.
74 struct Table {
75     start: usize,
76     height: usize, // number of lines
77     nbcols: usize, // number of columns
78 }
79 
80 // an internal struct used during col resizing
81 #[derive(Debug)]
82 struct Col {
83     idx: usize,       // index of the col
84     width: usize,     // col internal width
85     to_remove: usize, // what should be removed
86 }
87 
88 /// Determine suitable columns width from the current one and the
89 /// overall sum goal.
90 /// No width can go below 3.
91 /// This function should be called only when the goal is attainable
92 /// and when there's reduction to be done.
reduce_col_widths(widths: &mut Vec<usize>, goal: usize)93 fn reduce_col_widths(widths: &mut Vec<usize>, goal: usize) {
94     let sum: usize = widths.iter().sum();
95     assert!(sum > goal);
96 
97     //- simple case 1 : there's only one col
98     if widths.len()==1 {
99         widths[0] = goal;
100         return;
101     }
102 
103     let mut cols: Vec<Col> = widths
104         .iter()
105         .enumerate()
106         .map(|(idx, width)| {
107             Col {
108                 idx,
109                 width: *width,
110                 to_remove: 0,
111             }
112         })
113         .collect();
114     cols.sort_by_key(|c| cmp::Reverse(c.width));
115 
116     let mut excess = sum - goal;
117 
118     // we do a first reduction, if possible, on columns wider
119     // than 5
120     let excess_of_wide_cols: usize = widths.iter()
121         .filter(|&w| *w > 5)
122         .map(|w| w - 5)
123         .sum();
124     if excess_of_wide_cols + goal > sum {
125         for col in &mut cols {
126             if col.width > 5 {
127                 let r = (sum-goal) * col.width / excess_of_wide_cols;
128                 let r = r.min(excess).min(col.width-5);
129                 excess -= r;
130                 col.to_remove += r;
131             } else {
132                 break; // due to sort
133             }
134         }
135     }
136 
137     if excess > 0 {
138         for col in cols.iter_mut() {
139             let w = col.width - col.to_remove;
140             if w > 3 {
141                 let dr = (w * excess / sum).min(w - 3);
142                 col.to_remove += dr;
143                 excess -= dr;
144             };
145         }
146     }
147 
148     cols.sort_by(|a, b| b.to_remove.cmp(&a.to_remove));
149 
150     //- general case, which could be improved
151     for col in &mut cols {
152         if col.to_remove < 3 {
153             excess += col.to_remove;
154             col.to_remove = 0;
155         }
156     }
157     while excess > 0 {
158         let mut nb_changed = 0;
159         for col in &mut cols {
160             if col.width - col.to_remove > 3 {
161                 col.to_remove += 1;
162                 excess -= 1;
163                 nb_changed += 1;
164                 if excess == 0 {
165                     break;
166                 }
167             }
168         }
169         if nb_changed == 0 {
170             break;
171         }
172     }
173     for c in cols {
174         widths[c.idx] -= c.to_remove;
175     }
176 }
177 
178 impl Table {
fix_columns(&mut self, lines: &mut Vec<FmtLine<'_>>, width: usize)179     pub fn fix_columns(&mut self, lines: &mut Vec<FmtLine<'_>>, width: usize) {
180         let mut nbcols = self.nbcols;
181         // let's first compute the initial widths of all columns
182         // (not counting the widths of the borders)
183         // We also add the missing cells
184         let mut widths: Vec<usize> = vec![0; nbcols];
185         for ir in self.start..self.start + self.height {
186             let line = &mut lines[ir];
187             if let FmtLine::TableRow(FmtTableRow { cells }) = line {
188                 for ic in 0..nbcols {
189                     if cells.len() <= ic {
190                         cells.push(FmtComposite::new());
191                     } else {
192                         widths[ic] = widths[ic].max(cells[ic].visible_length);
193                     }
194                 }
195             } else if let FmtLine::TableRule(rule) = line {
196                 rule.set_nbcols(nbcols);
197             } else {
198                 println!("not a table row, should not happen"); // should we panic ?
199             }
200         }
201         // let's find what we must do
202         let widths_sum: usize = widths.iter().sum();
203         let mut cols_removed = false;
204         if widths_sum + nbcols < width {
205             // it fits, all is well
206         } else if nbcols * 4 < width {
207             // we can keep all columns but we'll have to wrap them
208             reduce_col_widths(&mut widths, width - nbcols - 1);
209         } else {
210             // crisis behavior: we remove the columns which don't fit
211             nbcols = (width - 1) / 4;
212             cols_removed = true;
213             for ic in 0..nbcols {
214                 widths[ic] = 3;
215             }
216         }
217 
218         // Now we resize all cells and we insert new rows if necessary.
219         // We iterate in reverse order so that we can insert rows
220         //  without recomputing row indices.
221         for ir in (self.start..self.start + self.height).rev() {
222             let line = &mut lines[ir];
223             if let FmtLine::TableRow(FmtTableRow { cells }) = line {
224                 let mut cells_to_add: Vec<Vec<FmtComposite<'_>>> = Vec::new();
225                 cells.truncate(nbcols);
226                 for ic in 0..nbcols {
227                     if cells.len() <= ic {
228                         //FIXME isn't this already done ?
229                         cells.push(FmtComposite::new());
230                         continue;
231                     }
232                     cells_to_add.push(Vec::new());
233                     if cells[ic].visible_length > widths[ic] {
234                         // we must wrap the cell over several lines
235                         let mut composites = wrap::hard_wrap_composite(&cells[ic], widths[ic]);
236                         // the first composite replaces the cell, while the other
237                         // ones go to cells_to_add
238                         let mut drain = composites.drain(..);
239                         cells[ic] = drain.next().unwrap();
240                         for c in drain {
241                             cells_to_add[ic].push(c);
242                         }
243                     }
244                 }
245                 let nb_new_lines = cells_to_add.iter().fold(0, |m, cells| m.max(cells.len()));
246                 for inl in (0..nb_new_lines).rev() {
247                     let mut new_cells: Vec<FmtComposite<'_>> = Vec::new();
248                     for ic in 0..nbcols {
249                         new_cells.push(if cells_to_add[ic].len() > inl {
250                             cells_to_add[ic].remove(inl)
251                         } else {
252                             FmtComposite::new()
253                         });
254                     }
255                     let new_line = FmtLine::TableRow(FmtTableRow { cells: new_cells });
256                     lines.insert(ir + 1, new_line);
257                     self.height += 1;
258                 }
259             }
260         }
261         // Finally we iterate in normal order to specify alignment
262         // (the alignments of a row are the ones of the last rule line)
263         let mut current_aligns: Vec<Alignment> = vec![Alignment::Center; nbcols];
264         for ir in self.start..self.start + self.height {
265             let line = &mut lines[ir];
266             match line {
267                 FmtLine::TableRow(FmtTableRow { cells }) => {
268                     for ic in 0..nbcols {
269                         cells[ic].spacing = Some(Spacing {
270                             width: widths[ic],
271                             align: current_aligns[ic],
272                         });
273                     }
274                 }
275                 FmtLine::TableRule(rule) => {
276                     if cols_removed {
277                         rule.set_nbcols(nbcols);
278                     }
279                     if ir == self.start {
280                         rule.position = RelativePosition::Top;
281                     } else if ir == self.start + self.height - 1 {
282                         rule.position = RelativePosition::Bottom;
283                     }
284                     rule.widths[..nbcols].clone_from_slice(&widths[..nbcols]);
285                     current_aligns[..nbcols].clone_from_slice(&rule.aligns[..nbcols]);
286                 }
287                 _ => {
288                     panic!("It should be a table part");
289                 }
290             }
291         }
292     }
293 }
294 
295 /// find the positions of all tables
find_tables(lines: &[FmtLine<'_>]) -> Vec<Table>296 fn find_tables(lines: &[FmtLine<'_>]) -> Vec<Table> {
297     let mut tables: Vec<Table> = Vec::new();
298     let mut current: Option<Table> = None;
299     for (idx, line) in lines.iter().enumerate() {
300         match line {
301             FmtLine::TableRule(FmtTableRule { aligns, .. }) => match current.as_mut() {
302                 Some(b) => {
303                     b.height += 1;
304                     b.nbcols = b.nbcols.max(aligns.len());
305                 }
306                 None => {
307                     current = Some(Table {
308                         start: idx,
309                         height: 1,
310                         nbcols: aligns.len(),
311                     });
312                 }
313             },
314             FmtLine::TableRow(FmtTableRow { cells }) => match current.as_mut() {
315                 Some(b) => {
316                     b.height += 1;
317                     b.nbcols = b.nbcols.max(cells.len());
318                 }
319                 None => {
320                     current = Some(Table {
321                         start: idx,
322                         height: 1,
323                         nbcols: cells.len(),
324                     });
325                 }
326             },
327             _ => {
328                 if let Some(c) = current.take() {
329                     tables.push(c);
330                 }
331             }
332         }
333     }
334     if let Some(c) = current.take() {
335         tables.push(c);
336     }
337     tables
338 }
339 
340 /// Modify the rows of all tables in order to ensure it fits the widths
341 /// and all cells have the widths of their column.
342 ///
343 /// Some lines may be added to the table in the process, which means any
344 ///  precedent indexing might be invalid.
fix_all_tables(lines: &mut Vec<FmtLine<'_>>, width: usize)345 pub fn fix_all_tables(lines: &mut Vec<FmtLine<'_>>, width: usize) {
346     for tbl in find_tables(lines).iter_mut().rev() {
347         tbl.fix_columns(lines, width);
348     }
349 }
350 
351 #[cfg(test)]
352 mod col_reduction_tests {
353 
354     use super::*;
355 
356     #[test]
test_col_reduction_1_col()357     fn test_col_reduction_1_col() {
358         let mut widths = vec![500];
359         reduce_col_widths(&mut widths, 100);
360         assert_eq!(widths, &[100]);
361     }
362     #[test]
test_col_reduction_bug_01()363     fn test_col_reduction_bug_01() {
364         // test for a bug giving a width of 0 to the first col
365         let mut widths = vec![3, 1033, 4, 10, 20, 5];
366         reduce_col_widths(&mut widths, 148);
367         for &width in &widths {
368             assert!(width > 2);
369         }
370     }
371     #[test]
test_col_reduction_bug_unpublished_01()372     fn test_col_reduction_bug_unpublished_01() {
373         let widths = vec![ 3, 4, 11, 5, 15, 4, 9, 5, 4, 47 ];
374         let sum: usize = widths.iter().sum();
375         for goal in 30..sum {
376             let mut widths = widths.clone();
377             reduce_col_widths(&mut widths, goal);
378             println!("widths after reduction: {:?}", &widths);
379             for &width in &widths {
380                 assert!(width > 2);
381             }
382             let sum: usize = widths.iter().sum();
383             dbg!(sum);
384             assert!(sum<=goal);
385         }
386     }
387 }
388