1#[derive(Debug)]
2enum Attr {
3  KeyValue(String, String),
4  Spread(String),
5}
6
7#[derive(Debug)]
8enum Node {
9  Element {
10    tag: String,
11    attrs: Vec<Attr>,
12    children: Vec<Node>,
13  },
14  Text(String),
15  JSExpression(String),
16}
17
18struct Parser<'a> {
19  input: &'a [u8],
20  pos: usize,
21}
22
23impl<'a> Parser<'a> {
24  fn new(input: &'a str) -> Self {
25    Parser {
26      input: input.as_bytes(),
27      pos: 0,
28    }
29  }
30
31  fn parse(&mut self) -> Vec<Node> {
32    let mut nodes = Vec::new();
33    while self.pos < self.input.len() {
34      self.skip_whitespace();
35      if self.starts_with("</") {
36        break;
37      } else if self.starts_with("<") {
38        nodes.push(self.parse_element());
39      } else {
40        nodes.push(self.parse_text());
41      }
42    }
43    nodes
44  }
45
46  fn parse_element(&mut self) -> Node {
47    self.consume("<");
48    let tag = self.consume_identifier();
49    let attrs = self.parse_attributes();
50
51    if self.starts_with("/>") {
52      self.consume("/>");
53      Node::Element {
54        tag,
55        attrs,
56        children: Vec::new(),
57      }
58    } else {
59      self.consume(">");
60      let children = self.parse();
61      self.consume("</");
62      let end_tag = self.consume_identifier();
63      assert_eq!(tag, end_tag, "Mismatched closing tag");
64      self.consume(">");
65      Node::Element {
66        tag,
67        attrs,
68        children,
69      }
70    }
71  }
72
73  fn parse_braced_content(&mut self) -> Node {
74    self.consume("{");
75    let mut output = String::new();
76
77    while self.pos < self.input.len() {
78      if self.starts_with("}") {
79        self.consume("}");
80        break;
81      } else if self.starts_with("<") {
82        let jsx_node = self.parse_element();
84        let compiled_jsx = compile_node(&jsx_node, None);
85        output.push_str(&compiled_jsx);
86      } else {
87        output.push(self.advance());
88      }
89    }
90
91    Node::JSExpression(output.trim().to_string())
92  }
93
94  fn parse_attributes(&mut self) -> Vec<Attr> {
95    let mut attrs = Vec::new();
96    loop {
97      self.skip_whitespace();
98      if self.peek() == '>' || self.starts_with("/>") {
99        break;
100      }
101
102      if self.starts_with("{...") {
103        self.consume("{...");
104        let mut expr = String::new();
105        while self.peek() != '}' {
106          expr.push(self.advance());
107        }
108        self.consume("}");
109        attrs.push(Attr::Spread(expr.trim().to_string()));
110        continue;
111      }
112
113      let name = self.consume_identifier();
114      self.skip_whitespace();
115
116      let value = if self.starts_with("=") {
117        self.consume("=");
118        self.skip_whitespace();
119        if self.starts_with("\"") {
120          format!("\"{}\"", self.consume_quoted_string())
121        } else if self.starts_with("{") {
122          self.parse_braced_attribute()
123        } else {
124          panic!("Expected attribute value");
125        }
126      } else {
127        "true".to_string()
129      };
130
131      attrs.push(Attr::KeyValue(name, value));
132    }
133    attrs
134  }
135
136  fn parse_braced_attribute(&mut self) -> String {
137    self.consume("{");
138    let mut output = String::new();
139
140    while self.pos < self.input.len() {
141      if self.starts_with("}") {
142        self.consume("}");
143        break;
144      } else if self.starts_with("<") {
145        let jsx_node = self.parse_element();
147        output.push_str(&compile_node(&jsx_node, None));
148      } else {
149        output.push(self.advance());
150      }
151    }
152
153    output.trim().to_string()
154  }
155
156  fn parse_text(&mut self) -> Node {
157    let mut text = String::new();
158    while self.pos < self.input.len() && !self.starts_with("<") && !self.starts_with("{") {
159      text.push(self.advance());
160    }
161    if self.starts_with("{") {
162      return self.parse_braced_content();
163    }
164    Node::Text(text.trim().to_string())
165  }
166
167  fn starts_with(&self, s: &str) -> bool {
170    self.input[self.pos..].starts_with(s.as_bytes())
171  }
172
173  fn peek(&self) -> char {
174    self.input[self.pos] as char
175  }
176
177  fn advance(&mut self) -> char {
178    let c = self.input[self.pos] as char;
179    self.pos += 1;
180    c
181  }
182
183  fn consume(&mut self, s: &str) {
184    assert!(self.starts_with(s), "Expected '{}'", s);
185    self.pos += s.len();
186  }
187
188  fn consume_identifier(&mut self) -> String {
189    let mut ident = String::new();
190    while self.pos < self.input.len() {
191      let c = self.peek();
192      if c.is_alphanumeric() || c == '-' || c == '_' {
193        ident.push(self.advance());
194      } else {
195        break;
196      }
197    }
198    ident
199  }
200
201  fn consume_quoted_string(&mut self) -> String {
202    self.consume("\"");
203    let mut value = String::new();
204    while self.peek() != '"' {
205      value.push(self.advance());
206    }
207    self.consume("\"");
208    value
209  }
210
211  fn skip_whitespace(&mut self) {
212    while self.pos < self.input.len() && self.peek().is_whitespace() {
213      self.advance();
214    }
215  }
216}
217
218fn compile_node(node: &Node, pragma: Option<String>) -> String {
219  match node {
220    Node::Text(text) => {
221      if text.trim().is_empty() {
222        String::new()
223      } else if text.starts_with('{') && text.ends_with('}') {
224        text[1..text.len() - 1].trim().to_string()
226      } else {
227        format!(r#""{}""#, text)
228      }
229    }
230    Node::Element {
231      tag,
232      attrs,
233      children,
234    } => {
235      let mut parts = Vec::new();
236      for attr in attrs {
237        match attr {
238          Attr::KeyValue(k, v) => {
239            if v.starts_with("jsx(") || v.contains("(") || v.contains("=>") || v.contains(".") {
240              parts.push(format!(r#"{k}: {}"#, v)); } else {
242              parts.push(format!(r#"{k}: {}"#, v)); }
244          }
245          Attr::Spread(expr) => {
246            parts.push(format!("...{}", expr));
247          }
248        }
249      }
250      let props = format!("{{{}}}", parts.join(", "));
251
252      let compiled_children: Vec<String> = children
253        .iter()
254        .map(|x| compile_node(x, pragma.clone()))
255        .filter(|c| !c.is_empty())
256        .collect();
257      let children_js = if compiled_children.is_empty() {
258        "null".to_string()
259      } else {
260        compiled_children.join(", ")
261      };
262
263      let element = if tag == &tag.to_lowercase() {
264        format!(r#""{}""#, tag)
265      } else {
266        tag.to_string()
267      };
268
269      let prefix = pragma.unwrap_or("JSX.prototype.new".to_string());
270
271      format!(r#"{prefix}({element}, {props}, {children_js})"#)
272    }
273    Node::JSExpression(code) => code.clone(),
274  }
275}
276
277#[derive(Debug, Clone)]
278enum Token {
279  Symbol(String),
280  String(String),
281  Identifier(String),
282  Comment(String),
283  Whitespace(String),
284  #[allow(unused)]
285  Other(String),
286}
287
288fn tokenize(source: &str) -> Vec<Token> {
289  let mut tokens = vec![];
290  let mut chars = source.chars().peekable();
291
292  while let Some(&c) = chars.peek() {
293    if c.is_whitespace() {
294      let mut ws = String::new();
295      while let Some(&c2) = chars.peek() {
296        if c2.is_whitespace() {
297          ws.push(c2);
298          chars.next();
299        } else {
300          break;
301        }
302      }
303      tokens.push(Token::Whitespace(ws));
304    } else if c == '"' || c == '\'' {
305      let quote = c;
306      let mut s = String::new();
307      s.push(c);
308      chars.next();
309      for ch in chars.by_ref() {
310        s.push(ch);
311        if ch == quote {
312          break;
313        }
314      }
315      tokens.push(Token::String(s));
316    } else if c == '/' && chars.clone().nth(1) == Some('/') {
317      let mut comment = String::new();
318      for ch in chars.by_ref() {
319        comment.push(ch);
320        if ch == '\n' {
321          break;
322        }
323      }
324      tokens.push(Token::Comment(comment));
325    } else if c.is_alphanumeric() || c == '_' {
326      let mut ident = String::new();
327      while let Some(&ch) = chars.peek() {
328        if ch.is_alphanumeric() || ch == '_' {
329          ident.push(ch);
330          chars.next();
331        } else {
332          break;
333        }
334      }
335      tokens.push(Token::Identifier(ident));
336    } else {
337      let mut sym = String::new();
338      sym.push(c);
339      chars.next();
340
341      if sym == "<" && chars.peek() == Some(&'/') {
343        sym.push('/');
344        chars.next();
345      }
346
347      tokens.push(Token::Symbol(sym));
348    }
349  }
350
351  tokens
352}
353
354fn compile_jsx_fragments(tokens: &[Token], pragma: Option<String>) -> String {
355  let mut output = String::new();
356  let mut i = 0;
357
358  while i < tokens.len() {
359    let is_open = if let Token::Symbol(ref sym1) = tokens[i] {
361      if sym1 == "<" {
362        if let Some(Token::Symbol(sym2)) = tokens.get(i + 1) {
363          sym2 == ">"
364        } else {
365          false
366        }
367      } else {
368        false
369      }
370    } else {
371      false
372    };
373
374    if is_open {
375      i += 2;
377
378      let mut jsx = String::new();
379      while i + 1 < tokens.len() {
380        let is_close = if let Token::Symbol(sym1) = &tokens[i] {
382          if sym1 == "</" {
383            if let Some(Token::Symbol(sym2)) = tokens.get(i + 1) {
384              sym2 == ">"
385            } else {
386              false
387            }
388          } else {
389            false
390          }
391        } else {
392          false
393        };
394
395        if is_close {
396          i += 2;
398          break; }
400
401        match &tokens[i] {
402          Token::Whitespace(s)
403          | Token::Identifier(s)
404          | Token::Symbol(s)
405          | Token::String(s)
406          | Token::Comment(s)
407          | Token::Other(s) => jsx.push_str(s),
408        }
409        i += 1;
410      }
411
412      let mut parser = Parser::new(&jsx);
413      for node in parser.parse() {
414        output.push_str(&compile_node(&node, pragma.clone()));
415      }
416
417      continue;
418    }
419
420    match &tokens[i] {
421      Token::Whitespace(s)
422      | Token::Identifier(s)
423      | Token::Symbol(s)
424      | Token::String(s)
425      | Token::Comment(s)
426      | Token::Other(s) => output.push_str(s),
427    }
428    i += 1;
429  }
430
431  output
432}
433
434pub fn compile_jsx(input: String, pragma: Option<String>) -> String {
435  compile_jsx_fragments(&tokenize(&input), pragma)
436}
437
438#[cfg(test)]
439mod tests {
440  use super::*;
441
442  #[test]
443  fn test_jsx() {
444    assert_eq!(
445      compile_jsx(
446        "<><div>{something.map((i) => <p>{i}</p>)}<Element name={<i>{u}</i>} /></div></>".into(),
447        None
448      ),
449      "JSX.prototype.new(\"div\", {}, something.map((i) => JSX.prototype.new(\"p\", {}, i)), JSX.prototype.new(Element, {name: JSX.prototype.new(\"i\", {}, u)}, null))"
450    )
451  }
452}