From 0ec784034a6b526354997276e62ea0fadaa0151f Mon Sep 17 00:00:00 2001 From: jutty Date: Tue, 6 Jan 2026 05:56:20 -0300 Subject: [PATCH] Implement list token --- src/syntax/content/parser/context.rs | 14 +++++- src/syntax/content/parser/context/block.rs | 58 +++++++++++++++++++++- src/syntax/content/parser/token.rs | 15 +++--- src/syntax/content/parser/token/item.rs | 41 +++++++++++++++ src/syntax/content/parser/token/list.rs | 47 ++++++++++++++++++ 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 src/syntax/content/parser/token/item.rs create mode 100644 src/syntax/content/parser/token/list.rs diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index d7c26ff..0ae3197 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -1,6 +1,9 @@ use crate::syntax::content::parser::{ state::State, - token::{Token, paragraph::Paragraph, preformat::PreFormat}, + token::{ + Token, paragraph::Paragraph, preformat::PreFormat, list::List, + item::Item, + }, }; pub mod anchor; @@ -17,6 +20,8 @@ pub struct Context { pub enum Block { Paragraph, Header(u8), + Item(bool), + List(bool), PreFormat, None, } @@ -38,6 +43,13 @@ pub fn close(state: &State, tokens: &mut Vec) { Block::Paragraph => { tokens.push(Token::Paragraph(Paragraph::new(false))); }, + Block::Item(ordered) => { + tokens.push(Token::Item(Item::new(false))); + tokens.push(Token::List(List::new(false, ordered))); + }, + Block::List(ordered) => { + tokens.push(Token::List(List::new(false, ordered))); + }, Block::Header(_) => panic!("End of input with open header"), Block::None => (), } diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index a432300..87efd48 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -10,7 +10,7 @@ use crate::{ state::State, token::{ Token, header::Header, preformat::PreFormat, - paragraph::Paragraph, literal::Literal, + paragraph::Paragraph, literal::Literal, list::List, item::Item, }, }, }, @@ -42,6 +42,16 @@ pub fn parse( state.context.block = Block::Header(header.level()); tokens.push(Token::Header(header)); return true; + } else if List::probe(lexeme) { + let ordered = lexeme.match_as_char('+'); + log!("Block Context: None -> Item on {lexeme}"); + state.context.block = Block::Item(ordered); + tokens.push(Token::List(List::new(true, ordered))); + tokens.push(Token::Item(Item::new(true))); + // List::probe implies a dash followed by a space, + // both of which sould not be rendered literally + iterator.next(); + return true; } else if Paragraph::probe(lexeme) { log!("Block Context: None -> Paragraph on {lexeme}"); state.context.block = Block::Paragraph; @@ -72,6 +82,28 @@ pub fn parse( state.context.block = Block::None; } }, + Block::List(ordered) => { + if List::probe_end(lexeme) { + tokens.push(Token::List(List::new(false, ordered))); + log!("Block Context: List -> None on {lexeme}"); + state.context.block = Block::None; + } else if Item::probe(lexeme) { + tokens.push(Token::Item(Item::new(true))); + log!("Block Context: List -> Item on {lexeme}"); + state.context.block = Block::Item(ordered); + // Item::probe implies a dash followed by a space, + // both of which sould not be rendered literally + iterator.next(); + return true; + } + }, + Block::Item(ordered) => { + if Item::probe_end(lexeme) { + tokens.push(Token::Item(Item::new(false))); + log!("Block Context: Item -> List on {lexeme}"); + state.context.block = Block::List(ordered); + } + }, } false } @@ -139,4 +171,28 @@ mod tests { let level = Level::from(u); assert_eq!(level.to_string(), "6"); } + + #[test] + fn unordered_list_at_eoi() { + assert_eq!( + read("- a\n- b\n- c"), + "" + ); + } + + #[test] + fn unordered_list_with_content_before() { + assert_eq!( + read("_e e_\n\n- a\n- b\n- c"), + "

e e

\n\n", + ); + } + + #[test] + fn unordered_list_with_content_after() { + assert_eq!( + read("- a\n- b\n- c\n\nd",), + "\n

d

" + ); + } } diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index fca3424..88d6578 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -9,13 +9,17 @@ pub mod header; pub mod preformat; pub mod code; pub mod oblique; +pub mod list; +pub mod item; #[derive(Debug, Eq, PartialEq, Clone)] pub enum Token { Anchor(anchor::Anchor), Code(code::Code), Header(header::Header), + Item(item::Item), LineBreak(linebreak::LineBreak), + List(list::List), Literal(literal::Literal), Oblique(oblique::Oblique), Paragraph(paragraph::Paragraph), @@ -29,7 +33,9 @@ impl Token { Token::Anchor(ref d) => d.render(), Token::Code(ref d) => d.render(), Token::Header(ref d) => d.render(), + Token::Item(ref d) => d.render(), Token::LineBreak(ref d) => d.render(), + Token::List(ref d) => d.render(), Token::Literal(ref d) => d.render(), Token::Oblique(ref d) => d.render(), Token::Paragraph(ref d) => d.render(), @@ -45,7 +51,9 @@ impl std::fmt::Display for Token { Token::Anchor(ref d) => format!("{d}"), Token::Code(ref d) => format!("{d}"), Token::Header(ref d) => format!("{d}"), + Token::Item(ref d) => format!("{d}"), Token::LineBreak(ref d) => format!("{d}"), + Token::List(ref d) => format!("{d}"), Token::Literal(ref d) => format!("{d}"), Token::Oblique(ref d) => format!("{d}"), Token::Paragraph(ref d) => format!("{d}"), @@ -56,10 +64,3 @@ impl std::fmt::Display for Token { write!(f, "Tk:{data}") } } - -#[cfg(test)] -mod tests { - - #[test] - fn smoke() {} -} diff --git a/src/syntax/content/parser/token/item.rs b/src/syntax/content/parser/token/item.rs new file mode 100644 index 0000000..7d03bd6 --- /dev/null +++ b/src/syntax/content/parser/token/item.rs @@ -0,0 +1,41 @@ +use crate::syntax::content::{Parseable, Lexeme}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Item { + open: bool, +} + +impl Parseable for Item { + fn probe(lexeme: &Lexeme) -> bool { + (lexeme.match_as_char('-') || lexeme.match_as_char('+')) + && lexeme.match_next_as_char(' ') + } + + fn lex(_lexeme: &Lexeme) -> Item { + Item { open: false } + } + + fn render(&self) -> String { + if self.open { + String::from("
  • ") + } else { + String::from("
  • ") + } + } +} + +impl Item { + pub fn new(open: bool) -> Item { + Item { open } + } + + pub fn probe_end(lexeme: &Lexeme) -> bool { + lexeme.match_as_char('\n') + } +} + +impl std::fmt::Display for Item { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Item [{}]", if self.open { "open" } else { "closed" }) + } +} diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs new file mode 100644 index 0000000..513fa2d --- /dev/null +++ b/src/syntax/content/parser/token/list.rs @@ -0,0 +1,47 @@ +use crate::syntax::content::{Parseable, Lexeme}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct List { + open: bool, + ordered: bool, +} + +impl Parseable for List { + fn probe(lexeme: &Lexeme) -> bool { + (lexeme.match_as_char('-') || lexeme.match_as_char('+')) + && lexeme.match_next_as_char(' ') + } + + fn lex(_lexeme: &Lexeme) -> List { + panic!("Attempt to lex a List directly from a lexeme") + } + + fn render(&self) -> String { + + let bar = if self.open { "" } else { "/" }; + let tag = if self.ordered { "ol" } else { "ul" }; + + format!("<{bar}{tag}>") + } +} + +impl List { + pub fn new(open: bool, ordered: bool) -> List { + List { open, ordered } + } + + pub fn probe_end(lexeme: &Lexeme) -> bool { + lexeme.match_as_char('\n') + } +} + +impl std::fmt::Display for List { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "List [{} {}]", + if self.open { "open" } else { "closed" }, + if self.ordered { "ordered" } else { "unordered" }, + ) + } +}