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