ftml/parsing/rule/impls/
table.rs

1/*
2 * parsing/rule/impls/table.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2025 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use super::prelude::*;
22use crate::tree::{Alignment, Table, TableCell, TableRow};
23use std::mem;
24use std::num::NonZeroU32;
25
26#[derive(Debug)]
27struct TableCellStart {
28    align: Option<Alignment>,
29    header: bool,
30    column_span: NonZeroU32,
31}
32
33pub const RULE_TABLE: Rule = Rule {
34    name: "table",
35    position: LineRequirement::StartOfLine,
36    try_consume_fn,
37};
38
39fn try_consume_fn<'r, 't>(
40    parser: &mut Parser<'r, 't>,
41) -> ParseResult<'r, 't, Elements<'t>> {
42    debug!("Trying to parse simple table");
43    let mut rows = Vec::new();
44    let mut errors = Vec::new();
45    let mut _paragraph_break = false;
46
47    'table: loop {
48        debug!("Parsing next table row");
49
50        let mut cells = Vec::new();
51
52        macro_rules! build_row {
53            () => {
54                rows.push(TableRow {
55                    cells: mem::take(&mut cells),
56                    attributes: AttributeMap::new(),
57                })
58            };
59        }
60
61        macro_rules! finish_table {
62            () => {
63                if rows.is_empty() {
64                    // No rows were successfully parsed, fail.
65                    return Err(parser.make_err(ParseErrorKind::RuleFailed));
66                } else {
67                    // At least one row was created, end it here.
68                    break 'table;
69                }
70            };
71        }
72
73        // Loop for each cell in the row
74        'row: loop {
75            debug!("Parsing next table cell");
76            let mut elements = Vec::new();
77            let TableCellStart {
78                align,
79                header,
80                column_span,
81            } = match parse_cell_start(parser)? {
82                Some(cell_start) => cell_start,
83                None => finish_table!(),
84            };
85
86            macro_rules! build_cell {
87                () => {
88                    cells.push(TableCell {
89                        elements: mem::take(&mut elements),
90                        header,
91                        column_span,
92                        align,
93                        attributes: AttributeMap::new(),
94                    })
95                };
96            }
97
98            // Loop for each element in the cell
99            'cell: loop {
100                trace!("Parsing next element (length {})", elements.len());
101                match parser.next_two_tokens() {
102                    // End the cell or row
103                    (
104                        Token::TableColumn
105                        | Token::TableColumnTitle
106                        | Token::TableColumnCenter
107                        | Token::TableColumnRight,
108                        Some(next),
109                    ) => {
110                        trace!(
111                            "Ending cell, row, or table (next token '{}')",
112                            next.name(),
113                        );
114                        match next {
115                            // End the table entirely, there's a newline in between,
116                            // or it's the end of input.
117                            //
118                            // For both ending the table and the row, we must step
119                            // to consume the final table column token.
120                            Token::ParagraphBreak | Token::InputEnd => {
121                                build_cell!();
122                                build_row!();
123                                parser.step()?;
124                                break 'table;
125                            }
126
127                            // Only end the row, continue the table.
128                            Token::LineBreak => {
129                                build_cell!();
130                                parser.step_n(2)?;
131                                break 'row;
132                            }
133
134                            // Otherwise, the cell is finished, and we proceed to the next one.
135                            _ => break 'cell,
136                        }
137                    }
138
139                    // Ignore leading whitespace
140                    (Token::Whitespace, _) if elements.is_empty() => {
141                        trace!("Ignoring leading whitespace");
142                        parser.step()?;
143                        continue 'cell;
144                    }
145
146                    // Ignore trailing whitespace
147                    (
148                        Token::Whitespace,
149                        Some(
150                            Token::TableColumn
151                            | Token::TableColumnTitle
152                            | Token::TableColumnCenter
153                            | Token::TableColumnRight,
154                        ),
155                    ) => {
156                        trace!("Ignoring trailing whitespace");
157                        parser.step()?;
158                        continue 'cell;
159                    }
160
161                    // Invalid tokens
162                    (Token::LineBreak | Token::ParagraphBreak | Token::InputEnd, _) => {
163                        trace!("Invalid termination tokens in table, ending");
164                        finish_table!();
165                    }
166
167                    // Consume tokens like normal
168                    _ => {
169                        trace!("Consuming cell contents as elements");
170
171                        let new_elements =
172                            consume(parser)?.chain(&mut errors, &mut _paragraph_break);
173
174                        elements.extend(new_elements);
175                    }
176                }
177            }
178
179            build_cell!();
180        }
181
182        build_row!();
183    }
184
185    // Build table
186    let mut attributes = AttributeMap::new();
187    attributes.insert("class", cow!("wj-table"));
188
189    let table = Table { rows, attributes };
190    ok!(false; Element::Table(table), errors)
191}
192
193/// Parse out the cell settings from the start.
194///
195/// Cells have a few settings, such as alignment, and most importantly
196/// here, their span, which is specified by having multiple
197/// `Token::TableColumn` (`||`) adjacent together.
198///
199/// If `Ok(None)` is returned, then the end of the input wasn't reached,
200/// but this is not a valid cell start.
201///
202/// This is not an `Err(_)` case, because this may simply signal the end
203/// of the table if it already has rows.
204fn parse_cell_start(parser: &mut Parser) -> Result<Option<TableCellStart>, ParseError> {
205    let mut span = 0;
206
207    macro_rules! increase_span {
208        () => {{
209            span += 1;
210            parser.step()?;
211        }};
212    }
213
214    let (align, header) = loop {
215        match parser.current().token {
216            // Style cases, terminal
217            // NOTE: There is no TableColumnLeft
218            Token::TableColumnTitle => {
219                increase_span!();
220                break (None, true);
221            }
222            Token::TableColumnCenter => {
223                increase_span!();
224                break (Some(Alignment::Center), false);
225            }
226            Token::TableColumnRight => {
227                increase_span!();
228                break (Some(Alignment::Right), false);
229            }
230
231            // Regular column, iterate to see if it has a span
232            Token::TableColumn => increase_span!(),
233
234            // Regular column, terminal
235            _ if span > 0 => break (None, false),
236
237            // No span depth, just an invalid token
238            _ => return Ok(None),
239        }
240    };
241
242    let column_span =
243        NonZeroU32::new(span).expect("Cell start exited without column span");
244
245    Ok(Some(TableCellStart {
246        align,
247        header,
248        column_span,
249    }))
250}