Skip to main content

tex_fmt/
table.rs

1//! Formatting tables
2
3use crate::format::{Pattern, State};
4use crate::regexes::{TABLES_BEGIN, TABLES_END};
5use crate::LINE_END;
6use itertools::Itertools;
7use regex::Regex;
8
9// Remove all double spaces from the table
10fn remove_double_spaces(text: &str) -> String {
11    let re = Regex::new(r"(\S) {2,}").unwrap();
12    re.replace_all(text, "$1 ").to_string()
13}
14
15// Add line breaks after "\\"
16fn add_line_breaks(text: &str) -> (String, bool) {
17    let re_break = Regex::new(r"\\\\ .*\S").unwrap();
18    let re_indent = Regex::new(r"^\s*\S").unwrap();
19    let re_first_non_white = Regex::new(r"\S.*").unwrap();
20    let re_to_break = Regex::new(r"^[^\\]*\\\\").unwrap();
21    let mut new_text = String::new();
22    let mut finished: bool = true;
23    for line in text.lines() {
24        if re_break.is_match(line) {
25            finished = false;
26            let indent = re_indent.find(line).map_or("", |m| {
27                let s = m.as_str();
28                &s[..s.len() - 1]
29            });
30            let next_line_long = re_break.find(line).map_or("", |m| {
31                let s = m.as_str();
32                &s[2..]
33            });
34            let next_line = re_first_non_white
35                .find(next_line_long)
36                .map_or("", |m| m.as_str());
37            let this_line = re_to_break.find(line).map_or("", |m| m.as_str());
38            new_text.push_str(this_line);
39            new_text.push_str(LINE_END);
40            new_text.push_str(indent);
41            new_text.push_str(next_line);
42            new_text.push_str(LINE_END);
43        } else {
44            new_text.push_str(line);
45            new_text.push_str(LINE_END);
46        }
47    }
48    (new_text, finished)
49}
50
51// Get the starting positions of all delims
52fn get_positions(text: &str) -> Vec<Vec<usize>> {
53    text.lines()
54        .map(|l| {
55            let mut prev = None;
56            l.char_indices()
57                .filter_map(|(i, c)| {
58                    let is_match = c == '&' && prev != Some('\\');
59                    prev = Some(c);
60                    if is_match {
61                        Some(i)
62                    } else {
63                        None
64                    }
65                })
66                .collect()
67        })
68        .collect()
69}
70
71// Find the desired new positions of the delims
72fn get_new_positions(positions: &[Vec<usize>]) -> Vec<Vec<usize>> {
73    let n_delims = positions.iter().map(std::vec::Vec::len).max().unwrap();
74    let mut new_positions = positions.to_owned();
75    for j in 0..n_delims {
76        let positions: Vec<Option<usize>> =
77            new_positions.iter().map(|l| l.get(j).copied()).collect();
78        let new_position: usize = positions.iter().max().unwrap().unwrap();
79        for line in &mut new_positions {
80            let n_delims_line = line.len();
81            if n_delims_line > j {
82                let offset = new_position - line[j];
83                for pos in line.iter_mut().skip(j) {
84                    *pos += offset;
85                }
86            }
87        }
88    }
89    new_positions
90}
91
92// Calculate the cumulative offsets needed
93fn get_offsets(
94    positions: &[Vec<usize>],
95    new_positions: &[Vec<usize>],
96) -> Vec<Vec<usize>> {
97    let mut offsets = vec![];
98    for l in 0..positions.len() {
99        let mut prev_offset = 0;
100        let mut offset = vec![];
101        for j in 0..positions[l].len() {
102            offset.push(new_positions[l][j] - positions[l][j] - prev_offset);
103            prev_offset = new_positions[l][j] - positions[l][j];
104        }
105        offsets.push(offset);
106    }
107    offsets
108}
109
110// Use the offsets to format one line of the table
111fn format_table_line(line: &str, offsets_delims_row: &[usize]) -> String {
112    let mut new_line = String::new();
113    let mut j = 0;
114    for c in line.chars() {
115        if c == '&' {
116            let offset = offsets_delims_row.get(j).copied().unwrap_or(0);
117            new_line.extend(std::iter::repeat_n(' ', offset));
118            j += 1;
119        }
120        new_line.push(c);
121    }
122    new_line
123}
124
125// Format a single table
126fn format_table(text: &str) -> String {
127    let mut clean_text = remove_double_spaces(text);
128
129    // add line breaks
130    let mut finished: bool;
131    (clean_text, finished) = add_line_breaks(&clean_text);
132    let max_line_break_attempts = 10;
133    for _attempt in 0..max_line_break_attempts {
134        if finished {
135            break;
136        }
137        (clean_text, finished) = add_line_breaks(&clean_text);
138    }
139
140    let positions = get_positions(&clean_text);
141    let new_positions = get_new_positions(&positions);
142    let offsets = get_offsets(&positions, &new_positions);
143    let mut new_text = String::new();
144    for (linum, line) in clean_text.lines().enumerate() {
145        let new_line = format_table_line(line, &offsets[linum]);
146        new_text.push_str(&new_line);
147        new_text.push_str(LINE_END);
148    }
149    new_text
150}
151
152fn contains_table_begin(line: &str) -> bool {
153    TABLES_BEGIN.iter().any(|r| line.contains(r))
154}
155
156fn contains_table_end(line: &str) -> bool {
157    TABLES_END.iter().any(|r| line.contains(r))
158}
159
160// Locate all the tables in the text
161fn find_table_positions(text: &str) -> Vec<(usize, usize)> {
162    let mut table_positions = vec![];
163    let mut begin: usize = 0;
164    let mut end: usize;
165    for (linum, line) in text.lines().enumerate() {
166        if contains_table_begin(line) {
167            begin = linum;
168        } else if contains_table_end(line) {
169            end = linum;
170            table_positions.push((begin, end));
171        }
172    }
173    table_positions
174}
175
176// Format all the tables in the text
177#[must_use]
178pub fn format_tables(text: &str) -> String {
179    let table_positions = find_table_positions(text);
180    if table_positions.is_empty() {
181        return text.to_string();
182    }
183
184    let first_table_begin = table_positions[0].0;
185    let mut new_text: String =
186        text.lines().take(first_table_begin).join(LINE_END);
187    new_text.push_str(LINE_END);
188
189    for (t, table_position) in table_positions.iter().enumerate() {
190        // format each table
191        let begin = table_position.0;
192        let end = table_position.1;
193        let table_text: String = text
194            .lines()
195            .skip(begin)
196            .take(end - begin + 1)
197            .join(LINE_END);
198        let new_table_text = format_table(&table_text);
199        new_text.push_str(&new_table_text);
200
201        // format text following each table
202        let next_table_begin = if t + 1 < table_positions.len() {
203            table_positions[t + 1].0
204        } else {
205            text.lines().count()
206        };
207        let next_text: String = text
208            .lines()
209            .skip(end + 1)
210            .take(next_table_begin - end - 1)
211            .join(LINE_END);
212        new_text.push_str(&next_text);
213        new_text.push_str(LINE_END);
214    }
215
216    new_text
217}
218
219/// Information on the table state of a line
220#[derive(Clone, Debug)]
221pub struct Table {
222    /// Whether the line is in a table
223    pub actual: bool,
224    /// Whether the line appears to be in a table
225    pub visual: bool,
226}
227
228impl Table {
229    /// Construct a new table state
230    #[must_use]
231    pub const fn new() -> Self {
232        Self {
233            actual: false,
234            visual: false,
235        }
236    }
237}
238
239impl Default for Table {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245// Check if a line is inside a table
246#[must_use]
247pub fn is_inside_table(line: &str, state: &State, pattern: &Pattern) -> Table {
248    let begin = pattern.contains_env_begin && contains_table_begin(line);
249    let end = pattern.contains_env_end && contains_table_end(line);
250    let actual: bool;
251    let visual: bool;
252    if begin {
253        actual = true;
254        visual = true;
255    } else if end {
256        actual = false;
257        visual = true;
258    } else {
259        actual = state.table.actual;
260        visual = state.table.actual;
261    }
262
263    Table { actual, visual }
264}