orgize 0.9.0

A Rust library for parsing orgmode files.
Documentation
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());
}