1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
use std::borrow::Cow;

use nom::{
    error::{make_error, ErrorKind},
    Err, IResult,
};

use crate::parse::combinators::{blank_lines_count, line, lines_while};

/// Table Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_type"))]
pub enum Table<'a> {
    /// "org" type table
    #[cfg_attr(feature = "ser", serde(rename = "org"))]
    Org {
        #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
        tblfm: Option<Cow<'a, str>>,
        /// Numbers of blank lines between last table's line and next non-blank
        /// line or buffer's end
        post_blank: usize,
        has_header: bool,
    },
    /// "table.el" type table
    #[cfg_attr(feature = "ser", serde(rename = "table.el"))]
    TableEl {
        value: Cow<'a, str>,
        /// Numbers of blank lines between last table's line and next non-blank
        /// line or buffer's end
        post_blank: usize,
    },
}

impl Table<'_> {
    pub fn parse_table_el(input: &str) -> Option<(&str, Table)> {
        Self::parse_table_el_internal(input).ok()
    }

    fn parse_table_el_internal(input: &str) -> IResult<&str, Table, ()> {
        let (_, first_line) = line(input)?;

        let first_line = first_line.trim();

        // Table.el tables start at lines beginning with "+-" string and followed by plus or minus signs
        if !first_line.starts_with("+-")
            || first_line
                .as_bytes()
                .iter()
                .any(|&c| c != b'+' && c != b'-')
        {
            // TODO: better error kind
            return Err(Err::Error(make_error(input, ErrorKind::Many0)));
        }

        // Table.el tables end at the first line not starting with either a vertical line or a plus sign.
        let (input, content) = lines_while(|line| {
            let line = line.trim_start();
            line.starts_with('|') || line.starts_with('+')
        })(input)?;

        let (input, post_blank) = blank_lines_count(input)?;

        Ok((
            input,
            Table::TableEl {
                value: content.into(),
                post_blank,
            },
        ))
    }

    pub fn into_owned(self) -> Table<'static> {
        match self {
            Table::Org {
                tblfm,
                post_blank,
                has_header,
            } => Table::Org {
                tblfm: tblfm.map(Into::into).map(Cow::Owned),
                post_blank,
                has_header,
            },
            Table::TableEl { value, post_blank } => Table::TableEl {
                value: value.into_owned().into(),
                post_blank,
            },
        }
    }
}

/// Table Row Element
///
/// # Syntax
///
/// ```text
/// |   0 |   1 |   2 | <- TableRow::Body
/// |   0 |   1 |   2 | <- TableRow::Body
/// ```
///
/// ```text
/// |-----+-----+-----| <- ignores
/// |   0 |   1 |   2 | <- TableRow::Header
/// |   0 |   1 |   2 | <- TableRow::Header
/// |-----+-----+-----| <- TableRow::HeaderRule
/// |   0 |   1 |   2 | <- TableRow::Body
/// |-----+-----+-----| <- TableRow::BodyRule
/// |   0 |   1 |   2 | <- TableRow::Body
/// |-----+-----+-----| <- TableRow::BodyRule
/// |-----+-----+-----| <- TableRow::BodyRule
/// |   0 |   1 |   2 | <- TableRow::Body
/// |-----+-----+-----| <- ignores
/// ```
///
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_row_type"))]
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
pub enum TableRow {
    /// This row is part of table header
    Header,
    /// This row is part of table body
    Body,
    /// This row is between table header and body
    HeaderRule,
    /// This row is between table body and next body
    BodyRule,
}

/// Table Cell Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_cell_type"))]
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
pub enum TableCell {
    /// Header cell
    Header,
    /// Body cell, or standard cell
    Body,
}

#[test]
fn parse_table_el_() {
    assert_eq!(
        Table::parse_table_el(
            r#"  +---+
  |   |
  +---+

"#
        ),
        Some((
            "",
            Table::TableEl {
                value: r#"  +---+
  |   |
  +---+
"#
                .into(),
                post_blank: 1
            }
        ))
    );
    assert!(Table::parse_table_el("").is_none());
    assert!(Table::parse_table_el("+----|---").is_none());
}