fob_gen/
jsx.rs

1//! JSX/React element building support
2//!
3//! This module provides ergonomic builders for JSX elements using OXC's AST.
4
5use oxc_allocator::Allocator;
6use oxc_ast::ast::*;
7use oxc_ast::{AstBuilder, NONE};
8use oxc_span::{Atom, SPAN};
9
10/// JSX element builder
11///
12/// Provides methods for building JSX elements, attributes, and children.
13pub struct JsxBuilder<'a> {
14    ast: AstBuilder<'a>,
15}
16
17impl<'a> JsxBuilder<'a> {
18    /// Create a new JSX builder
19    pub fn new(alloc: &'a Allocator) -> Self {
20        Self {
21            ast: AstBuilder::new(alloc),
22        }
23    }
24
25    /// Get the underlying AST builder
26    pub fn ast(&self) -> &AstBuilder<'a> {
27        &self.ast
28    }
29
30    /// Create a JSX element: `<Tag attr="value">children</Tag>`
31    ///
32    /// # Examples
33    ///
34    /// ```ignore
35    /// let jsx = JsxBuilder::new(&allocator);
36    /// let element = jsx.element(
37    ///     "div",
38    ///     vec![jsx.attr("className", jsx.string_attr("container"))],
39    ///     vec![jsx.text("Hello")],
40    ///     false,
41    /// );
42    /// ```
43    pub fn element(
44        &self,
45        name: impl Into<Atom<'a>>,
46        attributes: Vec<JSXAttributeItem<'a>>,
47        children: Vec<JSXChild<'a>>,
48        self_closing: bool,
49    ) -> JSXElement<'a> {
50        let name_atom = name.into();
51
52        // Create opening element name
53        let opening_ident = self.ast.jsx_identifier(SPAN, name_atom);
54        let opening_name = JSXElementName::Identifier(self.ast.alloc(opening_ident));
55
56        let attrs_vec = self.ast.vec_from_iter(attributes);
57
58        // Build JSXOpeningElement using AstBuilder
59        // Note: self_closing is inferred from absence of closing_element
60        let opening_element = self
61            .ast
62            .jsx_opening_element(SPAN, opening_name, NONE, attrs_vec);
63
64        // Create closing element if not self-closing (None = self-closing)
65        let closing_element: Option<JSXClosingElement> = if self_closing {
66            None
67        } else {
68            let closing_ident = self.ast.jsx_identifier(SPAN, name_atom);
69            let closing_name = JSXElementName::Identifier(self.ast.alloc(closing_ident));
70            Some(self.closing_element(closing_name))
71        };
72
73        let children_vec = self.ast.vec_from_iter(children);
74        self.ast.jsx_element(
75            SPAN,
76            opening_element,
77            children_vec,
78            closing_element.map(|e| self.ast.alloc(e)),
79        )
80    }
81
82    /// Create a JSX closing element
83    fn closing_element(&self, name: JSXElementName<'a>) -> JSXClosingElement<'a> {
84        JSXClosingElement { span: SPAN, name }
85    }
86
87    /// Create a JSX attribute: `name="value"`
88    pub fn attr(
89        &self,
90        name: impl Into<Atom<'a>>,
91        value: Option<JSXAttributeValue<'a>>,
92    ) -> JSXAttributeItem<'a> {
93        let attr_name = self.ast.jsx_attribute_name_identifier(SPAN, name);
94        let attr = self.ast.jsx_attribute(SPAN, attr_name, value);
95        JSXAttributeItem::Attribute(self.ast.alloc(attr))
96    }
97
98    /// Create a string attribute value: `"value"`
99    pub fn string_attr(&self, value: impl Into<Atom<'a>>) -> JSXAttributeValue<'a> {
100        JSXAttributeValue::StringLiteral(self.ast.alloc(self.ast.string_literal(SPAN, value, None)))
101    }
102
103    /// Create an expression attribute value: `{expr}`
104    pub fn expr_attr(&self, expr: Expression<'a>) -> JSXAttributeValue<'a> {
105        JSXAttributeValue::ExpressionContainer(self.ast.alloc(JSXExpressionContainer {
106            span: SPAN,
107            expression: JSXExpression::from(expr),
108        }))
109    }
110
111    /// Create a JSX text child
112    pub fn text(&self, value: impl Into<Atom<'a>>) -> JSXChild<'a> {
113        let value_atom = value.into();
114        let text = self.ast.jsx_text(SPAN, value_atom, Some(value_atom));
115        JSXChild::Text(self.ast.alloc(text))
116    }
117
118    /// Create a JSX element child
119    pub fn child(&self, element: JSXElement<'a>) -> JSXChild<'a> {
120        JSXChild::Element(self.ast.alloc(element))
121    }
122
123    /// Create a JSX expression child: `{expr}`
124    pub fn expr_child(&self, expr: Expression<'a>) -> JSXChild<'a> {
125        JSXChild::ExpressionContainer(self.ast.alloc(JSXExpressionContainer {
126            span: SPAN,
127            expression: JSXExpression::from(expr),
128        }))
129    }
130
131    /// Create a JSX fragment: `<>children</>`
132    pub fn fragment(&self, children: Vec<JSXChild<'a>>) -> JSXFragment<'a> {
133        JSXFragment {
134            span: SPAN,
135            opening_fragment: JSXOpeningFragment { span: SPAN },
136            closing_fragment: JSXClosingFragment { span: SPAN },
137            children: self.ast.vec_from_iter(children),
138        }
139    }
140
141    /// Convert JSX element to expression
142    pub fn jsx_expr(&self, element: JSXElement<'a>) -> Expression<'a> {
143        Expression::JSXElement(self.ast.alloc(element))
144    }
145
146    /// Convert JSX fragment to expression
147    pub fn jsx_fragment_expr(&self, fragment: JSXFragment<'a>) -> Expression<'a> {
148        Expression::JSXFragment(self.ast.alloc(fragment))
149    }
150}