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}