ftml 1.41.0

Foundation Text Markup Language - a library to render Wikidot text as HTML
Documentation
/*
 * parsing/rule/impls/table.rs
 *
 * ftml - Library to parse Wikidot text
 * Copyright (C) 2019-2026 Wikijump Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

use super::prelude::*;
use crate::tree::{Alignment, Table, TableCell, TableRow, TableType};
use std::mem;
use std::num::NonZeroU32;

#[derive(Debug)]
struct TableCellStart {
    align: Option<Alignment>,
    header: bool,
    column_span: NonZeroU32,
}

pub const RULE_TABLE: Rule = Rule {
    name: "table",
    position: LineRequirement::StartOfLine,
    try_consume_fn,
};

fn try_consume_fn<'r, 't>(
    parser: &mut Parser<'r, 't>,
) -> ParseResult<'r, 't, Elements<'t>> {
    debug!("Trying to parse simple table");
    let mut rows = Vec::new();
    let mut errors = Vec::new();
    let mut _paragraph_break = false;

    'table: loop {
        debug!("Parsing next table row");

        let mut cells = Vec::new();

        macro_rules! build_row {
            () => {
                rows.push(TableRow {
                    cells: mem::take(&mut cells),
                    attributes: AttributeMap::new(),
                })
            };
        }

        macro_rules! finish_table {
            () => {
                if rows.is_empty() {
                    // No rows were successfully parsed, fail.
                    return Err(parser.make_err(ParseErrorKind::RuleFailed));
                } else {
                    // At least one row was created, end it here.
                    break 'table;
                }
            };
        }

        // Loop for each cell in the row
        'row: loop {
            debug!("Parsing next table cell");
            let mut elements = Vec::new();
            let TableCellStart {
                align,
                header,
                column_span,
            } = match parse_cell_start(parser)? {
                Some(cell_start) => cell_start,
                None => finish_table!(),
            };

            macro_rules! build_cell {
                () => {
                    cells.push(TableCell {
                        elements: mem::take(&mut elements),
                        header,
                        column_span,
                        align,
                        attributes: AttributeMap::new(),
                    })
                };
            }

            // Loop for each element in the cell
            'cell: loop {
                trace!("Parsing next element (length {})", elements.len());
                match parser.next_two_tokens() {
                    // End the cell or row
                    (
                        Token::TableColumn
                        | Token::TableColumnTitle
                        | Token::TableColumnCenter
                        | Token::TableColumnRight,
                        Some(next),
                    ) => {
                        trace!(
                            "Ending cell, row, or table (next token '{}')",
                            next.name(),
                        );
                        match next {
                            // End the table entirely, there's a newline in between,
                            // or it's the end of input.
                            //
                            // For both ending the table and the row, we must step
                            // to consume the final table column token.
                            Token::ParagraphBreak | Token::InputEnd => {
                                build_cell!();
                                build_row!();
                                parser.step()?;
                                break 'table;
                            }

                            // Only end the row, continue the table.
                            Token::LineBreak => {
                                build_cell!();
                                parser.step_n(2)?;
                                break 'row;
                            }

                            // Otherwise, the cell is finished, and we proceed to the next one.
                            _ => break 'cell,
                        }
                    }

                    // Ignore leading whitespace
                    (Token::Whitespace, _) if elements.is_empty() => {
                        trace!("Ignoring leading whitespace");
                        parser.step()?;
                        continue 'cell;
                    }

                    // Ignore trailing whitespace
                    (
                        Token::Whitespace,
                        Some(
                            Token::TableColumn
                            | Token::TableColumnTitle
                            | Token::TableColumnCenter
                            | Token::TableColumnRight,
                        ),
                    ) => {
                        trace!("Ignoring trailing whitespace");
                        parser.step()?;
                        continue 'cell;
                    }

                    // Invalid tokens
                    (Token::LineBreak | Token::ParagraphBreak | Token::InputEnd, _) => {
                        trace!("Invalid termination tokens in table, ending");
                        finish_table!();
                    }

                    // Consume tokens like normal
                    _ => {
                        trace!("Consuming cell contents as elements");

                        let new_elements =
                            consume(parser)?.chain(&mut errors, &mut _paragraph_break);

                        elements.extend(new_elements);
                    }
                }
            }

            build_cell!();
        }

        build_row!();
    }

    // Build table
    let attributes = AttributeMap::new();
    let table = Table {
        rows,
        attributes,
        table_type: TableType::Simple,
    };
    ok!(false; Element::Table(table), errors)
}

/// Parse out the cell settings from the start.
///
/// Cells have a few settings, such as alignment, and most importantly
/// here, their span, which is specified by having multiple
/// `Token::TableColumn` (`||`) adjacent together.
///
/// If `Ok(None)` is returned, then the end of the input wasn't reached,
/// but this is not a valid cell start.
///
/// This is not an `Err(_)` case, because this may simply signal the end
/// of the table if it already has rows.
fn parse_cell_start(parser: &mut Parser) -> Result<Option<TableCellStart>, ParseError> {
    let mut span = 0;

    macro_rules! increase_span {
        () => {{
            span += 1;
            parser.step()?;
        }};
    }

    let (align, header) = loop {
        match parser.current().token {
            // Style cases, terminal
            // NOTE: There is no TableColumnLeft
            Token::TableColumnTitle => {
                increase_span!();
                break (None, true);
            }
            Token::TableColumnCenter => {
                increase_span!();
                break (Some(Alignment::Center), false);
            }
            Token::TableColumnRight => {
                increase_span!();
                break (Some(Alignment::Right), false);
            }

            // Regular column, iterate to see if it has a span
            Token::TableColumn => increase_span!(),

            // Regular column, terminal
            _ if span > 0 => break (None, false),

            // No span depth, just an invalid token
            _ => return Ok(None),
        }
    };

    let column_span =
        NonZeroU32::new(span).expect("Cell start exited without column span");

    Ok(Some(TableCellStart {
        align,
        header,
        column_span,
    }))
}