Skip to main content

swc_ecma_transforms_react/jsx/
mod.rs

1#![allow(clippy::redundant_allocation)]
2
3use std::sync::RwLock;
4
5use bytes_str::BytesStr;
6use once_cell::sync::Lazy;
7use rustc_hash::FxHashMap;
8use serde::{Deserialize, Serialize};
9use string_enum::StringEnum;
10use swc_atoms::{
11    atom,
12    wtf8::{Wtf8, Wtf8Buf},
13    Atom, Wtf8Atom,
14};
15use swc_common::{
16    comments::{Comment, CommentKind, Comments},
17    errors::HANDLER,
18    sync::Lrc,
19    util::take::Take,
20    FileName, Mark, SourceMap, Span, Spanned, SyntaxContext, DUMMY_SP,
21};
22use swc_config::merge::Merge;
23use swc_ecma_ast::*;
24use swc_ecma_hooks::VisitMutHook;
25use swc_ecma_parser::{parse_file_as_expr, Syntax};
26use swc_ecma_utils::{
27    drop_span, prepend_stmt, private_ident, quote_ident, str::is_line_terminator, ExprFactory,
28    StmtLike,
29};
30use swc_ecma_visit::VisitMut;
31
32use self::static_check::should_use_create_element;
33use crate::refresh::options::{deserialize_refresh, RefreshOptions};
34
35mod static_check;
36#[cfg(test)]
37mod tests;
38
39/// https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#runtime
40#[derive(StringEnum, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
41pub enum Runtime {
42    /// `automatic`
43    Automatic,
44    /// `classic`
45    Classic,
46    /// `preserve`
47    Preserve,
48}
49
50/// Note: This will changed in v2
51impl Default for Runtime {
52    fn default() -> Self {
53        Runtime::Classic
54    }
55}
56
57#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, Merge)]
58#[serde(rename_all = "camelCase")]
59#[serde(deny_unknown_fields)]
60pub struct Options {
61    /// If this is `true`, swc will behave just like babel 8 with
62    /// `BABEL_8_BREAKING: true`.
63    #[serde(skip, default)]
64    pub next: Option<bool>,
65
66    #[serde(default)]
67    pub runtime: Option<Runtime>,
68
69    /// For automatic runtime
70    #[serde(default)]
71    pub import_source: Option<Atom>,
72
73    #[serde(default)]
74    pub pragma: Option<BytesStr>,
75    #[serde(default)]
76    pub pragma_frag: Option<BytesStr>,
77
78    #[serde(default)]
79    pub throw_if_namespace: Option<bool>,
80
81    #[serde(default)]
82    pub development: Option<bool>,
83
84    // @babel/plugin-transform-react-jsx: Since "useBuiltIns" is removed in Babel 8, you can remove
85    // it from the config.
86    #[deprecated(
87        since = "0.167.4",
88        note = r#"Since `useBuiltIns` is removed in swc, you can remove it from the config."#
89    )]
90    #[serde(default, alias = "useBuiltIns")]
91    pub use_builtins: Option<bool>,
92
93    // '@babel/plugin-transform-react-jsx: Since Babel 8, an inline object with spread elements is
94    // always used, and the "useSpread" option is no longer available. Please remove it from your
95    // config.',
96    #[deprecated(
97        since = "0.167.4",
98        note = r#"An inline object with spread elements is always used, and the `useSpread` option is no longer available. Please remove it from your config."#
99    )]
100    #[serde(default)]
101    pub use_spread: Option<bool>,
102
103    #[serde(default, deserialize_with = "deserialize_refresh")]
104    // default to disabled since this is still considered as experimental by now
105    pub refresh: Option<RefreshOptions>,
106}
107
108#[cfg(feature = "concurrent")]
109macro_rules! static_str {
110    ($s:expr) => {{
111        static VAL: Lazy<BytesStr> = Lazy::new(|| $s.into());
112        VAL.clone()
113    }};
114}
115
116#[cfg(not(feature = "concurrent"))]
117macro_rules! static_str {
118    ($s:expr) => {
119        $s.into()
120    };
121}
122
123pub fn default_import_source() -> Atom {
124    atom!("react")
125}
126
127pub fn default_pragma() -> BytesStr {
128    static_str!("React.createElement")
129}
130
131pub fn default_pragma_frag() -> BytesStr {
132    static_str!("React.Fragment")
133}
134
135fn default_throw_if_namespace() -> bool {
136    true
137}
138
139/// Parse `src` to use as a `pragma` or `pragmaFrag` in jsx.
140pub fn parse_expr_for_jsx(
141    cm: &SourceMap,
142    name: &str,
143    src: BytesStr,
144    top_level_mark: Mark,
145) -> Box<Expr> {
146    let fm = cm.new_source_file(cache_filename(name), src);
147
148    parse_file_as_expr(
149        &fm,
150        Syntax::default(),
151        Default::default(),
152        None,
153        &mut Vec::new(),
154    )
155    .map_err(|e| {
156        if HANDLER.is_set() {
157            HANDLER.with(|h| {
158                e.into_diagnostic(h)
159                    .note("Failed to parse jsx pragma")
160                    .emit()
161            })
162        }
163    })
164    .map(drop_span)
165    .map(|mut expr| {
166        apply_mark(&mut expr, top_level_mark);
167        expr
168    })
169    .unwrap_or_else(|()| {
170        panic!(
171            "failed to parse jsx option {}: '{}' is not an expression",
172            name, fm.src,
173        )
174    })
175}
176
177fn apply_mark(e: &mut Expr, mark: Mark) {
178    match e {
179        Expr::Ident(i) => {
180            i.ctxt = i.ctxt.apply_mark(mark);
181        }
182        Expr::Member(MemberExpr { obj, .. }) => {
183            apply_mark(obj, mark);
184        }
185        _ => {}
186    }
187}
188
189/// `@babel/plugin-transform-react-jsx`
190///
191/// Turn JSX into React function calls
192///
193///
194/// `top_level_mark` should be [Mark] passed to
195/// [swc_ecma_transforms_base::resolver::resolver_with_mark].
196///
197///
198/// # Parameters
199///
200/// ## `top_level_ctxt`
201///
202/// This is used to reference `React` defined by the user.
203///
204/// e.g.
205///
206/// ```js
207/// import React from 'react';
208/// ```
209pub fn hook<C>(
210    cm: Lrc<SourceMap>,
211    comments: Option<C>,
212    options: Options,
213    top_level_mark: Mark,
214    unresolved_mark: Mark,
215) -> impl VisitMutHook<()>
216where
217    C: Comments,
218{
219    Jsx {
220        cm: cm.clone(),
221        top_level_mark,
222        unresolved_mark,
223        runtime: options.runtime.unwrap_or_default(),
224        import_source: options.import_source.unwrap_or_else(default_import_source),
225        import_jsx: None,
226        import_jsxs: None,
227        import_fragment: None,
228        import_create_element: None,
229
230        pragma: Lrc::new(parse_expr_for_jsx(
231            &cm,
232            "pragma",
233            options.pragma.unwrap_or_else(default_pragma),
234            top_level_mark,
235        )),
236        comments,
237        pragma_frag: Lrc::new(parse_expr_for_jsx(
238            &cm,
239            "pragmaFrag",
240            options.pragma_frag.unwrap_or_else(default_pragma_frag),
241            top_level_mark,
242        )),
243        development: options.development.unwrap_or_default(),
244        throw_if_namespace: options
245            .throw_if_namespace
246            .unwrap_or_else(default_throw_if_namespace),
247        top_level_node: true,
248    }
249}
250
251// Re-export for compatibility
252pub fn jsx<C>(
253    cm: Lrc<SourceMap>,
254    comments: Option<C>,
255    options: Options,
256    top_level_mark: Mark,
257    unresolved_mark: Mark,
258) -> impl Pass + VisitMut
259where
260    C: Comments,
261{
262    use swc_ecma_hooks::VisitMutWithHook;
263    use swc_ecma_visit::visit_mut_pass;
264
265    visit_mut_pass(VisitMutWithHook {
266        hook: hook(cm, comments, options, top_level_mark, unresolved_mark),
267        context: (),
268    })
269}
270
271struct Jsx<C>
272where
273    C: Comments,
274{
275    cm: Lrc<SourceMap>,
276
277    top_level_mark: Mark,
278    unresolved_mark: Mark,
279
280    runtime: Runtime,
281    /// For automatic runtime.
282    import_source: Atom,
283    /// For automatic runtime.
284    import_jsx: Option<Ident>,
285    /// For automatic runtime.
286    import_jsxs: Option<Ident>,
287    /// For automatic runtime.
288    import_create_element: Option<Ident>,
289    /// For automatic runtime.
290    import_fragment: Option<Ident>,
291    top_level_node: bool,
292
293    pragma: Lrc<Box<Expr>>,
294    comments: Option<C>,
295    pragma_frag: Lrc<Box<Expr>>,
296    development: bool,
297    throw_if_namespace: bool,
298}
299
300#[derive(Debug, Default, Clone, PartialEq, Eq)]
301pub struct JsxDirectives {
302    pub runtime: Option<Runtime>,
303
304    /// For automatic runtime.
305    pub import_source: Option<Atom>,
306
307    /// Parsed from `@jsx`
308    pub pragma: Option<Lrc<Box<Expr>>>,
309
310    /// Parsed from `@jsxFrag`
311    pub pragma_frag: Option<Lrc<Box<Expr>>>,
312}
313
314impl JsxDirectives {
315    pub fn from_comments(
316        cm: &SourceMap,
317        _: Span,
318        comments: &[Comment],
319        top_level_mark: Mark,
320    ) -> Self {
321        let mut res = JsxDirectives::default();
322
323        for cmt in comments {
324            if cmt.kind != CommentKind::Block {
325                continue;
326            }
327
328            for line in cmt.text.lines() {
329                let mut line = line.trim();
330                if line.starts_with('*') {
331                    line = line[1..].trim();
332                }
333
334                if !line.starts_with("@jsx") {
335                    continue;
336                }
337
338                let mut words = line.split_whitespace();
339                loop {
340                    let pragma = words.next();
341                    if pragma.is_none() {
342                        break;
343                    }
344                    let val = words.next();
345
346                    match pragma {
347                        Some("@jsxRuntime") => match val {
348                            Some("classic") => res.runtime = Some(Runtime::Classic),
349                            Some("automatic") => res.runtime = Some(Runtime::Automatic),
350                            None => {}
351                            _ => {
352                                if HANDLER.is_set() {
353                                    HANDLER.with(|handler| {
354                                        handler
355                                            .struct_span_err(
356                                                cmt.span,
357                                                "Runtime must be either `classic` or `automatic`.",
358                                            )
359                                            .emit()
360                                    });
361                                }
362                            }
363                        },
364                        Some("@jsxImportSource") => {
365                            if let Some(src) = val {
366                                res.runtime = Some(Runtime::Automatic);
367                                res.import_source = Some(Atom::new(src));
368                            }
369                        }
370                        Some("@jsxFrag") => {
371                            if let Some(src) = val {
372                                if is_valid_for_pragma(src) {
373                                    // TODO: Optimize
374                                    let mut e = parse_expr_for_jsx(
375                                        cm,
376                                        "module-jsx-pragma-frag",
377                                        cache_source(src),
378                                        top_level_mark,
379                                    );
380                                    e.set_span(cmt.span);
381                                    res.pragma_frag = Some(e.into())
382                                }
383                            }
384                        }
385                        Some("@jsx") => {
386                            if let Some(src) = val {
387                                if is_valid_for_pragma(src) {
388                                    // TODO: Optimize
389                                    let mut e = parse_expr_for_jsx(
390                                        cm,
391                                        "module-jsx-pragma",
392                                        cache_source(src),
393                                        top_level_mark,
394                                    );
395                                    e.set_span(cmt.span);
396                                    res.pragma = Some(e.into());
397                                }
398                            }
399                        }
400                        _ => {}
401                    }
402                }
403            }
404        }
405
406        res
407    }
408}
409
410#[cfg(feature = "concurrent")]
411fn cache_filename(name: &str) -> Lrc<FileName> {
412    static FILENAME_CACHE: Lazy<RwLock<FxHashMap<String, Lrc<FileName>>>> =
413        Lazy::new(|| RwLock::new(FxHashMap::default()));
414
415    {
416        let cache = FILENAME_CACHE
417            .read()
418            .expect("Failed to read FILENAME_CACHE");
419        if let Some(f) = cache.get(name) {
420            return f.clone();
421        }
422    }
423
424    let file = Lrc::new(FileName::Internal(format!("jsx-config-{name}.js")));
425
426    {
427        let mut cache = FILENAME_CACHE
428            .write()
429            .expect("Failed to write FILENAME_CACHE");
430        cache.insert(name.to_string(), file.clone());
431    }
432
433    file
434}
435
436#[cfg(not(feature = "concurrent"))]
437fn cache_filename(name: &str) -> Lrc<FileName> {
438    Lrc::new(FileName::Internal(format!("jsx-config-{name}.js")))
439}
440
441#[cfg(feature = "concurrent")]
442fn cache_source(src: &str) -> BytesStr {
443    use rustc_hash::FxHashSet;
444
445    static CACHE: Lazy<RwLock<FxHashSet<BytesStr>>> =
446        Lazy::new(|| RwLock::new(FxHashSet::default()));
447
448    {
449        let cache = CACHE.write().unwrap();
450
451        if let Some(cached) = cache.get(src) {
452            return cached.clone();
453        }
454    }
455
456    let cached: BytesStr = src.to_string().into();
457    {
458        let mut cache = CACHE.write().unwrap();
459        cache.insert(cached.clone());
460    }
461    cached
462}
463
464#[cfg(not(feature = "concurrent"))]
465fn cache_source(src: &str) -> BytesStr {
466    // We cannot cache because Rc does not implement Send.
467    src.to_string().into()
468}
469
470fn is_valid_for_pragma(s: &str) -> bool {
471    if s.is_empty() {
472        return false;
473    }
474
475    if !s.starts_with(|c: char| Ident::is_valid_start(c)) {
476        return false;
477    }
478
479    for c in s.chars() {
480        if !Ident::is_valid_continue(c) && c != '.' {
481            return false;
482        }
483    }
484
485    true
486}
487
488impl<C> Jsx<C>
489where
490    C: Comments,
491{
492    /// Process JSX attribute value, handling JSXElements and JSXFragments
493    fn process_attr_value(&mut self, value: Option<JSXAttrValue>) -> Box<Expr> {
494        match value {
495            Some(JSXAttrValue::JSXElement(el)) => Box::new(self.jsx_elem_to_expr(*el)),
496            Some(JSXAttrValue::JSXFragment(frag)) => Box::new(self.jsx_frag_to_expr(frag)),
497            Some(JSXAttrValue::JSXExprContainer(container)) => match container.expr {
498                JSXExpr::Expr(e) => e,
499                JSXExpr::JSXEmptyExpr(_) => panic!("empty expression container"),
500                #[cfg(swc_ast_unknown)]
501                _ => panic!("unable to access unknown nodes"),
502            },
503            Some(v) => jsx_attr_value_to_expr(v).expect("empty expression container?"),
504            None => true.into(),
505        }
506    }
507
508    fn inject_runtime<T, F>(&mut self, body: &mut Vec<T>, inject: F)
509    where
510        T: StmtLike,
511        // Fn(Vec<(local, imported)>, src, body)
512        F: Fn(Vec<(Ident, IdentName)>, &str, &mut Vec<T>),
513    {
514        if self.runtime == Runtime::Automatic {
515            if let Some(local) = self.import_create_element.take() {
516                inject(
517                    vec![(local, quote_ident!("createElement"))],
518                    &self.import_source,
519                    body,
520                );
521            }
522
523            let imports = self.import_jsx.take();
524            let imports = if self.development {
525                imports
526                    .map(|local| (local, quote_ident!("jsxDEV")))
527                    .into_iter()
528                    .chain(
529                        self.import_fragment
530                            .take()
531                            .map(|local| (local, quote_ident!("Fragment"))),
532                    )
533                    .collect::<Vec<_>>()
534            } else {
535                imports
536                    .map(|local| (local, quote_ident!("jsx")))
537                    .into_iter()
538                    .chain(
539                        self.import_jsxs
540                            .take()
541                            .map(|local| (local, quote_ident!("jsxs"))),
542                    )
543                    .chain(
544                        self.import_fragment
545                            .take()
546                            .map(|local| (local, quote_ident!("Fragment"))),
547                    )
548                    .collect::<Vec<_>>()
549            };
550
551            if !imports.is_empty() {
552                let jsx_runtime = if self.development {
553                    "jsx-dev-runtime"
554                } else {
555                    "jsx-runtime"
556                };
557
558                let value = format!("{}/{}", self.import_source, jsx_runtime);
559                inject(imports, &value, body)
560            }
561        }
562    }
563
564    fn jsx_frag_to_expr(&mut self, el: JSXFragment) -> Expr {
565        let mut span = el.span();
566
567        if let Some(comments) = &self.comments {
568            if span.lo.is_dummy() {
569                span.lo = Span::dummy_with_cmt().lo;
570            }
571
572            comments.add_pure_comment(span.lo);
573        }
574
575        match self.runtime {
576            Runtime::Automatic => {
577                let fragment = self
578                    .import_fragment
579                    .get_or_insert_with(|| private_ident!("_Fragment"))
580                    .clone();
581
582                let mut props_obj = ObjectLit {
583                    span: DUMMY_SP,
584                    props: Vec::new(),
585                };
586
587                let children = el
588                    .children
589                    .into_iter()
590                    .filter_map(|child| self.jsx_elem_child_to_expr(child))
591                    .map(Some)
592                    .collect::<Vec<_>>();
593
594                let use_jsxs = match children.len() {
595                    0 => false,
596                    1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
597                        props_obj
598                            .props
599                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
600                                key: PropName::Ident(quote_ident!("children")),
601                                value: children.into_iter().next().flatten().unwrap().expr,
602                            }))));
603
604                        false
605                    }
606                    _ => {
607                        props_obj
608                            .props
609                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
610                                key: PropName::Ident(quote_ident!("children")),
611                                value: ArrayLit {
612                                    span: DUMMY_SP,
613                                    elems: children,
614                                }
615                                .into(),
616                            }))));
617                        true
618                    }
619                };
620
621                let jsx = if use_jsxs && !self.development {
622                    self.import_jsxs
623                        .get_or_insert_with(|| private_ident!("_jsxs"))
624                        .clone()
625                } else {
626                    let jsx = if self.development { "_jsxDEV" } else { "_jsx" };
627                    self.import_jsx
628                        .get_or_insert_with(|| private_ident!(jsx))
629                        .clone()
630                };
631
632                // Build args Vec directly
633                let args = if self.development {
634                    vec![
635                        fragment.as_arg(),
636                        props_obj.as_arg(),
637                        Expr::undefined(DUMMY_SP).as_arg(),
638                        use_jsxs.as_arg(),
639                    ]
640                } else {
641                    vec![fragment.as_arg(), props_obj.as_arg()]
642                };
643
644                CallExpr {
645                    span,
646                    callee: jsx.as_callee(),
647                    args,
648                    ..Default::default()
649                }
650                .into()
651            }
652            Runtime::Classic => {
653                // Build args Vec directly for better performance
654                let children_capacity = el.children.len();
655                let mut args = Vec::with_capacity(2 + children_capacity);
656
657                args.push((*self.pragma_frag).clone().as_arg());
658                args.push(Lit::Null(Null { span: DUMMY_SP }).as_arg());
659
660                // Add children
661                for child in el.children {
662                    if let Some(expr) = self.jsx_elem_child_to_expr(child) {
663                        args.push(expr);
664                    }
665                }
666
667                CallExpr {
668                    span,
669                    callee: (*self.pragma).clone().as_callee(),
670                    args,
671                    ..Default::default()
672                }
673                .into()
674            }
675            Runtime::Preserve => unreachable!(),
676        }
677    }
678
679    /// # Automatic
680    ///
681    /// <div></div> => jsx('div', null);
682    ///
683    /// # Classic
684    ///
685    /// <div></div> => React.createElement('div', null);
686    fn jsx_elem_to_expr(&mut self, el: JSXElement) -> Expr {
687        let top_level_node = self.top_level_node;
688        let mut span = el.span();
689        let use_create_element = should_use_create_element(&el.opening.attrs);
690        self.top_level_node = false;
691
692        let name = self.jsx_name(el.opening.name);
693
694        if let Some(comments) = &self.comments {
695            if span.lo.is_dummy() {
696                span.lo = Span::dummy_with_cmt().lo;
697            }
698
699            comments.add_pure_comment(span.lo);
700        }
701
702        match self.runtime {
703            Runtime::Automatic => {
704                // function jsx(tagName: string, props: { children: Node[], ... }, key: string)
705
706                // Pre-allocate with estimated capacity
707                let estimated_props_capacity = el.opening.attrs.len() + 1; // attrs + potential children
708                let mut props_obj = ObjectLit {
709                    span: DUMMY_SP,
710                    props: Vec::with_capacity(estimated_props_capacity),
711                };
712
713                let mut key = None;
714                let mut source_props = None;
715                let mut self_props = None;
716
717                for attr in el.opening.attrs {
718                    match attr {
719                        JSXAttrOrSpread::JSXAttr(attr) => {
720                            //
721                            match attr.name {
722                                JSXAttrName::Ident(i) => {
723                                    //
724                                    if !use_create_element && i.sym == "key" {
725                                        key = attr
726                                            .value
727                                            .and_then(jsx_attr_value_to_expr)
728                                            .map(|expr| expr.as_arg());
729
730                                        if key.is_none() && HANDLER.is_set() {
731                                            HANDLER.with(|handler| {
732                                                handler
733                                                    .struct_span_err(
734                                                        i.span,
735                                                        "The value of property 'key' should not \
736                                                         be empty",
737                                                    )
738                                                    .emit();
739                                            });
740                                        }
741                                        continue;
742                                    }
743
744                                    if !use_create_element
745                                        && *i.sym == *"__source"
746                                        && self.development
747                                    {
748                                        if source_props.is_some() {
749                                            panic!("Duplicate __source is found");
750                                        }
751                                        source_props = attr
752                                            .value
753                                            .and_then(jsx_attr_value_to_expr)
754                                            .map(|expr| expr.as_arg());
755                                        assert_ne!(
756                                            source_props, None,
757                                            "value of property '__source' should not be empty"
758                                        );
759                                        continue;
760                                    }
761
762                                    if !use_create_element
763                                        && *i.sym == *"__self"
764                                        && self.development
765                                    {
766                                        if self_props.is_some() {
767                                            panic!("Duplicate __self is found");
768                                        }
769                                        self_props = attr
770                                            .value
771                                            .and_then(jsx_attr_value_to_expr)
772                                            .map(|expr| expr.as_arg());
773                                        assert_ne!(
774                                            self_props, None,
775                                            "value of property '__self' should not be empty"
776                                        );
777                                        continue;
778                                    }
779
780                                    let value = self.process_attr_value(attr.value);
781
782                                    // TODO: Check if `i` is a valid identifier.
783                                    let key = if i.sym.contains('-') {
784                                        PropName::Str(Str {
785                                            span: i.span,
786                                            raw: None,
787                                            value: i.sym.into(),
788                                        })
789                                    } else {
790                                        PropName::Ident(i)
791                                    };
792                                    props_obj.props.push(PropOrSpread::Prop(Box::new(
793                                        Prop::KeyValue(KeyValueProp { key, value }),
794                                    )));
795                                }
796                                JSXAttrName::JSXNamespacedName(JSXNamespacedName {
797                                    ns,
798                                    name,
799                                    ..
800                                }) => {
801                                    if self.throw_if_namespace && HANDLER.is_set() {
802                                        HANDLER.with(|handler| {
803                                            handler
804                                                .struct_span_err(
805                                                    span,
806                                                    "JSX Namespace is disabled by default because \
807                                                     react does not support it yet. You can \
808                                                     specify jsc.transform.react.throwIfNamespace \
809                                                     to false to override default behavior",
810                                                )
811                                                .emit()
812                                        });
813                                    }
814
815                                    let value = self.process_attr_value(attr.value);
816
817                                    let str_value = format!("{}:{}", ns.sym, name.sym);
818                                    let key = Str {
819                                        span,
820                                        raw: None,
821                                        value: str_value.into(),
822                                    };
823                                    let key = PropName::Str(key);
824
825                                    props_obj.props.push(PropOrSpread::Prop(Box::new(
826                                        Prop::KeyValue(KeyValueProp { key, value }),
827                                    )));
828                                }
829                                #[cfg(swc_ast_unknown)]
830                                _ => panic!("unable to access unknown nodes"),
831                            }
832                        }
833                        JSXAttrOrSpread::SpreadElement(attr) => match *attr.expr {
834                            Expr::Object(obj) => {
835                                props_obj.props.extend(obj.props);
836                            }
837                            _ => {
838                                props_obj.props.push(PropOrSpread::Spread(attr));
839                            }
840                        },
841                        #[cfg(swc_ast_unknown)]
842                        _ => panic!("unable to access unknown nodes"),
843                    }
844                }
845
846                let mut children = el
847                    .children
848                    .into_iter()
849                    .filter_map(|child| self.jsx_elem_child_to_expr(child))
850                    .map(Some)
851                    .collect::<Vec<_>>();
852
853                let use_jsxs = match children.len() {
854                    0 => false,
855                    1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
856                        if !use_create_element {
857                            props_obj
858                                .props
859                                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
860                                    key: PropName::Ident(quote_ident!("children")),
861                                    value: children
862                                        .take()
863                                        .into_iter()
864                                        .next()
865                                        .flatten()
866                                        .unwrap()
867                                        .expr,
868                                }))));
869                        }
870
871                        false
872                    }
873                    _ => {
874                        props_obj
875                            .props
876                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
877                                key: PropName::Ident(quote_ident!("children")),
878                                value: ArrayLit {
879                                    span: DUMMY_SP,
880                                    elems: children.take(),
881                                }
882                                .into(),
883                            }))));
884                        true
885                    }
886                };
887
888                let jsx = if use_create_element {
889                    self.import_create_element
890                        .get_or_insert_with(|| private_ident!("_createElement"))
891                        .clone()
892                } else if use_jsxs && !self.development {
893                    self.import_jsxs
894                        .get_or_insert_with(|| private_ident!("_jsxs"))
895                        .clone()
896                } else {
897                    let jsx = if self.development { "_jsxDEV" } else { "_jsx" };
898                    self.import_jsx
899                        .get_or_insert_with(|| private_ident!(jsx))
900                        .clone()
901                };
902
903                self.top_level_node = top_level_node;
904
905                // Build args Vec directly instead of using iterator chains
906                let args = if use_create_element {
907                    let mut args = Vec::with_capacity(2 + children.len());
908                    args.push(name.as_arg());
909                    args.push(props_obj.as_arg());
910                    args.extend(children.into_iter().flatten());
911                    args
912                } else if self.development {
913                    let mut args = Vec::with_capacity(6);
914                    args.push(name.as_arg());
915                    args.push(props_obj.as_arg());
916
917                    // set undefined literal to key if key is None
918                    let key = key.unwrap_or_else(|| Expr::undefined(DUMMY_SP).as_arg());
919                    args.push(key);
920
921                    args.push(use_jsxs.as_arg());
922
923                    // set undefined literal to __source if __source is None
924                    let source_props =
925                        source_props.unwrap_or_else(|| Expr::undefined(DUMMY_SP).as_arg());
926                    args.push(source_props);
927
928                    // set undefined literal to __self if __self is None
929                    let self_props =
930                        self_props.unwrap_or_else(|| Expr::undefined(DUMMY_SP).as_arg());
931                    args.push(self_props);
932
933                    args
934                } else {
935                    let mut args = Vec::with_capacity(if key.is_some() { 3 } else { 2 });
936                    args.push(name.as_arg());
937                    args.push(props_obj.as_arg());
938                    if let Some(key) = key {
939                        args.push(key);
940                    }
941                    args
942                };
943                CallExpr {
944                    span,
945                    callee: jsx.as_callee(),
946                    args,
947                    ..Default::default()
948                }
949                .into()
950            }
951            Runtime::Classic => {
952                // Build args Vec directly for better performance
953                let children_capacity = el.children.len();
954                let mut args = Vec::with_capacity(2 + children_capacity);
955
956                args.push(name.as_arg());
957                args.push(self.fold_attrs_for_classic(el.opening.attrs).as_arg());
958
959                // Add children
960                for child in el.children {
961                    if let Some(expr) = self.jsx_elem_child_to_expr(child) {
962                        args.push(expr);
963                    }
964                }
965
966                CallExpr {
967                    span,
968                    callee: (*self.pragma).clone().as_callee(),
969                    args,
970                    ..Default::default()
971                }
972                .into()
973            }
974            Runtime::Preserve => unreachable!(),
975        }
976    }
977
978    fn jsx_elem_child_to_expr(&mut self, c: JSXElementChild) -> Option<ExprOrSpread> {
979        self.top_level_node = false;
980
981        Some(match c {
982            JSXElementChild::JSXText(text) => {
983                // TODO(kdy1): Optimize
984                let value = jsx_text_to_str_with_raw(&text.value, &text.raw);
985                let s = Str {
986                    span: text.span,
987                    raw: None,
988                    value,
989                };
990
991                if s.value.is_empty() {
992                    return None;
993                }
994
995                Lit::Str(s).as_arg()
996            }
997            JSXElementChild::JSXExprContainer(JSXExprContainer {
998                expr: JSXExpr::Expr(e),
999                ..
1000            }) => e.as_arg(),
1001            JSXElementChild::JSXExprContainer(JSXExprContainer {
1002                expr: JSXExpr::JSXEmptyExpr(..),
1003                ..
1004            }) => return None,
1005            JSXElementChild::JSXElement(el) => self.jsx_elem_to_expr(*el).as_arg(),
1006            JSXElementChild::JSXFragment(el) => self.jsx_frag_to_expr(el).as_arg(),
1007            JSXElementChild::JSXSpreadChild(JSXSpreadChild { span, expr, .. }) => ExprOrSpread {
1008                spread: Some(span),
1009                expr,
1010            },
1011            #[cfg(swc_ast_unknown)]
1012            _ => panic!("unable to access unknown nodes"),
1013        })
1014    }
1015
1016    fn fold_attrs_for_classic(&mut self, attrs: Vec<JSXAttrOrSpread>) -> Box<Expr> {
1017        if attrs.is_empty() {
1018            return Lit::Null(Null { span: DUMMY_SP }).into();
1019        }
1020        let attr_cnt = attrs.len();
1021
1022        let mut props = Vec::new();
1023        for attr in attrs {
1024            match attr {
1025                JSXAttrOrSpread::JSXAttr(attr) => {
1026                    props.push(PropOrSpread::Prop(Box::new(self.attr_to_prop(attr))))
1027                }
1028                JSXAttrOrSpread::SpreadElement(spread) => {
1029                    if attr_cnt == 1 {
1030                        return spread.expr;
1031                    }
1032                    // babel does some optimizations
1033                    match *spread.expr {
1034                        Expr::Object(obj) => props.extend(obj.props),
1035                        _ => props.push(PropOrSpread::Spread(spread)),
1036                    }
1037                }
1038                #[cfg(swc_ast_unknown)]
1039                _ => panic!("unable to access unknown nodes"),
1040            }
1041        }
1042
1043        let obj = ObjectLit {
1044            span: DUMMY_SP,
1045            props,
1046        };
1047
1048        obj.into()
1049    }
1050
1051    fn attr_to_prop(&mut self, a: JSXAttr) -> Prop {
1052        let key = to_prop_name(a.name);
1053        let value = a
1054            .value
1055            .map(|v| match v {
1056                JSXAttrValue::Str(s) => {
1057                    let value = transform_jsx_attr_str(&s.value);
1058
1059                    Lit::Str(Str {
1060                        span: s.span,
1061                        raw: None,
1062                        value: value.into(),
1063                    })
1064                    .into()
1065                }
1066                JSXAttrValue::JSXExprContainer(JSXExprContainer {
1067                    expr: JSXExpr::Expr(e),
1068                    ..
1069                }) => e,
1070                JSXAttrValue::JSXElement(element) => Box::new(self.jsx_elem_to_expr(*element)),
1071                JSXAttrValue::JSXFragment(fragment) => Box::new(self.jsx_frag_to_expr(fragment)),
1072                JSXAttrValue::JSXExprContainer(JSXExprContainer {
1073                    span: _,
1074                    expr: JSXExpr::JSXEmptyExpr(_),
1075                }) => unreachable!("attr_to_prop(JSXEmptyExpr)"),
1076                #[cfg(swc_ast_unknown)]
1077                _ => panic!("unable to access unknown nodes"),
1078            })
1079            .unwrap_or_else(|| {
1080                Lit::Bool(Bool {
1081                    span: key.span(),
1082                    value: true,
1083                })
1084                .into()
1085            });
1086        Prop::KeyValue(KeyValueProp { key, value })
1087    }
1088}
1089
1090impl<C> Jsx<C>
1091where
1092    C: Comments,
1093{
1094    /// If we found required jsx directives, we returns true.
1095    fn parse_directives(&mut self, span: Span) -> bool {
1096        let mut found = false;
1097
1098        let directives = self.comments.with_leading(span.lo, |comments| {
1099            JsxDirectives::from_comments(&self.cm, span, comments, self.top_level_mark)
1100        });
1101
1102        let JsxDirectives {
1103            runtime,
1104            import_source,
1105            pragma,
1106            pragma_frag,
1107        } = directives;
1108
1109        if let Some(runtime) = runtime {
1110            found = true;
1111            self.runtime = runtime;
1112        }
1113
1114        if let Some(import_source) = import_source {
1115            found = true;
1116            self.import_source = import_source;
1117        }
1118
1119        if let Some(pragma) = pragma {
1120            if let Runtime::Automatic = self.runtime {
1121                if HANDLER.is_set() {
1122                    HANDLER.with(|handler| {
1123                        handler
1124                            .struct_span_err(
1125                                pragma.span(),
1126                                "pragma cannot be set when runtime is automatic",
1127                            )
1128                            .emit()
1129                    });
1130                }
1131            }
1132
1133            found = true;
1134            self.pragma = pragma;
1135        }
1136
1137        if let Some(pragma_frag) = pragma_frag {
1138            if let Runtime::Automatic = self.runtime {
1139                if HANDLER.is_set() {
1140                    HANDLER.with(|handler| {
1141                        handler
1142                            .struct_span_err(
1143                                pragma_frag.span(),
1144                                "pragmaFrag cannot be set when runtime is automatic",
1145                            )
1146                            .emit()
1147                    });
1148                }
1149            }
1150
1151            found = true;
1152            self.pragma_frag = pragma_frag;
1153        }
1154
1155        found
1156    }
1157}
1158
1159impl<C> VisitMutHook<()> for Jsx<C>
1160where
1161    C: Comments,
1162{
1163    /// Called after visiting children of an expression.
1164    ///
1165    /// This is where we transform JSX syntax to JavaScript function calls.
1166    /// By doing this in exit_expr (after children are visited), we ensure that
1167    /// jsx_src and jsx_self have already added their __source and __self
1168    /// attributes.
1169    fn exit_expr(&mut self, expr: &mut Expr, _ctx: &mut ()) {
1170        let top_level_node = self.top_level_node;
1171        let mut did_work = false;
1172
1173        if let Expr::JSXElement(el) = expr {
1174            did_work = true;
1175            // <div></div> => React.createElement('div', null);
1176            *expr = self.jsx_elem_to_expr(*el.take());
1177        } else if let Expr::JSXFragment(frag) = expr {
1178            // <></> => React.createElement(React.Fragment, null);
1179            did_work = true;
1180            *expr = self.jsx_frag_to_expr(frag.take());
1181        } else if let Expr::Paren(ParenExpr {
1182            expr: inner_expr, ..
1183        }) = expr
1184        {
1185            if let Expr::JSXElement(el) = &mut **inner_expr {
1186                did_work = true;
1187                *expr = self.jsx_elem_to_expr(*el.take());
1188            } else if let Expr::JSXFragment(frag) = &mut **inner_expr {
1189                // <></> => React.createElement(React.Fragment, null);
1190                did_work = true;
1191                *expr = self.jsx_frag_to_expr(frag.take());
1192            }
1193        }
1194
1195        if did_work {
1196            self.top_level_node = false;
1197        }
1198
1199        self.top_level_node = top_level_node;
1200    }
1201
1202    fn enter_module(&mut self, module: &mut Module, _ctx: &mut ()) {
1203        self.parse_directives(module.span);
1204
1205        for item in &module.body {
1206            let span = item.span();
1207            if self.parse_directives(span) {
1208                break;
1209            }
1210        }
1211    }
1212
1213    fn exit_module(&mut self, module: &mut Module, _ctx: &mut ()) {
1214        if self.runtime == Runtime::Automatic {
1215            self.inject_runtime(&mut module.body, |imports, src, stmts| {
1216                let specifiers = imports
1217                    .into_iter()
1218                    .map(|(local, imported)| {
1219                        ImportSpecifier::Named(ImportNamedSpecifier {
1220                            span: DUMMY_SP,
1221                            local,
1222                            imported: Some(ModuleExportName::Ident(imported.into())),
1223                            is_type_only: false,
1224                        })
1225                    })
1226                    .collect();
1227
1228                prepend_stmt(
1229                    stmts,
1230                    ImportDecl {
1231                        span: DUMMY_SP,
1232                        specifiers,
1233                        src: Str {
1234                            span: DUMMY_SP,
1235                            raw: None,
1236                            value: src.into(),
1237                        }
1238                        .into(),
1239                        type_only: Default::default(),
1240                        with: Default::default(),
1241                        phase: Default::default(),
1242                    }
1243                    .into(),
1244                )
1245            });
1246        }
1247    }
1248
1249    fn enter_script(&mut self, script: &mut Script, _ctx: &mut ()) {
1250        self.parse_directives(script.span);
1251
1252        for item in &script.body {
1253            let span = item.span();
1254            if self.parse_directives(span) {
1255                break;
1256            }
1257        }
1258    }
1259
1260    fn exit_script(&mut self, script: &mut Script, _ctx: &mut ()) {
1261        if self.runtime == Runtime::Automatic {
1262            let mark = self.unresolved_mark;
1263            self.inject_runtime(&mut script.body, |imports, src, stmts| {
1264                prepend_stmt(stmts, add_require(imports, src, mark))
1265            });
1266        }
1267    }
1268}
1269
1270// const { createElement } = require('react')
1271// const { jsx: jsx } = require('react/jsx-runtime')
1272fn add_require(imports: Vec<(Ident, IdentName)>, src: &str, unresolved_mark: Mark) -> Stmt {
1273    VarDecl {
1274        span: DUMMY_SP,
1275        kind: VarDeclKind::Const,
1276        declare: false,
1277        decls: vec![VarDeclarator {
1278            span: DUMMY_SP,
1279            name: Pat::Object(ObjectPat {
1280                span: DUMMY_SP,
1281                props: imports
1282                    .into_iter()
1283                    .map(|(local, imported)| {
1284                        if imported.sym != local.sym {
1285                            ObjectPatProp::KeyValue(KeyValuePatProp {
1286                                key: PropName::Ident(imported),
1287                                value: Box::new(Pat::Ident(local.into())),
1288                            })
1289                        } else {
1290                            ObjectPatProp::Assign(AssignPatProp {
1291                                span: DUMMY_SP,
1292                                key: local.into(),
1293                                value: None,
1294                            })
1295                        }
1296                    })
1297                    .collect(),
1298                optional: false,
1299                type_ann: None,
1300            }),
1301            // require('react')
1302            init: Some(Box::new(Expr::Call(CallExpr {
1303                span: DUMMY_SP,
1304                callee: Callee::Expr(Box::new(Expr::Ident(Ident {
1305                    ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1306                    sym: atom!("require"),
1307                    optional: false,
1308                    ..Default::default()
1309                }))),
1310                args: vec![ExprOrSpread {
1311                    spread: None,
1312                    expr: Box::new(Expr::Lit(Lit::Str(Str {
1313                        span: DUMMY_SP,
1314                        value: src.into(),
1315                        raw: None,
1316                    }))),
1317                }],
1318                ..Default::default()
1319            }))),
1320            definite: false,
1321        }],
1322        ..Default::default()
1323    }
1324    .into()
1325}
1326
1327impl<C> Jsx<C>
1328where
1329    C: Comments,
1330{
1331    fn jsx_name(&self, name: JSXElementName) -> Box<Expr> {
1332        let span = name.span();
1333        match name {
1334            JSXElementName::Ident(i) => {
1335                if i.sym == "this" {
1336                    return ThisExpr { span }.into();
1337                }
1338
1339                // If it starts with lowercase
1340                if i.as_ref().starts_with(|c: char| c.is_ascii_lowercase()) {
1341                    Lit::Str(Str {
1342                        span,
1343                        raw: None,
1344                        value: i.sym.into(),
1345                    })
1346                    .into()
1347                } else {
1348                    i.into()
1349                }
1350            }
1351            JSXElementName::JSXNamespacedName(JSXNamespacedName {
1352                ref ns, ref name, ..
1353            }) => {
1354                if self.throw_if_namespace && HANDLER.is_set() {
1355                    HANDLER.with(|handler| {
1356                        handler
1357                            .struct_span_err(
1358                                span,
1359                                "JSX Namespace is disabled by default because react does not \
1360                                 support it yet. You can specify \
1361                                 jsc.transform.react.throwIfNamespace to false to override \
1362                                 default behavior",
1363                            )
1364                            .emit()
1365                    });
1366                }
1367
1368                let value = format!("{}:{}", ns.sym, name.sym);
1369
1370                Lit::Str(Str {
1371                    span,
1372                    raw: None,
1373                    value: value.into(),
1374                })
1375                .into()
1376            }
1377            JSXElementName::JSXMemberExpr(JSXMemberExpr { obj, prop, .. }) => {
1378                fn convert_obj(obj: JSXObject) -> Box<Expr> {
1379                    let span = obj.span();
1380
1381                    (match obj {
1382                        JSXObject::Ident(i) => {
1383                            if i.sym == "this" {
1384                                Expr::This(ThisExpr { span })
1385                            } else {
1386                                i.into()
1387                            }
1388                        }
1389                        JSXObject::JSXMemberExpr(e) => MemberExpr {
1390                            span,
1391                            obj: convert_obj(e.obj),
1392                            prop: MemberProp::Ident(e.prop),
1393                        }
1394                        .into(),
1395                        #[cfg(swc_ast_unknown)]
1396                        _ => panic!("unable to access unknown nodes"),
1397                    })
1398                    .into()
1399                }
1400                MemberExpr {
1401                    span,
1402                    obj: convert_obj(obj),
1403                    prop: MemberProp::Ident(prop),
1404                }
1405                .into()
1406            }
1407            #[cfg(swc_ast_unknown)]
1408            _ => panic!("unable to access unknown nodes"),
1409        }
1410    }
1411}
1412
1413fn to_prop_name(n: JSXAttrName) -> PropName {
1414    let span = n.span();
1415
1416    match n {
1417        JSXAttrName::Ident(i) => {
1418            if i.sym.contains('-') {
1419                PropName::Str(Str {
1420                    span,
1421                    raw: None,
1422                    value: i.sym.into(),
1423                })
1424            } else {
1425                PropName::Ident(i)
1426            }
1427        }
1428        JSXAttrName::JSXNamespacedName(JSXNamespacedName { ns, name, .. }) => {
1429            let value = format!("{}:{}", ns.sym, name.sym);
1430
1431            PropName::Str(Str {
1432                span,
1433                raw: None,
1434                value: value.into(),
1435            })
1436        }
1437        #[cfg(swc_ast_unknown)]
1438        _ => panic!("unable to access unknown nodes"),
1439    }
1440}
1441
1442/// https://github.com/microsoft/TypeScript/blob/9e20e032effad965567d4a1e1c30d5433b0a3332/src/compiler/transformers/jsx.ts#L572-L608
1443///
1444/// JSX trims whitespace at the end and beginning of lines, except that the
1445/// start/end of a tag is considered a start/end of a line only if that line is
1446/// on the same line as the closing tag. See examples in
1447/// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
1448/// See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
1449///
1450/// An equivalent algorithm would be:
1451/// - If there is only one line, return it.
1452/// - If there is only whitespace (but multiple lines), return `undefined`.
1453/// - Split the text into lines.
1454/// - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
1455/// - Decode entities on each line (individually).
1456/// - Remove empty lines and join the rest with " ".
1457///
1458/// This version takes both `value` (decoded) and `raw` (original source) to
1459/// preserve whitespace that was explicitly encoded as HTML entities like
1460/// `&#32;`, `&#9;`, `&#10;`, `&#13;`.
1461#[inline]
1462fn jsx_text_to_str_with_raw(value: &Atom, raw: &Atom) -> Wtf8Atom {
1463    // Fast path: if no HTML entities (raw == value), use the simple algorithm
1464    if value.as_str() == raw.as_str() {
1465        return jsx_text_to_str_impl(value).into();
1466    }
1467
1468    // Build a mask of which byte positions in value came from HTML entities
1469    let entity_mask = build_entity_mask(value, raw);
1470
1471    jsx_text_to_str_with_entity_mask(value, &entity_mask).into()
1472}
1473
1474/// Build a mask indicating which character positions in `value` came from HTML
1475/// entities in `raw`.
1476///
1477/// Returns a Vec<bool> where true means the character at that index was from an
1478/// entity.
1479fn build_entity_mask(value: &str, raw: &str) -> Vec<bool> {
1480    let mut mask = vec![false; value.chars().count()];
1481    let mut value_char_idx = 0;
1482    let mut raw_chars = raw.chars().peekable();
1483
1484    while let Some(raw_c) = raw_chars.next() {
1485        if raw_c == '&' {
1486            // Possible HTML entity
1487            let mut entity_chars: Vec<char> = vec!['&'];
1488            let mut found_semicolon = false;
1489
1490            // Collect up to 10 characters to match entity pattern
1491            for _ in 0..10 {
1492                if let Some(&next_c) = raw_chars.peek() {
1493                    entity_chars.push(next_c);
1494                    raw_chars.next();
1495                    if next_c == ';' {
1496                        found_semicolon = true;
1497                        break;
1498                    }
1499                } else {
1500                    break;
1501                }
1502            }
1503
1504            if found_semicolon && is_valid_entity(&entity_chars) {
1505                // This was a valid entity - mark this position in value as from
1506                // entity
1507                if value_char_idx < mask.len() {
1508                    mask[value_char_idx] = true;
1509                }
1510                value_char_idx += 1;
1511            } else {
1512                // Not a valid entity, the '&' is literal
1513                value_char_idx += 1;
1514                // The other characters we consumed are also literal
1515                for _ in 1..entity_chars.len() {
1516                    value_char_idx += 1;
1517                }
1518            }
1519        } else {
1520            // Regular character
1521            value_char_idx += 1;
1522        }
1523    }
1524
1525    mask
1526}
1527
1528/// Check if the collected characters form a valid HTML entity
1529fn is_valid_entity(chars: &[char]) -> bool {
1530    if chars.len() < 3 {
1531        return false;
1532    }
1533    if chars[0] != '&' || chars[chars.len() - 1] != ';' {
1534        return false;
1535    }
1536
1537    let inner: String = chars[1..chars.len() - 1].iter().collect();
1538
1539    if let Some(stripped) = inner.strip_prefix('#') {
1540        // Numeric entity
1541        if let Some(hex) = stripped
1542            .strip_prefix('x')
1543            .or_else(|| stripped.strip_prefix('X'))
1544        {
1545            // Hex: &#xHHHH;
1546            !hex.is_empty() && hex.chars().all(|c| c.is_ascii_hexdigit())
1547        } else {
1548            // Decimal: &#DDDD;
1549            !stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit())
1550        }
1551    } else {
1552        // Named entity - check against known entities
1553        is_known_html_entity(&inner)
1554    }
1555}
1556
1557/// Check if name is a known HTML entity
1558fn is_known_html_entity(name: &str) -> bool {
1559    // Common HTML entities that decode to whitespace or other characters
1560    // This list matches the entities defined in swc_ecma_lexer jsx.rs xhtml!
1561    // macro
1562    matches!(
1563        name,
1564        "nbsp"
1565            | "iexcl"
1566            | "cent"
1567            | "pound"
1568            | "curren"
1569            | "yen"
1570            | "brvbar"
1571            | "sect"
1572            | "uml"
1573            | "copy"
1574            | "ordf"
1575            | "laquo"
1576            | "not"
1577            | "shy"
1578            | "reg"
1579            | "macr"
1580            | "deg"
1581            | "plusmn"
1582            | "sup2"
1583            | "sup3"
1584            | "acute"
1585            | "micro"
1586            | "para"
1587            | "middot"
1588            | "cedil"
1589            | "sup1"
1590            | "ordm"
1591            | "raquo"
1592            | "frac14"
1593            | "frac12"
1594            | "frac34"
1595            | "iquest"
1596            | "Agrave"
1597            | "Aacute"
1598            | "Acirc"
1599            | "Atilde"
1600            | "Auml"
1601            | "Aring"
1602            | "AElig"
1603            | "Ccedil"
1604            | "Egrave"
1605            | "Eacute"
1606            | "Ecirc"
1607            | "Euml"
1608            | "Igrave"
1609            | "Iacute"
1610            | "Icirc"
1611            | "Iuml"
1612            | "ETH"
1613            | "Ntilde"
1614            | "Ograve"
1615            | "Oacute"
1616            | "Ocirc"
1617            | "Otilde"
1618            | "Ouml"
1619            | "times"
1620            | "Oslash"
1621            | "Ugrave"
1622            | "Uacute"
1623            | "Ucirc"
1624            | "Uuml"
1625            | "Yacute"
1626            | "THORN"
1627            | "szlig"
1628            | "agrave"
1629            | "aacute"
1630            | "acirc"
1631            | "atilde"
1632            | "auml"
1633            | "aring"
1634            | "aelig"
1635            | "ccedil"
1636            | "egrave"
1637            | "eacute"
1638            | "ecirc"
1639            | "euml"
1640            | "igrave"
1641            | "iacute"
1642            | "icirc"
1643            | "iuml"
1644            | "eth"
1645            | "ntilde"
1646            | "ograve"
1647            | "oacute"
1648            | "ocirc"
1649            | "otilde"
1650            | "ouml"
1651            | "divide"
1652            | "oslash"
1653            | "ugrave"
1654            | "uacute"
1655            | "ucirc"
1656            | "uuml"
1657            | "yacute"
1658            | "thorn"
1659            | "yuml"
1660            | "OElig"
1661            | "oelig"
1662            | "Scaron"
1663            | "scaron"
1664            | "Yuml"
1665            | "fnof"
1666            | "circ"
1667            | "tilde"
1668            | "Alpha"
1669            | "Beta"
1670            | "Gamma"
1671            | "Delta"
1672            | "Epsilon"
1673            | "Zeta"
1674            | "Eta"
1675            | "Theta"
1676            | "Iota"
1677            | "Kappa"
1678            | "Lambda"
1679            | "Mu"
1680            | "Nu"
1681            | "Xi"
1682            | "Omicron"
1683            | "Pi"
1684            | "Rho"
1685            | "Sigma"
1686            | "Tau"
1687            | "Upsilon"
1688            | "Phi"
1689            | "Chi"
1690            | "Psi"
1691            | "Omega"
1692            | "alpha"
1693            | "beta"
1694            | "gamma"
1695            | "delta"
1696            | "epsilon"
1697            | "zeta"
1698            | "eta"
1699            | "theta"
1700            | "iota"
1701            | "kappa"
1702            | "lambda"
1703            | "mu"
1704            | "nu"
1705            | "xi"
1706            | "omicron"
1707            | "pi"
1708            | "rho"
1709            | "sigmaf"
1710            | "sigma"
1711            | "tau"
1712            | "upsilon"
1713            | "phi"
1714            | "chi"
1715            | "psi"
1716            | "omega"
1717            | "thetasym"
1718            | "upsih"
1719            | "piv"
1720            | "ensp"
1721            | "emsp"
1722            | "thinsp"
1723            | "zwnj"
1724            | "zwj"
1725            | "lrm"
1726            | "rlm"
1727            | "ndash"
1728            | "mdash"
1729            | "lsquo"
1730            | "rsquo"
1731            | "sbquo"
1732            | "ldquo"
1733            | "rdquo"
1734            | "bdquo"
1735            | "dagger"
1736            | "Dagger"
1737            | "bull"
1738            | "hellip"
1739            | "permil"
1740            | "prime"
1741            | "Prime"
1742            | "lsaquo"
1743            | "rsaquo"
1744            | "oline"
1745            | "frasl"
1746            | "euro"
1747            | "image"
1748            | "weierp"
1749            | "real"
1750            | "trade"
1751            | "alefsym"
1752            | "larr"
1753            | "uarr"
1754            | "rarr"
1755            | "darr"
1756            | "harr"
1757            | "crarr"
1758            | "lArr"
1759            | "uArr"
1760            | "rArr"
1761            | "dArr"
1762            | "hArr"
1763            | "forall"
1764            | "part"
1765            | "exist"
1766            | "empty"
1767            | "nabla"
1768            | "isin"
1769            | "notin"
1770            | "ni"
1771            | "prod"
1772            | "sum"
1773            | "minus"
1774            | "lowast"
1775            | "radic"
1776            | "prop"
1777            | "infin"
1778            | "ang"
1779            | "and"
1780            | "or"
1781            | "cap"
1782            | "cup"
1783            | "int"
1784            | "there4"
1785            | "sim"
1786            | "cong"
1787            | "asymp"
1788            | "ne"
1789            | "equiv"
1790            | "le"
1791            | "ge"
1792            | "sub"
1793            | "sup"
1794            | "nsub"
1795            | "sube"
1796            | "supe"
1797            | "oplus"
1798            | "otimes"
1799            | "perp"
1800            | "sdot"
1801            | "lceil"
1802            | "rceil"
1803            | "lfloor"
1804            | "rfloor"
1805            | "lang"
1806            | "rang"
1807            | "loz"
1808            | "spades"
1809            | "clubs"
1810            | "hearts"
1811            | "diams"
1812            | "quot"
1813            | "amp"
1814            | "lt"
1815            | "gt"
1816    )
1817}
1818
1819/// JSX text processing with entity mask - preserves whitespace from HTML
1820/// entities
1821fn jsx_text_to_str_with_entity_mask(t: &str, entity_mask: &[bool]) -> Atom {
1822    // Fast path: if no line terminators and no trimmable whitespace
1823    // (whitespace that's not from entities at the leading edge)
1824    let chars: Vec<char> = t.chars().collect();
1825    let has_line_terminator = chars.iter().any(|&c| is_line_terminator(c));
1826
1827    // For single-line text, we keep all whitespace (matching original behavior)
1828    // The original jsx_text_to_str_impl preserves leading/trailing whitespace on
1829    // single-line text
1830    if !t.is_empty() && !has_line_terminator {
1831        return t.into();
1832    }
1833
1834    let mut acc: Option<String> = None;
1835    let mut only_line: Option<String> = None;
1836    let mut line_start: Option<usize> = Some(0);
1837    let mut line_end: Option<usize> = None;
1838    // The first line preserves leading whitespace; subsequent lines trim it.
1839    let mut is_first_line = true;
1840
1841    for (char_idx, c) in chars.iter().enumerate() {
1842        let is_from_entity = *entity_mask.get(char_idx).unwrap_or(&false);
1843
1844        if is_line_terminator(*c) {
1845            // Process current line - trim both leading AND trailing (intermediate
1846            // line)
1847            if let (Some(start), Some(end)) = (line_start, line_end) {
1848                let line_text =
1849                    extract_line_content(&chars, start, end, entity_mask, !is_first_line, true);
1850                add_line_of_jsx_text_owned(line_text, &mut acc, &mut only_line);
1851            }
1852            is_first_line = false;
1853            line_start = None;
1854            line_end = None;
1855        } else if !is_white_space_single_line(*c) || is_from_entity {
1856            // Non-whitespace or entity-derived whitespace - counts as content
1857            line_end = Some(char_idx + 1);
1858            if line_start.is_none() {
1859                line_start = Some(char_idx);
1860            }
1861        }
1862    }
1863
1864    // Handle final line. Leading whitespace is preserved only if this is still
1865    // the first line (single-line input).
1866    if let Some(start) = line_start {
1867        let line_text = extract_line_content(
1868            &chars,
1869            start,
1870            chars.len(),
1871            entity_mask,
1872            !is_first_line,
1873            false,
1874        );
1875        add_line_of_jsx_text_owned(line_text, &mut acc, &mut only_line);
1876    }
1877
1878    if let Some(acc) = acc {
1879        acc.into()
1880    } else if let Some(only_line) = only_line {
1881        only_line.into()
1882    } else {
1883        "".into()
1884    }
1885}
1886
1887/// Extract line content, optionally trimming non-entity whitespace from edges
1888///
1889/// - `trim_leading`: if true, trim leading non-entity whitespace
1890/// - `trim_trailing`: if true, trim trailing non-entity whitespace
1891fn extract_line_content(
1892    chars: &[char],
1893    start: usize,
1894    end: usize,
1895    entity_mask: &[bool],
1896    trim_leading: bool,
1897    trim_trailing: bool,
1898) -> String {
1899    // Find first non-trimmable position (if trim_leading is true)
1900    let mut actual_start = start;
1901    if trim_leading {
1902        while actual_start < end {
1903            let c = chars[actual_start];
1904            let is_from_entity = *entity_mask.get(actual_start).unwrap_or(&false);
1905            if !is_white_space_single_line(c) || is_from_entity {
1906                break;
1907            }
1908            actual_start += 1;
1909        }
1910    }
1911
1912    // Find last non-trimmable position (if trim_trailing is true)
1913    let mut actual_end = end;
1914    if trim_trailing {
1915        while actual_end > actual_start {
1916            let c = chars[actual_end - 1];
1917            let is_from_entity = *entity_mask.get(actual_end - 1).unwrap_or(&false);
1918            if !is_white_space_single_line(c) || is_from_entity {
1919                break;
1920            }
1921            actual_end -= 1;
1922        }
1923    }
1924
1925    chars[actual_start..actual_end].iter().collect()
1926}
1927
1928/// Owned version of add_line_of_jsx_text for use with entity mask processing
1929fn add_line_of_jsx_text_owned(
1930    line: String,
1931    acc: &mut Option<String>,
1932    only_line: &mut Option<String>,
1933) {
1934    if line.is_empty() {
1935        return;
1936    }
1937
1938    if let Some(buffer) = acc.as_mut() {
1939        buffer.push(' ');
1940        buffer.push_str(&line);
1941    } else if let Some(only_line_content) = only_line.take() {
1942        let mut buffer = String::with_capacity(line.len() * 2);
1943        buffer.push_str(&only_line_content);
1944        buffer.push(' ');
1945        buffer.push_str(&line);
1946        *acc = Some(buffer);
1947    } else {
1948        *only_line = Some(line);
1949    }
1950}
1951
1952#[allow(dead_code)]
1953#[inline]
1954fn jsx_text_to_str<'a, T>(t: &'a T) -> Wtf8Atom
1955where
1956    &'a T: Into<&'a Wtf8>,
1957    T: ?Sized,
1958{
1959    let t = t.into();
1960    // Fast path: JSX text is almost always valid UTF-8
1961    if let Some(s) = t.as_str() {
1962        return jsx_text_to_str_impl(s).into();
1963    }
1964
1965    // Slow path: Handle Wtf8 with surrogates (extremely rare)
1966    jsx_text_to_str_wtf8_impl(t)
1967}
1968
1969/// Handle JSX text with surrogates
1970fn jsx_text_to_str_wtf8_impl(t: &Wtf8) -> Wtf8Atom {
1971    let mut acc: Option<Wtf8Buf> = None;
1972    let mut only_line: Option<(usize, usize)> = None; // (start, end) byte positions
1973    let mut first_non_whitespace: Option<usize> = Some(0);
1974    let mut last_non_whitespace: Option<usize> = None;
1975
1976    let mut byte_pos = 0;
1977    for cp in t.code_points() {
1978        let c = cp.to_char_lossy();
1979        let cp_value = cp.to_u32();
1980
1981        // Calculate byte length of this code point in WTF-8
1982        let cp_byte_len = if cp_value < 0x80 {
1983            1
1984        } else if cp_value < 0x800 {
1985            2
1986        } else if cp_value < 0x10000 {
1987            3
1988        } else {
1989            4
1990        };
1991
1992        if is_line_terminator(c) {
1993            if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
1994                add_line_of_jsx_text_wtf8(first, last, t, &mut acc, &mut only_line);
1995            }
1996            first_non_whitespace = None;
1997        } else if !is_white_space_single_line(c) {
1998            last_non_whitespace = Some(byte_pos + cp_byte_len);
1999            if first_non_whitespace.is_none() {
2000                first_non_whitespace.replace(byte_pos);
2001            }
2002        }
2003
2004        byte_pos += cp_byte_len;
2005    }
2006
2007    // Handle final line
2008    if let Some(first) = first_non_whitespace {
2009        add_line_of_jsx_text_wtf8(first, t.len(), t, &mut acc, &mut only_line);
2010    }
2011
2012    if let Some(acc) = acc {
2013        acc.into()
2014    } else if let Some((start, end)) = only_line {
2015        t.slice(start, end).into()
2016    } else {
2017        Wtf8Atom::default()
2018    }
2019}
2020
2021/// Helper for adding lines of JSX text when handling Wtf8 with surrogates
2022fn add_line_of_jsx_text_wtf8(
2023    line_start: usize,
2024    line_end: usize,
2025    source: &Wtf8,
2026    acc: &mut Option<Wtf8Buf>,
2027    only_line: &mut Option<(usize, usize)>,
2028) {
2029    if let Some((only_start, only_end)) = only_line.take() {
2030        // Second line - create accumulator
2031        let mut buffer = Wtf8Buf::with_capacity(source.len());
2032        buffer.push_wtf8(source.slice(only_start, only_end));
2033        buffer.push_str(" ");
2034        buffer.push_wtf8(source.slice(line_start, line_end));
2035        *acc = Some(buffer);
2036    } else if let Some(ref mut buffer) = acc {
2037        // Subsequent lines
2038        buffer.push_str(" ");
2039        buffer.push_wtf8(source.slice(line_start, line_end));
2040    } else {
2041        // First line
2042        *only_line = Some((line_start, line_end));
2043    }
2044}
2045
2046/// Internal implementation that works with &str
2047#[inline]
2048fn jsx_text_to_str_impl(t: &str) -> Atom {
2049    // Fast path: if no line terminators and no leading/trailing whitespace
2050    if !t.is_empty()
2051        && !t.chars().any(is_line_terminator)
2052        && !t.starts_with(is_white_space_single_line)
2053        && !t.ends_with(is_white_space_single_line)
2054    {
2055        return t.into();
2056    }
2057
2058    let mut acc: Option<String> = None;
2059    let mut only_line: Option<&str> = None;
2060    let mut first_non_whitespace: Option<usize> = Some(0);
2061    let mut last_non_whitespace: Option<usize> = None;
2062
2063    for (index, c) in t.char_indices() {
2064        if is_line_terminator(c) {
2065            if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
2066                let line_text = &t[first..last];
2067                add_line_of_jsx_text(line_text, &mut acc, &mut only_line);
2068            }
2069            first_non_whitespace = None;
2070        } else if !is_white_space_single_line(c) {
2071            last_non_whitespace = Some(index + c.len_utf8());
2072            if first_non_whitespace.is_none() {
2073                first_non_whitespace.replace(index);
2074            }
2075        }
2076    }
2077
2078    if let Some(first) = first_non_whitespace {
2079        let line_text = &t[first..];
2080        add_line_of_jsx_text(line_text, &mut acc, &mut only_line);
2081    }
2082
2083    if let Some(acc) = acc {
2084        acc.into()
2085    } else if let Some(only_line) = only_line {
2086        only_line.into()
2087    } else {
2088        "".into()
2089    }
2090}
2091
2092/// [TODO]: Re-validate this whitespace handling logic.
2093///
2094/// We cannot use [swc_ecma_utils::str::is_white_space_single_line] because
2095/// HTML entities (like `&nbsp;` → `\u{00a0}`) are pre-processed by the parser,
2096/// making it impossible to distinguish them from literal Unicode characters. We
2097/// should never trim HTML entities.
2098///
2099/// As a reference, Babel only trims regular spaces and tabs, so this is a
2100/// simplified implementation already in use.
2101/// https://github.com/babel/babel/blob/e5c8dc7330cb2f66c37637677609df90b31ff0de/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts#L28-L39
2102fn is_white_space_single_line(c: char) -> bool {
2103    matches!(c, ' ' | '\t')
2104}
2105
2106// less allocations trick from OXC
2107// https://github.com/oxc-project/oxc/blob/4c35f4abb6874bd741b84b34df7889637425e9ea/crates/oxc_transformer/src/jsx/jsx_impl.rs#L1061-L1091
2108fn add_line_of_jsx_text<'a>(
2109    trimmed_line: &'a str,
2110    acc: &mut Option<String>,
2111    only_line: &mut Option<&'a str>,
2112) {
2113    if let Some(buffer) = acc.as_mut() {
2114        // Already some text in accumulator. Push a space before this line is added to
2115        // `acc`.
2116        buffer.push(' ');
2117    } else if let Some(only_line_content) = only_line.take() {
2118        // This is the 2nd line containing text. Previous line did not contain any HTML
2119        // entities. Generate an accumulator containing previous line and a
2120        // trailing space. Current line will be added to the accumulator after
2121        // it.
2122        let mut buffer = String::with_capacity(trimmed_line.len() * 2); // rough estimate
2123        buffer.push_str(only_line_content);
2124        buffer.push(' ');
2125        *acc = Some(buffer);
2126    }
2127
2128    // [TODO]: Decode any HTML entities in this line
2129
2130    // For now, just use the trimmed line directly
2131    if let Some(buffer) = acc.as_mut() {
2132        buffer.push_str(trimmed_line);
2133    } else {
2134        // This is the first line containing text, and there are no HTML entities in
2135        // this line. Record this line in `only_line`.
2136        // If this turns out to be the only line, we won't need to construct a String,
2137        // so avoid all copying.
2138        *only_line = Some(trimmed_line);
2139    }
2140}
2141
2142fn jsx_attr_value_to_expr(v: JSXAttrValue) -> Option<Box<Expr>> {
2143    Some(match v {
2144        JSXAttrValue::Str(s) => {
2145            let value = transform_jsx_attr_str(&s.value);
2146
2147            Lit::Str(Str {
2148                span: s.span,
2149                raw: None,
2150                value: value.into(),
2151            })
2152            .into()
2153        }
2154        JSXAttrValue::JSXExprContainer(e) => match e.expr {
2155            JSXExpr::JSXEmptyExpr(_) => None?,
2156            JSXExpr::Expr(e) => e,
2157            #[cfg(swc_ast_unknown)]
2158            _ => panic!("unable to access unknown nodes"),
2159        },
2160        JSXAttrValue::JSXElement(e) => e.into(),
2161        JSXAttrValue::JSXFragment(f) => f.into(),
2162        #[cfg(swc_ast_unknown)]
2163        _ => panic!("unable to access unknown nodes"),
2164    })
2165}
2166
2167fn transform_jsx_attr_str(v: &Wtf8) -> Wtf8Buf {
2168    // Fast path: check if transformation is needed
2169    let needs_transform = v.code_points().any(|cp| {
2170        if let Some(c) = cp.to_char() {
2171            matches!(
2172                c,
2173                '\u{0008}' | '\u{000c}' | '\n' | '\r' | '\t' | '\u{000b}' | '\0'
2174            )
2175        } else {
2176            false
2177        }
2178    });
2179
2180    if !needs_transform {
2181        return v.to_owned();
2182    }
2183
2184    let single_quote = false;
2185    let mut buf = Wtf8Buf::with_capacity(v.len());
2186    let mut iter = v.code_points().peekable();
2187
2188    while let Some(code_point) = iter.next() {
2189        if let Some(c) = code_point.to_char() {
2190            match c {
2191                '\u{0008}' => buf.push_str("\\b"),
2192                '\u{000c}' => buf.push_str("\\f"),
2193                ' ' => buf.push_char(' '),
2194
2195                '\n' | '\r' | '\t' => {
2196                    buf.push_char(' ');
2197
2198                    while let Some(next) = iter.peek() {
2199                        if next.to_char() == Some(' ') {
2200                            iter.next();
2201                        } else {
2202                            break;
2203                        }
2204                    }
2205                }
2206                '\u{000b}' => buf.push_str("\\v"),
2207                '\0' => buf.push_str("\\x00"),
2208
2209                '\'' if single_quote => buf.push_str("\\'"),
2210                '"' if !single_quote => buf.push_char('"'),
2211
2212                '\x01'..='\x0f' | '\x10'..='\x1f' => {
2213                    buf.push_char(c);
2214                }
2215
2216                '\x20'..='\x7e' => {
2217                    //
2218                    buf.push_char(c);
2219                }
2220                '\u{7f}'..='\u{ff}' => {
2221                    buf.push_char(c);
2222                }
2223
2224                _ => {
2225                    buf.push_char(c);
2226                }
2227            }
2228        } else {
2229            buf.push(code_point);
2230        }
2231    }
2232
2233    buf
2234}