Skip to main content

yamd/nodes/
heading.rs

1use std::fmt::Display;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use super::Anchor;
7
8#[derive(Debug, PartialEq, Clone, Eq)]
9#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10#[cfg_attr(feature = "serde", serde(tag = "type", content = "value"))]
11pub enum HeadingNodes {
12    Text(String),
13    Anchor(Anchor),
14}
15
16impl From<String> for HeadingNodes {
17    fn from(text: String) -> Self {
18        Self::Text(text)
19    }
20}
21
22impl From<Anchor> for HeadingNodes {
23    fn from(anchor: Anchor) -> Self {
24        Self::Anchor(anchor)
25    }
26}
27
28impl Display for HeadingNodes {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            HeadingNodes::Text(text) => {
32                write!(f, "{}", text.replace("\n\n", "\\\n\n").replace("#", "\\#"))
33            }
34            HeadingNodes::Anchor(anchor) => write!(f, "{}", anchor),
35        }
36    }
37}
38
39/// # Heading
40///
41/// Starts with [Hash](type@crate::lexer::TokenKind::Hash) of length < 7, followed by
42/// [Space](type@crate::lexer::TokenKind::Space).
43///
44/// [Level](Heading::level) is determined by the amount of [Hash](type@crate::lexer::TokenKind::Hash)'es
45/// before [Space](type@crate::lexer::TokenKind::Space).
46///
47/// [Body](Heading::body) can contain one or more:
48///
49/// - [Anchor]
50/// - [String]
51///
52/// Example:
53///
54/// ```text
55/// ### Header can contain an [anchor](#) or regular text.
56/// ```
57///
58/// HTML equivalent:
59///
60/// ```html
61/// <h3>Header can contain an <a href="#">anchor</a> or regular text.</h3>
62/// ```
63#[derive(Debug, PartialEq, Clone, Eq)]
64#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
65pub struct Heading {
66    pub level: u8,
67    pub body: Vec<HeadingNodes>,
68}
69
70impl Heading {
71    pub fn new(level: u8, nodes: Vec<HeadingNodes>) -> Self {
72        Self { level, body: nodes }
73    }
74}
75
76impl Display for Heading {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{} ", "#".repeat(self.level as usize))?;
79        for node in &self.body {
80            write!(f, "{}", node)?;
81        }
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::nodes::{Anchor, Heading, HeadingNodes};
89
90    #[test]
91    fn heading() {
92        let heading = Heading::new(
93            3,
94            vec![
95                HeadingNodes::from("Header can contain an ".to_string()),
96                HeadingNodes::from(Anchor::new("anchor".to_string(), "#".to_string())),
97                HeadingNodes::from(" or regular text.".to_string()),
98            ],
99        );
100        assert_eq!(
101            heading.to_string(),
102            "### Header can contain an [anchor](#) or regular text."
103        );
104    }
105
106    #[test]
107    fn heading_with_hash() {
108        let heading = Heading::new(3, vec![HeadingNodes::from("# ##".to_string())]);
109        assert_eq!(heading.to_string(), "### \\# \\#\\#");
110    }
111}