diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index 094ffae..861b9c1 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -8,6 +8,7 @@ pub mod inline; pub mod anchor; pub mod list; pub mod quote; +pub mod table; #[derive(Clone, Default, Debug)] pub struct Context { @@ -22,6 +23,7 @@ pub enum Block { List, PreFormat, Quote, + Table, Verse, #[default] None, @@ -54,6 +56,9 @@ pub fn close(state: &State, tokens: &mut Vec) { Block::Quote => { panic!("End of input with open quote") }, + Block::Table => { + panic!("End of input with open table") + }, Block::Verse => { tokens.push(Token::Verse(Verse::new(false))); }, diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index 16711bd..292bdf0 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -9,7 +9,7 @@ use crate::{ Block, Lexeme, State, Token, token::{ Header, List, LineBreak, Literal, Paragraph, PreFormat, Quote, - Verse, + Table, Verse, }, }, }, @@ -59,6 +59,11 @@ pub fn parse( iterator.next(); iterator.next(); return true; + } else if Table::probe(lexeme) { + log!(VERBOSE, "Block Context: None -> Table on {lexeme}"); + state.context.block = Block::Table; + iterator.next(); + return true; } else if Paragraph::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Paragraph on {lexeme}"); state.context.block = Block::Paragraph; @@ -95,6 +100,9 @@ pub fn parse( Block::Quote => { return super::quote::parse(lexeme, state, tokens, iterator, graph); }, + Block::Table => { + return super::table::parse(lexeme, state, tokens, iterator, graph); + }, Block::Verse => { if Verse::probe_end(lexeme) { tokens.push(Token::Verse(Verse::new(false))); diff --git a/src/syntax/content/parser/context/table.rs b/src/syntax/content/parser/context/table.rs new file mode 100644 index 0000000..0aa95d2 --- /dev/null +++ b/src/syntax/content/parser/context/table.rs @@ -0,0 +1,114 @@ +use std::{iter::Peekable, slice::Iter}; + +use crate::{ + graph::Graph, + prelude::*, + syntax::content::parser::{ + Lexeme, State, Token, context::Block, format, state, token::Table, + }, +}; + +/// Handles open table contexts until a table is fully parsed. +/// +/// A return of `true` will trigger a continue in the outer parser, +/// skipping any further parsing of the current lexeme. +/// +/// # Panics +/// This parser can handle only the Table context, and will panic if passed an +/// unrelated context since it has no knowledge on how to handle them. +pub fn parse( + lexeme: &Lexeme, + state: &mut State, + tokens: &mut Vec, + iterator: &mut Peekable>, + graph: &Graph, +) -> bool { + let buffer = &mut state.buffers.table; + let candidate = &mut buffer.candidate; + + let mut parse_text = |text: &str| { + let (parsed_text, text_tokens) = format(text, graph); + state.format_tokens.extend_from_slice(&text_tokens); + parsed_text + }; + + #[allow(clippy::wildcard_enum_match_arm)] + match state.context.block { + Block::Table => { + if Table::probe_end(lexeme) { + log!(VERBOSE, "Probed end of table on {lexeme}"); + + if buffer.in_header { + log!(VERBOSE, "Adding unterminated header: {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else if buffer.in_cell { + log!(VERBOSE, "Adding unterminated cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else { + log!(VERBOSE, "Adding undelimited cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } + + tokens.push(Token::Table(candidate.clone())); + log!(VERBOSE, "Block Context: Table -> None on {lexeme}"); + state.context.block = Block::None; + *buffer = state::TableBuffer::default(); + iterator.next(); + } else if lexeme.match_char('\n') { + log!(VERBOSE, "Adding row: found newline on {lexeme}"); + if !buffer.cell.is_empty() { + + if buffer.in_header { + log!(VERBOSE, "Adding unterminated header: {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else if buffer.in_cell { + log!(VERBOSE, "Adding unterminated cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else { + log!(VERBOSE, "Adding undelimited cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } + + } + candidate.add_row(vec![]); + } else if lexeme.match_char_triple(' ', '!', ' ') { + log!(VERBOSE, "Adding header: found spaced ! on {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else if lexeme.match_char_triple(' ', '|', ' ') { + log!(VERBOSE, "Adding cell: found spaced | on {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else { + log!(VERBOSE, "Extending cell text on {lexeme}"); + buffer.cell.push_str(lexeme.text().as_str()); + } + }, + _ => { + panic!("Table context parser called to handle non-table context") + }, + } + true +} diff --git a/src/syntax/content/parser/state.rs b/src/syntax/content/parser/state.rs index 0059548..cea6f6f 100644 --- a/src/syntax/content/parser/state.rs +++ b/src/syntax/content/parser/state.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::syntax::content::parser::{ Token, context::Context, - token::{Anchor, Item, List, Quote}, + token::{Anchor, Item, List, Quote, Table}, }; #[derive(Clone, Default, Debug)] @@ -28,6 +28,7 @@ pub struct Buffers { pub anchor: AnchorBuffer, pub list: ListBuffer, pub quote: QuoteBuffer, + pub table: TableBuffer, } #[derive(Default, Clone, Debug)] @@ -50,6 +51,14 @@ pub struct QuoteBuffer { pub in_citation: bool, } +#[derive(Default, Clone, Debug)] +pub struct TableBuffer { + pub candidate: Table, + pub cell: String, + pub in_cell: bool, + pub in_header: bool, +} + impl std::fmt::Display for AnchorBuffer { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let display_text = if self.text.is_empty() { diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index a64f48b..cfbb39b 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -14,14 +14,15 @@ pub mod paragraph; pub mod preformat; pub mod quote; pub mod strike; +pub mod table; pub mod underline; pub mod verse; pub use { anchor::Anchor, bold::Bold, checkbox::CheckBox, code::Code, header::Header, item::Item, linebreak::LineBreak, list::List, literal::Literal, - oblique::Oblique, paragraph::Paragraph, preformat::PreFormat, - strike::Strike, underline::Underline, quote::Quote, verse::Verse, + oblique::Oblique, paragraph::Paragraph, preformat::PreFormat, quote::Quote, + strike::Strike, table::Table, underline::Underline, verse::Verse, }; #[derive(Debug, Eq, PartialEq, Clone)] @@ -40,6 +41,7 @@ pub enum Token { Paragraph(Paragraph), PreFormat(PreFormat), Quote(Quote), + Table(Table), Underline(Underline), Verse(Verse), } @@ -61,6 +63,7 @@ impl Token { Token::Paragraph(d) => d.render(), Token::PreFormat(d) => d.render(), Token::Quote(d) => d.render(), + Token::Table(d) => d.render(), Token::Underline(d) => d.render(), Token::Verse(d) => d.render(), } @@ -82,6 +85,7 @@ impl Token { Token::Paragraph(d) => d.flatten(), Token::PreFormat(d) => d.flatten(), Token::Quote(d) => d.flatten(), + Token::Table(d) => d.flatten(), Token::Underline(d) => d.flatten(), Token::Verse(d) => d.flatten(), } @@ -105,6 +109,7 @@ impl std::fmt::Display for Token { Token::Paragraph(d) => format!("{d}"), Token::PreFormat(d) => format!("{d}"), Token::Quote(d) => format!("{d}"), + Token::Table(d) => format!("{d}"), Token::Underline(d) => format!("{d}"), Token::Verse(d) => format!("{d}"), }; diff --git a/src/syntax/content/parser/token/table.rs b/src/syntax/content/parser/token/table.rs new file mode 100644 index 0000000..8d0b811 --- /dev/null +++ b/src/syntax/content/parser/token/table.rs @@ -0,0 +1,72 @@ +use crate::syntax::content::{Parseable, parser::Lexeme}; + +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct Table { + pub headers: Vec, + pub contents: Vec>, +} + +impl Table { + pub fn probe_end(lexeme: &Lexeme) -> bool { + lexeme.match_char_triple('\n', '%', '\n') || lexeme.last() + } + + pub fn add_header(&mut self, header: &str) { + self.headers.push(header.trim().to_string()); + } + + pub fn add_row(&mut self, row: Vec) { + self.contents.push(row); + } + + pub fn add_cell(&mut self, content: &str) { + if let Some(last) = self.contents.last_mut() { + last.push(content.trim().to_string()); + } + } +} + +impl Parseable for Table { + fn probe(lexeme: &Lexeme) -> bool { + lexeme.match_char_triple('\n', '%', '\n') + } + + fn lex(_lexeme: &Lexeme) -> Table { + Table::default() + } + + fn render(&self) -> String { + let mut xml = String::from("\n\n"); + + if !self.headers.is_empty() { + xml.push_str("\n"); + for header in &self.headers { + xml.push_str(format!("\n").as_str()); + } + xml.push_str("\n\n"); + } + + for row in &self.contents { + if !row.is_empty() { + xml.push_str("\n"); + for cell in row { + xml.push_str(format!("\n").as_str()); + } + xml.push_str("\n\n"); + } + } + + xml.push_str("\n
{header}
{cell}
\n"); + xml + } + + fn flatten(&self) -> String { + String::default() + } +} + +impl std::fmt::Display for Table { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Table") + } +}