diff --git a/Cargo.toml b/Cargo.toml index 1964100..bfd86ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yamd" -version = "0.12.1" +version = "0.13.1" edition = "2021" license = "MIT OR Apache-2.0" description = "Yet Another Markdown Document (flavor)" @@ -12,6 +12,6 @@ keywords = ["markdown", "parser"] pretty_assertions = "1.4.0" [dependencies] -serde = { version = "1.0.193", features = ["derive"] } -chrono = { version = "0.4.31", features = ["serde"] } -serde_yaml = "0.9.27" +serde = { version = "1.0.197", features = ["derive"] } +chrono = { version = "0.4.37", features = ["serde"] } +serde_yaml = "0.9.34" diff --git a/src/lib.rs b/src/lib.rs index 8e5b356..23f8dd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,7 +196,7 @@ //! use nodes::yamd::Yamd; -use toolkit::deserializer::Deserializer; +use toolkit::parser::Parse; pub mod nodes; mod toolkit; @@ -209,7 +209,7 @@ mod toolkit; /// let yamd = deserialize(input).unwrap(); /// ``` pub fn deserialize(input: &str) -> Option { - Yamd::deserialize(input) + Yamd::parse(input, 0, None).map(|(yamd, _)| yamd) } /// Serialize a Yamd struct into a string diff --git a/src/nodes/anchor.rs b/src/nodes/anchor.rs index 29fc3f2..255eac7 100644 --- a/src/nodes/anchor.rs +++ b/src/nodes/anchor.rs @@ -2,11 +2,7 @@ use std::fmt::{Display, Formatter}; use serde::Serialize; -use crate::{ - toolkit::context::Context, - toolkit::deserializer::Deserializer, - toolkit::{matcher::Matcher, node::Node}, -}; +use crate::toolkit::{context::Context, parser::Parse}; /// Representation of an anchor #[derive(Debug, PartialEq, Serialize, Clone)] @@ -30,18 +26,28 @@ impl Display for Anchor { } } -impl Node for Anchor { - fn len(&self) -> usize { - self.text.len() + self.url.len() + 4 - } -} - -impl Deserializer for Anchor { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(text) = matcher.get_match("[", "]", false) { - if let Some(url) = matcher.get_match("(", ")", false) { - return Some(Anchor::new(text.body, url.body)); +impl Parse for Anchor { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with('[') { + if let Some(middle) = input[current_position + 1..].find("](") { + let mut level = 1; + for (i, c) in input[current_position + middle + 3..].char_indices() { + if c == '(' { + level += 1; + } else if c == ')' { + level -= 1; + } + if level == 0 { + return Some(( + Anchor::new( + &input[current_position + 1..current_position + middle + 1], + &input[current_position + middle + 3 + ..current_position + middle + 3 + i], + ), + middle + 3 + i + 1, + )); + } + } } } None @@ -50,8 +56,9 @@ impl Deserializer for Anchor { #[cfg(test)] mod tests { + use crate::toolkit::parser::Parse; + use super::Anchor; - use crate::toolkit::{deserializer::Deserializer, node::Node}; use pretty_assertions::assert_eq; #[test] @@ -68,27 +75,30 @@ mod tests { } #[test] - fn deserialize() { - assert_eq!(Anchor::deserialize("[1](2)"), Some(Anchor::new("1", "2"))); - assert_eq!(Anchor::deserialize("[1"), None); - assert_eq!(Anchor::deserialize("[1](2"), None); + fn parse() { + assert_eq!( + Anchor::parse("[1](2)", 0, None), + Some((Anchor::new("1", "2"), 6)) + ); + assert_eq!(Anchor::parse("[1", 0, None), None); + assert_eq!(Anchor::parse("[1](2", 0, None), None); } #[test] fn deserilalze_with_parentesis_in_url() { assert_eq!( - Anchor::deserialize( - "[the Rope data structure](https://en.wikipedia.org/wiki/Rope_(data_structure))" + Anchor::parse( + "[the Rope data structure](https://en.wikipedia.org/wiki/Rope_(data_structure))", + 0, + None ), - Some(Anchor::new( - "the Rope data structure", - "https://en.wikipedia.org/wiki/Rope_(data_structure)" + Some(( + Anchor::new( + "the Rope data structure", + "https://en.wikipedia.org/wiki/Rope_(data_structure)" + ), + 78 )) ); } - - #[test] - fn len() { - assert_eq!(Anchor::new("a", "b").len(), 6); - } } diff --git a/src/nodes/bold.rs b/src/nodes/bold.rs index ef1d4b1..3936d58 100644 --- a/src/nodes/bold.rs +++ b/src/nodes/bold.rs @@ -3,93 +3,94 @@ use std::fmt::Display; use serde::Serialize; use crate::{ - nodes::italic::Italic, - nodes::strikethrough::Strikethrough, - nodes::text::Text, + nodes::{italic::Italic, strikethrough::Strikethrough, text::Text}, toolkit::{ context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, MaybeNode}, - matcher::Matcher, - node::Node, + parser::{parse_to_consumer, parse_to_parser, Branch, Consumer, Parse, Parser}, }, }; #[derive(Debug, PartialEq, Serialize, Clone)] #[serde(tag = "type")] pub enum BoldNodes { + Italic(Italic), + Strikethrough(Strikethrough), Text(Text), - I(Italic), - S(Strikethrough), -} - -impl From for BoldNodes { - fn from(value: Text) -> Self { - BoldNodes::Text(value) - } } impl From for BoldNodes { - fn from(value: Italic) -> Self { - BoldNodes::I(value) + fn from(i: Italic) -> Self { + BoldNodes::Italic(i) } } impl From for BoldNodes { - fn from(value: Strikethrough) -> Self { - BoldNodes::S(value) - } -} - -impl Display for BoldNodes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - BoldNodes::Text(node) => write!(f, "{}", node), - BoldNodes::I(node) => write!(f, "{}", node), - BoldNodes::S(node) => write!(f, "{}", node), - } + fn from(s: Strikethrough) -> Self { + BoldNodes::Strikethrough(s) } } -impl Node for BoldNodes { - fn len(&self) -> usize { - match self { - BoldNodes::Text(node) => node.len(), - BoldNodes::I(node) => node.len(), - BoldNodes::S(node) => node.len(), - } +impl From for BoldNodes { + fn from(t: Text) -> Self { + BoldNodes::Text(t) } } #[derive(Debug, PartialEq, Serialize, Clone, Default)] pub struct Bold { - pub nodes: Vec, + nodes: Vec, } -impl Bold { - pub fn new(nodes: Vec) -> Self { - Self { nodes } +impl Branch for Bold { + fn get_parsers(&self) -> Vec> { + vec![ + parse_to_parser::(), + parse_to_parser::(), + ] } -} -impl Branch for Bold { - fn push>(&mut self, element: BC) { - self.nodes.push(element.into()); + fn push_node(&mut self, node: BoldNodes) { + self.nodes.push(node); } - fn get_maybe_nodes() -> Vec> { - vec![Italic::maybe_node(), Strikethrough::maybe_node()] + fn get_consumer(&self) -> Option> { + Some(parse_to_consumer::()) } +} - fn get_fallback_node() -> Option> { - Some(Box::new(|str| Text::new(str).into())) +impl Parse for Bold { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with("**") { + if let Some(end) = input[current_position + 2..].find("**") { + let b = Bold::new(vec![]); + return Some(( + b.parse_branch( + &input[current_position + 2..current_position + 2 + end], + "", + None, + ) + .expect("bold should always succed"), + end + 4, + )); + } + } + None } +} - fn get_outer_token_length(&self) -> usize { - 4 +impl Bold { + pub fn new(nodes: Vec) -> Self { + Self { nodes } } +} - fn is_empty(&self) -> bool { - self.nodes.is_empty() +impl Display for BoldNodes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BoldNodes::Text(node) => write!(f, "{}", node), + BoldNodes::Italic(node) => write!(f, "{}", node), + BoldNodes::Strikethrough(node) => write!(f, "{}", node), + } } } @@ -107,40 +108,18 @@ impl Display for Bold { } } -impl Node for Bold { - fn len(&self) -> usize { - self.nodes.iter().map(|node| node.len()).sum::() + 4 - } -} - -impl Deserializer for Bold { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(bold) = matcher.get_match("**", "**", false) { - return Self::parse_branch(bold.body, "", Self::default()); - } - None - } -} - #[cfg(test)] mod tests { use crate::{ - nodes::bold::Bold, - nodes::italic::Italic, - nodes::strikethrough::Strikethrough, - nodes::text::Text, - toolkit::{ - deserializer::{Branch, Deserializer}, - node::Node, - }, + nodes::{bold::Bold, italic::Italic, strikethrough::Strikethrough, text::Text}, + toolkit::parser::{Branch, Parse}, }; use pretty_assertions::assert_eq; #[test] fn only_text() { let mut b = Bold::default(); - b.push(Text::new("B as bold")); + b.push_node(Text::new("B as bold").into()); let str = b.to_string(); assert_eq!(str, "**B as bold**".to_string()); } @@ -159,39 +138,21 @@ mod tests { #[test] fn from_string() { assert_eq!( - Bold::deserialize("**b**"), - Some(Bold::new(vec![Text::new("b").into()])) + Bold::parse("**b**", 0, None), + Some((Bold::new(vec![Text::new("b").into()]), 5)) ); assert_eq!( - Bold::deserialize("**b ~~st~~ _i t_**"), - Some(Bold::new(vec![ - Text::new("b ").into(), - Strikethrough::new("st").into(), - Text::new(" ").into(), - Italic::new("i t").into() - ])) - ); - } - - #[test] - fn len() { - assert_eq!(Bold::new(vec![Text::new("T").into()]).len(), 5); - assert_eq!( - Bold::new(vec![Text::new("T").into(), Strikethrough::new("S").into()]).len(), - 10 + Bold::parse("**b ~~st~~ _i t_**", 0, None), + Some(( + Bold::new(vec![ + Text::new("b ").into(), + Strikethrough::new("st").into(), + Text::new(" ").into(), + Italic::new("i t").into() + ]), + 18 + )) ); } - - #[test] - fn default() { - assert_eq!(Bold::default(), Bold::default()); - } - - #[test] - fn empty_bold() { - let b = Bold::new(vec![]); - assert_eq!(b.len(), 4); - assert_eq!(b.is_empty(), true); - } } diff --git a/src/nodes/code.rs b/src/nodes/code.rs index f82c24c..bff4e9d 100644 --- a/src/nodes/code.rs +++ b/src/nodes/code.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{context::Context, parser::Parse}; #[derive(Debug, PartialEq, Serialize, Clone)] pub struct Code { @@ -25,18 +25,23 @@ impl Display for Code { } } -impl Node for Code { - fn len(&self) -> usize { - self.lang.len() + self.code.len() + 8 - } -} - -impl Deserializer for Code { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(lang) = matcher.get_match("```", "\n", false) { - if let Some(code) = matcher.get_match("", "\n```", false) { - return Some(Self::new(lang.body, code.body)); +impl Parse for Code { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> + where + Self: Sized, + { + if input[current_position..].starts_with("```") { + if let Some(lang) = input[current_position + 3..].find('\n') { + if let Some(end) = input[current_position + 3 + lang + 1..].find("\n```") { + return Some(( + Code::new( + &input[current_position + 3..current_position + 3 + lang], + &input[current_position + 3 + lang + 1 + ..current_position + 3 + lang + 1 + end], + ), + 3 + lang + 1 + end + 4, + )); + } } } None @@ -45,10 +50,7 @@ impl Deserializer for Code { #[cfg(test)] mod tests { - use crate::{ - nodes::code::Code, - toolkit::{deserializer::Deserializer, node::Node}, - }; + use crate::{nodes::code::Code, toolkit::parser::Parse}; use pretty_assertions::assert_eq; #[test] @@ -60,20 +62,13 @@ mod tests { } #[test] - fn len() { - assert_eq!(Code::new('r', 'b').len(), 10); - } - - #[test] - fn deserializer() { - assert_eq!( - Code::deserialize("```rust\nlet a=1;\n```"), - Some(Code::new("rust", "let a=1;")) - ); + fn parser() { assert_eq!( - Code::deserialize("```rust\nlet a=1;\n```\n\n"), - Some(Code::new("rust", "let a=1;")) + Code::parse("```rust\nlet a=1;\n```", 0, None), + Some((Code::new("rust", "let a=1;"), 20)) ); - assert_eq!(Code::deserialize("```rust\nlet a=1;\n"), None); + assert_eq!(Code::parse("```rust\nlet a=1;\n", 0, None), None); + assert_eq!(Code::parse("not a code block", 0, None), None); + assert_eq!(Code::parse("``````", 0, None), None); } } diff --git a/src/nodes/collapsible.rs b/src/nodes/collapsible.rs index da7ccc9..4217ba9 100644 --- a/src/nodes/collapsible.rs +++ b/src/nodes/collapsible.rs @@ -4,9 +4,7 @@ use serde::Serialize; use crate::toolkit::{ context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, FallbackNode, MaybeNode}, - matcher::Matcher, - node::Node, + parser::{parse_to_consumer, parse_to_parser, Branch, Consumer, Parse, Parser}, }; use super::{ @@ -44,22 +42,6 @@ impl Display for CollapsibleNodes { } } -impl Node for CollapsibleNodes { - fn len(&self) -> usize { - match self { - CollapsibleNodes::P(node) => node.len(), - CollapsibleNodes::H(node) => node.len(), - CollapsibleNodes::Image(node) => node.len(), - CollapsibleNodes::ImageGallery(node) => node.len(), - CollapsibleNodes::List(node) => node.len(), - CollapsibleNodes::Embed(node) => node.len(), - CollapsibleNodes::Divider(node) => node.len(), - CollapsibleNodes::Code(node) => node.len(), - CollapsibleNodes::Collapsible(node) => node.len(), - } - } -} - impl From for CollapsibleNodes { fn from(value: Paragraph) -> Self { Self::P(value) @@ -145,61 +127,58 @@ impl Display for Collapsible { } } -impl Node for Collapsible { - fn len(&self) -> usize { - let delimeter_len = if self.is_empty() { - 0 - } else { - (self.nodes.len() - 1) * 2 - }; - self.nodes.iter().map(|node| node.len()).sum::() - + delimeter_len - + self.get_outer_token_length() - } -} - impl Branch for Collapsible { - fn push>(&mut self, node: CanBeNode) { - self.nodes.push(node.into()); - } - - fn get_maybe_nodes() -> Vec> { + fn get_parsers(&self) -> Vec> { vec![ - Heading::maybe_node(), - Image::maybe_node(), - ImageGallery::maybe_node(), - List::maybe_node(), - Embed::maybe_node(), - Divider::maybe_node(), - Code::maybe_node(), - Collapsible::maybe_node(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), ] } - fn get_fallback_node() -> Option> { - Some(Paragraph::fallback_node()) - } - - fn get_outer_token_length(&self) -> usize { - 7 + self.title.len() + fn get_consumer(&self) -> Option> { + Some(parse_to_consumer::()) } - fn is_empty(&self) -> bool { - self.nodes.is_empty() + fn push_node(&mut self, node: CollapsibleNodes) { + self.nodes.push(node); } } -impl Deserializer for Collapsible { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(tab) = matcher.get_match("{% ", "\n%}", false) { - let mut inner_matcher = Matcher::new(tab.body); - if let Some(title) = inner_matcher.get_match("", "\n", false) { - return Self::parse_branch( - inner_matcher.get_rest(), - "\n\n", - Self::new(title.body, vec![]), - ); +impl Parse for Collapsible { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with("{% ") { + let start = current_position + 3; + if let Some(end_of_title) = input[start..].find('\n') { + let title = &input[start..start + end_of_title]; + let mut level = 1; + for (index, _) in input[start + end_of_title..].char_indices() { + if input[index + start + end_of_title + 1..].starts_with("{% ") { + level += 1; + } else if input[index + start + end_of_title + 1..].starts_with("\n%}") { + level -= 1; + } + if level == 0 { + let colapsible = Collapsible::new(title, vec![]); + + return Some(( + colapsible + .parse_branch( + &input[start + end_of_title + 1 + ..start + end_of_title + 1 + index], + "\n\n", + None, + ) + .expect("collapsible branch should always succeed"), + 3 + end_of_title + 1 + index + 3, + )); + } + } } } None @@ -207,46 +186,41 @@ impl Deserializer for Collapsible { } #[cfg(test)] -mod cfg { +mod tests { use pretty_assertions::assert_eq; use crate::{ nodes::{ - bold::Bold, code::Code, collapsible::Collapsible, divider::Divider, embed::Embed, - heading::Heading, image::Image, image_gallery::ImageGallery, list::List, - list::ListTypes::*, list_item::ListItem, list_item_content::ListItemContent, - paragraph::Paragraph, text::Text, - }, - toolkit::{ - deserializer::{Branch, Deserializer}, - node::Node, + bold::Bold, + code::Code, + collapsible::Collapsible, + divider::Divider, + embed::Embed, + heading::Heading, + image::Image, + image_gallery::ImageGallery, + list::{List, ListTypes::*}, + list_item::ListItem, + paragraph::Paragraph, + text::Text, }, + toolkit::parser::Parse, }; #[test] - fn test_collapsible_deserialize() { + fn test_collapsible_parse() { assert_eq!( - Collapsible::deserialize("{% Title\n# Heading\n%}"), - Some(Collapsible::new( - "Title", - vec![Heading::new(1, vec![Text::new("Heading").into()]).into()] + Collapsible::parse("{% Title\n# Heading\n%}", 0, None), + Some(( + Collapsible::new( + "Title", + vec![Heading::new(1, vec![Text::new("Heading").into()]).into()] + ), + 21 )) ); } - #[test] - fn test_collapsible_len() { - assert_eq!( - Collapsible::new( - "Title", - vec![Heading::new(1, vec![Text::new("Heading").into()]).into()] - ) - .len(), - 21 - ); - assert_eq!(Collapsible::new("Title", vec![]).len(), 12); - } - #[test] fn test_collapsible_serialize() { assert_eq!( @@ -260,9 +234,12 @@ mod cfg { } #[test] - fn fail_to_deseiralize_collapsible() { - assert_eq!(Collapsible::deserialize("I am not an accordion tab"), None); - assert_eq!(Collapsible::deserialize("{% \n%}"), None); + fn fail_to_parse_collapsible() { + assert_eq!( + Collapsible::parse("I am not an accordion tab", 0, None), + None + ); + assert_eq!(Collapsible::parse("{% \n%}", 0, None), None); } #[test] @@ -316,17 +293,18 @@ t**b** List::new( Unordered, 0, - vec![ListItem::new_with_nested_list( + vec![ListItem::new( Unordered, 0, - ListItemContent::new(vec![Text::new("one").into()]), + Paragraph::new(vec![Text::new("one").into()]), Some(List::new( Unordered, 1, vec![ListItem::new( Unordered, 1, - ListItemContent::new(vec![Text::new("two").into()]), + Paragraph::new(vec![Text::new("two").into()]), + None, ) .into()], )), @@ -344,13 +322,15 @@ t**b** ], ); assert_eq!(tab.to_string(), input); - assert_eq!(Collapsible::deserialize(input), Some(tab)); + assert_eq!(Collapsible::parse(input, 0, None), Some((tab, input.len()))); } #[test] - fn empty_tab() { - let tab = Collapsible::new::<&str>("", vec![]); - assert_eq!(tab.len(), 7); - assert_eq!(tab.is_empty(), true); + fn parse_empty() { + let input = "{% Title\n\n%}"; + assert_eq!( + Collapsible::parse(input, 0, None), + Some((Collapsible::new("Title", vec![]), input.len())) + ); } } diff --git a/src/nodes/divider.rs b/src/nodes/divider.rs index 8155de0..1ff5b6a 100644 --- a/src/nodes/divider.rs +++ b/src/nodes/divider.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{context::Context, parser::Parse}; #[derive(Debug, PartialEq, Serialize, Clone, Default)] pub struct Divider {} @@ -19,39 +19,26 @@ impl Display for Divider { } } -impl Node for Divider { - fn len(&self) -> usize { - 5 - } -} - -impl Deserializer for Divider { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if matcher.get_match("-----", "\n\n", true).is_some() { - return Some(Divider {}); +impl Parse for Divider { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> + where + Self: Sized, + { + if input[current_position..].starts_with("-----") { + Some((Divider::new(), 5)) + } else { + None } - None } } - #[cfg(test)] mod tests { - use crate::{ - nodes::divider::Divider, - toolkit::{deserializer::Deserializer, node::Node}, - }; + use crate::{nodes::divider::Divider, toolkit::parser::Parse}; use pretty_assertions::assert_eq; #[test] - fn deserialize() { - assert_eq!(Divider::deserialize("-----"), Some(Divider {})); - assert_eq!(Divider::deserialize("----\n\n"), None); - } - - #[test] - fn len() { - assert_eq!(Divider::new().len(), 5); + fn parse() { + assert_eq!(Divider::parse("-----", 0, None), Some((Divider {}, 5))); } #[test] diff --git a/src/nodes/embed.rs b/src/nodes/embed.rs index 1a5dd8f..f738415 100644 --- a/src/nodes/embed.rs +++ b/src/nodes/embed.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{context::Context, parser::Parse}; #[derive(Debug, PartialEq, Serialize, Clone)] pub struct Embed { @@ -25,21 +25,23 @@ impl Display for Embed { } } -impl Node for Embed { - fn len(&self) -> usize { - 5 + self.kind.len() + self.args.len() - } -} - -impl Deserializer for Embed { - fn deserialize_with_context( - input: &str, - _: Option, - ) -> Option { - let mut matcher = Matcher::new(input); - if let Some(embed) = matcher.get_match("{{", "}}", false) { - if let Some((kind, args)) = embed.body.split_once('|') { - return Some(Self::new(kind.to_string(), args.to_string())); +impl Parse for Embed { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> + where + Self: Sized, + { + if input[current_position..].starts_with("{{") { + if let Some(middle) = input[current_position + 2..].find('|') { + if let Some(end) = input[current_position + 2 + middle..].find("}}") { + return Some(( + Embed::new( + &input[current_position + 2..current_position + 2 + middle], + &input[current_position + 2 + middle + 1 + ..current_position + 2 + middle + end], + ), + 2 + middle + end + 2, + )); + } } } None @@ -48,10 +50,7 @@ impl Deserializer for Embed { #[cfg(test)] mod tests { - use crate::{ - nodes::embed::Embed, - toolkit::{deserializer::Deserializer, node::Node}, - }; + use crate::{nodes::embed::Embed, toolkit::parser::Parse}; #[test] fn serializer() { @@ -62,26 +61,23 @@ mod tests { } #[test] - fn len() { - assert_eq!(Embed::new("y", "h",).len(), 7); - } - - #[test] - fn deserialize() { + fn parse() { assert_eq!( - Embed::deserialize_with_context( + Embed::parse( "{{youtube|https://www.youtube.com/embed/wsfdjlkjsdf}}", + 0, None ), - Some(Embed::new( - "youtube", - "https://www.youtube.com/embed/wsfdjlkjsdf", + Some(( + Embed::new("youtube", "https://www.youtube.com/embed/wsfdjlkjsdf",), + 53 )) ); } #[test] - fn failed_deserialize() { - assert_eq!(Embed::deserialize_with_context("{{youtube}}", None), None); + fn failed_parse() { + assert_eq!(Embed::parse("{{youtube}}", 0, None), None); + assert_eq!(Embed::parse("{{youtube|", 0, None), None); } } diff --git a/src/nodes/heading.rs b/src/nodes/heading.rs index 60bb546..a7e748f 100644 --- a/src/nodes/heading.rs +++ b/src/nodes/heading.rs @@ -4,9 +4,7 @@ use serde::Serialize; use crate::toolkit::{ context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, FallbackNode, MaybeNode}, - matcher::Matcher, - node::Node, + parser::{parse_to_consumer, parse_to_parser, Branch, Consumer, Parse, Parser}, }; use super::{anchor::Anchor, text::Text}; @@ -30,15 +28,6 @@ impl From for HeadingNodes { } } -impl Node for HeadingNodes { - fn len(&self) -> usize { - match self { - Self::Text(text) => text.len(), - Self::A(anchor) => anchor.len(), - } - } -} - impl Display for HeadingNodes { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -68,18 +57,42 @@ impl Heading { } } -impl Deserializer for Heading { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let start_tokens = ["###### ", "##### ", "#### ", "### ", "## ", "# "]; - - for (i, start_token) in start_tokens.iter().enumerate() { - let mut matcher = Matcher::new(input); - if let Some(heading) = matcher.get_match(start_token, "\n\n", true) { - return Self::parse_branch( - heading.body, - "", - Self::new((start_tokens.len() - i).try_into().unwrap_or(1), vec![]), - ); +impl Branch for Heading { + fn push_node(&mut self, node: HeadingNodes) { + self.nodes.push(node); + } + + fn get_parsers(&self) -> Vec> { + vec![parse_to_parser::()] + } + + fn get_consumer(&self) -> Option> { + Some(parse_to_consumer::()) + } +} + +impl Parse for Heading { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + let start_tokens = ["# ", "## ", "### ", "#### ", "##### ", "###### "]; + + for start_token in start_tokens.iter() { + if input[current_position..].starts_with(start_token) { + let end = input[current_position + start_token.len()..] + .find("\n\n") + .unwrap_or(input[current_position + start_token.len()..].len()); + let heading = Heading::new((start_token.len() - 1).try_into().unwrap_or(1), vec![]); + + return Some(( + heading + .parse_branch( + &input[current_position + start_token.len() + ..current_position + start_token.len() + end], + "", + None, + ) + .expect("heading should always succeed"), + start_token.len() + end, + )); } } @@ -99,40 +112,12 @@ impl Display for Heading { } } -impl Node for Heading { - fn len(&self) -> usize { - self.nodes.iter().map(|n| n.len()).sum::() + self.get_outer_token_length() - } -} - -impl Branch for Heading { - fn push>(&mut self, node: I) { - self.nodes.push(node.into()); - } - - fn get_maybe_nodes() -> Vec> { - vec![Anchor::maybe_node()] - } - - fn get_fallback_node() -> Option> { - Some(Text::fallback_node()) - } - - fn get_outer_token_length(&self) -> usize { - self.level as usize + 1 - } - - fn is_empty(&self) -> bool { - self.nodes.is_empty() - } -} - #[cfg(test)] mod tests { use super::Heading; use crate::{ nodes::{anchor::Anchor, text::Text}, - toolkit::{deserializer::Deserializer, node::Node}, + toolkit::parser::Parse, }; use pretty_assertions::assert_eq; @@ -167,40 +152,32 @@ mod tests { #[test] fn from_string() { assert_eq!( - Heading::deserialize("## Header"), - Some(Heading::new(2, vec![Text::new("Header").into()])) - ); - assert_eq!( - Heading::deserialize("### Head"), - Some(Heading::new(3, vec![Text::new("Head").into()])) + Heading::parse("## Header", 0, None), + Some((Heading::new(2, vec![Text::new("Header").into()]), 9)) ); assert_eq!( - Heading::deserialize("### Head\n\nsome other thing"), - Some(Heading::new(3, vec![Text::new("Head").into()])) + Heading::parse("### Head", 0, None), + Some((Heading::new(3, vec![Text::new("Head").into()]), 8)) ); - assert_eq!(Heading::deserialize("not a header"), None); - assert_eq!(Heading::deserialize("######"), None); - assert_eq!(Heading::deserialize("######also not a header"), None); - } - - #[test] - fn len() { - assert_eq!(Heading::new(1, vec![Text::new("h").into()]).len(), 3); - assert_eq!(Heading::new(2, vec![Text::new("h").into()]).len(), 4); + assert_eq!(Heading::parse("not a header", 0, None), None); + assert_eq!(Heading::parse("######", 0, None), None); + assert_eq!(Heading::parse("######also not a header", 0, None), None); } #[test] fn with_anchor() { let str = "## hey [a](b)"; - let h = Heading::deserialize(str); + let h = Heading::parse(str, 0, None); assert_eq!( h, - Some(Heading::new( - 2, - vec![Text::new("hey ").into(), Anchor::new("a", "b").into()] + Some(( + Heading::new( + 2, + vec![Text::new("hey ").into(), Anchor::new("a", "b").into()] + ), + 13 )) ); - assert_eq!(h.as_ref().unwrap().len(), 13); - assert_eq!(h.as_ref().unwrap().to_string(), str); + assert_eq!(h.map(|(node, _)| node.to_string()).unwrap(), str); } } diff --git a/src/nodes/highlight.rs b/src/nodes/highlight.rs index 6aec1da..a632ece 100644 --- a/src/nodes/highlight.rs +++ b/src/nodes/highlight.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{context::Context, parser::Parse}; use super::paragraph::Paragraph; @@ -51,44 +51,44 @@ impl Display for Highlight { } } -impl Node for Highlight { - fn len(&self) -> usize { - let delimiter_length = if self.nodes.is_empty() { - 0 - } else { - (self.nodes.len() - 1) * 2 - }; - self.nodes.iter().map(|node| node.len()).sum::() - + delimiter_length - + 8 - + self.title.as_ref().map_or(0, |title| title.len() + 4) - + self.icon.as_ref().map_or(0, |icon| icon.len() + 3) - } -} - -impl Deserializer for Highlight { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut outer_matcher = Matcher::new(input); - if let Some(highlight) = outer_matcher.get_match(">>>\n", "\n>>>", false) { - let mut matcher = Matcher::new(highlight.body); - let title = matcher - .get_match(">> ", "\n", false) - .map(|title| title.body); - - let icon = matcher.get_match("> ", "\n", false).map(|icon| icon.body); - return Some(Self::new( - title, - icon, - matcher - .get_rest() +impl Parse for Highlight { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> + where + Self: Sized, + { + if input[current_position..].starts_with(">>>\n") { + if let Some(end) = input[current_position + 4..].find("\n>>>") { + let mut start = current_position + 4; + let mut title = None; + let mut icon = None; + if input[start..current_position + 4 + end].starts_with(">> ") { + start += 3; + if let Some(local_end) = input[start..current_position + 4 + end].find('\n') { + title = Some(input[start..start + local_end].to_string()); + start += local_end + 1; + } + } + if input[start..current_position + 4 + end].starts_with("> ") { + start += 2; + if let Some(local_end) = input[start..current_position + 4 + end].find('\n') { + icon = Some(input[start..start + local_end].to_string()); + start += local_end + 1; + } + } + let mut nodes = vec![]; + input[start..current_position + 4 + end] .split("\n\n") - .map(|paragraph| { - Paragraph::deserialize(paragraph).expect("Paragraph always deserializes") - }) - .collect::>(), - )); + .for_each(|node| { + let (node, _) = Paragraph::parse(node, 0, None) + .expect("Paragraph should never fail to parse"); + nodes.push(node); + }); + return Some(( + Highlight::new(title, icon, nodes), + input[current_position..current_position + 4 + end + 4].len(), + )); + } } - None } } @@ -97,37 +97,10 @@ impl Deserializer for Highlight { mod tests { use crate::{ nodes::{highlight::Highlight, paragraph::Paragraph, text::Text}, - toolkit::{deserializer::Deserializer, node::Node}, + toolkit::parser::Parse, }; use pretty_assertions::assert_eq; - #[test] - fn len() { - assert_eq!( - Highlight::new( - Some("h"), - Some("i"), - vec![ - Paragraph::new(vec![Text::new("t").into()]).into(), - Paragraph::new(vec![Text::new("t").into()]).into() - ] - ) - .len(), - 21 - ); - assert_eq!( - Highlight::new::( - None, - None, - vec![ - Paragraph::new(vec![Text::new("t").into()]).into(), - Paragraph::new(vec![Text::new("t").into()]).into() - ] - ) - .len(), - 12 - ); - } #[test] fn serialize() { assert_eq!( @@ -135,8 +108,8 @@ mod tests { Some("h"), Some("i"), vec![ - Paragraph::new(vec![Text::new("t").into()]).into(), - Paragraph::new(vec![Text::new("t").into()]).into() + Paragraph::new(vec![Text::new("t").into()]), + Paragraph::new(vec![Text::new("t").into()]) ] ) .to_string(), @@ -147,8 +120,8 @@ mod tests { None, None, vec![ - Paragraph::new(vec![Text::new("t").into()]).into(), - Paragraph::new(vec![Text::new("t").into()]).into() + Paragraph::new(vec![Text::new("t").into()]), + Paragraph::new(vec![Text::new("t").into()]) ] ) .to_string(), @@ -157,36 +130,27 @@ mod tests { } #[test] - fn deserialize() { + fn parse() { assert_eq!( - Highlight::deserialize(">>>\n>> h\n> i\nt\n\nt\n>>>"), - Some(Highlight::new( - Some("h"), - Some("i"), - vec![ - Paragraph::new(vec![Text::new("t").into()]).into(), - Paragraph::new(vec![Text::new("t").into()]).into() - ] + Highlight::parse(">>>\n>> h\n> i\nt\n\nt\n>>>", 0, None), + Some(( + Highlight::new( + Some("h"), + Some("i"), + vec![ + Paragraph::new(vec![Text::new("t").into()]), + Paragraph::new(vec![Text::new("t").into()]) + ] + ), + 21 )) ); - - assert_eq!( - Highlight::deserialize(">>>\n>> h\n> i\nt\n\nt2\n>>>\n\n"), - Some(Highlight::new( - Some("h"), - Some("i"), - vec![ - Paragraph::new(vec![Text::new("t").into()]).into(), - Paragraph::new(vec![Text::new("t2").into()]).into() - ] - )) - ) } #[test] fn empty_highlight() { let highlight = Highlight::new::(None, None, vec![]); - assert_eq!(highlight.len(), 8); + assert_eq!(highlight.to_string(), ">>>\n\n>>>"); } #[test] @@ -198,18 +162,20 @@ test test2 >>>"; - let highlight = Highlight::deserialize(input).unwrap(); - assert_eq!(highlight.len(), input.len()); + let highlight = Highlight::parse(input, 0, None).unwrap(); assert_eq!( highlight, - Highlight::new::<&str, &str>( - None, - None, - vec![ - Paragraph::new(vec![]).into(), - Paragraph::new(vec![Text::new("test").into()]).into(), - Paragraph::new(vec![Text::new("test2").into()]).into(), - ] + ( + Highlight::new::<&str, &str>( + None, + None, + vec![ + Paragraph::new(vec![]).into(), + Paragraph::new(vec![Text::new("test").into()]).into(), + Paragraph::new(vec![Text::new("test2").into()]).into(), + ] + ), + 21 ) ); } diff --git a/src/nodes/image.rs b/src/nodes/image.rs index 1a31fdd..0167f58 100644 --- a/src/nodes/image.rs +++ b/src/nodes/image.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{context::Context, parser::Parse}; #[derive(Debug, PartialEq, Serialize, Clone)] pub struct Image { @@ -25,18 +25,28 @@ impl Display for Image { } } -impl Node for Image { - fn len(&self) -> usize { - self.alt.len() + self.src.len() + 5 - } -} - -impl Deserializer for Image { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(alt) = matcher.get_match("![", "]", false) { - if let Some(url) = matcher.get_match("(", ")", false) { - return Some(Self::new(alt.body, url.body)); +impl Parse for Image { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with("![") { + if let Some(middle) = input[current_position + 2..].find("](") { + let mut level = 1; + for (i, c) in input[current_position + 2 + middle + 2..].char_indices() { + if c == '(' { + level += 1; + } else if c == ')' { + level -= 1; + } + if level == 0 { + return Some(( + Image::new( + &input[current_position + 2..current_position + 2 + middle], + &input[current_position + 2 + middle + 2 + ..current_position + 2 + middle + 2 + i], + ), + 2 + middle + 2 + i + 1, + )); + } + } } } None @@ -45,8 +55,9 @@ impl Deserializer for Image { #[cfg(test)] mod tests { + use crate::toolkit::parser::Parse; + use super::Image; - use crate::toolkit::{deserializer::Deserializer, node::Node}; use pretty_assertions::assert_eq; #[test] @@ -55,17 +66,25 @@ mod tests { } #[test] - fn len() { - assert_eq!(Image::new('a', 'u').len(), 7); + fn parser() { + assert_eq!( + Image::parse("![alt](url)", 0, None), + Some((Image::new("alt", "url"), 11)) + ); + assert_eq!(Image::parse("![alt](url", 0, None), None); + assert_eq!(Image::parse("[alt](url)", 0, None), None); + assert_eq!(Image::parse("![alt]", 0, None), None); } #[test] - fn deserializer() { + fn nested() { + let input = "![hello [there]](url with (parenthesis))"; assert_eq!( - Image::deserialize("![alt](url)"), - Some(Image::new("alt", "url")) - ); - assert_eq!(Image::deserialize("![alt](url"), None); - assert_eq!(Image::deserialize("[alt](url)"), None); + Image::parse("![hello [there]](url with (parenthesis))", 0, None), + Some(( + Image::new("hello [there]", "url with (parenthesis)"), + input.len() + )) + ) } } diff --git a/src/nodes/image_gallery.rs b/src/nodes/image_gallery.rs index 5c27052..d715bdc 100644 --- a/src/nodes/image_gallery.rs +++ b/src/nodes/image_gallery.rs @@ -4,9 +4,7 @@ use serde::Serialize; use crate::toolkit::{ context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, MaybeNode}, - matcher::Matcher, - node::Node, + parser::{parse_to_parser, Branch, Consumer, Parse, Parser}, }; use super::image::Image; @@ -25,14 +23,6 @@ impl Display for ImageGalleryNodes { } } -impl Node for ImageGalleryNodes { - fn len(&self) -> usize { - match self { - ImageGalleryNodes::Image(node) => node.len(), - } - } -} - impl From for ImageGalleryNodes { fn from(value: Image) -> Self { ImageGalleryNodes::Image(value) @@ -72,61 +62,45 @@ impl Display for ImageGallery { } } -impl Node for ImageGallery { - fn len(&self) -> usize { - let delimiter_len = if self.is_empty() { - 0 - } else { - self.nodes.len() - 1 - }; - self.nodes.iter().map(|node| node.len()).sum::() - + delimiter_len - + self.get_outer_token_length() - } -} - -impl Deserializer for ImageGallery { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(image_gallery) = matcher.get_match("!!!\n", "\n!!!", false) { - return Self::parse_branch(image_gallery.body, "\n", Self::default()); - } - None - } -} - impl Branch for ImageGallery { - fn push>(&mut self, node: CanBeNode) { - self.nodes.push(node.into()) - } - - fn get_maybe_nodes() -> Vec> { - vec![Image::maybe_node()] + fn get_parsers(&self) -> Vec> { + vec![parse_to_parser::()] } - fn get_fallback_node() -> Option> { + fn get_consumer(&self) -> Option> { None } - fn get_outer_token_length(&self) -> usize { - 8 + fn push_node(&mut self, node: ImageGalleryNodes) { + self.nodes.push(node); } +} - fn is_empty(&self) -> bool { - self.nodes.is_empty() +impl Parse for ImageGallery { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> + where + Self: Sized, + { + if input[current_position..].starts_with("!!!\n") { + if let Some(end) = input[current_position + 4..].find("\n!!!") { + let gallery = ImageGallery::new(vec![]); + if let Some(node) = gallery.parse_branch( + &input[current_position + 4..current_position + 4 + end], + "\n", + None, + ) { + return Some((node, 4 + end + 4)); + } + } + } + None } } #[cfg(test)] mod tests { use super::ImageGallery; - use crate::{ - nodes::image::Image, - toolkit::{ - deserializer::{Branch, Deserializer}, - node::Node, - }, - }; + use crate::{nodes::image::Image, toolkit::parser::Parse}; use pretty_assertions::assert_eq; #[test] @@ -150,39 +124,35 @@ mod tests { } #[test] - fn len() { + fn parse() { assert_eq!( - ImageGallery::new(vec![ - Image::new("a", "u").into(), - Image::new("a2", "u2").into() - ],) - .len(), - 25 + ImageGallery::parse("!!!\n![a](u)\n![a2](u2)\n!!!", 0, None), + Some(( + ImageGallery::new(vec![ + Image::new("a", "u").into(), + Image::new("a2", "u2").into() + ]), + 25 + )) ); } #[test] - fn deserialize() { + fn default() { + assert_eq!(ImageGallery::default().to_string(), "!!!\n\n!!!"); + } + + #[test] + fn fail_parse() { + assert_eq!(ImageGallery::parse("not a gallery", 0, None), None); assert_eq!( - ImageGallery::deserialize("!!!\n![a](u)\n![a2](u2)\n!!!"), - Some(ImageGallery::new(vec![ - Image::new("a", "u").into(), - Image::new("a2", "u2").into() - ],)) + ImageGallery::parse("!!!\n![a](u)\n![a2](u2)!!!", 0, None), + None ); assert_eq!( - ImageGallery::deserialize("!!!\n![a](u)\n![a2](u2)\n!!!\n\n"), - Some(ImageGallery::new(vec![ - Image::new("a", "u").into(), - Image::new("a2", "u2").into() - ],)) + ImageGallery::parse("!!!\n![a](u)\n![a2](u2)\n", 0, None), + None ); - } - - #[test] - fn empty_gallery() { - let gal = ImageGallery::new(vec![]); - assert_eq!(gal.len(), 8); - assert_eq!(gal.is_empty(), true); + assert_eq!(ImageGallery::parse("!!!\nrandom\n!!!", 0, None), None); } } diff --git a/src/nodes/inline_code.rs b/src/nodes/inline_code.rs index 2c8a0a8..fb25046 100644 --- a/src/nodes/inline_code.rs +++ b/src/nodes/inline_code.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; +use crate::toolkit::{context::Context, parser::Parse}; #[derive(Debug, PartialEq, Serialize, Clone)] pub struct InlineCode { @@ -21,17 +21,15 @@ impl Display for InlineCode { } } -impl Node for InlineCode { - fn len(&self) -> usize { - self.text.len() + 2 - } -} - -impl Deserializer for InlineCode { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(inline_code) = matcher.get_match("`", "`", false) { - return Some(InlineCode::new(inline_code.body)); +impl Parse for InlineCode { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with('`') { + if let Some(end) = input[current_position + 1..].find('`') { + return Some(( + InlineCode::new(&input[current_position + 1..current_position + end + 1]), + end + 2, + )); + } } None } @@ -39,8 +37,9 @@ impl Deserializer for InlineCode { #[cfg(test)] mod tests { + use crate::toolkit::parser::Parse; + use super::InlineCode; - use crate::toolkit::deserializer::Deserializer; use pretty_assertions::assert_eq; #[test] @@ -51,11 +50,14 @@ mod tests { #[test] fn from_string() { - assert_eq!(InlineCode::deserialize("`1`"), Some(InlineCode::new('1'))); assert_eq!( - InlineCode::deserialize("`const \nfoo='bar'`"), - Some(InlineCode::new("const \nfoo='bar'")) + InlineCode::parse("`1`", 0, None), + Some((InlineCode::new('1'), 3)) + ); + assert_eq!( + InlineCode::parse("`const \nfoo='bar'`", 0, None), + Some((InlineCode::new("const \nfoo='bar'"), 18)) ); - assert_eq!(InlineCode::deserialize("`a"), None); + assert_eq!(InlineCode::parse("`a", 0, None), None); } } diff --git a/src/nodes/italic.rs b/src/nodes/italic.rs index 8ec0232..ee2ab60 100644 --- a/src/nodes/italic.rs +++ b/src/nodes/italic.rs @@ -2,50 +2,44 @@ use std::fmt::{Display, Formatter}; use serde::Serialize; -use crate::{ - toolkit::{context::Context, deserializer::Deserializer}, - toolkit::{matcher::Matcher, node::Node}, -}; +use crate::toolkit::{context::Context, parser::Parse}; /// Representation of an Italic text #[derive(Debug, PartialEq, Serialize, Clone)] pub struct Italic { - pub text: String, + text: String, } impl Italic { - pub fn new>(text: S) -> Self { + pub fn new>(text: IS) -> Self { Italic { text: text.into() } } } -impl Display for Italic { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "_{}_", self.text) - } -} - -impl Node for Italic { - fn len(&self) -> usize { - self.text.len() + 2 +impl Parse for Italic { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with('_') { + if let Some(end) = input[current_position + 1..].find('_') { + return Some(( + Italic::new(&input[current_position + 1..current_position + 1 + end]), + end + 2, + )); + } + } + None } } -impl Deserializer for Italic { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(italic) = matcher.get_match("_", "_", false) { - return Some(Italic::new(italic.body)); - } - - None +impl Display for Italic { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "_{}_", self.text) } } #[cfg(test)] mod tests { use super::Italic; - use crate::toolkit::{deserializer::Deserializer, node::Node}; + use crate::toolkit::parser::Parse; use pretty_assertions::assert_eq; #[test] @@ -62,25 +56,24 @@ mod tests { #[test] fn from_string() { - assert_eq!(Italic::deserialize("_italic_"), Some(Italic::new("italic"))); assert_eq!( - Italic::deserialize("_italic_not"), - Some(Italic::new("italic")) + Italic::parse("_italic_", 0, None), + Some((Italic::new("italic"), 8)) ); assert_eq!( - Italic::deserialize("_it alic_not"), - Some(Italic::new("it alic")) + Italic::parse("_italic_not", 0, None), + Some((Italic::new("italic"), 8)) ); - assert_eq!(Italic::deserialize("not italic_not"), None); - assert_eq!(Italic::deserialize("*italic not"), None); assert_eq!( - Italic::deserialize("_ita\nlic_"), - Some(Italic::new("ita\nlic")) + Italic::parse("_it alic_not", 0, None), + Some((Italic::new("it alic"), 9)) ); - } - - #[test] - fn len() { - assert_eq!(Italic::new("i").len(), 3); + assert_eq!(Italic::parse("not italic_not", 0, None), None); + assert_eq!(Italic::parse("*italic not", 0, None), None); + assert_eq!( + Italic::parse("_ita\nlic_", 0, None), + Some((Italic::new("ita\nlic"), 9)) + ); + assert_eq!(Italic::parse("_italic", 0, None), None); } } diff --git a/src/nodes/list.rs b/src/nodes/list.rs index 37cd926..f09bd73 100644 --- a/src/nodes/list.rs +++ b/src/nodes/list.rs @@ -1,15 +1,10 @@ -use std::fmt::Display; +use std::{fmt::Display, usize}; use serde::Serialize; -use crate::toolkit::{ - context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, MaybeNode}, - matcher::Matcher, - node::Node, -}; +use crate::toolkit::{context::Context, parser::Parse}; -use super::list_item::ListItem; +use super::{list_item::ListItem, paragraph::Paragraph}; #[derive(Debug, PartialEq, Clone, Serialize)] pub enum ListTypes { @@ -17,43 +12,15 @@ pub enum ListTypes { Ordered, } -#[derive(Debug, PartialEq, Serialize, Clone)] -#[serde(tag = "type")] -pub enum ListNodes { - ListItem(ListItem), -} - -impl Display for ListNodes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ListNodes::ListItem(node) => write!(f, "{}", node), - } - } -} - -impl Node for ListNodes { - fn len(&self) -> usize { - match self { - ListNodes::ListItem(node) => node.len(), - } - } -} - -impl From for ListNodes { - fn from(value: ListItem) -> Self { - ListNodes::ListItem(value) - } -} - #[derive(Debug, PartialEq, Serialize, Clone)] pub struct List { pub list_type: ListTypes, pub level: usize, - pub nodes: Vec, + pub nodes: Vec, } impl List { - pub fn new(list_type: ListTypes, level: usize, nodes: Vec) -> Self { + pub fn new(list_type: ListTypes, level: usize, nodes: Vec) -> Self { Self { list_type, level, @@ -61,26 +28,75 @@ impl List { } } - fn get_level_from_context(ctx: &Option) -> usize { - if let Some(actual_ctx) = ctx { - if let Some(level) = actual_ctx.get_usize_value("level") { - return level + 1; + fn get_text_slice_and_nested_list<'a>(&self, input: &'a str) -> (&'a str, Option) { + if let Some((left, right)) = input.split_once('\n') { + let mut ctx = Context::new(); + ctx.add("level", self.level + 1); + if let Some((list, consumed)) = List::parse(right, 0, Some(&ctx)) { + if consumed == right.len() { + return (left, Some(list)); + } } } - 0 + (input, None) } - pub fn create_context(level: usize, list_type: &ListTypes) -> Context { - let mut ctx = Context::new(); - ctx.add( - "list_type", - match list_type { + fn parse_list_items(&mut self, input: &str) -> usize { + let mut end = 2 + self.level; + while end < input.len() { + let list_type = match self.list_type { ListTypes::Unordered => '-', ListTypes::Ordered => '+', - }, - ); - ctx.add("level", level); - ctx + }; + let new_position = input[end..] + .find(format!("\n{}{} ", " ".repeat(self.level), list_type).as_str()) + .map_or(input.len(), |pos| pos + end); + + let (text_slice, nested_list) = + self.get_text_slice_and_nested_list(&input[end..new_position]); + + self.nodes.push(ListItem::new( + self.list_type.clone(), + self.level, + Paragraph::parse(text_slice, 0, None) + .map(|(paragraph, _)| paragraph) + .expect("paragraph should always succeed"), + nested_list, + )); + + end = if new_position == input.len() { + new_position + } else { + new_position + 3 + self.level + }; + } + end + } +} + +impl Parse for List { + fn parse(input: &str, current_position: usize, ctx: Option<&Context>) -> Option<(Self, usize)> { + let level = match ctx { + Some(ctx) => ctx.get_usize_value("level").unwrap_or(0), + None => 0, + }; + + if input[current_position..].starts_with(format!("{}- ", " ".repeat(level)).as_str()) { + let end = input[current_position..] + .find("\n\n") + .map_or(input.len(), |pos| pos + current_position); + let mut list = List::new(ListTypes::Unordered, level, vec![]); + let end = list.parse_list_items(&input[current_position..end]); + return Some((list, end)); + } + + if input[current_position..].starts_with(format!("{}+ ", " ".repeat(level)).as_str()) { + let mut list = List::new(ListTypes::Ordered, level, vec![]); + let end = list.parse_list_items(input); + return Some((list, end)); + } + + None } } @@ -98,74 +114,12 @@ impl Display for List { } } -impl Node for List { - fn len(&self) -> usize { - let add = if self.is_empty() { - 0 - } else { - self.nodes.len() - 1 - }; - self.nodes.iter().map(|node| node.len()).sum::() + add - } - fn context(&self) -> Option { - Some(Self::create_context(self.level, &self.list_type)) - } -} - -impl Deserializer for List { - fn deserialize_with_context(input: &str, ctx: Option) -> Option { - let level = Self::get_level_from_context(&ctx); - let mut matcher = Matcher::new(input); - if let Some(unordered_list) = - matcher.get_match(format!("{}- ", " ".repeat(level)).as_str(), "\n\n", true) - { - return Self::parse_branch( - &input[..unordered_list.start_token.len() + unordered_list.body.len()], - "\n", - Self::new(ListTypes::Unordered, level, vec![]), - ); - } else if let Some(ordered_list) = - matcher.get_match(format!("{}+ ", " ".repeat(level)).as_str(), "\n\n", true) - { - let res = Self::parse_branch( - &input[..ordered_list.start_token.len() + ordered_list.body.len()], - "\n", - Self::new(ListTypes::Ordered, level, vec![]), - ); - return res; - } - None - } -} - -impl Branch for List { - fn push>(&mut self, node: CanBeNode) { - self.nodes.push(node.into()) - } - - fn get_maybe_nodes() -> Vec> { - vec![ListItem::maybe_node()] - } - - fn get_fallback_node() -> Option> { - None - } - - fn get_outer_token_length(&self) -> usize { - 0 - } - - fn is_empty(&self) -> bool { - self.nodes.is_empty() - } -} - #[cfg(test)] mod tests { use super::{List, ListTypes}; use crate::{ - nodes::{list_item::ListItem, list_item_content::ListItemContent, text::Text}, - toolkit::{deserializer::Deserializer, node::Node}, + nodes::{list_item::ListItem, paragraph::Paragraph, text::Text}, + toolkit::{context::Context, parser::Parse}, }; use pretty_assertions::assert_eq; @@ -179,15 +133,15 @@ mod tests { ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("unordered list item").into()],) - ) - .into(), + Paragraph::new(vec![Text::new("unordered list item").into()],), + None + ), ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("unordered list item").into()],) + Paragraph::new(vec![Text::new("unordered list item").into()],), + None ) - .into(), ], } .to_string(), @@ -201,15 +155,15 @@ mod tests { ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("unordered list item").into()],) - ) - .into(), + Paragraph::new(vec![Text::new("unordered list item").into()],), + None + ), ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("unordered list item").into()],) + Paragraph::new(vec![Text::new("unordered list item").into()],), + None ) - .into(), ], } .to_string(), @@ -226,15 +180,15 @@ mod tests { ListItem::new( ListTypes::Ordered, 0, - ListItemContent::new(vec![Text::new("ordered list item").into()]), - ) - .into(), + Paragraph::new(vec![Text::new("ordered list item").into()]), + None, + ), ListItem::new( ListTypes::Ordered, 0, - ListItemContent::new(vec![Text::new("ordered list item").into()]), - ) - .into(), + Paragraph::new(vec![Text::new("ordered list item").into()]), + None, + ), ], ); @@ -242,190 +196,149 @@ mod tests { } #[test] - fn deserialize_wrong_level() { - assert_eq!( - List::deserialize_with_context( - "- level 0\n- level 0", - Some(List::create_context(1, &ListTypes::Unordered)) - ), - None - ); + fn parse_wrong_level() { + let mut ctx = Context::new(); + ctx.add("level", 1); + assert_eq!(List::parse("- level 0\n- level 0", 0, Some(&ctx)), None); } #[test] - fn deserialize_unordered() { - assert_eq!( - List::deserialize("- level 0\n- level 0"), - Some(List::new( - ListTypes::Unordered, - 0, - vec![ - ListItem::new( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ListItem::new( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ], - )) - ); + fn parse_unordered() { assert_eq!( - List::deserialize("- level 0\n- level 0\n\n"), - Some(List::new( - ListTypes::Unordered, - 0, - vec![ - ListItem::new( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ListItem::new( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ], + List::parse("- level 0\n- level 0", 0, None), + Some(( + List::new( + ListTypes::Unordered, + 0, + vec![ + ListItem::new( + ListTypes::Unordered, + 0, + Paragraph::new(vec![Text::new("level 0").into()]), + None + ), + ListItem::new( + ListTypes::Unordered, + 0, + Paragraph::new(vec![Text::new("level 0").into()]), + None + ) + ], + ), + 19 )) ); } #[test] - fn deserialize_ordered() { - assert_eq!( - List::deserialize("+ level 0\n+ level 0"), - Some(List::new( - ListTypes::Ordered, - 0, - vec![ - ListItem::new( - ListTypes::Ordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ListItem::new( - ListTypes::Ordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ], - )) - ); + fn parse_ordered() { assert_eq!( - List::deserialize("+ level 0\n+ level 0\n\n"), - Some(List::new( - ListTypes::Ordered, - 0, - vec![ - ListItem::new( - ListTypes::Ordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ListItem::new( - ListTypes::Ordered, - 0, - ListItemContent::new(vec![Text::new("level 0").into()]) - ) - .into(), - ], + List::parse("+ level 0\n+ level 0", 0, None), + Some(( + List::new( + ListTypes::Ordered, + 0, + vec![ + ListItem::new( + ListTypes::Ordered, + 0, + Paragraph::new(vec![Text::new("level 0").into()]), + None + ), + ListItem::new( + ListTypes::Ordered, + 0, + Paragraph::new(vec![Text::new("level 0").into()]), + None + ), + ], + ), + 19 )) ); } #[test] - fn deserialize_mixed() { + fn parse_mixed() { let list = List::new( ListTypes::Ordered, 0, - vec![ListItem::new_with_nested_list( + vec![ListItem::new( ListTypes::Ordered, 0, - ListItemContent::new(vec![Text::new("level 0").into()]), + Paragraph::new(vec![Text::new("level 0").into()]), Some(List::new( ListTypes::Unordered, 1, vec![ListItem::new( ListTypes::Unordered, 1, - ListItemContent::new(vec![Text::new("level 0").into()]), - ) - .into()], + Paragraph::new(vec![Text::new("level 0").into()]), + None, + )], )), ) .into()], ); - assert_eq!(List::deserialize("+ level 0\n - level 0"), Some(list)); + assert_eq!( + List::parse("+ level 0\n - level 0", 0, None), + Some((list, 20)) + ); } #[test] - fn deserialized_nested() { + fn parsed_nested() { let list = List::new( ListTypes::Unordered, 0, - vec![ListItem::new_with_nested_list( + vec![ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("one").into()]).into(), + Paragraph::new(vec![Text::new("one").into()]).into(), Some(List::new( ListTypes::Unordered, 1, vec![ListItem::new( ListTypes::Unordered, 1, - ListItemContent::new(vec![Text::new("two").into()]), - ) - .into()], + Paragraph::new(vec![Text::new("two").into()]), + None, + )], )), - ) - .into()], + )], ); let input = r#"- one - - two - -"#; - assert_eq!(List::deserialize(input), Some(list)); + - two"#; + assert_eq!(List::parse(input, 0, None), Some((list, 12))); } #[test] - fn len() { + fn parse_nested() { + let input = r#"- one + - two +something"#; let list = List::new( - ListTypes::Ordered, + ListTypes::Unordered, 0, - vec![ - ListItem::new( - ListTypes::Ordered, - 0, - ListItemContent::new(vec![Text::new("l").into()]), - ) - .into(), - ListItem::new( - ListTypes::Ordered, - 0, - ListItemContent::new(vec![Text::new("l").into()]), - ) - .into(), - ], + vec![ListItem::new( + ListTypes::Unordered, + 0, + Paragraph::new(vec![Text::new("one").into()]).into(), + Some(List::new( + ListTypes::Unordered, + 1, + vec![ListItem::new( + ListTypes::Unordered, + 1, + Paragraph::new(vec![Text::new("two\nsomething").into()]), + None, + )], + )), + )], ); - assert_eq!(list.len(), 7); - } - #[test] - fn empty_list() { - let list = List::new(ListTypes::Ordered, 0, vec![]); - assert_eq!(list.len(), 0); + assert_eq!(List::parse(input, 0, None), Some((list, input.len()))); } } diff --git a/src/nodes/list_item.rs b/src/nodes/list_item.rs index bf63a0b..0b50781 100644 --- a/src/nodes/list_item.rs +++ b/src/nodes/list_item.rs @@ -2,30 +2,24 @@ use std::fmt::Display; use serde::Serialize; -use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node}; - use super::{ list::{List, ListTypes}, - list_item_content::ListItemContent, + paragraph::Paragraph, }; #[derive(Debug, PartialEq, Serialize, Clone)] pub struct ListItem { pub list_type: ListTypes, pub level: usize, - pub text: ListItemContent, + pub text: Paragraph, pub nested_list: Option, } impl ListItem { - pub fn new(list_type: ListTypes, level: usize, text: ListItemContent) -> Self { - Self::new_with_nested_list(list_type, level, text, None) - } - - pub fn new_with_nested_list( + pub fn new( list_type: ListTypes, level: usize, - text: ListItemContent, + text: Paragraph, nested_list: Option, ) -> Self { Self { @@ -35,23 +29,6 @@ impl ListItem { nested_list, } } - fn get_list_type_from_context(ctx: &Option) -> ListTypes { - if let Some(ctx) = ctx { - if let Some(list_type) = ctx.get_char_value("list_type") { - if list_type == '+' { - return ListTypes::Ordered; - } - } - } - ListTypes::Unordered - } - - fn get_level_from_context(ctx: &Option) -> usize { - match ctx { - Some(ctx) => ctx.get_usize_value("level").unwrap_or(0), - None => 0, - } - } } impl Display for ListItem { @@ -73,63 +50,13 @@ impl Display for ListItem { } } -impl Node for ListItem { - fn len(&self) -> usize { - self.nested_list.as_ref().map_or(0, |list| list.len() + 1) - + self.text.len() - + self.level - + 2 - } -} - -impl Deserializer for ListItem { - fn deserialize_with_context(input: &str, ctx: Option) -> Option { - let level = Self::get_level_from_context(&ctx); - let list_type = match Self::get_list_type_from_context(&ctx) { - ListTypes::Unordered => "-", - ListTypes::Ordered => "+", - }; - let mut matcher = Matcher::new(input); - if let Some(list_item) = matcher.get_match( - format!("{}{} ", " ".repeat(level), list_type).as_str(), - format!("\n{}{} ", " ".repeat(level), list_type).as_str(), - true, - ) { - if let Some(text) = ListItemContent::deserialize(list_item.body) { - let mut nested_list = None; - if text.len() + 1 < list_item.body.len() { - let mut ctx = Context::new(); - ctx.add("level", level); - if let Some(list) = - List::deserialize_with_context(&list_item.body[text.len() + 1..], Some(ctx)) - { - nested_list = Some(list); - } else { - return None; - } - } - return Some(Self::new_with_nested_list( - Self::get_list_type_from_context(&ctx), - level, - text, - nested_list, - )); - } - } - None - } -} - #[cfg(test)] mod tests { use super::ListItem; - use crate::{ - nodes::{ - list::{List, ListTypes}, - list_item_content::ListItemContent, - text::Text, - }, - toolkit::{context::Context, deserializer::Deserializer, node::Node}, + use crate::nodes::{ + list::{List, ListTypes}, + paragraph::Paragraph, + text::Text, }; use pretty_assertions::assert_eq; @@ -139,7 +66,8 @@ mod tests { ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("test").into()]) + Paragraph::new(vec![Text::new("test").into()]), + None ) .to_string(), "- test".to_string() @@ -149,24 +77,26 @@ mod tests { ListItem::new( ListTypes::Ordered, 0, - ListItemContent::new(vec![Text::new("test").into()]) + Paragraph::new(vec![Text::new("test").into()]), + None ) .to_string(), "+ test".to_string() ); assert_eq!( - ListItem::new_with_nested_list( + ListItem::new( ListTypes::Unordered, 0, - ListItemContent::new(vec![Text::new("test").into()]), + Paragraph::new(vec![Text::new("test").into()]), Some(List::new( ListTypes::Unordered, 1, vec![ListItem::new( ListTypes::Unordered, 1, - ListItemContent::new(vec![Text::new("test").into()]) + Paragraph::new(vec![Text::new("test").into()]), + None ) .into()] )) @@ -175,86 +105,4 @@ mod tests { "- test\n - test".to_string() ); } - - #[test] - fn len() { - assert_eq!( - ListItem::new( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("test").into()]) - ) - .len(), - 6 - ); - - assert_eq!( - ListItem::new_with_nested_list( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("test").into()]), - Some(List::new( - ListTypes::Unordered, - 1, - vec![ListItem::new( - ListTypes::Unordered, - 1, - ListItemContent::new(vec![Text::new("test").into()]) - ) - .into()] - )) - ) - .len(), - 14 - ); - } - - #[test] - fn deserialize() { - assert_eq!( - ListItem::deserialize("- test").unwrap(), - ListItem::new( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("test").into()]) - ) - ); - } - - #[test] - fn deserialize_with_nested_list() { - assert_eq!( - ListItem::deserialize("- 111111\n - 22222"), - Some(ListItem::new_with_nested_list( - ListTypes::Unordered, - 0, - ListItemContent::new(vec![Text::new("111111").into()]), - Some(List::new( - ListTypes::Unordered, - 1, - vec![ListItem::new( - ListTypes::Unordered, - 1, - ListItemContent::new(vec![Text::new("22222").into()]) - ) - .into()] - )) - )) - ); - } - - #[test] - fn deserialize_with_nested_list_and_text() { - assert_eq!(ListItem::deserialize("- 111111\n - 22222\n 33333"), None); - } - - #[test] - fn deserialize_with_wrong_context() { - let mut ctx = Context::new(); - ctx.add("list_type", '-'); - assert_eq!( - ListItem::deserialize_with_context("+ test", Some(ctx)), - None - ); - } } diff --git a/src/nodes/list_item_content.rs b/src/nodes/list_item_content.rs deleted file mode 100644 index 186f7f6..0000000 --- a/src/nodes/list_item_content.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::fmt::Display; - -use serde::Serialize; - -use crate::toolkit::{ - context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, FallbackNode, MaybeNode}, - matcher::Matcher, - node::Node, -}; - -use super::{ - anchor::Anchor, bold::Bold, inline_code::InlineCode, italic::Italic, - strikethrough::Strikethrough, text::Text, -}; - -#[derive(Debug, PartialEq, Serialize, Clone)] -#[serde(tag = "type")] -pub enum ListItemContentNodes { - A(Anchor), - B(Bold), - I(Italic), - S(Strikethrough), - Text(Text), - InlineCode(InlineCode), -} - -impl From for ListItemContentNodes { - fn from(value: Anchor) -> Self { - ListItemContentNodes::A(value) - } -} - -impl From for ListItemContentNodes { - fn from(value: Bold) -> Self { - ListItemContentNodes::B(value) - } -} - -impl From for ListItemContentNodes { - fn from(value: Italic) -> Self { - ListItemContentNodes::I(value) - } -} - -impl From for ListItemContentNodes { - fn from(value: Strikethrough) -> Self { - ListItemContentNodes::S(value) - } -} - -impl From for ListItemContentNodes { - fn from(value: Text) -> Self { - ListItemContentNodes::Text(value) - } -} - -impl From for ListItemContentNodes { - fn from(value: InlineCode) -> Self { - ListItemContentNodes::InlineCode(value) - } -} - -impl Display for ListItemContentNodes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ListItemContentNodes::A(node) => write!(f, "{}", node), - ListItemContentNodes::B(node) => write!(f, "{}", node), - ListItemContentNodes::I(node) => write!(f, "{}", node), - ListItemContentNodes::S(node) => write!(f, "{}", node), - ListItemContentNodes::Text(node) => write!(f, "{}", node), - ListItemContentNodes::InlineCode(node) => write!(f, "{}", node), - } - } -} - -impl Node for ListItemContentNodes { - fn len(&self) -> usize { - match self { - ListItemContentNodes::A(node) => node.len(), - ListItemContentNodes::B(node) => node.len(), - ListItemContentNodes::I(node) => node.len(), - ListItemContentNodes::S(node) => node.len(), - ListItemContentNodes::Text(node) => node.len(), - ListItemContentNodes::InlineCode(node) => node.len(), - } - } -} - -#[derive(Debug, PartialEq, Serialize, Clone)] -pub struct ListItemContent { - pub nodes: Vec, -} - -impl Display for ListItemContent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - self.nodes - .iter() - .map(|node| node.to_string()) - .collect::>() - .join(""), - ) - } -} - -impl Node for ListItemContent { - fn len(&self) -> usize { - self.nodes.iter().map(|node| node.len()).sum::() - } -} - -impl ListItemContent { - pub fn new(nodes: Vec) -> Self { - ListItemContent { nodes } - } -} - -impl Default for ListItemContent { - fn default() -> Self { - Self::new(vec![]) - } -} - -impl Branch for ListItemContent { - fn push>(&mut self, node: CanBeNode) { - self.nodes.push(node.into()); - } - - fn get_maybe_nodes() -> Vec> { - vec![ - Anchor::maybe_node(), - Bold::maybe_node(), - Italic::maybe_node(), - Strikethrough::maybe_node(), - InlineCode::maybe_node(), - ] - } - - fn get_fallback_node() -> Option> { - Some(Text::fallback_node()) - } - - fn get_outer_token_length(&self) -> usize { - 0 - } - - fn is_empty(&self) -> bool { - self.nodes.is_empty() - } -} - -impl Deserializer for ListItemContent { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut m = Matcher::new(input); - if let Some(list_item_content) = m.get_match("", "\n", true) { - return Self::parse_branch(list_item_content.body, "", Self::default()); - } - None - } -} - -#[cfg(test)] -mod test { - use super::ListItemContent; - use crate::{ - nodes::{ - anchor::Anchor, bold::Bold, inline_code::InlineCode, italic::Italic, - strikethrough::Strikethrough, text::Text, - }, - toolkit::{ - deserializer::{Branch, Deserializer}, - node::Node, - }, - }; - use pretty_assertions::assert_eq; - - #[test] - fn test_consume_all() { - let input = "This is a list item content"; - let expected = ListItemContent::new(vec![Text::new(input).into()]); - assert_eq!(ListItemContent::deserialize(input), Some(expected)); - } - - #[test] - fn test_consume_all_with_newline() { - let input = "This is a list item content\nAnd this is not"; - let expected = ListItemContent::new(vec![Text::new("This is a list item content").into()]); - assert_eq!(ListItemContent::deserialize(input), Some(expected)); - } - - #[test] - fn len() { - assert_eq!(ListItemContent::default().len(), 0); - assert_eq!( - ListItemContent::new(vec![Text::new("Hello").into()]).len(), - 5 - ); - } - - #[test] - fn serialize() { - assert_eq!(ListItemContent::default().to_string(), ""); - assert_eq!( - ListItemContent::new(vec![Text::new("Hello").into()]).to_string(), - "Hello" - ); - } - - #[test] - fn deserialize_with_all_nodes() { - assert_eq!( - ListItemContent::deserialize( - "simple text **bold text** `let foo='bar';` [a](u) _I_ ~~S~~" - ), - Some(ListItemContent::new(vec![ - Text::new("simple text ").into(), - Bold::new(vec![Text::new("bold text").into()]).into(), - Text::new(" ").into(), - InlineCode::new("let foo='bar';").into(), - Text::new(" ").into(), - Anchor::new("a", "u").into(), - Text::new(" ").into(), - Italic::new("I").into(), - Text::new(" ").into(), - Strikethrough::new("S").into(), - ])) - ); - } - #[test] - fn serialize_with_all_nodes() { - assert_eq!( - "simple text **bold text** `let foo='bar';` [a](u) _I_ ~~S~~", - ListItemContent::new(vec![ - Text::new("simple text ").into(), - Bold::new(vec![Text::new("bold text").into()]).into(), - Text::new(" ").into(), - InlineCode::new("let foo='bar';").into(), - Text::new(" ").into(), - Anchor::new("a", "u").into(), - Text::new(" ").into(), - Italic::new("I").into(), - Text::new(" ").into(), - Strikethrough::new("S").into(), - ]) - .to_string() - ); - } - - #[test] - fn empty_list_item_content() { - let list_item_content = ListItemContent::default(); - assert_eq!(list_item_content.len(), 0); - assert_eq!(list_item_content.is_empty(), true); - } -} diff --git a/src/nodes/metadata.rs b/src/nodes/metadata.rs index 2fdb722..6f0b8e1 100644 --- a/src/nodes/metadata.rs +++ b/src/nodes/metadata.rs @@ -1,9 +1,10 @@ use std::fmt::Display; -use crate::toolkit::{matcher::Matcher, node::Node}; use chrono::{DateTime, FixedOffset}; use serde::{Deserialize, Serialize}; +use crate::toolkit::{context::Context, parser::Parse}; + #[derive(Debug, PartialEq, Serialize, Default, Clone, Deserialize)] pub struct Metadata { #[serde(skip_serializing_if = "Option::is_none")] @@ -18,8 +19,6 @@ pub struct Metadata { pub tags: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub is_draft: Option, - #[serde(skip)] - pub consumed_length: Option, } impl Metadata { @@ -37,18 +36,19 @@ impl Metadata { preview: preview.map(|p| p.into()), is_draft: None, tags, - consumed_length: None, } } +} - pub fn deserialize(input: &str) -> Option { - let mut matcher = Matcher::new(input); - if let Some(metadata) = matcher.get_match("---\n", "---", false) { - let mut meta: Metadata = serde_yaml::from_str(metadata.body).ok()?; - meta.consumed_length = Some(metadata.len()); - return Some(meta); +impl Parse for Metadata { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with("---\n") { + let start = current_position + 4; + if let Some(end) = input[start..].find("\n---") { + let meta: Metadata = serde_yaml::from_str(&input[start..start + end]).ok()?; + return Some((meta, end + 8)); + } } - None } } @@ -68,13 +68,6 @@ impl Display for Metadata { } } -impl Node for Metadata { - fn len(&self) -> usize { - self.consumed_length - .unwrap_or_else(|| self.to_string().len()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -96,31 +89,7 @@ mod tests { } #[test] - fn test_len() { - let metadata = Metadata::new( - Some("title"), - Some(DateTime::parse_from_rfc3339("2022-01-01T00:00:00+02:00").unwrap()), - Some("image"), - Some("preview"), - Some(vec!["tag1".to_string(), "tag2".to_string()]), - ); - assert_eq!(metadata.len(), metadata.to_string().len()); - } - - #[test] - fn len_with_one_tag() { - let metadata = Metadata::new( - Some("title"), - Some(DateTime::parse_from_rfc3339("2022-01-01T00:00:00+02:00").unwrap()), - Some("image"), - Some("preview"), - Some(vec!["tag1".to_string()]), - ); - assert_eq!(metadata.len(), metadata.to_string().len()); - } - - #[test] - fn test_deserialize() { + fn test_parse() { let metadata = Metadata { title: Some("title".to_string()), date: Some(DateTime::parse_from_rfc3339("2022-12-30T20:33:55+01:00").unwrap()), @@ -128,47 +97,50 @@ mod tests { preview: Some("preview".to_string()), tags: Some(vec!["tag1".to_string(), "tag2".to_string()]), is_draft: Some(true), - consumed_length: Some(117), }; let str = "---\ntitle: title\ndate: 2022-12-30T20:33:55+01:00\nimage: image\npreview: preview\ntags:\n- tag1\n- tag2\nis_draft: true\n---"; - assert_eq!(Metadata::deserialize(str), Some(metadata)); + assert_eq!(Metadata::parse(str, 0, None), Some((metadata, str.len()))); } #[test] - fn deserialize_empty() { + fn parse_empty() { assert_eq!( - Metadata::deserialize("---\n---"), - Some(Metadata { - title: None, - date: None, - image: None, - preview: None, - tags: None, - is_draft: None, - consumed_length: Some(7) - }) + Metadata::parse("---\n\n---", 0, None), + Some(( + Metadata { + title: None, + date: None, + image: None, + preview: None, + tags: None, + is_draft: None, + }, + 8 + )) ); } #[test] - fn deserialize_fail() { - assert_eq!(Metadata::deserialize("random string"), None); - assert_eq!(Metadata::deserialize("---\nrandom string---"), None); + fn parse_fail() { + assert_eq!(Metadata::parse("random string", 0, None), None); + assert_eq!(Metadata::parse("---\nrandom string---", 0, None), None); } #[test] - fn deserialize_only_with_title() { + fn parse_only_with_title() { assert_eq!( - Metadata::deserialize("---\ntitle: header\n---"), - Some(Metadata { - title: Some("header".to_string()), - preview: None, - date: None, - image: None, - tags: None, - is_draft: None, - consumed_length: Some(21) - }) + Metadata::parse("---\ntitle: header\n---", 0, None), + Some(( + Metadata { + title: Some("header".to_string()), + preview: None, + date: None, + image: None, + tags: None, + is_draft: None, + }, + 21 + )) ); } @@ -179,13 +151,12 @@ mod tests { Metadata::new::<&str>(None, None, None, None, None) ); assert_eq!(Metadata::default().to_string(), ""); - assert_eq!(Metadata::default().len(), 0); } #[test] fn deserialize_with_quotes() { let input = "---\ntitle: \"header\"\n---"; - let m = Metadata::deserialize(input); - assert_eq!(input.len(), m.unwrap().len()); + let m = Metadata::parse(input, 0, None); + assert_eq!(input.len(), m.unwrap().1); } } diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index 514ac4c..2c0d16f 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -12,7 +12,6 @@ pub mod inline_code; pub mod italic; pub mod list; pub mod list_item; -pub mod list_item_content; pub mod metadata; pub mod paragraph; pub mod strikethrough; diff --git a/src/nodes/paragraph.rs b/src/nodes/paragraph.rs index c98033f..1ba91ca 100644 --- a/src/nodes/paragraph.rs +++ b/src/nodes/paragraph.rs @@ -2,15 +2,15 @@ use std::fmt::Display; use serde::Serialize; -use crate::nodes::{ - anchor::Anchor, bold::Bold, inline_code::InlineCode, italic::Italic, - strikethrough::Strikethrough, text::Text, -}; -use crate::toolkit::node::Node; -use crate::toolkit::{ - context::Context, - deserializer::{Branch, DefinitelyNode, Deserializer, FallbackNode, MaybeNode}, - matcher::Matcher, +use crate::{ + nodes::{ + anchor::Anchor, bold::Bold, inline_code::InlineCode, italic::Italic, + strikethrough::Strikethrough, text::Text, + }, + toolkit::{ + context::Context, + parser::{parse_to_consumer, parse_to_parser, Branch, Consumer, Parse, Parser}, + }, }; #[derive(Debug, PartialEq, Serialize, Clone)] @@ -73,19 +73,6 @@ impl Display for ParagraphNodes { } } -impl Node for ParagraphNodes { - fn len(&self) -> usize { - match self { - ParagraphNodes::A(node) => node.len(), - ParagraphNodes::B(node) => node.len(), - ParagraphNodes::I(node) => node.len(), - ParagraphNodes::S(node) => node.len(), - ParagraphNodes::Text(node) => node.len(), - ParagraphNodes::InlineCode(node) => node.len(), - } - } -} - #[derive(Debug, PartialEq, Serialize, Clone)] pub struct Paragraph { pub nodes: Vec, @@ -97,49 +84,12 @@ impl Paragraph { } } -impl Branch for Paragraph { - fn push>(&mut self, element: TP) { - self.nodes.push(element.into()); - } - - fn get_maybe_nodes() -> Vec> { - vec![ - Anchor::maybe_node(), - Bold::maybe_node(), - Italic::maybe_node(), - Strikethrough::maybe_node(), - InlineCode::maybe_node(), - ] - } - - fn get_fallback_node() -> Option> { - Some(Text::fallback_node()) - } - - fn get_outer_token_length(&self) -> usize { - 0 - } - - fn is_empty(&self) -> bool { - self.nodes.is_empty() - } -} impl Default for Paragraph { fn default() -> Self { Self::new(vec![]) } } -impl Deserializer for Paragraph { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(paragraph) = matcher.get_match("", "\n\n", true) { - return Self::parse_branch(paragraph.body, "", Self::default()); - } - None - } -} - impl Display for Paragraph { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -154,18 +104,38 @@ impl Display for Paragraph { } } -impl Node for Paragraph { - fn len(&self) -> usize { - self.nodes.iter().map(|node| node.len()).sum::() +impl Branch for Paragraph { + fn get_parsers(&self) -> Vec> { + vec![ + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + ] + } + + fn get_consumer(&self) -> Option> { + Some(parse_to_consumer::()) + } + + fn push_node(&mut self, node: ParagraphNodes) { + self.nodes.push(node); } } -impl FallbackNode for Paragraph { - fn fallback_node() -> DefinitelyNode - where - Self: Into, - { - Box::new(|input| Paragraph::deserialize(input).unwrap_or_default().into()) +impl Parse for Paragraph { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + let end = input[current_position..] + .find("\n\n") + .unwrap_or(input.len()); + let paragraph = Paragraph::default(); + Some(( + paragraph + .parse_branch(&input[current_position..end], "", None) + .expect("paragraph should always succed"), + end, + )) } } @@ -173,22 +143,20 @@ impl FallbackNode for Paragraph { mod tests { use super::Paragraph; use crate::{ - nodes::bold::Bold, - nodes::inline_code::InlineCode, - nodes::{anchor::Anchor, italic::Italic, strikethrough::Strikethrough, text::Text}, - toolkit::{ - deserializer::{Branch, Deserializer}, - node::Node, + nodes::{ + anchor::Anchor, bold::Bold, inline_code::InlineCode, italic::Italic, + strikethrough::Strikethrough, text::Text, }, + toolkit::parser::{Branch, Parse}, }; use pretty_assertions::assert_eq; #[test] fn push() { let mut p = Paragraph::default(); - p.push(Text::new("simple text ")); - p.push(Bold::new(vec![Text::new("bold text").into()])); - p.push(InlineCode::new("let foo='bar';")); + p.push_node(Text::new("simple text ").into()); + p.push_node(Bold::new(vec![Text::new("bold text").into()]).into()); + p.push_node(InlineCode::new("let foo='bar';").into()); assert_eq!( p.to_string(), @@ -213,24 +181,24 @@ mod tests { } #[test] - fn deserialize() { + fn parse() { assert_eq!( - Paragraph::deserialize("simple text **bold text**`let foo='bar';`[t](u)"), - Some(Paragraph::new(vec![ - Text::new("simple text ").into(), - Bold::new(vec![Text::new("bold text").into()]).into(), - InlineCode::new("let foo='bar';").into(), - Anchor::new("t", "u").into() - ])) + Paragraph::parse( + "simple text **bold text**`let foo='bar';`[t](u)_I_~~S~~", + 0, + None + ), + Some(( + Paragraph::new(vec![ + Text::new("simple text ").into(), + Bold::new(vec![Text::new("bold text").into()]).into(), + InlineCode::new("let foo='bar';").into(), + Anchor::new("t", "u").into(), + Italic::new("I").into(), + Strikethrough::new("S").into() + ]), + 55 + )) ); - assert_eq!( - Paragraph::deserialize("1 2\n\n3"), - Some(Paragraph::new(vec![Text::new("1 2").into()])) - ); - } - #[test] - fn len() { - assert_eq!(Paragraph::default().len(), 0); - assert_eq!(Paragraph::default().is_empty(), true); } } diff --git a/src/nodes/strikethrough.rs b/src/nodes/strikethrough.rs index 826fcbe..d52fba6 100644 --- a/src/nodes/strikethrough.rs +++ b/src/nodes/strikethrough.rs @@ -1,11 +1,6 @@ -use std::fmt::{Display, Formatter}; - +use crate::toolkit::{context::Context, parser::Parse}; use serde::Serialize; - -use crate::{ - toolkit::{context::Context, deserializer::Deserializer}, - toolkit::{matcher::Matcher, node::Node}, -}; +use std::fmt::{Display, Formatter}; /// Representation of strike through #[derive(Debug, PartialEq, Serialize, Clone)] @@ -19,32 +14,30 @@ impl Strikethrough { } } -impl Display for Strikethrough { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "~~{}~~", self.text) - } -} - -impl Node for Strikethrough { - fn len(&self) -> usize { - self.text.len() + 4 +impl Parse for Strikethrough { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + if input[current_position..].starts_with("~~") { + if let Some(end) = input[current_position + 2..].find("~~") { + return Some(( + Strikethrough::new(&input[current_position + 2..current_position + 2 + end]), + end + 4, + )); + } + } + None } } -impl Deserializer for Strikethrough { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let mut matcher = Matcher::new(input); - if let Some(strikethrough) = matcher.get_match("~~", "~~", false) { - return Some(Strikethrough::new(strikethrough.body)); - } - None +impl Display for Strikethrough { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "~~{}~~", self.text) } } #[cfg(test)] mod tests { use super::Strikethrough; - use crate::toolkit::{deserializer::Deserializer, node::Node}; + use crate::toolkit::parser::Parse; use pretty_assertions::assert_eq; #[test] @@ -62,23 +55,17 @@ mod tests { #[test] fn parse() { assert_eq!( - Strikethrough::deserialize("~~2+2=5~~"), - Some(Strikethrough::new("2+2=5")) + Strikethrough::parse("~~2+2=5~~", 0, None), + Some((Strikethrough::new("2+2=5"), 9)) ); assert_eq!( - Strikethrough::deserialize("~~is~~not"), - Some(Strikethrough::new("is")) + Strikethrough::parse("~~is~~not", 0, None), + Some((Strikethrough::new("is"), 6)) ); - assert_eq!(Strikethrough::deserialize("~~not"), None); + assert_eq!(Strikethrough::parse("~~not", 0, None), None); assert_eq!( - Strikethrough::deserialize("~~i\ns~~"), - Some(Strikethrough::new("i\ns")) + Strikethrough::parse("~~i\ns~~", 0, None), + Some((Strikethrough::new("i\ns"), 7)) ); } - - #[test] - fn len() { - assert_eq!(Strikethrough::new("s").len(), 5); - assert_eq!(Strikethrough::new("st").len(), 6); - } } diff --git a/src/nodes/text.rs b/src/nodes/text.rs index 76ce99f..79b4a08 100644 --- a/src/nodes/text.rs +++ b/src/nodes/text.rs @@ -1,56 +1,38 @@ -use std::fmt::Display; - +use crate::toolkit::{context::Context, parser::Parse}; use serde::Serialize; +use std::fmt::Display; -use crate::toolkit::{ - context::Context, - deserializer::{DefinitelyNode, Deserializer, FallbackNode}, - node::Node, -}; - -/// Representation of a regular text #[derive(Debug, PartialEq, Serialize, Clone)] pub struct Text { - pub text: String, + text: String, } impl Text { - pub fn new>(text: S) -> Self { + pub fn new>(text: IS) -> Self { Text { text: text.into() } } } -impl Deserializer for Text { - fn deserialize_with_context(input: &str, _: Option) -> Option { - Some(Text::new(input.to_string())) +impl Parse for Text { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + Some(( + Text::new(&input[current_position..]), + input.len() - current_position, + )) } } impl Display for Text { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self.text) - } -} - -impl Node for Text { - fn len(&self) -> usize { - self.text.len() - } -} - -impl FallbackNode for Text { - fn fallback_node() -> DefinitelyNode - where - Self: Into, - { - Box::new(|input| Text::new(input).into()) + write!(f, "{}", self.text) } } #[cfg(test)] mod tests { + use crate::toolkit::parser::Parse; + use super::Text; - use crate::toolkit::deserializer::Deserializer; use pretty_assertions::assert_eq; #[test] @@ -67,6 +49,6 @@ mod tests { #[test] fn from_string() { - assert_eq!(Text::deserialize("t"), Some(Text::new("t"))); + assert_eq!(Text::parse("t", 0, None), Some((Text::new("t"), 1))); } } diff --git a/src/nodes/yamd.rs b/src/nodes/yamd.rs index cf0d18c..5b8d3e1 100644 --- a/src/nodes/yamd.rs +++ b/src/nodes/yamd.rs @@ -2,16 +2,15 @@ use std::fmt::Display; use serde::Serialize; -use crate::{ - nodes::heading::Heading, - nodes::paragraph::Paragraph, - toolkit::deserializer::{Branch, DefinitelyNode, Deserializer, FallbackNode, MaybeNode}, - toolkit::{context::Context, node::Node}, +use crate::toolkit::{ + context::Context, + parser::{parse_to_consumer, parse_to_parser, Branch, Consumer, Parse, Parser}, }; use super::{ - code::Code, collapsible::Collapsible, divider::Divider, embed::Embed, highlight::Highlight, - image::Image, image_gallery::ImageGallery, list::List, metadata::Metadata, + code::Code, collapsible::Collapsible, divider::Divider, embed::Embed, heading::Heading, + highlight::Highlight, image::Image, image_gallery::ImageGallery, list::List, + metadata::Metadata, paragraph::Paragraph, }; #[derive(Debug, PartialEq, Serialize, Clone)] @@ -106,77 +105,16 @@ impl Display for YamdNodes { } } -impl Node for YamdNodes { - fn len(&self) -> usize { - match self { - YamdNodes::P(node) => node.len(), - YamdNodes::H(node) => node.len(), - YamdNodes::Image(node) => node.len(), - YamdNodes::Code(node) => node.len(), - YamdNodes::List(node) => node.len(), - YamdNodes::ImageGallery(node) => node.len(), - YamdNodes::Highlight(node) => node.len(), - YamdNodes::Divider(node) => node.len(), - YamdNodes::Embed(node) => node.len(), - YamdNodes::Collapsible(node) => node.len(), - } - } -} - /// Yamd is a parent node for every node. #[derive(Debug, PartialEq, Serialize, Clone, Default)] pub struct Yamd { - pub metadata: Metadata, + pub metadata: Option, pub nodes: Vec, } impl Yamd { pub fn new(metadata: Option, nodes: Vec) -> Self { - Self { - metadata: metadata.unwrap_or_default(), - nodes, - } - } -} - -impl Branch for Yamd { - fn push>(&mut self, element: TC) { - self.nodes.push(element.into()); - } - - fn get_maybe_nodes() -> Vec> { - vec![ - Heading::maybe_node(), - Image::maybe_node(), - Code::maybe_node(), - List::maybe_node(), - ImageGallery::maybe_node(), - Highlight::maybe_node(), - Divider::maybe_node(), - Embed::maybe_node(), - Collapsible::maybe_node(), - ] - } - - fn get_fallback_node() -> Option> { - Some(Paragraph::fallback_node()) - } - - fn get_outer_token_length(&self) -> usize { - let len = self.metadata.len(); - len + if len == 0 { 0 } else { 2 } - } - - fn is_empty(&self) -> bool { - self.nodes.is_empty() - } -} - -impl Deserializer for Yamd { - fn deserialize_with_context(input: &str, _: Option) -> Option { - let metadata = Metadata::deserialize(input); - let metadata_len = metadata.as_ref().map(|m| m.len() + 2).unwrap_or(0); - Self::parse_branch(&input[metadata_len..], "\n\n", Self::new(metadata, vec![])) + Self { metadata, nodes } } } @@ -184,9 +122,10 @@ impl Display for Yamd { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{}{}{}", - self.metadata, - if self.metadata.len() == 0 { "" } else { "\n\n" }, + "{}{}", + self.metadata + .as_ref() + .map_or(String::new(), |m| format!("{m}\n\n")), self.nodes .iter() .map(|node| node.to_string()) @@ -196,16 +135,40 @@ impl Display for Yamd { } } -impl Node for Yamd { - fn len(&self) -> usize { - let delimeter_len = if self.is_empty() { - 0 - } else { - (self.nodes.len() - 1) * 2 - }; - self.nodes.iter().map(|node| node.len()).sum::() - + delimeter_len - + self.get_outer_token_length() +impl Branch for Yamd { + fn get_parsers(&self) -> Vec> { + vec![ + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + parse_to_parser::(), + ] + } + + fn get_consumer(&self) -> Option> { + Some(parse_to_consumer::()) + } + + fn push_node(&mut self, node: YamdNodes) { + self.nodes.push(node); + } +} + +impl Parse for Yamd { + fn parse(input: &str, current_position: usize, _: Option<&Context>) -> Option<(Self, usize)> { + let (metadata, consumed_length) = Metadata::parse(input, current_position, None) + .map_or((None, 0), |(m, l)| (Some(m), l + 2)); + + let yamd = Self::new(metadata, vec![]); + let yamd = yamd + .parse_branch(&input[current_position + consumed_length..], "\n\n", None) + .expect("yamd should never fail"); + Some((yamd, input.len() - current_position)) } } @@ -213,27 +176,25 @@ impl Node for Yamd { mod tests { use super::Yamd; use crate::{ - nodes::heading::Heading, - nodes::paragraph::Paragraph, nodes::{ bold::Bold, code::Code, collapsible::Collapsible, divider::Divider, embed::Embed, + heading::Heading, highlight::Highlight, image::Image, image_gallery::ImageGallery, italic::Italic, list::{List, ListTypes::Unordered}, list_item::ListItem, - list_item_content::ListItemContent, metadata::Metadata, + paragraph::Paragraph, strikethrough::Strikethrough, text::Text, }, - toolkit::deserializer::Branch, - toolkit::{deserializer::Deserializer, node::Node}, + toolkit::parser::{Branch, Parse}, }; use chrono::DateTime; use pretty_assertions::assert_eq; @@ -292,8 +253,8 @@ end"#; #[test] fn push() { let mut t = Yamd::new(None, vec![]); - t.push(Heading::new(1, vec![Text::new("header").into()])); - t.push(Paragraph::new(vec![Text::new("text").into()])); + t.push_node(Heading::new(1, vec![Text::new("header").into()]).into()); + t.push_node(Paragraph::new(vec![Text::new("text").into()]).into()); assert_eq!(t.to_string(), "# header\n\ntext".to_string()); } @@ -313,76 +274,77 @@ end"#; } #[test] - fn deserialize() { + fn parse() { assert_eq!( - Yamd::deserialize(TEST_CASE), - Some(Yamd::new( - Some(Metadata { - title: Some("test".to_string()), - date: Some( - DateTime::parse_from_str( - "2022-01-01 00:00:00 +02:00", - "%Y-%m-%d %H:%M:%S %z" + Yamd::parse(TEST_CASE, 0, None), + Some(( + Yamd::new( + Some(Metadata { + title: Some("test".to_string()), + date: Some( + DateTime::parse_from_str( + "2022-01-01 00:00:00 +02:00", + "%Y-%m-%d %H:%M:%S %z" + ) + .unwrap() + ), + image: Some("image".to_string()), + preview: Some("preview".to_string()), + tags: Some(vec!["tag1".to_string(), "tag2".to_string()]), + is_draft: None, + }), + vec![ + Heading::new(1, vec![Text::new("hello").into()]).into(), + Code::new("rust", "let a=1;").into(), + Paragraph::new(vec![ + Text::new("t").into(), + Bold::new(vec![Text::new("b").into()]).into() + ]) + .into(), + Image::new('a', 'u').into(), + ImageGallery::new(vec![ + Image::new("a", "u").into(), + Image::new("a2", "u2").into() + ],) + .into(), + Highlight::new( + Some("H"), + Some("I"), + vec![ + Paragraph::new(vec![Strikethrough::new("s").into()]), + Paragraph::new(vec![Italic::new("I").into()]) + ] ) - .unwrap() - ), - image: Some("image".to_string()), - preview: Some("preview".to_string()), - tags: Some(vec!["tag1".to_string(), "tag2".to_string()]), - is_draft: None, - consumed_length: Some(101), - }), - vec![ - Heading::new(1, vec![Text::new("hello").into()]).into(), - Code::new("rust", "let a=1;").into(), - Paragraph::new(vec![ - Text::new("t").into(), - Bold::new(vec![Text::new("b").into()]).into() - ]) - .into(), - Image::new('a', 'u').into(), - ImageGallery::new(vec![ - Image::new("a", "u").into(), - Image::new("a2", "u2").into() - ],) - .into(), - Highlight::new( - Some("H"), - Some("I"), - vec![ - Paragraph::new(vec![Strikethrough::new("s").into()]).into(), - Paragraph::new(vec![Italic::new("I").into()]).into() - ] - ) - .into(), - Divider::new().into(), - List::new( - Unordered, - 0, - vec![ListItem::new_with_nested_list( + .into(), + Divider::new().into(), + List::new( Unordered, 0, - ListItemContent::new(vec![Text::new("one").into()]), - Some(List::new( + vec![ListItem::new( Unordered, - 1, - vec![ListItem::new( + 0, + Paragraph::new(vec![Text::new("one").into()]), + Some(List::new( Unordered, 1, - ListItemContent::new(vec![Text::new("two").into()]) - ) - .into()] - )) + vec![ListItem::new( + Unordered, + 1, + Paragraph::new(vec![Text::new("two").into()]), + None + )] + )) + )] ) - .into()] - ) - .into(), - Embed::new("youtube", "123",).into(), - Embed::new("cloudinary_gallery", "cloud_name&tag",).into(), - Collapsible::new("collapsible", vec![]).into(), - Collapsible::new("one more collapsible", vec![]).into(), - Paragraph::new(vec![Text::new("end").into()]).into() - ] + .into(), + Embed::new("youtube", "123",).into(), + Embed::new("cloudinary_gallery", "cloud_name&tag",).into(), + Collapsible::new("collapsible", vec![]).into(), + Collapsible::new("one more collapsible", vec![]).into(), + Paragraph::new(vec![Text::new("end").into()]).into() + ] + ), + TEST_CASE.len() )) ); } @@ -422,8 +384,8 @@ end"#; Some("H"), Some("I"), vec![ - Paragraph::new(vec![Strikethrough::new("s").into()]).into(), - Paragraph::new(vec![Italic::new("I").into()]).into() + Paragraph::new(vec![Strikethrough::new("s").into()]), + Paragraph::new(vec![Italic::new("I").into()]) ] ) .into(), @@ -431,23 +393,22 @@ end"#; List::new( Unordered, 0, - vec![ListItem::new_with_nested_list( + vec![ListItem::new( Unordered, 0, - ListItemContent::new(vec![Text::new("one").into()]).into(), + Paragraph::new(vec![Text::new("one").into()]), List::new( Unordered, 1, vec![ListItem::new( Unordered, 1, - ListItemContent::new(vec![Text::new("two").into()]) - ) - .into()] + Paragraph::new(vec![Text::new("two").into()]), + None + )] ) .into() - ) - .into()] + )] ) .into(), Embed::new("youtube", "123",).into(), @@ -478,8 +439,8 @@ end"#; Paragraph::new(vec![Text::new("3").into()]).into(), ], ); - let actual = Yamd::deserialize(input).unwrap(); - assert_eq!(expected, actual); + let actual = Yamd::parse(input, 0, None).unwrap(); + assert_eq!(expected, actual.0); } #[test] @@ -494,15 +455,8 @@ end"#; Heading::new(1, vec![Text::new("header").into()]).into(), ], ); - let actual = Yamd::deserialize(input).unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn empty_yamd() { - let yamd = Yamd::new(None, vec![]); - assert_eq!(yamd.len(), 0); - assert_eq!(yamd.is_empty(), true); + let actual = Yamd::parse(input, 0, None).unwrap(); + assert_eq!(expected, actual.0); } #[test] @@ -512,8 +466,8 @@ end"#; None, vec![Paragraph::new(vec![Text::new("text - text").into()]).into()], ); - let actual = Yamd::deserialize(input).unwrap(); - assert_eq!(expected, actual); + let actual = Yamd::parse(input, 0, None).unwrap(); + assert_eq!(expected, actual.0); } #[test] @@ -526,7 +480,7 @@ end"#; Paragraph::new(vec![]).into(), ], ); - let actual = Yamd::deserialize(input).unwrap(); - assert_eq!(expected, actual); + let actual = Yamd::parse(input, 0, None).unwrap(); + assert_eq!(expected, actual.0); } } diff --git a/src/toolkit/context.rs b/src/toolkit/context.rs index 0725fa0..b848da7 100644 --- a/src/toolkit/context.rs +++ b/src/toolkit/context.rs @@ -2,10 +2,11 @@ use std::collections::HashMap; /// Context allows to pass arbitrary amount of key/value pairs between nodes in a type safe way /// +/// TODO: used only in list.rs, should be removed +/// #[derive(Debug, Clone)] pub enum ContextValues { Usize(usize), - Char(char), } impl From for ContextValues { @@ -14,12 +15,6 @@ impl From for ContextValues { } } -impl From for ContextValues { - fn from(value: char) -> Self { - ContextValues::Char(value) - } -} - #[derive(Clone, Debug, Default)] pub struct Context { inner: HashMap, @@ -42,13 +37,6 @@ impl Context { } None } - - pub fn get_char_value(&self, key: impl Into) -> Option { - if let Some(ContextValues::Char(value)) = self.inner.get(&key.into()) { - return Some(*value); - } - None - } } #[cfg(test)] @@ -65,21 +53,12 @@ mod tests { assert_eq!(ctx.get_usize_value("not_usize_value"), None); } - #[test] - fn char_value() { - let mut ctx = Context::new(); - ctx.add("char_value", 'c'); - - assert_eq!(ctx.get_char_value("char_value"), Some('c')); - assert_eq!(ctx.get_char_value("not_char_value"), None); - } - #[test] fn default() { let mut ctx = Context::default(); - ctx.add("char_value", 'c'); + ctx.add("usize_value", 1); - assert_eq!(ctx.get_char_value("char_value"), Some('c')); - assert_eq!(ctx.get_char_value("not_char_value"), None); + assert_eq!(ctx.get_usize_value("usize_value"), Some(1)); + assert_eq!(ctx.get_usize_value("not_usize_value"), None); } } diff --git a/src/toolkit/deserializer.rs b/src/toolkit/deserializer.rs deleted file mode 100644 index b11099f..0000000 --- a/src/toolkit/deserializer.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::fmt::Display; - -use super::{context::Context, node::Node}; - -pub trait Branch -where - BranchNodes: Node + Display, -{ - fn push>(&mut self, node: CanBeNode); - fn get_maybe_nodes() -> Vec>; - fn get_fallback_node() -> Option>; - fn get_outer_token_length(&self) -> usize; - fn is_empty(&self) -> bool; - fn parse_branch(input: &str, delimeter: &str, mut branch: Self) -> Option - where - Self: Sized + Deserializer + Node, - { - let mut current_position = 0; - let mut next_position = 0; - let mut fallback_position = 0; - let maybe_nodes = Self::get_maybe_nodes(); - while current_position < input.len() { - let slice = &input[current_position..]; - next_position += slice.chars().next().unwrap().len_utf8(); - if delimeter.is_empty() || slice.starts_with(delimeter) || current_position == 0 { - let slice = if current_position == 0 { - slice - } else { - &slice[delimeter.len()..] - }; - for parser in &maybe_nodes { - if let Some(node) = parser(slice, branch.context()) { - while fallback_position != current_position { - fallback_position = branch - .fallback(&input[fallback_position..current_position], delimeter); - } - branch.push(node); - next_position = branch.len() - branch.get_outer_token_length(); - fallback_position = next_position; - break; - } - } - } - current_position = next_position; - } - while fallback_position < input.len() { - if Self::get_fallback_node().is_none() { - return None; - } else { - fallback_position = branch.fallback(&input[fallback_position..], delimeter); - } - } - Some(branch) - } - - fn fallback(&mut self, slice: &str, delimeter: &str) -> usize - where - Self: Node, - { - let slice = if self.is_empty() || slice.len() <= delimeter.len() { - slice - } else { - &slice[delimeter.len()..] - }; - if !slice.is_empty() { - self.push( - Self::get_fallback_node() - .map(|f| f(slice)) - .expect("Fallback node should always be available"), - ); - } - self.len() - self.get_outer_token_length() - } -} - -pub type MaybeNode = Box) -> Option>; -pub type DefinitelyNode = Box BranchNodes>; - -pub trait FallbackNode { - fn fallback_node() -> DefinitelyNode - where - Self: Into; -} - -pub trait Deserializer { - fn deserialize_with_context(input: &str, ctx: Option) -> Option - where - Self: Sized; - - fn deserialize(input: &str) -> Option - where - Self: Sized, - { - Self::deserialize_with_context(input, None) - } -} diff --git a/src/toolkit/matcher.rs b/src/toolkit/matcher.rs deleted file mode 100644 index b08264c..0000000 --- a/src/toolkit/matcher.rs +++ /dev/null @@ -1,254 +0,0 @@ -pub struct Matcher<'input> { - input: &'input str, - position: usize, -} - -#[derive(Debug, PartialEq)] -pub struct Match<'input> { - pub start_token: &'input str, - pub body: &'input str, - pub end_token: &'input str, -} - -impl Match<'_> { - pub fn len(&self) -> usize { - self.start_token.len() + self.body.len() + self.end_token.len() - } -} - -impl<'input> Matcher<'input> { - pub fn new(input: &'input str) -> Self { - Self { input, position: 0 } - } - - pub fn get_match( - &mut self, - start_sequence: &'input str, - end_sequence: &'input str, - match_end_of_input: bool, - ) -> Option> { - if !match_end_of_input - && !start_sequence.is_empty() - && !end_sequence.is_empty() - && start_sequence.len() == end_sequence.len() - && start_sequence != end_sequence - { - return self.get_balanced_match(start_sequence, end_sequence); - } - self.get_unbalanced_match(start_sequence, end_sequence, match_end_of_input) - } - - fn iterate( - &self, - sequence: &str, - start_position: usize, - fail_fast: bool, - match_end_of_input: bool, - ) -> Option<(usize, usize)> { - if sequence.is_empty() { - return Some((start_position, 0)); - } else { - if match_end_of_input && start_position == self.input.len() { - return Some((start_position, 0)); - } - if self.input[start_position..].starts_with(sequence) { - return Some((start_position + sequence.len(), sequence.len())); - } - for (index, _) in self.input.char_indices().skip(start_position) { - if self.input[index..].starts_with(sequence) { - return Some((index + sequence.len(), sequence.len())); - } else if fail_fast { - return None; - } - } - if match_end_of_input { - return Some((self.input.len(), 0)); - } - } - None - } - - fn get_unbalanced_match( - &mut self, - start_sequence: &'input str, - end_sequence: &'input str, - match_end_of_input: bool, - ) -> Option> { - if let Some((start_token_end_index, start_token_lenght)) = - self.iterate(start_sequence, self.position, true, false) - { - if let Some((end_token_end_index, end_token_length)) = self.iterate( - end_sequence, - start_token_end_index, - false, - match_end_of_input, - ) { - self.position = end_token_end_index; - return Some(Match { - start_token: &self.input - [(start_token_end_index - start_token_lenght)..start_token_end_index], - body: &self.input - [start_token_end_index..end_token_end_index - end_token_length], - end_token: &self.input - [(end_token_end_index - end_token_length)..end_token_end_index], - }); - } - } - None - } - - fn get_balanced_match( - &mut self, - start_sequence: &'input str, - end_sequence: &'input str, - ) -> Option> { - let mut balance = 1; - if let Some((start_token_end_index, _)) = - self.iterate(start_sequence, self.position, true, false) - { - for (index, _) in self.input.char_indices().skip(start_token_end_index) { - if self.input[index..].starts_with(start_sequence) { - balance += 1; - } else if self.input[index..].starts_with(end_sequence) { - balance -= 1; - if balance == 0 { - let end_token_end_index = index + end_sequence.len(); - let previous_position = self.position; - self.position = end_token_end_index; - return Some(Match { - start_token: &self.input[previous_position..start_token_end_index], - body: &self.input[start_token_end_index..index], - end_token: &self.input[index..end_token_end_index], - }); - } - } - } - } - None - } - - pub fn get_rest(&self) -> &'input str { - &self.input[self.position..] - } -} - -#[cfg(test)] -mod tests { - use crate::toolkit::matcher::{Match, Matcher}; - use pretty_assertions::assert_eq; - - #[test] - fn get_match() { - let mut matcher = Matcher::new("*italic 😉*~~one more~~ statement"); - assert_eq!( - matcher.get_match("*", "*", false), - Some(Match { - start_token: "*", - body: "italic 😉", - end_token: "*" - }) - ); - assert_eq!( - matcher.get_match("~~", "~~", false), - Some(Match { - start_token: "~~", - body: "one more", - end_token: "~~", - }) - ); - } - - #[test] - fn get_match_with_end_of_input() { - let mut matcher = Matcher::new("*italic*~~one more"); - assert_eq!( - matcher.get_match("*", "*", false), - Some(Match { - start_token: "*", - body: "italic", - end_token: "*" - }) - ); - assert_eq!( - matcher.get_match("~~", "~~", true), - Some(Match { - start_token: "~~", - body: "one more", - end_token: "", - }) - ); - } - - #[test] - fn continue_for_balanced_match() { - let mut matcher = Matcher::new("[link 😉](url)"); - assert_eq!( - matcher.get_match("[", "]", false), - Some(Match { - start_token: "[", - body: "link 😉", - end_token: "]" - }) - ); - assert_eq!( - matcher.get_match("(", ")", false), - Some(Match { - start_token: "(", - body: "url", - end_token: ")" - }) - ); - } - - #[test] - fn get_match_with_empty_start_token_and_macth_end() { - let mut matcher = Matcher::new("t"); - assert_eq!( - matcher.get_match("", "\n", true), - Some(Match { - start_token: "", - body: "t", - end_token: "" - }) - ) - } - - #[test] - fn get_match_with_empty_body() { - let mut matcher = Matcher::new("--"); - assert_eq!( - matcher.get_match("--", "\n", true), - Some(Match { - start_token: "--", - body: "", - end_token: "" - }) - ) - } - - #[test] - fn patterns_with_non_equal_length_can_not_be_balanced() { - let mut matcher = Matcher::new("(()t"); - assert_eq!( - matcher.get_match("(", ")t", false), - Some(Match { - start_token: "(", - body: "(", - end_token: ")t" - }) - ) - } - - #[test] - fn get_balanced_match() { - let mut matcher = Matcher::new("{{}}"); - assert_eq!( - matcher.get_match("{", "}", false), - Some(Match { - start_token: "{", - body: "{}", - end_token: "}" - }) - ); - } -} diff --git a/src/toolkit/mod.rs b/src/toolkit/mod.rs index e7fdcb7..b5c6020 100644 --- a/src/toolkit/mod.rs +++ b/src/toolkit/mod.rs @@ -1,4 +1,2 @@ pub mod context; -pub mod deserializer; -pub mod matcher; -pub mod node; +pub mod parser; diff --git a/src/toolkit/node.rs b/src/toolkit/node.rs deleted file mode 100644 index d0a315b..0000000 --- a/src/toolkit/node.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::{ - context::Context, - deserializer::{Deserializer, MaybeNode}, -}; - -pub trait Node { - fn len(&self) -> usize; - fn maybe_node() -> MaybeNode - where - Self: Sized + Deserializer + Into, - { - Box::new(|input, ctx| { - if let Some(node) = Self::deserialize_with_context(input, ctx) { - return Some(node.into()); - } - None - }) - } - - fn context(&self) -> Option { - None - } -} diff --git a/src/toolkit/parser.rs b/src/toolkit/parser.rs new file mode 100644 index 0000000..c0dd60d --- /dev/null +++ b/src/toolkit/parser.rs @@ -0,0 +1,103 @@ +use std::usize; + +use super::context::Context; + +pub type Parser = Box) -> Option<(N, usize)>>; +pub type Consumer = Box) -> (N, usize)>; + +pub fn parse_to_parser() -> Parser +where + P: Parse + Sized + Into, +{ + Box::new(move |input, current_position, ctx| { + P::parse(input, current_position, ctx).map(|(n, consumed)| (n.into(), consumed)) + }) +} + +pub fn parse_to_consumer() -> Consumer +where + P: Parse + Sized + Into, +{ + Box::new(move |input, current_position, ctx| { + let (n, consumed) = + P::parse(input, current_position, ctx).expect("consumer shoud never fail"); + (n.into(), consumed) + }) +} + +pub trait Parse { + fn parse(input: &str, current_position: usize, ctx: Option<&Context>) -> Option<(Self, usize)> + where + Self: Sized; +} + +pub trait Branch { + fn get_parsers(&self) -> Vec>; + fn get_consumer(&self) -> Option>; + fn push_node(&mut self, node: N); + fn consume( + &mut self, + from: &mut Option, + input: &str, + delimeter: &str, + ctx: Option<&Context>, + ) where + Self: Sized, + { + if let (Some(from), Some(consumer)) = (&from, self.get_consumer()) { + let mut position = *from; + while position < input.len() { + if !delimeter.is_empty() && input[position..].starts_with(delimeter) { + position += delimeter.len(); + } + + let (node, consumed) = consumer(&input[position..], 0, ctx); + self.push_node(node); + position += consumed; + } + } + *from = None; + } + + fn parse_branch(mut self, input: &str, delimeter: &str, ctx: Option) -> Option + where + Self: Sized, + { + let parsers = self.get_parsers(); + let ctx = ctx.as_ref(); + let mut position = 0; + let mut should_consume: Option = None; + + while position < input.len() { + let start = position; + let starts_with_delimeter = + delimeter.is_empty() || input[position..].starts_with(delimeter); + + if position == 0 || starts_with_delimeter { + if position != 0 && starts_with_delimeter { + position += delimeter.len(); + } + + if let Some((node, parsed)) = parsers.iter().find_map(|p| p(input, position, ctx)) { + self.consume(&mut should_consume, &input[..start], delimeter, ctx); + position += parsed; + self.push_node(node); + } + } + + if start == position { + let _ = self.get_consumer()?; + should_consume = should_consume.or(Some(position)); + position += &input[position..] + .chars() + .next() + .expect("always to have next character") + .len_utf8(); + } + } + + self.consume(&mut should_consume, input, delimeter, ctx); + + Some(self) + } +}