Skip to content

Commit

Permalink
heading node can contain anchors (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lurk authored Nov 29, 2023
1 parent e40fa37 commit 80ea636
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "yamd"
version = "0.11.1"
version = "0.12.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Yet Another Markdown Document (flavor)"
Expand Down
28 changes: 17 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@
//!
//! ## Elements:
//!
//! ### Heading
//!
//! Element that starts with one to seven "#" characters followed by space, followed by text, and ends with a new line
//! or EOF
//!
//! Example: ```# header``` or ```###### header```
//!
//! ### List
//!
//! Can contain nested lists. Each nesting level equals to number of spaces before list item.
Expand Down Expand Up @@ -194,6 +187,13 @@
//! %}
//! ```
//!
//! ### Heading
//!
//! Element that starts with one to seven "#" characters followed by space, followed by text and/or link, and ends
//! with a new line or EOF
//!
//! Example: ```# header``` or ```###### header``` or ```# [header](url)``` or ```# header [link](url)```
//!
use nodes::yamd::Yamd;
use toolkit::deserializer::Deserializer;
Expand Down Expand Up @@ -227,20 +227,26 @@ pub fn serialize(input: &Yamd) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::nodes::{anchor::Anchor, heading::Heading, paragraph::Paragraph};
use crate::nodes::{anchor::Anchor, heading::Heading, paragraph::Paragraph, text::Text};
use pretty_assertions::assert_eq;

#[test]
fn test_deserialize() {
let input = "# header";
let expected = Yamd::new(None, vec![Heading::new("header", 1).into()]);
let expected = Yamd::new(
None,
vec![Heading::new(1, vec![Text::new("header").into()]).into()],
);
let actual = deserialize(input).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn test_serialize() {
let input = Yamd::new(None, vec![Heading::new("header", 1).into()]);
let input = Yamd::new(
None,
vec![Heading::new(1, vec![Text::new("header").into()]).into()],
);
let expected = "# header";
let actual = serialize(&input);
assert_eq!(expected, actual);
Expand All @@ -252,7 +258,7 @@ mod tests {
let expected = Yamd::new(
None,
vec![
Heading::new("🤔", 2).into(),
Heading::new(2, vec![Text::new("🤔").into()]).into(),
Paragraph::new(vec![Anchor::new("link 😉", "url").into()]).into(),
],
);
Expand Down
23 changes: 17 additions & 6 deletions src/nodes/collapsible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,19 @@ mod cfg {
Collapsible::deserialize("{% Title\n# Heading\n%}"),
Some(Collapsible::new(
"Title",
vec![Heading::new("Heading", 1).into()]
vec![Heading::new(1, vec![Text::new("Heading").into()]).into()]
))
);
}

#[test]
fn test_collapsible_len() {
assert_eq!(
Collapsible::new("Title", vec![Heading::new("Heading", 1).into()]).len(),
Collapsible::new(
"Title",
vec![Heading::new(1, vec![Text::new("Heading").into()]).into()]
)
.len(),
21
);
assert_eq!(Collapsible::new("Title", vec![]).len(), 12);
Expand All @@ -246,7 +250,11 @@ mod cfg {
#[test]
fn test_collapsible_serialize() {
assert_eq!(
Collapsible::new("Title", vec![Heading::new("Heading", 1).into()]).to_string(),
Collapsible::new(
"Title",
vec![Heading::new(1, vec![Text::new("Heading").into()]).into()]
)
.to_string(),
"{% Title\n# Heading\n%}"
);
}
Expand Down Expand Up @@ -291,7 +299,7 @@ t**b**
let tab = Collapsible::new(
"Title",
vec![
Heading::new("hello", 1).into(),
Heading::new(1, vec![Text::new("hello").into()]).into(),
Code::new("rust", "let a=1;").into(),
Paragraph::new(vec![
Text::new("t").into(),
Expand Down Expand Up @@ -328,8 +336,11 @@ t**b**
.into(),
Embed::new("youtube", "123").into(),
Embed::new("cloudinary_gallery", "cloud_name&tag").into(),
Collapsible::new("nested collapsible", vec![Heading::new("nested", 1).into()])
.into(),
Collapsible::new(
"nested collapsible",
vec![Heading::new(1, vec![Text::new("nested").into()]).into()],
)
.into(),
],
);
assert_eq!(tab.to_string(), input);
Expand Down
132 changes: 112 additions & 20 deletions src/nodes/heading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,66 @@ use std::fmt::Display;

use serde::Serialize;

use crate::toolkit::{context::Context, deserializer::Deserializer, matcher::Matcher, node::Node};
use crate::toolkit::{
context::Context,
deserializer::{Branch, DefinitelyNode, Deserializer, FallbackNode, MaybeNode},
matcher::Matcher,
node::Node,
};

use super::{anchor::Anchor, text::Text};

#[derive(Debug, PartialEq, Serialize, Clone)]
pub enum HeadingNodes {
Text(Text),
Anchor(Anchor),
}

impl From<Text> for HeadingNodes {
fn from(text: Text) -> Self {
Self::Text(text)
}
}

impl From<Anchor> for HeadingNodes {
fn from(anchor: Anchor) -> Self {
Self::Anchor(anchor)
}
}

impl Node for HeadingNodes {
fn len(&self) -> usize {
match self {
Self::Text(text) => text.len(),
Self::Anchor(anchor) => anchor.len(),
}
}
}

impl Display for HeadingNodes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(text) => write!(f, "{}", text),
Self::Anchor(anchor) => write!(f, "{}", anchor),
}
}
}

#[derive(Debug, PartialEq, Serialize, Clone)]
pub struct Heading {
pub level: u8,
pub text: String,
pub nodes: Vec<HeadingNodes>,
}

impl Heading {
pub fn new<S: Into<String>>(text: S, level: u8) -> Self {
pub fn new(level: u8, nodes: Vec<HeadingNodes>) -> Self {
let normalized_level = match level {
0 => 1,
7.. => 6,
l => l,
};
Heading {
text: text.into(),
nodes,
level: normalized_level,
}
}
Expand All @@ -31,10 +74,11 @@ impl Deserializer for Heading {
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 Some(Self::new(
return Self::parse_branch(
heading.body,
(start_tokens.len() - i).try_into().unwrap_or(1),
));
"",
Self::new((start_tokens.len() - i).try_into().unwrap_or(1), vec![]),
);
}
}

Expand All @@ -45,60 +89,93 @@ impl Deserializer for Heading {
impl Display for Heading {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let level = String::from('#').repeat(self.level as usize);
write!(f, "{} {}", level, self.text)
write!(
f,
"{} {}",
level,
self.nodes.iter().map(|n| n.to_string()).collect::<String>()
)
}
}

impl Node for Heading {
fn len(&self) -> usize {
self.text.len() + self.level as usize + 1
self.nodes.iter().map(|n| n.len()).sum::<usize>() + self.get_outer_token_length()
}
}

impl Branch<HeadingNodes> for Heading {
fn push<I: Into<HeadingNodes>>(&mut self, node: I) {
self.nodes.push(node.into());
}

fn get_maybe_nodes() -> Vec<MaybeNode<HeadingNodes>> {
vec![Anchor::maybe_node()]
}

fn get_fallback_node() -> Option<DefinitelyNode<HeadingNodes>> {
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::toolkit::{deserializer::Deserializer, node::Node};
use crate::{
nodes::{anchor::Anchor, text::Text},
toolkit::{deserializer::Deserializer, node::Node},
};
use pretty_assertions::assert_eq;

#[test]
fn level_one() {
assert_eq!(Heading::new("Header", 1).to_string(), "# Header");
assert_eq!(
Heading::new(1, vec![Text::new("Header").into()]).to_string(),
"# Header"
);
}

#[test]
fn level_gt_six() {
let h = Heading::new("Header", 7).to_string();
let h = Heading::new(7, vec![Text::new("Header").into()]).to_string();
assert_eq!(h, "###### Header");
let h = Heading::new("Header", 34).to_string();
let h = Heading::new(34, vec![Text::new("Header").into()]).to_string();
assert_eq!(h, "###### Header");
}

#[test]
fn level_eq_zero() {
let h = Heading::new("Header", 0).to_string();
let h = Heading::new(0, vec![Text::new("Header").into()]).to_string();
assert_eq!(h, "# Header");
}

#[test]
fn level_eq_four() {
let h = Heading::new("Header", 4).to_string();
let h = Heading::new(4, vec![Text::new("Header").into()]).to_string();
assert_eq!(h, "#### Header");
}

#[test]
fn from_string() {
assert_eq!(
Heading::deserialize("## Header"),
Some(Heading::new("Header", 2))
Some(Heading::new(2, vec![Text::new("Header").into()]))
);
assert_eq!(
Heading::deserialize("### Head"),
Some(Heading::new("Head", 3))
Some(Heading::new(3, vec![Text::new("Head").into()]))
);
assert_eq!(
Heading::deserialize("### Head\n\nsome other thing"),
Some(Heading::new("Head", 3))
Some(Heading::new(3, vec![Text::new("Head").into()]))
);
assert_eq!(Heading::deserialize("not a header"), None);
assert_eq!(Heading::deserialize("######"), None);
Expand All @@ -107,7 +184,22 @@ mod tests {

#[test]
fn len() {
assert_eq!(Heading::new("h", 1).len(), 3);
assert_eq!(Heading::new("h", 2).len(), 4);
assert_eq!(Heading::new(1, vec![Text::new("h").into()]).len(), 3);
assert_eq!(Heading::new(2, vec![Text::new("h").into()]).len(), 4);
}

#[test]
fn with_anchor() {
let str = "## hey [a](b)";
let h = Heading::deserialize(str);
assert_eq!(
h,
Some(Heading::new(
2,
vec![Text::new("hey ").into(), Anchor::new("a", "b").into()]
))
);
assert_eq!(h.as_ref().unwrap().len(), 13);
assert_eq!(h.as_ref().unwrap().to_string(), str);
}
}
10 changes: 5 additions & 5 deletions src/nodes/yamd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ end"#;
#[test]
fn push() {
let mut t = Yamd::new(None, vec![]);
t.push(Heading::new("header", 1));
t.push(Heading::new(1, vec![Text::new("header").into()]));
t.push(Paragraph::new(vec![Text::new("text").into()]));

assert_eq!(t.to_string(), "# header\n\ntext".to_string());
Expand All @@ -303,7 +303,7 @@ end"#;
let t: String = Yamd::new(
None,
vec![
Heading::new("header", 1).into(),
Heading::new(1, vec![Text::new("header").into()]).into(),
Paragraph::new(vec![Text::new("text").into()]).into(),
],
)
Expand Down Expand Up @@ -333,7 +333,7 @@ end"#;
consumed_length: Some(101),
}),
vec![
Heading::new("hello", 1).into(),
Heading::new(1, vec![Text::new("hello").into()]).into(),
Code::new("rust", "let a=1;").into(),
Paragraph::new(vec![
Text::new("t").into(),
Expand Down Expand Up @@ -405,7 +405,7 @@ end"#;
Some(vec!["tag1".to_string(), "tag2".to_string()]),
)),
vec![
Heading::new("hello", 1).into(),
Heading::new(1, vec![Text::new("hello").into()]).into(),
Code::new("rust", "let a=1;").into(),
Paragraph::new(vec![
Text::new("t").into(),
Expand Down Expand Up @@ -491,7 +491,7 @@ end"#;
Paragraph::new(vec![Text::new("1").into()]).into(),
Paragraph::new(vec![Text::new("2").into()]).into(),
Paragraph::new(vec![Text::new("3").into()]).into(),
Heading::new("header", 1).into(),
Heading::new(1, vec![Text::new("header").into()]).into(),
],
);
let actual = Yamd::deserialize(input).unwrap();
Expand Down

0 comments on commit 80ea636

Please sign in to comment.