rew_jsx/
lib.rs

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        // parse JSX and compile immediately
83        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      self.consume("=");
116      self.skip_whitespace();
117      let value = if self.starts_with("\"") {
118        self.consume_quoted_string()
119      } else if self.starts_with("{") {
120        self.parse_braced_attribute()
121      } else {
122        panic!("Expected attribute value");
123      };
124      attrs.push(Attr::KeyValue(name, value));
125    }
126    attrs
127  }
128
129  fn parse_braced_attribute(&mut self) -> String {
130    self.consume("{");
131    let mut output = String::new();
132
133    while self.pos < self.input.len() {
134      if self.starts_with("}") {
135        self.consume("}");
136        break;
137      } else if self.starts_with("<") {
138        // Parse JSX and compile immediately
139        let jsx_node = self.parse_element();
140        output.push_str(&compile_node(&jsx_node, None));
141      } else {
142        output.push(self.advance());
143      }
144    }
145
146    output.trim().to_string()
147  }
148
149  fn parse_text(&mut self) -> Node {
150    let mut text = String::new();
151    while self.pos < self.input.len() && !self.starts_with("<") && !self.starts_with("{") {
152      text.push(self.advance());
153    }
154    if self.starts_with("{") {
155      return self.parse_braced_content();
156    }
157    Node::Text(text.trim().to_string())
158  }
159
160  // Utilities
161
162  fn starts_with(&self, s: &str) -> bool {
163    self.input[self.pos..].starts_with(s.as_bytes())
164  }
165
166  fn peek(&self) -> char {
167    self.input[self.pos] as char
168  }
169
170  fn advance(&mut self) -> char {
171    let c = self.input[self.pos] as char;
172    self.pos += 1;
173    c
174  }
175
176  fn consume(&mut self, s: &str) {
177    assert!(self.starts_with(s), "Expected '{}'", s);
178    self.pos += s.len();
179  }
180
181  fn consume_identifier(&mut self) -> String {
182    let mut ident = String::new();
183    while self.pos < self.input.len() {
184      let c = self.peek();
185      if c.is_alphanumeric() || c == '-' || c == '_' {
186        ident.push(self.advance());
187      } else {
188        break;
189      }
190    }
191    ident
192  }
193
194  // fn consume_braced_expression(&mut self) -> String {
195  //   self.consume("{");
196  //   let mut expr = String::new();
197  //   let mut depth = 1;
198  //   while self.pos < self.input.len() && depth > 0 {
199  //     let c = self.advance();
200  //     if c == '{' {
201  //       depth += 1;
202  //     } else if c == '}' {
203  //       depth -= 1;
204  //       if depth == 0 {
205  //         break;
206  //       }
207  //     }
208  //     expr.push(c);
209  //   }
210  //   format!("{{{}}}", expr.trim())
211  // }
212
213  fn consume_quoted_string(&mut self) -> String {
214    self.consume("\"");
215    let mut value = String::new();
216    while self.peek() != '"' {
217      value.push(self.advance());
218    }
219    self.consume("\"");
220    value
221  }
222
223  fn skip_whitespace(&mut self) {
224    while self.pos < self.input.len() && self.peek().is_whitespace() {
225      self.advance();
226    }
227  }
228}
229
230fn compile_node(node: &Node, pragma: Option<String>) -> String {
231  match node {
232    Node::Text(text) => {
233      if text.trim().is_empty() {
234        String::new()
235      } else if text.starts_with('{') && text.ends_with('}') {
236        // dynamic expression
237        text[1..text.len() - 1].trim().to_string()
238      } else {
239        format!(r#""{}""#, text)
240      }
241    }
242    Node::Element {
243      tag,
244      attrs,
245      children,
246    } => {
247      let mut parts = Vec::new();
248      for attr in attrs {
249        match attr {
250          Attr::KeyValue(k, v) => {
251            if v.starts_with("jsx(") || v.contains("(") || v.contains("=>") || v.contains(".") {
252              parts.push(format!(r#"{k}: {}"#, v)); // direct expression
253            } else {
254              parts.push(format!(r#"{k}: "{}""#, v)); // string literal
255            }
256          }
257          Attr::Spread(expr) => {
258            parts.push(format!("...{}", expr));
259          }
260        }
261      }
262      let props = format!("{{{}}}", parts.join(", "));
263
264      let compiled_children: Vec<String> = children
265        .iter()
266        .map(|x| compile_node(x, pragma.clone()))
267        .filter(|c| !c.is_empty())
268        .collect();
269      let children_js = if compiled_children.is_empty() {
270        "null".to_string()
271      } else {
272        compiled_children.join(", ")
273      };
274
275      let element = if tag == &tag.to_lowercase() {
276        format!(r#""{}""#, tag)
277      } else {
278        tag.to_string()
279      };
280
281      let prefix = pragma.unwrap_or("JSX.prototype.new".to_string());
282
283      format!(r#"{prefix}({element}, {props}, {children_js})"#)
284    }
285    Node::JSExpression(code) => code.clone(),
286  }
287}
288
289#[derive(Debug, Clone)]
290enum Token {
291  Symbol(String),
292  String(String),
293  Identifier(String),
294  Comment(String),
295  Whitespace(String),
296  #[allow(unused)]
297  Other(String),
298}
299
300fn tokenize(source: &str) -> Vec<Token> {
301  let mut tokens = vec![];
302  let mut chars = source.chars().peekable();
303
304  while let Some(&c) = chars.peek() {
305    if c.is_whitespace() {
306      let mut ws = String::new();
307      while let Some(&c2) = chars.peek() {
308        if c2.is_whitespace() {
309          ws.push(c2);
310          chars.next();
311        } else {
312          break;
313        }
314      }
315      tokens.push(Token::Whitespace(ws));
316    } else if c == '"' || c == '\'' {
317      let quote = c;
318      let mut s = String::new();
319      s.push(c);
320      chars.next();
321      for ch in chars.by_ref() {
322        s.push(ch);
323        if ch == quote {
324          break;
325        }
326      }
327      tokens.push(Token::String(s));
328    } else if c == '/' && chars.clone().nth(1) == Some('/') {
329      let mut comment = String::new();
330      for ch in chars.by_ref() {
331        comment.push(ch);
332        if ch == '\n' {
333          break;
334        }
335      }
336      tokens.push(Token::Comment(comment));
337    } else if c.is_alphanumeric() || c == '_' {
338      let mut ident = String::new();
339      while let Some(&ch) = chars.peek() {
340        if ch.is_alphanumeric() || ch == '_' {
341          ident.push(ch);
342          chars.next();
343        } else {
344          break;
345        }
346      }
347      tokens.push(Token::Identifier(ident));
348    } else {
349      let mut sym = String::new();
350      sym.push(c);
351      chars.next();
352
353      // Look ahead for compound symbols
354      if sym == "<" && chars.peek() == Some(&'/') {
355        sym.push('/');
356        chars.next();
357      }
358
359      tokens.push(Token::Symbol(sym));
360    }
361  }
362
363  tokens
364}
365
366fn compile_jsx_fragments(tokens: &[Token], pragma: Option<String>) -> String {
367  let mut output = String::new();
368  let mut i = 0;
369
370  while i < tokens.len() {
371    // complex bullshit over here
372    let is_open = if let Token::Symbol(ref sym1) = tokens[i] {
373      if sym1 == "<" {
374        if let Some(Token::Symbol(sym2)) = tokens.get(i + 1) {
375          sym2 == ">"
376        } else {
377          false
378        }
379      } else {
380        false
381      }
382    } else {
383      false
384    };
385
386    if is_open {
387      // fuck < and >
388      i += 2;
389
390      let mut jsx = String::new();
391      while i + 1 < tokens.len() {
392        // complex stuff
393        let is_close = if let Token::Symbol(sym1) = &tokens[i] {
394          if sym1 == "</" {
395            if let Some(Token::Symbol(sym2)) = tokens.get(i + 1) {
396              sym2 == ">"
397            } else {
398              false
399            }
400          } else {
401            false
402          }
403        } else {
404          false
405        };
406
407        if is_close {
408          // fuck < and />
409          i += 2;
410          break; // fuck off
411        }
412
413        match &tokens[i] {
414          Token::Whitespace(s)
415          | Token::Identifier(s)
416          | Token::Symbol(s)
417          | Token::String(s)
418          | Token::Comment(s)
419          | Token::Other(s) => jsx.push_str(s),
420        }
421        i += 1;
422      }
423
424      let mut parser = Parser::new(&jsx);
425      for node in parser.parse() {
426        output.push_str(&compile_node(&node, pragma.clone()));
427      }
428
429      continue;
430    }
431
432    match &tokens[i] {
433      Token::Whitespace(s)
434      | Token::Identifier(s)
435      | Token::Symbol(s)
436      | Token::String(s)
437      | Token::Comment(s)
438      | Token::Other(s) => output.push_str(s),
439    }
440    i += 1;
441  }
442
443  output
444}
445
446pub fn compile_jsx(input: String, pragma: Option<String>) -> String {
447  compile_jsx_fragments(&tokenize(&input), pragma)
448}
449
450#[cfg(test)]
451mod tests {
452  use super::*;
453
454  #[test]
455  fn test_jsx(){
456    assert_eq!(
457      compile_jsx("<><div>{something.map((i) => <p>{i}</p>)}<Element name={<i>{u}</i>} /></div></>".into(), None),
458      "JSX.prototype.new(\"div\", {}, something.map((i) => JSX.prototype.new(\"p\", {}, i)), JSX.prototype.new(Element, {name: JSX.prototype.new(\"i\", {}, u)}, null))"
459    )
460  }
461}