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 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 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 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 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)); } else {
235 parts.push(format!(r#"{k}: {}"#, v)); }
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 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 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 i += 2;
370
371 let mut jsx = String::new();
372 while i + 1 < tokens.len() {
373 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 i += 2;
391 break; }
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}