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#[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}