mtml_parser/
serializer.rs

1extern crate clap;
2use clap::{Parser, ValueEnum};
3
4use super::ast::{Node::*, *};
5
6#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
7pub enum FunctionTagStyle {
8    Dollar = 1,
9    SelfClosing,
10}
11
12#[derive(Debug, Clone, Parser)]
13#[command(author, version, about, long_about = None)]
14pub struct Options {
15    #[arg(short, long, default_value = "mt:")]
16    pub prefix: String,
17    #[arg(short, long, value_enum, default_value = "dollar")]
18    pub function_tag_style: FunctionTagStyle,
19}
20
21fn attribute_to_string(attr: Attribute) -> String {
22    format!(
23        " {}={}",
24        attr.name,
25        attr.values
26            .iter()
27            .map({
28                |AttributeValue { value, .. }| {
29                    if value.contains("\"") {
30                        format!("'{}'", value)
31                    } else {
32                        format!(r#""{}""#, value)
33                    }
34                }
35            })
36            .collect::<Vec<String>>()
37            .join(",")
38    )
39}
40
41/// Serialize AST to MTML document.
42///
43/// # Examples
44///
45/// ```
46/// use mtml_parser::{parse, serialize};
47///
48/// let node = match parse("<body><mt:Entries><mt:EntryTitle /></mt:Entries></body>") {
49///   Ok(node) => node,
50///   Err(err) => panic!("{}", err),
51/// };
52/// serialize(node, None);
53/// ```
54pub fn serialize(node: Node, options: Option<Options>) -> String {
55    let mut s = String::new();
56    let options = options.unwrap_or(Options {
57        prefix: "mt:".to_string(),
58        function_tag_style: FunctionTagStyle::Dollar,
59    });
60
61    match node {
62        Root(RootNode { children }) => {
63            for child in children {
64                s.push_str(&serialize(child, Some(options.clone())));
65            }
66        }
67        Text(TextNode { value, .. }) => {
68            s.push_str(value.as_str());
69        }
70        FunctionTag(FunctionTagNode {
71            name, attributes, ..
72        }) => {
73            let pre_sign = if name.to_lowercase() == "else" || name.to_lowercase() == "elseif" {
74                ""
75            } else if options.function_tag_style == FunctionTagStyle::Dollar {
76                "$"
77            } else {
78                ""
79            };
80
81            let post_sign = if name.to_lowercase() == "else" || name.to_lowercase() == "elseif" {
82                ""
83            } else if options.function_tag_style == FunctionTagStyle::Dollar {
84                "$"
85            } else {
86                "/"
87            };
88
89            s.push_str(&format!("<{}{}{}", pre_sign, options.prefix, name));
90            for attr in attributes {
91                s.push_str(&attribute_to_string(attr))
92            }
93            s.push_str(&format!("{}>", post_sign));
94        }
95        BlockTag(BlockTagNode {
96            name,
97            children,
98            attributes,
99            ..
100        }) => {
101            s.push_str(&format!("<{}{}", options.prefix, name));
102            for attr in attributes {
103                s.push_str(&attribute_to_string(attr))
104            }
105            s.push_str(">");
106            for child in children {
107                s.push_str(&serialize(child, Some(options.clone())));
108            }
109            s.push_str(&format!("</{}{}>", options.prefix, name));
110        }
111    }
112
113    return s;
114}
115
116#[cfg(test)]
117mod tests {
118    use super::super::parser::*;
119    use super::*;
120
121    const INPUT: &str = r#"
122<html>
123  <body>
124    <mt:Entries    limit="10">
125      <mtEntryTitle encode_html='1'/>
126    </mt:Entries>
127  </body>
128</html>"#;
129
130    #[test]
131    fn test_serialize() {
132        let root = parse(INPUT).unwrap();
133        let serialized = serialize(root, None);
134        assert_eq!(
135            serialized,
136            r#"
137<html>
138  <body>
139    <mt:Entries limit="10">
140      <$mt:EntryTitle encode_html="1"$>
141    </mt:Entries>
142  </body>
143</html>"#
144        )
145    }
146
147    #[test]
148    fn test_serialize_self_closing() {
149        let root = parse(INPUT).unwrap();
150        let serialized = serialize(
151            root,
152            Some(Options {
153                prefix: "mt:".to_string(),
154                function_tag_style: FunctionTagStyle::SelfClosing,
155            }),
156        );
157        assert_eq!(
158            serialized,
159            r#"
160<html>
161  <body>
162    <mt:Entries limit="10">
163      <mt:EntryTitle encode_html="1"/>
164    </mt:Entries>
165  </body>
166</html>"#
167        )
168    }
169
170    #[test]
171    fn test_serialize_prefix() {
172        let root = parse(INPUT).unwrap();
173        let serialized = serialize(
174            root,
175            Some(Options {
176                prefix: "MT".to_string(),
177                function_tag_style: FunctionTagStyle::Dollar,
178            }),
179        );
180        assert_eq!(
181            serialized,
182            r#"
183<html>
184  <body>
185    <MTEntries limit="10">
186      <$MTEntryTitle encode_html="1"$>
187    </MTEntries>
188  </body>
189</html>"#
190        )
191    }
192
193    #[test]
194    fn test_serialize_single_quote() {
195        let root = parse(
196            r#"
197<html>
198  <body>
199    <mt:Entries limit="10">
200      <mtEntryTitle replace='"',"'"/>
201    </mt:Entries>
202  </body>
203</html>"#,
204        )
205        .unwrap();
206        let serialized = serialize(
207            root,
208            Some(Options {
209                prefix: "mt:".to_string(),
210                function_tag_style: FunctionTagStyle::Dollar,
211            }),
212        );
213        assert_eq!(
214            serialized,
215            r#"
216<html>
217  <body>
218    <mt:Entries limit="10">
219      <$mt:EntryTitle replace='"',"'"$>
220    </mt:Entries>
221  </body>
222</html>"#
223        )
224    }
225
226    #[test]
227    fn test_serialize_else_tag() {
228        let root = parse(
229            r#"
230<html>
231  <body>
232    <mt:If name="foo">
233    Foo
234    <mt:ElseIf name="bar">
235    Bar
236    <mt:Else>
237    Baz
238    </mt:If>
239  </body>
240</html>"#,
241        )
242        .unwrap();
243        let serialized = serialize(root, None);
244        assert_eq!(
245            serialized,
246            r#"
247<html>
248  <body>
249    <mt:If name="foo">
250    Foo
251    <mt:ElseIf name="bar">
252    Bar
253    <mt:Else>
254    Baz
255    </mt:If>
256  </body>
257</html>"#
258        )
259    }
260}