1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
use proc_macro::TokenStream;
use quote::quote;
use syn::{
    parse::{discouraged::Speculative, ParseBuffer},
    Expr, ExprLit, Result, Token,
};
use syn_rsx::{parse_with_config, Node, NodeType, ParserConfig};

fn walk_nodes(nodes: Vec<Node>, nodes_context: Option<NodeType>) -> (String, Vec<Expr>) {
    let mut out = String::new();
    let mut values = vec![];

    for node in nodes {
        match node.node_type {
            NodeType::Element => {
                let name = node.name_as_string().expect("unexpected missing node name");
                out.push_str(&format!("<{}", name));

                // attributes
                let (html_string, attribute_values) =
                    walk_nodes(node.attributes, Some(NodeType::Attribute));
                out.push_str(&html_string);
                values.extend(attribute_values);
                out.push('>');

                // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
                match name.as_str() {
                    "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
                    | "meta" | "param" | "source" | "track" | "wbr" => continue,
                    _ => (),
                }

                // children
                let (html_string, children_values) =
                    walk_nodes(node.children, Some(NodeType::Element));
                out.push_str(&html_string);
                values.extend(children_values);

                out.push_str(&format!("</{}>", name));
            }
            NodeType::Attribute => {
                out.push_str(&format!(
                    " {}",
                    node.name_as_string().expect("unexpected missing node name")
                ));
                if node.value.is_some() {
                    out.push_str(r#"="{}""#);
                    values.push(node.value.expect("unexpected missing node value"));
                }
            }
            NodeType::Text | NodeType::Block => {
                if let Some(nodes_context) = &nodes_context {
                    // If the nodes context is attribute we prefix with whitespace
                    if nodes_context == &NodeType::Attribute {
                        out.push(' ');
                    }
                }

                out.push_str("{}");
                values.push(node.value.expect("unexpected missing node value"));
            }
            NodeType::Fragment => {
                let (html_string, children_values) =
                    walk_nodes(node.children, Some(NodeType::Fragment));
                out.push_str(&html_string);
                values.extend(children_values);
            }
            NodeType::Comment => {
                out.push_str("<!-- {} -->");
                values.push(node.value.expect("unexpected missing node value"));
            }
            NodeType::Doctype => {
                let value = node
                    .value_as_string()
                    .expect("unexpected missing node value");
                out.push_str(&format!("<!DOCTYPE {}>", value));
            }
        }
    }

    (out, values)
}

/// Converts HTML to `String`
///
/// This macro should only be used from the `yate` crate, not directly, as
/// things will break otherwise.
#[proc_macro]
pub fn html(tokens: TokenStream) -> TokenStream {
    match parse_with_config(
        tokens,
        ParserConfig::new().transform_block(|input| {
            let fork = input.fork();
            if let Ok(text) = parse_escape_syntax(&fork) {
                input.advance_to(&fork);
                if !input.is_empty() {
                    return Err(input.error("escape syntax should end after the last %"));
                }

                Ok(Some(quote! { yate::html_escape::encode_text(#text) }))
            } else {
                Ok(None)
            }
        }),
    ) {
        Ok(nodes) => {
            let (html_string, values) = walk_nodes(nodes, None);
            quote! { format!(#html_string, #(#values),*) }
        }
        Err(error) => error.to_compile_error(),
    }
    .into()
}

fn parse_escape_syntax(input: &ParseBuffer) -> Result<ExprLit> {
    input.parse::<Token![%]>()?;
    input.parse::<Token![=]>()?;
    let text = input.parse::<ExprLit>()?;
    input.parse::<Token![%]>()?;

    Ok(text)
}