use crate::constants::{COLON, HYPHEN, LBRACK, NEWLINE, PERIOD, PLUS, RBRACK, RPAREN, SPACE, STAR};
use crate::node_pool::NodeID;
use crate::parse::parse_element;
use crate::types::{Cursor, Expr, MatchError, ParseOpts, Parseable, Parser, Result};
use crate::utils::Match;
#[derive(Debug, Clone)]
pub struct Item<'a> {
pub bullet: BulletKind,
pub counter_set: Option<&'a str>,
pub check_box: Option<CheckBox>,
pub tag: Option<&'a str>,
pub children: Vec<NodeID>,
}
impl<'a> Parseable<'a> for Item<'a> {
fn parse(
parser: &mut Parser<'a>,
mut cursor: Cursor<'a>,
parent: Option<NodeID>,
mut parse_opts: ParseOpts,
) -> Result<NodeID> {
let start = cursor.index;
let bullet_match = BulletKind::parse(cursor)?;
let bullet = bullet_match.obj;
cursor.move_to(bullet_match.end);
let counter_set: Option<&'a str> = if let Ok(counter_match) = parse_counter_set(cursor) {
cursor.move_to(counter_match.end);
Some(counter_match.obj)
} else {
None
};
let check_box: Option<CheckBox> = if let Ok(check_box_match) = CheckBox::parse(cursor) {
cursor.move_to(check_box_match.end);
Some(check_box_match.obj)
} else {
None
};
let tag: Option<&str> = if let BulletKind::Unordered = bullet {
if let Ok(tag_match) = parse_tag(cursor) {
cursor.move_to(tag_match.end);
Some(tag_match.obj)
} else {
None
}
} else {
None
};
let reserve_id = parser.pool.reserve_id();
let mut children: Vec<NodeID> = Vec::new();
let mut blank_obj: Option<NodeID> = None;
cursor.skip_ws();
if cursor.try_curr()? == NEWLINE {
cursor.next();
} else {
parse_opts.list_line = true;
}
let mut prev_ind = cursor.index;
while let Ok(element_id) = parse_element(parser, cursor, Some(reserve_id), parse_opts) {
let pool_loc = &parser.pool[element_id];
match &pool_loc.obj {
Expr::BlankLine => {
if blank_obj.is_some() {
cursor.index = prev_ind;
break;
} else {
blank_obj = Some(element_id);
prev_ind = cursor.index;
}
}
Expr::Item(_) => {
break;
}
_ => {
if let Some(blank_id) = blank_obj {
children.push(blank_id);
blank_obj = None;
}
children.push(element_id);
}
}
parse_opts.list_line = false;
cursor.move_to(pool_loc.end);
}
Ok(parser.alloc_with_id(
Self {
bullet,
counter_set,
check_box,
tag,
children,
},
start,
cursor.index,
parent,
reserve_id,
))
}
}
#[derive(Debug, Clone, Copy)]
pub enum BulletKind {
Unordered,
Ordered(CounterKind),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CounterKind {
Letter(u8),
Number(u8),
}
impl BulletKind {
pub(crate) fn parse(mut cursor: Cursor) -> Result<Match<BulletKind>> {
let start = cursor.index;
match cursor.curr() {
STAR | HYPHEN | PLUS => {
if cursor.peek(1)?.is_ascii_whitespace() {
Ok(Match {
start,
end: cursor.index + if cursor.peek(1)? == NEWLINE { 1 } else { 2 },
obj: BulletKind::Unordered,
})
} else {
Err(MatchError::InvalidLogic)
}
}
chr if chr.is_ascii_alphanumeric() => {
let num_match = cursor.fn_while(|chr| {
chr.is_ascii_alphanumeric()
})?;
cursor.index = num_match.end;
if !((cursor.curr() == PERIOD || cursor.curr() == RPAREN)
&& cursor.peek(1)?.is_ascii_whitespace())
{
return Err(MatchError::InvalidLogic);
}
let bullet_kind = if num_match.len() == 1 {
let temp = num_match.obj.as_bytes()[0];
if temp.is_ascii_alphabetic() {
BulletKind::Ordered(CounterKind::Letter(temp))
} else if temp.is_ascii_digit() {
BulletKind::Ordered(CounterKind::Number(temp - 48))
} else {
Err(MatchError::InvalidLogic)?
}
} else {
BulletKind::Ordered(CounterKind::Number(
num_match.obj.parse().or(Err(MatchError::InvalidLogic))?,
))
};
Ok(Match {
start,
end: cursor.index + if cursor.peek(1)? == NEWLINE { 1 } else { 2 },
obj: bullet_kind,
})
}
_ => Err(MatchError::InvalidLogic),
}
}
}
fn parse_counter_set(mut cursor: Cursor) -> Result<Match<&str>> {
let start = cursor.index;
cursor.is_index_valid()?;
cursor.skip_ws();
if cursor.curr() != LBRACK && cursor.peek(1)? != b'@' {
Err(MatchError::InvalidLogic)?
}
cursor.index += 2;
let num_match = cursor.fn_while(|chr| chr.is_ascii_alphanumeric())?;
cursor.index = num_match.end;
if cursor.curr() != RBRACK {
Err(MatchError::InvalidLogic)?;
}
let counter_kind = if num_match.len() == 1 {
let temp = num_match.obj.as_bytes()[0];
if temp.is_ascii_alphanumeric() {
num_match.obj
} else {
return Err(MatchError::InvalidLogic);
}
} else {
if num_match
.obj
.as_bytes()
.iter()
.all(|byte| byte.is_ascii_digit())
{
num_match.obj
} else {
return Err(MatchError::InvalidLogic);
}
};
Ok(Match {
start,
end: cursor.index + 1,
obj: counter_kind,
})
}
fn parse_tag(mut cursor: Cursor) -> Result<Match<&str>> {
let start = cursor.index;
cursor.is_index_valid()?;
cursor.skip_ws();
let end = loop {
match cursor.try_curr()? {
COLON => {
if cursor[cursor.index - 1].is_ascii_whitespace()
&& COLON == cursor.peek(1)?
&& cursor.peek(2)?.is_ascii_whitespace()
{
break cursor.index + 2;
} else {
cursor.next();
}
}
NEWLINE => Err(MatchError::EofError)?,
_ => cursor.next(),
}
};
Ok(Match {
start,
end,
obj: cursor.clamp_backwards(start).trim(),
})
}
#[derive(Debug, Clone)]
pub enum CheckBox {
Intermediate,
Off,
On,
}
impl From<&CheckBox> for &str {
fn from(value: &CheckBox) -> Self {
match value {
CheckBox::Intermediate => "-",
CheckBox::Off => " ",
CheckBox::On => "X",
}
}
}
impl CheckBox {
fn parse(mut cursor: Cursor) -> Result<Match<CheckBox>> {
let start = cursor.index;
cursor.is_index_valid()?;
cursor.skip_ws();
if cursor.curr() != LBRACK && cursor.peek(2)? != RBRACK {
return Err(MatchError::EofError);
}
Ok(Match {
start,
end: cursor.index + 3,
obj: match cursor[cursor.index + 1].to_ascii_lowercase() {
b'x' => Self::On,
SPACE => Self::Off,
HYPHEN => Self::Intermediate,
_ => Err(MatchError::InvalidLogic)?,
},
})
}
}
#[cfg(test)]
mod tests {
}