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}