etch 0.4.2

Not just a text formatter, don't mark it down, etch it.
Documentation
use crate::types::{Attributes, Elements, Token};

pub trait Renderable {
    fn html(&self) -> String;
    fn text(&self) -> String;
}

impl Renderable for Token {
    fn html(&self) -> String {
        let borrowed = &self;

        borrowed.html()
    }

    fn text(&self) -> String {
        let borrowed = &self;

        borrowed.text()
    }
}

impl Renderable for &Token {
    fn html(&self) -> String {
        let mut result = String::new();

        match self {
            Token::Fragment { children } => {
                result.push_str(&children.html());
            }
            Token::Element {
                name,
                attributes,
                children,
                ..
            } => {
                result.push_str(&html_element(name, attributes, children));
            }
            Token::Span {
                attributes,
                children,
                ..
            } => {
                result.push_str(&html_element("span", attributes, children));
            }
            Token::Html { html } => {
                result.push_str(&html);
            }
            Token::Text { text } => {
                result.push_str(
                    &text
                        .replace('&', "&")
                        .replace('"', """)
                        .replace('<', "&lt;")
                        .replace('>', "&gt;"),
                );
            }
            Token::Whitespace => {
                result.push_str(" ");
            }
            Token::Placeholder { .. } | Token::Removed => {}
        }

        result
    }

    fn text(&self) -> String {
        let mut result = String::new();

        match self {
            Token::Fragment { children }
            | Token::Element { children, .. }
            | Token::Span { children, .. } => {
                result.push_str(&children.text());
            }
            Token::Html { html } => {
                result.push_str(html);
            }
            Token::Text { text } => {
                result.push_str(text);
            }
            Token::Whitespace => {
                result.push_str(" ");
            }
            Token::Placeholder { .. } | Token::Removed => {}
        };

        result
    }
}

impl Renderable for &Attributes {
    fn html(&self) -> String {
        let mut result = String::new();

        if let Some(attributes) = &self {
            for (key, value) in attributes {
                result.push_str(&format!(
                    r#" {}="{}""#,
                    key,
                    value
                        .replace('&', "&amp;")
                        .replace('"', "&quot;")
                        .replace('<', "&lt;")
                        .replace('>', "&gt;")
                ));
            }
        }

        result
    }

    fn text(&self) -> String {
        String::new()
    }
}

impl Renderable for &Elements {
    fn html(&self) -> String {
        let mut result = String::new();

        if let Some(elements) = &self {
            for element in elements {
                result.push_str(&element.html());
            }
        }

        result
    }

    fn text(&self) -> String {
        let mut result = String::new();

        if let Some(elements) = &self {
            for element in elements {
                result.push_str(&element.text());
            }
        }

        result
    }
}

enum ElementType {
    Text,
    LinkedText,
    Image,
    LinkedImage,
}

fn html_element(name: &str, attributes: &Attributes, elements: &Elements) -> String {
    let mut result = String::new();

    match if let Some(attributes) = attributes {
        if attributes.contains_key("href") && attributes.contains_key("src") {
            ElementType::LinkedImage
        } else if attributes.contains_key("src") {
            ElementType::Image
        } else if attributes.contains_key("href") {
            ElementType::LinkedText
        } else {
            ElementType::Text
        }
    } else {
        ElementType::Text
    } {
        ElementType::LinkedImage => {
            let mut attributes = attributes.clone().unwrap();
            let href = attributes.remove("href").unwrap();

            result.push_str(&format!(
                r#"<a href="{href}"><img{attributes} alt="{alt}"></a>"#,
                alt = &Token::Element {
                    name: "none".into(),
                    tags: None,
                    attributes: None,
                    children: elements.clone(),
                }
                .text(),
                attributes = (&Some(attributes)).html(),
                href = href,
            ));
        }
        ElementType::Image => result.push_str(&format!(
            r#"<img{attributes} alt="{alt}">"#,
            alt = &Token::Element {
                name: "none".into(),
                tags: None,
                attributes: None,
                children: elements.clone(),
            }
            .text(),
            attributes = attributes.html(),
        )),
        ElementType::LinkedText => result.push_str(&format!(
            "<{name}><a{attributes}>{children}</a></{name}>",
            name = name,
            attributes = attributes.html(),
            children = elements.html(),
        )),
        _ => result.push_str(&format!(
            "<{name}{attributes}>{children}</{name}>",
            name = name,
            attributes = attributes.html(),
            children = elements.html(),
        )),
    };

    result
}