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        format!("\"{}\"", 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_quoted_string(&mut self) -> String {
195    self.consume("\"");
196    let mut value = String::new();
197    while self.peek() != '"' {
198      value.push(self.advance());
199    }
200    self.consume("\"");
201    value
202  }
203
204  fn skip_whitespace(&mut self) {
205    while self.pos < self.input.len() && self.peek().is_whitespace() {
206      self.advance();
207    }
208  }
209}
210
211fn compile_node(node: &Node, pragma: Option<String>) -> String {
212  match node {
213    Node::Text(text) => {
214      if text.trim().is_empty() {
215        String::new()
216      } else if text.starts_with('{') && text.ends_with('}') {
217        // dynamic expression
218        text[1..text.len() - 1].trim().to_string()
219      } else {
220        format!(r#""{}""#, text)
221      }
222    }
223    Node::Element {
224      tag,
225      attrs,
226      children,
227    } => {
228      let mut parts = Vec::new();
229      for attr in attrs {
230        match attr {
231          Attr::KeyValue(k, v) => {
232            if v.starts_with("jsx(") || v.contains("(") || v.contains("=>") || v.contains(".") {
233              parts.push(format!(r#"{k}: {}"#, v)); // direct expression
234            } else {
235              parts.push(format!(r#"{k}: {}"#, v)); // string literal
236            }
237          }
238          Attr::Spread(expr) => {
239            parts.push(format!("...{}", expr));
240          }
241        }
242      }
243      let props = format!("{{{}}}", parts.join(", "));
244
245      let compiled_children: Vec<String> = children
246        .iter()
247        .map(|x| compile_node(x, pragma.clone()))
248        .filter(|c| !c.is_empty())
249        .collect();
250      let children_js = if compiled_children.is_empty() {
251        "null".to_string()
252      } else {
253        compiled_children.join(", ")
254      };
255
256      let element = if tag == &tag.to_lowercase() {
257        format!(r#""{}""#, tag)
258      } else {
259        tag.to_string()
260      };
261
262      let prefix = pragma.unwrap_or("JSX.prototype.new".to_string());
263
264      format!(r#"{prefix}({element}, {props}, {children_js})"#)
265    }
266    Node::JSExpression(code) => code.clone(),
267  }
268}
269
270#[derive(Debug, Clone)]
271enum Token {
272  Symbol(String),
273  String(String),
274  Identifier(String),
275  Comment(String),
276  Whitespace(String),
277  #[allow(unused)]
278  Other(String),
279}
280
281fn tokenize(source: &str) -> Vec<Token> {
282  let mut tokens = vec![];
283  let mut chars = source.chars().peekable();
284
285  while let Some(&c) = chars.peek() {
286    if c.is_whitespace() {
287      let mut ws = String::new();
288      while let Some(&c2) = chars.peek() {
289        if c2.is_whitespace() {
290          ws.push(c2);
291          chars.next();
292        } else {
293          break;
294        }
295      }
296      tokens.push(Token::Whitespace(ws));
297    } else if c == '"' || c == '\'' {
298      let quote = c;
299      let mut s = String::new();
300      s.push(c);
301      chars.next();
302      for ch in chars.by_ref() {
303        s.push(ch);
304        if ch == quote {
305          break;
306        }
307      }
308      tokens.push(Token::String(s));
309    } else if c == '/' && chars.clone().nth(1) == Some('/') {
310      let mut comment = String::new();
311      for ch in chars.by_ref() {
312        comment.push(ch);
313        if ch == '\n' {
314          break;
315        }
316      }
317      tokens.push(Token::Comment(comment));
318    } else if c.is_alphanumeric() || c == '_' {
319      let mut ident = String::new();
320      while let Some(&ch) = chars.peek() {
321        if ch.is_alphanumeric() || ch == '_' {
322          ident.push(ch);
323          chars.next();
324        } else {
325          break;
326        }
327      }
328      tokens.push(Token::Identifier(ident));
329    } else {
330      let mut sym = String::new();
331      sym.push(c);
332      chars.next();
333
334      // Look ahead for compound symbols
335      if sym == "<" && chars.peek() == Some(&'/') {
336        sym.push('/');
337        chars.next();
338      }
339
340      tokens.push(Token::Symbol(sym));
341    }
342  }
343
344  tokens
345}
346
347fn compile_jsx_fragments(tokens: &[Token], pragma: Option<String>) -> String {
348  let mut output = String::new();
349  let mut i = 0;
350
351  while i < tokens.len() {
352    // complex bullshit over here
353    let is_open = if let Token::Symbol(ref sym1) = tokens[i] {
354      if sym1 == "<" {
355        if let Some(Token::Symbol(sym2)) = tokens.get(i + 1) {
356          sym2 == ">"
357        } else {
358          false
359        }
360      } else {
361        false
362      }
363    } else {
364      false
365    };
366
367    if is_open {
368      // fuck < and >
369      i += 2;
370
371      let mut jsx = String::new();
372      while i + 1 < tokens.len() {
373        // complex stuff
374        let is_close = if let Token::Symbol(sym1) = &tokens[i] {
375          if sym1 == "</" {
376            if let Some(Token::Symbol(sym2)) = tokens.get(i + 1) {
377              sym2 == ">"
378            } else {
379              false
380            }
381          } else {
382            false
383          }
384        } else {
385          false
386        };
387
388        if is_close {
389          // fuck < and />
390          i += 2;
391          break; // fuck off
392        }
393
394        match &tokens[i] {
395          Token::Whitespace(s)
396          | Token::Identifier(s)
397          | Token::Symbol(s)
398          | Token::String(s)
399          | Token::Comment(s)
400          | Token::Other(s) => jsx.push_str(s),
401        }
402        i += 1;
403      }
404
405      let mut parser = Parser::new(&jsx);
406      for node in parser.parse() {
407        output.push_str(&compile_node(&node, pragma.clone()));
408      }
409
410      continue;
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) => output.push_str(s),
420    }
421    i += 1;
422  }
423
424  output
425}
426
427pub fn compile_jsx(input: String, pragma: Option<String>) -> String {
428  compile_jsx_fragments(&tokenize(&input), pragma)
429}
430
431#[cfg(test)]
432mod tests {
433  use super::*;
434
435  #[test]
436  fn test_jsx(){
437    assert_eq!(
438      compile_jsx("<><div>{something.map((i) => <p>{i}</p>)}<Element name={<i>{u}</i>} /></div></>".into(), None),
439      "JSX.prototype.new(\"div\", {}, something.map((i) => JSX.prototype.new(\"p\", {}, i)), JSX.prototype.new(Element, {name: JSX.prototype.new(\"i\", {}, u)}, null))"
440    )
441  }
442}