pug/
lib.rs

1#[macro_use]
2extern crate pest_derive;
3
4pub use pest::error::Error;
5pub use pest::RuleType;
6
7use pest::Parser;
8#[cfg(target_arch = "wasm32")]
9use wasm_bindgen::prelude::*;
10
11#[derive(Parser)]
12#[grammar = "pug.pest"]
13pub struct PugParser;
14
15fn generate(file: &str) -> Result<String, Error<Rule>> {
16    let mut file = PugParser::parse(Rule::file, file)?;
17    let mut html = String::new();
18
19    let mut previous_was_text = false;
20    let mut comment = None;
21    let mut indent = 0;
22    let mut tagstack: Vec<(usize, String)> = Vec::new();
23
24    for decl in file.next().unwrap().into_inner() {
25        match decl.as_rule() {
26            Rule::indent => {
27                indent = decl.as_str().len();
28
29                if let Some(ind) = comment {
30                    if indent > ind {
31                        continue;
32                    } else {
33                        comment = None;
34                    }
35                }
36
37                while let Some((ind, element)) = tagstack.last().cloned() {
38                    if ind >= indent {
39                        html.push_str("</");
40                        html.push_str(&element);
41                        html.push_str(">");
42                        tagstack.pop();
43                    } else {
44                        break;
45                    }
46                }
47            }
48            Rule::tag => {
49                if comment.is_some() {
50                    continue;
51                }
52                previous_was_text = false;
53
54                let mut element = "div".to_string();
55                let mut id = None;
56                let mut class = Vec::new();
57                let mut attrs = Vec::new();
58                for e in decl.into_inner() {
59                    match e.as_rule() {
60                        Rule::element => {
61                            element = e.as_str().to_string();
62                        }
63                        Rule::class => {
64                            class.push(e.into_inner().next().unwrap().as_str().to_string());
65                        }
66                        Rule::id => {
67                            id = Some(e.into_inner().next().unwrap().as_str().to_string());
68                        }
69                        Rule::attrs => {
70                            for e in e.into_inner() {
71                                let mut e = e.into_inner();
72                                let key = e.next().unwrap().as_str();
73                                let value = e.next().unwrap();
74                                if key == "id" {
75                                    id = Some(
76                                        value.into_inner().next().unwrap().as_str().to_string(),
77                                    );
78                                } else if key == "class" {
79                                    class.push(
80                                        value.into_inner().next().unwrap().as_str().to_string(),
81                                    );
82                                } else {
83                                    attrs.push(format!("{}={}", key, value.as_str()));
84                                }
85                            }
86                        }
87                        _ => unreachable!(),
88                    }
89                }
90
91                html.push('<');
92                html.push_str(&element);
93                if !class.is_empty() {
94                    html.push_str(" class=\"");
95                    html.push_str(&class.join(" "));
96                    html.push('"');
97                }
98                if let Some(id) = id {
99                    html.push_str(" id=\"");
100                    html.push_str(&id);
101                    html.push('"');
102                }
103                for attr in attrs {
104                    html.push(' ');
105                    html.push_str(&attr);
106                }
107                html.push('>');
108                tagstack.push((indent, element));
109            }
110            Rule::comment => {
111                if comment.is_some() {
112                    continue;
113                }
114                comment = Some(indent);
115            }
116            Rule::text => {
117                if comment.is_some() {
118                    continue;
119                }
120                if previous_was_text {
121                    html.push('\n')
122                }
123                html.push_str(decl.as_str());
124                previous_was_text = true;
125            }
126            Rule::EOI => {
127                for (_, element) in tagstack.drain(..).rev() {
128                    html.push_str("</");
129                    html.push_str(&element);
130                    html.push_str(">");
131                }
132            }
133            any => panic!(println!("parser bug. did not expect: {:?}", any)),
134        }
135    }
136
137    Ok(html)
138}
139
140/// Render a Pug template into html.
141#[cfg(not(target_arch = "wasm32"))]
142pub fn parse(mut file: String) -> Result<String, Error<Rule>> {
143    file.push('\n');
144
145    generate(&file)
146}
147
148#[cfg(target_arch = "wasm32")]
149#[wasm_bindgen]
150pub fn parse(mut file: String) -> Option<String> {
151    file.push('\n');
152
153    generate(&file).ok()
154}
155
156#[test]
157pub fn valid_identitifer_characters() {
158    let html = parse(
159        r#"a(a="b",a-:.b.="c"
160x="y")"#
161            .to_string(),
162    )
163    .unwrap();
164    assert_eq!(html, r#"<a a="b" a-:.b.="c" x="y"></a>"#);
165}
166
167#[test]
168pub fn emptyline() {
169    let html = parse(
170        r#"
171a
172  b
173
174  c
175
176"#
177        .to_string(),
178    )
179    .unwrap();
180    assert_eq!(html, r#"<a><b></b><c></c></a>"#);
181}
182
183#[test]
184pub fn dupclass() {
185    let html = parse(r#"a#x.b(id="v" class="c")"#.to_string()).unwrap();
186    assert_eq!(html, r#"<a class="b c" id="v"></a>"#);
187}
188
189#[test]
190pub fn preserve_newline_in_multiline_text() {
191    let html = parse(
192        r#"pre
193  | The pipe always goes at the beginning of its own line,
194  | not counting indentation.
195  |   lol look at me
196  |   getting all getho indent
197  |     watt"#
198            .to_string(),
199    )
200    .unwrap();
201    assert_eq!(
202        html,
203        r#"<pre>The pipe always goes at the beginning of its own line,
204not counting indentation.
205  lol look at me
206  getting all getho indent
207    watt</pre>"#
208    );
209}
210
211#[test]
212pub fn eoi() {
213    let html = parse(
214        r#"body#blorp.herp.derp
215  a(href="google.de")
216derp
217  yorlo jaja"#
218            .to_string(),
219    )
220    .unwrap();
221    assert_eq!(html,
222    r#"<body class="herp derp" id="blorp"><a href="google.de"></a></body><derp><yorlo>jaja</yorlo></derp>"#
223    );
224
225    let html = parse(
226        r#"body#blorp.herp.derp
227  a(href="google.de")
228derp
229  yorlo jaja
230  "#
231        .to_string(),
232    )
233    .unwrap();
234    assert_eq!(html,
235    r#"<body class="herp derp" id="blorp"><a href="google.de"></a></body><derp><yorlo>jaja</yorlo></derp>"#
236    );
237
238    let html = parse(
239        r#"body#blorp.herp.derp
240  a(href="google.de")
241derp
242  yorlo jaja
243
244
245
246"#
247        .to_string(),
248    )
249    .unwrap();
250    assert_eq!(html,
251    r#"<body class="herp derp" id="blorp"><a href="google.de"></a></body><derp><yorlo>jaja</yorlo></derp>"#
252    );
253}