swc_ecma_transforms_react/jsx/
mod.rs

1#![allow(clippy::redundant_allocation)]
2
3use std::{
4    iter::{self, once},
5    sync::RwLock,
6};
7
8use bytes_str::BytesStr;
9use once_cell::sync::Lazy;
10use rustc_hash::FxHashMap;
11use serde::{Deserialize, Serialize};
12use string_enum::StringEnum;
13use swc_atoms::{
14    atom,
15    wtf8::{Wtf8, Wtf8Buf},
16    Atom, Wtf8Atom,
17};
18use swc_common::{
19    comments::{Comment, CommentKind, Comments},
20    errors::HANDLER,
21    sync::Lrc,
22    util::take::Take,
23    FileName, Mark, SourceMap, Span, Spanned, SyntaxContext, DUMMY_SP,
24};
25use swc_config::merge::Merge;
26use swc_ecma_ast::*;
27use swc_ecma_parser::{parse_file_as_expr, Syntax};
28use swc_ecma_utils::{
29    drop_span, prepend_stmt, private_ident, quote_ident, str::is_line_terminator, ExprFactory,
30    StmtLike,
31};
32use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
33
34use self::static_check::should_use_create_element;
35use crate::refresh::options::{deserialize_refresh, RefreshOptions};
36
37mod static_check;
38#[cfg(test)]
39mod tests;
40
41/// https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#runtime
42#[derive(StringEnum, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
43pub enum Runtime {
44    /// `automatic`
45    Automatic,
46    /// `classic`
47    Classic,
48    /// `preserve`
49    Preserve,
50}
51
52/// Note: This will changed in v2
53impl Default for Runtime {
54    fn default() -> Self {
55        Runtime::Classic
56    }
57}
58
59#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, Merge)]
60#[serde(rename_all = "camelCase")]
61#[serde(deny_unknown_fields)]
62pub struct Options {
63    /// If this is `true`, swc will behave just like babel 8 with
64    /// `BABEL_8_BREAKING: true`.
65    #[serde(skip, default)]
66    pub next: Option<bool>,
67
68    #[serde(default)]
69    pub runtime: Option<Runtime>,
70
71    /// For automatic runtime
72    #[serde(default)]
73    pub import_source: Option<Atom>,
74
75    #[serde(default)]
76    pub pragma: Option<BytesStr>,
77    #[serde(default)]
78    pub pragma_frag: Option<BytesStr>,
79
80    #[serde(default)]
81    pub throw_if_namespace: Option<bool>,
82
83    #[serde(default)]
84    pub development: Option<bool>,
85
86    // @babel/plugin-transform-react-jsx: Since "useBuiltIns" is removed in Babel 8, you can remove
87    // it from the config.
88    #[deprecated(
89        since = "0.167.4",
90        note = r#"Since `useBuiltIns` is removed in swc, you can remove it from the config."#
91    )]
92    #[serde(default, alias = "useBuiltIns")]
93    pub use_builtins: Option<bool>,
94
95    // '@babel/plugin-transform-react-jsx: Since Babel 8, an inline object with spread elements is
96    // always used, and the "useSpread" option is no longer available. Please remove it from your
97    // config.',
98    #[deprecated(
99        since = "0.167.4",
100        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."#
101    )]
102    #[serde(default)]
103    pub use_spread: Option<bool>,
104
105    #[serde(default, deserialize_with = "deserialize_refresh")]
106    // default to disabled since this is still considered as experimental by now
107    pub refresh: Option<RefreshOptions>,
108}
109
110#[cfg(feature = "concurrent")]
111macro_rules! static_str {
112    ($s:expr) => {{
113        static VAL: Lazy<BytesStr> = Lazy::new(|| $s.into());
114        VAL.clone()
115    }};
116}
117
118#[cfg(not(feature = "concurrent"))]
119macro_rules! static_str {
120    ($s:expr) => {
121        $s.into()
122    };
123}
124
125pub fn default_import_source() -> Atom {
126    atom!("react")
127}
128
129pub fn default_pragma() -> BytesStr {
130    static_str!("React.createElement")
131}
132
133pub fn default_pragma_frag() -> BytesStr {
134    static_str!("React.Fragment")
135}
136
137fn default_throw_if_namespace() -> bool {
138    true
139}
140
141/// Parse `src` to use as a `pragma` or `pragmaFrag` in jsx.
142pub fn parse_expr_for_jsx(
143    cm: &SourceMap,
144    name: &str,
145    src: BytesStr,
146    top_level_mark: Mark,
147) -> Box<Expr> {
148    let fm = cm.new_source_file(cache_filename(name), src);
149
150    parse_file_as_expr(
151        &fm,
152        Syntax::default(),
153        Default::default(),
154        None,
155        &mut Vec::new(),
156    )
157    .map_err(|e| {
158        if HANDLER.is_set() {
159            HANDLER.with(|h| {
160                e.into_diagnostic(h)
161                    .note("Failed to parse jsx pragma")
162                    .emit()
163            })
164        }
165    })
166    .map(drop_span)
167    .map(|mut expr| {
168        apply_mark(&mut expr, top_level_mark);
169        expr
170    })
171    .unwrap_or_else(|()| {
172        panic!(
173            "failed to parse jsx option {}: '{}' is not an expression",
174            name, fm.src,
175        )
176    })
177}
178
179fn apply_mark(e: &mut Expr, mark: Mark) {
180    match e {
181        Expr::Ident(i) => {
182            i.ctxt = i.ctxt.apply_mark(mark);
183        }
184        Expr::Member(MemberExpr { obj, .. }) => {
185            apply_mark(obj, mark);
186        }
187        _ => {}
188    }
189}
190
191/// `@babel/plugin-transform-react-jsx`
192///
193/// Turn JSX into React function calls
194///
195///
196/// `top_level_mark` should be [Mark] passed to
197/// [swc_ecma_transforms_base::resolver::resolver_with_mark].
198///
199///
200/// # Parameters
201///
202/// ## `top_level_ctxt`
203///
204/// This is used to reference `React` defined by the user.
205///
206/// e.g.
207///
208/// ```js
209/// import React from 'react';
210/// ```
211pub fn jsx<C>(
212    cm: Lrc<SourceMap>,
213    comments: Option<C>,
214    options: Options,
215    top_level_mark: Mark,
216    unresolved_mark: Mark,
217) -> impl Pass + VisitMut
218where
219    C: Comments,
220{
221    visit_mut_pass(Jsx {
222        cm: cm.clone(),
223        top_level_mark,
224        unresolved_mark,
225        runtime: options.runtime.unwrap_or_default(),
226        import_source: options.import_source.unwrap_or_else(default_import_source),
227        import_jsx: None,
228        import_jsxs: None,
229        import_fragment: None,
230        import_create_element: None,
231
232        pragma: Lrc::new(parse_expr_for_jsx(
233            &cm,
234            "pragma",
235            options.pragma.unwrap_or_else(default_pragma),
236            top_level_mark,
237        )),
238        comments,
239        pragma_frag: Lrc::new(parse_expr_for_jsx(
240            &cm,
241            "pragmaFrag",
242            options.pragma_frag.unwrap_or_else(default_pragma_frag),
243            top_level_mark,
244        )),
245        development: options.development.unwrap_or_default(),
246        throw_if_namespace: options
247            .throw_if_namespace
248            .unwrap_or_else(default_throw_if_namespace),
249        top_level_node: true,
250    })
251}
252
253struct Jsx<C>
254where
255    C: Comments,
256{
257    cm: Lrc<SourceMap>,
258
259    top_level_mark: Mark,
260    unresolved_mark: Mark,
261
262    runtime: Runtime,
263    /// For automatic runtime.
264    import_source: Atom,
265    /// For automatic runtime.
266    import_jsx: Option<Ident>,
267    /// For automatic runtime.
268    import_jsxs: Option<Ident>,
269    /// For automatic runtime.
270    import_create_element: Option<Ident>,
271    /// For automatic runtime.
272    import_fragment: Option<Ident>,
273    top_level_node: bool,
274
275    pragma: Lrc<Box<Expr>>,
276    comments: Option<C>,
277    pragma_frag: Lrc<Box<Expr>>,
278    development: bool,
279    throw_if_namespace: bool,
280}
281
282#[derive(Debug, Default, Clone, PartialEq, Eq)]
283pub struct JsxDirectives {
284    pub runtime: Option<Runtime>,
285
286    /// For automatic runtime.
287    pub import_source: Option<Atom>,
288
289    /// Parsed from `@jsx`
290    pub pragma: Option<Lrc<Box<Expr>>>,
291
292    /// Parsed from `@jsxFrag`
293    pub pragma_frag: Option<Lrc<Box<Expr>>>,
294}
295
296impl JsxDirectives {
297    pub fn from_comments(
298        cm: &SourceMap,
299        _: Span,
300        comments: &[Comment],
301        top_level_mark: Mark,
302    ) -> Self {
303        let mut res = JsxDirectives::default();
304
305        for cmt in comments {
306            if cmt.kind != CommentKind::Block {
307                continue;
308            }
309
310            for line in cmt.text.lines() {
311                let mut line = line.trim();
312                if line.starts_with('*') {
313                    line = line[1..].trim();
314                }
315
316                if !line.starts_with("@jsx") {
317                    continue;
318                }
319
320                let mut words = line.split_whitespace();
321                loop {
322                    let pragma = words.next();
323                    if pragma.is_none() {
324                        break;
325                    }
326                    let val = words.next();
327
328                    match pragma {
329                        Some("@jsxRuntime") => match val {
330                            Some("classic") => res.runtime = Some(Runtime::Classic),
331                            Some("automatic") => res.runtime = Some(Runtime::Automatic),
332                            None => {}
333                            _ => {
334                                if HANDLER.is_set() {
335                                    HANDLER.with(|handler| {
336                                        handler
337                                            .struct_span_err(
338                                                cmt.span,
339                                                "Runtime must be either `classic` or `automatic`.",
340                                            )
341                                            .emit()
342                                    });
343                                }
344                            }
345                        },
346                        Some("@jsxImportSource") => {
347                            if let Some(src) = val {
348                                res.runtime = Some(Runtime::Automatic);
349                                res.import_source = Some(Atom::new(src));
350                            }
351                        }
352                        Some("@jsxFrag") => {
353                            if let Some(src) = val {
354                                if is_valid_for_pragma(src) {
355                                    // TODO: Optimize
356                                    let mut e = parse_expr_for_jsx(
357                                        cm,
358                                        "module-jsx-pragma-frag",
359                                        cache_source(src),
360                                        top_level_mark,
361                                    );
362                                    e.set_span(cmt.span);
363                                    res.pragma_frag = Some(e.into())
364                                }
365                            }
366                        }
367                        Some("@jsx") => {
368                            if let Some(src) = val {
369                                if is_valid_for_pragma(src) {
370                                    // TODO: Optimize
371                                    let mut e = parse_expr_for_jsx(
372                                        cm,
373                                        "module-jsx-pragma",
374                                        cache_source(src),
375                                        top_level_mark,
376                                    );
377                                    e.set_span(cmt.span);
378                                    res.pragma = Some(e.into());
379                                }
380                            }
381                        }
382                        _ => {}
383                    }
384                }
385            }
386        }
387
388        res
389    }
390}
391
392#[cfg(feature = "concurrent")]
393fn cache_filename(name: &str) -> Lrc<FileName> {
394    static FILENAME_CACHE: Lazy<RwLock<FxHashMap<String, Lrc<FileName>>>> =
395        Lazy::new(|| RwLock::new(FxHashMap::default()));
396
397    {
398        let cache = FILENAME_CACHE
399            .read()
400            .expect("Failed to read FILENAME_CACHE");
401        if let Some(f) = cache.get(name) {
402            return f.clone();
403        }
404    }
405
406    let file = Lrc::new(FileName::Internal(format!("jsx-config-{name}.js")));
407
408    {
409        let mut cache = FILENAME_CACHE
410            .write()
411            .expect("Failed to write FILENAME_CACHE");
412        cache.insert(name.to_string(), file.clone());
413    }
414
415    file
416}
417
418#[cfg(not(feature = "concurrent"))]
419fn cache_filename(name: &str) -> Lrc<FileName> {
420    Lrc::new(FileName::Internal(format!("jsx-config-{name}.js")))
421}
422
423#[cfg(feature = "concurrent")]
424fn cache_source(src: &str) -> BytesStr {
425    use rustc_hash::FxHashSet;
426
427    static CACHE: Lazy<RwLock<FxHashSet<BytesStr>>> =
428        Lazy::new(|| RwLock::new(FxHashSet::default()));
429
430    {
431        let cache = CACHE.write().unwrap();
432
433        if let Some(cached) = cache.get(src) {
434            return cached.clone();
435        }
436    }
437
438    let cached: BytesStr = src.to_string().into();
439    {
440        let mut cache = CACHE.write().unwrap();
441        cache.insert(cached.clone());
442    }
443    cached
444}
445
446#[cfg(not(feature = "concurrent"))]
447fn cache_source(src: &str) -> BytesStr {
448    // We cannot cache because Rc does not implement Send.
449    src.to_string().into()
450}
451
452fn is_valid_for_pragma(s: &str) -> bool {
453    if s.is_empty() {
454        return false;
455    }
456
457    if !s.starts_with(|c: char| Ident::is_valid_start(c)) {
458        return false;
459    }
460
461    for c in s.chars() {
462        if !Ident::is_valid_continue(c) && c != '.' {
463            return false;
464        }
465    }
466
467    true
468}
469
470impl<C> Jsx<C>
471where
472    C: Comments,
473{
474    fn inject_runtime<T, F>(&mut self, body: &mut Vec<T>, inject: F)
475    where
476        T: StmtLike,
477        // Fn(Vec<(local, imported)>, src, body)
478        F: Fn(Vec<(Ident, IdentName)>, &str, &mut Vec<T>),
479    {
480        if self.runtime == Runtime::Automatic {
481            if let Some(local) = self.import_create_element.take() {
482                inject(
483                    vec![(local, quote_ident!("createElement"))],
484                    &self.import_source,
485                    body,
486                );
487            }
488
489            let imports = self.import_jsx.take();
490            let imports = if self.development {
491                imports
492                    .map(|local| (local, quote_ident!("jsxDEV")))
493                    .into_iter()
494                    .chain(
495                        self.import_fragment
496                            .take()
497                            .map(|local| (local, quote_ident!("Fragment"))),
498                    )
499                    .collect::<Vec<_>>()
500            } else {
501                imports
502                    .map(|local| (local, quote_ident!("jsx")))
503                    .into_iter()
504                    .chain(
505                        self.import_jsxs
506                            .take()
507                            .map(|local| (local, quote_ident!("jsxs"))),
508                    )
509                    .chain(
510                        self.import_fragment
511                            .take()
512                            .map(|local| (local, quote_ident!("Fragment"))),
513                    )
514                    .collect::<Vec<_>>()
515            };
516
517            if !imports.is_empty() {
518                let jsx_runtime = if self.development {
519                    "jsx-dev-runtime"
520                } else {
521                    "jsx-runtime"
522                };
523
524                let value = format!("{}/{}", self.import_source, jsx_runtime);
525                inject(imports, &value, body)
526            }
527        }
528    }
529
530    fn jsx_frag_to_expr(&mut self, el: JSXFragment) -> Expr {
531        let mut span = el.span();
532
533        if let Some(comments) = &self.comments {
534            if span.lo.is_dummy() {
535                span.lo = Span::dummy_with_cmt().lo;
536            }
537
538            comments.add_pure_comment(span.lo);
539        }
540
541        match self.runtime {
542            Runtime::Automatic => {
543                let fragment = self
544                    .import_fragment
545                    .get_or_insert_with(|| private_ident!("_Fragment"))
546                    .clone();
547
548                let mut props_obj = ObjectLit {
549                    span: DUMMY_SP,
550                    props: Vec::new(),
551                };
552
553                let children = el
554                    .children
555                    .into_iter()
556                    .filter_map(|child| self.jsx_elem_child_to_expr(child))
557                    .map(Some)
558                    .collect::<Vec<_>>();
559
560                let use_jsxs = match children.len() {
561                    0 => false,
562                    1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
563                        props_obj
564                            .props
565                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
566                                key: PropName::Ident(quote_ident!("children")),
567                                value: children.into_iter().next().flatten().unwrap().expr,
568                            }))));
569
570                        false
571                    }
572                    _ => {
573                        props_obj
574                            .props
575                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
576                                key: PropName::Ident(quote_ident!("children")),
577                                value: ArrayLit {
578                                    span: DUMMY_SP,
579                                    elems: children,
580                                }
581                                .into(),
582                            }))));
583                        true
584                    }
585                };
586
587                let jsx = if use_jsxs && !self.development {
588                    self.import_jsxs
589                        .get_or_insert_with(|| private_ident!("_jsxs"))
590                        .clone()
591                } else {
592                    let jsx = if self.development { "_jsxDEV" } else { "_jsx" };
593                    self.import_jsx
594                        .get_or_insert_with(|| private_ident!(jsx))
595                        .clone()
596                };
597
598                let args = once(fragment.as_arg()).chain(once(props_obj.as_arg()));
599
600                let args = if self.development {
601                    args.chain(once(Expr::undefined(DUMMY_SP).as_arg()))
602                        .chain(once(use_jsxs.as_arg()))
603                        .collect()
604                } else {
605                    args.collect()
606                };
607
608                CallExpr {
609                    span,
610                    callee: jsx.as_callee(),
611                    args,
612                    ..Default::default()
613                }
614                .into()
615            }
616            Runtime::Classic => {
617                CallExpr {
618                    span,
619                    callee: (*self.pragma).clone().as_callee(),
620                    args: iter::once((*self.pragma_frag).clone().as_arg())
621                        // attribute: null
622                        .chain(iter::once(Lit::Null(Null { span: DUMMY_SP }).as_arg()))
623                        .chain({
624                            // Children
625                            el.children
626                                .into_iter()
627                                .filter_map(|c| self.jsx_elem_child_to_expr(c))
628                        })
629                        .collect(),
630                    ..Default::default()
631                }
632                .into()
633            }
634            Runtime::Preserve => unreachable!(),
635        }
636    }
637
638    /// # Automatic
639    ///
640    /// <div></div> => jsx('div', null);
641    ///
642    /// # Classic
643    ///
644    /// <div></div> => React.createElement('div', null);
645    fn jsx_elem_to_expr(&mut self, el: JSXElement) -> Expr {
646        let top_level_node = self.top_level_node;
647        let mut span = el.span();
648        let use_create_element = should_use_create_element(&el.opening.attrs);
649        self.top_level_node = false;
650
651        let name = self.jsx_name(el.opening.name);
652
653        if let Some(comments) = &self.comments {
654            if span.lo.is_dummy() {
655                span.lo = Span::dummy_with_cmt().lo;
656            }
657
658            comments.add_pure_comment(span.lo);
659        }
660
661        match self.runtime {
662            Runtime::Automatic => {
663                // function jsx(tagName: string, props: { children: Node[], ... }, key: string)
664
665                let mut props_obj = ObjectLit {
666                    span: DUMMY_SP,
667                    props: Vec::new(),
668                };
669
670                let mut key = None;
671                let mut source_props = None;
672                let mut self_props = None;
673
674                for attr in el.opening.attrs {
675                    match attr {
676                        JSXAttrOrSpread::JSXAttr(attr) => {
677                            //
678                            match attr.name {
679                                JSXAttrName::Ident(i) => {
680                                    //
681                                    if !use_create_element && i.sym == "key" {
682                                        key = attr
683                                            .value
684                                            .and_then(jsx_attr_value_to_expr)
685                                            .map(|expr| expr.as_arg());
686
687                                        if key.is_none() && HANDLER.is_set() {
688                                            HANDLER.with(|handler| {
689                                                handler
690                                                    .struct_span_err(
691                                                        i.span,
692                                                        "The value of property 'key' should not \
693                                                         be empty",
694                                                    )
695                                                    .emit();
696                                            });
697                                        }
698                                        continue;
699                                    }
700
701                                    if !use_create_element
702                                        && *i.sym == *"__source"
703                                        && self.development
704                                    {
705                                        if source_props.is_some() {
706                                            panic!("Duplicate __source is found");
707                                        }
708                                        source_props = attr
709                                            .value
710                                            .and_then(jsx_attr_value_to_expr)
711                                            .map(|expr| expr.as_arg());
712                                        assert_ne!(
713                                            source_props, None,
714                                            "value of property '__source' should not be empty"
715                                        );
716                                        continue;
717                                    }
718
719                                    if !use_create_element
720                                        && *i.sym == *"__self"
721                                        && self.development
722                                    {
723                                        if self_props.is_some() {
724                                            panic!("Duplicate __self is found");
725                                        }
726                                        self_props = attr
727                                            .value
728                                            .and_then(jsx_attr_value_to_expr)
729                                            .map(|expr| expr.as_arg());
730                                        assert_ne!(
731                                            self_props, None,
732                                            "value of property '__self' should not be empty"
733                                        );
734                                        continue;
735                                    }
736
737                                    let value = match attr.value {
738                                        Some(v) => jsx_attr_value_to_expr(v)
739                                            .expect("empty expression container?"),
740                                        None => true.into(),
741                                    };
742
743                                    // TODO: Check if `i` is a valid identifier.
744                                    let key = if i.sym.contains('-') {
745                                        PropName::Str(Str {
746                                            span: i.span,
747                                            raw: None,
748                                            value: i.sym.into(),
749                                        })
750                                    } else {
751                                        PropName::Ident(i)
752                                    };
753                                    props_obj.props.push(PropOrSpread::Prop(Box::new(
754                                        Prop::KeyValue(KeyValueProp { key, value }),
755                                    )));
756                                }
757                                JSXAttrName::JSXNamespacedName(JSXNamespacedName {
758                                    ns,
759                                    name,
760                                    ..
761                                }) => {
762                                    if self.throw_if_namespace && HANDLER.is_set() {
763                                        HANDLER.with(|handler| {
764                                            handler
765                                                .struct_span_err(
766                                                    span,
767                                                    "JSX Namespace is disabled by default because \
768                                                     react does not support it yet. You can \
769                                                     specify jsc.transform.react.throwIfNamespace \
770                                                     to false to override default behavior",
771                                                )
772                                                .emit()
773                                        });
774                                    }
775
776                                    let value = match attr.value {
777                                        Some(v) => jsx_attr_value_to_expr(v)
778                                            .expect("empty expression container?"),
779                                        None => true.into(),
780                                    };
781
782                                    let str_value = format!("{}:{}", ns.sym, name.sym);
783                                    let key = Str {
784                                        span,
785                                        raw: None,
786                                        value: str_value.into(),
787                                    };
788                                    let key = PropName::Str(key);
789
790                                    props_obj.props.push(PropOrSpread::Prop(Box::new(
791                                        Prop::KeyValue(KeyValueProp { key, value }),
792                                    )));
793                                }
794                                #[cfg(swc_ast_unknown)]
795                                _ => panic!("unable to access unknown nodes"),
796                            }
797                        }
798                        JSXAttrOrSpread::SpreadElement(attr) => match *attr.expr {
799                            Expr::Object(obj) => {
800                                props_obj.props.extend(obj.props);
801                            }
802                            _ => {
803                                props_obj.props.push(PropOrSpread::Spread(attr));
804                            }
805                        },
806                        #[cfg(swc_ast_unknown)]
807                        _ => panic!("unable to access unknown nodes"),
808                    }
809                }
810
811                let mut children = el
812                    .children
813                    .into_iter()
814                    .filter_map(|child| self.jsx_elem_child_to_expr(child))
815                    .map(Some)
816                    .collect::<Vec<_>>();
817
818                let use_jsxs = match children.len() {
819                    0 => false,
820                    1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
821                        if !use_create_element {
822                            props_obj
823                                .props
824                                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
825                                    key: PropName::Ident(quote_ident!("children")),
826                                    value: children
827                                        .take()
828                                        .into_iter()
829                                        .next()
830                                        .flatten()
831                                        .unwrap()
832                                        .expr,
833                                }))));
834                        }
835
836                        false
837                    }
838                    _ => {
839                        props_obj
840                            .props
841                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
842                                key: PropName::Ident(quote_ident!("children")),
843                                value: ArrayLit {
844                                    span: DUMMY_SP,
845                                    elems: children.take(),
846                                }
847                                .into(),
848                            }))));
849                        true
850                    }
851                };
852
853                let jsx = if use_create_element {
854                    self.import_create_element
855                        .get_or_insert_with(|| private_ident!("_createElement"))
856                        .clone()
857                } else if use_jsxs && !self.development {
858                    self.import_jsxs
859                        .get_or_insert_with(|| private_ident!("_jsxs"))
860                        .clone()
861                } else {
862                    let jsx = if self.development { "_jsxDEV" } else { "_jsx" };
863                    self.import_jsx
864                        .get_or_insert_with(|| private_ident!(jsx))
865                        .clone()
866                };
867
868                self.top_level_node = top_level_node;
869
870                let args = once(name.as_arg()).chain(once(props_obj.as_arg()));
871                let args = if use_create_element {
872                    args.chain(children.into_iter().flatten()).collect()
873                } else if self.development {
874                    // set undefined literal to key if key is None
875                    let key = match key {
876                        Some(key) => key,
877                        None => Expr::undefined(DUMMY_SP).as_arg(),
878                    };
879
880                    // set undefined literal to __source if __source is None
881                    let source_props = match source_props {
882                        Some(source_props) => source_props,
883                        None => Expr::undefined(DUMMY_SP).as_arg(),
884                    };
885
886                    // set undefined literal to __self if __self is None
887                    let self_props = match self_props {
888                        Some(self_props) => self_props,
889                        None => Expr::undefined(DUMMY_SP).as_arg(),
890                    };
891                    args.chain(once(key))
892                        .chain(once(use_jsxs.as_arg()))
893                        .chain(once(source_props))
894                        .chain(once(self_props))
895                        .collect()
896                } else {
897                    args.chain(key).collect()
898                };
899                CallExpr {
900                    span,
901                    callee: jsx.as_callee(),
902                    args,
903                    ..Default::default()
904                }
905                .into()
906            }
907            Runtime::Classic => {
908                CallExpr {
909                    span,
910                    callee: (*self.pragma).clone().as_callee(),
911                    args: iter::once(name.as_arg())
912                        .chain(iter::once({
913                            // Attributes
914                            self.fold_attrs_for_classic(el.opening.attrs).as_arg()
915                        }))
916                        .chain({
917                            // Children
918                            el.children
919                                .into_iter()
920                                .filter_map(|c| self.jsx_elem_child_to_expr(c))
921                        })
922                        .collect(),
923                    ..Default::default()
924                }
925                .into()
926            }
927            Runtime::Preserve => unreachable!(),
928        }
929    }
930
931    fn jsx_elem_child_to_expr(&mut self, c: JSXElementChild) -> Option<ExprOrSpread> {
932        self.top_level_node = false;
933
934        Some(match c {
935            JSXElementChild::JSXText(text) => {
936                // TODO(kdy1): Optimize
937                let value = jsx_text_to_str(&*text.value);
938                let s = Str {
939                    span: text.span,
940                    raw: None,
941                    value,
942                };
943
944                if s.value.is_empty() {
945                    return None;
946                }
947
948                Lit::Str(s).as_arg()
949            }
950            JSXElementChild::JSXExprContainer(JSXExprContainer {
951                expr: JSXExpr::Expr(e),
952                ..
953            }) => e.as_arg(),
954            JSXElementChild::JSXExprContainer(JSXExprContainer {
955                expr: JSXExpr::JSXEmptyExpr(..),
956                ..
957            }) => return None,
958            JSXElementChild::JSXElement(el) => self.jsx_elem_to_expr(*el).as_arg(),
959            JSXElementChild::JSXFragment(el) => self.jsx_frag_to_expr(el).as_arg(),
960            JSXElementChild::JSXSpreadChild(JSXSpreadChild { span, expr, .. }) => ExprOrSpread {
961                spread: Some(span),
962                expr,
963            },
964            #[cfg(swc_ast_unknown)]
965            _ => panic!("unable to access unknown nodes"),
966        })
967    }
968
969    fn fold_attrs_for_classic(&mut self, attrs: Vec<JSXAttrOrSpread>) -> Box<Expr> {
970        if attrs.is_empty() {
971            return Lit::Null(Null { span: DUMMY_SP }).into();
972        }
973        let attr_cnt = attrs.len();
974
975        let mut props = Vec::new();
976        for attr in attrs {
977            match attr {
978                JSXAttrOrSpread::JSXAttr(attr) => {
979                    props.push(PropOrSpread::Prop(Box::new(self.attr_to_prop(attr))))
980                }
981                JSXAttrOrSpread::SpreadElement(spread) => {
982                    if attr_cnt == 1 {
983                        return spread.expr;
984                    }
985                    // babel does some optimizations
986                    match *spread.expr {
987                        Expr::Object(obj) => props.extend(obj.props),
988                        _ => props.push(PropOrSpread::Spread(spread)),
989                    }
990                }
991                #[cfg(swc_ast_unknown)]
992                _ => panic!("unable to access unknown nodes"),
993            }
994        }
995
996        let obj = ObjectLit {
997            span: DUMMY_SP,
998            props,
999        };
1000
1001        obj.into()
1002    }
1003
1004    fn attr_to_prop(&mut self, a: JSXAttr) -> Prop {
1005        let key = to_prop_name(a.name);
1006        let value = a
1007            .value
1008            .map(|v| match v {
1009                JSXAttrValue::Str(s) => {
1010                    let value = transform_jsx_attr_str(&s.value);
1011
1012                    Lit::Str(Str {
1013                        span: s.span,
1014                        raw: None,
1015                        value: value.into(),
1016                    })
1017                    .into()
1018                }
1019                JSXAttrValue::JSXExprContainer(JSXExprContainer {
1020                    expr: JSXExpr::Expr(e),
1021                    ..
1022                }) => e,
1023                JSXAttrValue::JSXElement(element) => Box::new(self.jsx_elem_to_expr(*element)),
1024                JSXAttrValue::JSXFragment(fragment) => Box::new(self.jsx_frag_to_expr(fragment)),
1025                JSXAttrValue::JSXExprContainer(JSXExprContainer {
1026                    span: _,
1027                    expr: JSXExpr::JSXEmptyExpr(_),
1028                }) => unreachable!("attr_to_prop(JSXEmptyExpr)"),
1029                #[cfg(swc_ast_unknown)]
1030                _ => panic!("unable to access unknown nodes"),
1031            })
1032            .unwrap_or_else(|| {
1033                Lit::Bool(Bool {
1034                    span: key.span(),
1035                    value: true,
1036                })
1037                .into()
1038            });
1039        Prop::KeyValue(KeyValueProp { key, value })
1040    }
1041}
1042
1043impl<C> Jsx<C>
1044where
1045    C: Comments,
1046{
1047    /// If we found required jsx directives, we returns true.
1048    fn parse_directives(&mut self, span: Span) -> bool {
1049        let mut found = false;
1050
1051        let directives = self.comments.with_leading(span.lo, |comments| {
1052            JsxDirectives::from_comments(&self.cm, span, comments, self.top_level_mark)
1053        });
1054
1055        let JsxDirectives {
1056            runtime,
1057            import_source,
1058            pragma,
1059            pragma_frag,
1060        } = directives;
1061
1062        if let Some(runtime) = runtime {
1063            found = true;
1064            self.runtime = runtime;
1065        }
1066
1067        if let Some(import_source) = import_source {
1068            found = true;
1069            self.import_source = import_source;
1070        }
1071
1072        if let Some(pragma) = pragma {
1073            if let Runtime::Automatic = self.runtime {
1074                if HANDLER.is_set() {
1075                    HANDLER.with(|handler| {
1076                        handler
1077                            .struct_span_err(
1078                                pragma.span(),
1079                                "pragma cannot be set when runtime is automatic",
1080                            )
1081                            .emit()
1082                    });
1083                }
1084            }
1085
1086            found = true;
1087            self.pragma = pragma;
1088        }
1089
1090        if let Some(pragma_frag) = pragma_frag {
1091            if let Runtime::Automatic = self.runtime {
1092                if HANDLER.is_set() {
1093                    HANDLER.with(|handler| {
1094                        handler
1095                            .struct_span_err(
1096                                pragma_frag.span(),
1097                                "pragmaFrag cannot be set when runtime is automatic",
1098                            )
1099                            .emit()
1100                    });
1101                }
1102            }
1103
1104            found = true;
1105            self.pragma_frag = pragma_frag;
1106        }
1107
1108        found
1109    }
1110}
1111
1112impl<C> VisitMut for Jsx<C>
1113where
1114    C: Comments,
1115{
1116    noop_visit_mut_type!();
1117
1118    fn visit_mut_expr(&mut self, expr: &mut Expr) {
1119        let top_level_node = self.top_level_node;
1120        let mut did_work = false;
1121
1122        if let Expr::JSXElement(el) = expr {
1123            did_work = true;
1124            // <div></div> => React.createElement('div', null);
1125            *expr = self.jsx_elem_to_expr(*el.take());
1126        } else if let Expr::JSXFragment(frag) = expr {
1127            // <></> => React.createElement(React.Fragment, null);
1128            did_work = true;
1129            *expr = self.jsx_frag_to_expr(frag.take());
1130        } else if let Expr::Paren(ParenExpr {
1131            expr: inner_expr, ..
1132        }) = expr
1133        {
1134            if let Expr::JSXElement(el) = &mut **inner_expr {
1135                did_work = true;
1136                *expr = self.jsx_elem_to_expr(*el.take());
1137            } else if let Expr::JSXFragment(frag) = &mut **inner_expr {
1138                // <></> => React.createElement(React.Fragment, null);
1139                did_work = true;
1140                *expr = self.jsx_frag_to_expr(frag.take());
1141            }
1142        }
1143
1144        if did_work {
1145            self.top_level_node = false;
1146        }
1147
1148        expr.visit_mut_children_with(self);
1149
1150        self.top_level_node = top_level_node;
1151    }
1152
1153    fn visit_mut_module(&mut self, module: &mut Module) {
1154        self.parse_directives(module.span);
1155
1156        for item in &module.body {
1157            let span = item.span();
1158            if self.parse_directives(span) {
1159                break;
1160            }
1161        }
1162
1163        module.visit_mut_children_with(self);
1164
1165        if self.runtime == Runtime::Automatic {
1166            self.inject_runtime(&mut module.body, |imports, src, stmts| {
1167                let specifiers = imports
1168                    .into_iter()
1169                    .map(|(local, imported)| {
1170                        ImportSpecifier::Named(ImportNamedSpecifier {
1171                            span: DUMMY_SP,
1172                            local,
1173                            imported: Some(ModuleExportName::Ident(imported.into())),
1174                            is_type_only: false,
1175                        })
1176                    })
1177                    .collect();
1178
1179                prepend_stmt(
1180                    stmts,
1181                    ImportDecl {
1182                        span: DUMMY_SP,
1183                        specifiers,
1184                        src: Str {
1185                            span: DUMMY_SP,
1186                            raw: None,
1187                            value: src.into(),
1188                        }
1189                        .into(),
1190                        type_only: Default::default(),
1191                        with: Default::default(),
1192                        phase: Default::default(),
1193                    }
1194                    .into(),
1195                )
1196            });
1197        }
1198    }
1199
1200    fn visit_mut_script(&mut self, script: &mut Script) {
1201        self.parse_directives(script.span);
1202
1203        for item in &script.body {
1204            let span = item.span();
1205            if self.parse_directives(span) {
1206                break;
1207            }
1208        }
1209
1210        script.visit_mut_children_with(self);
1211
1212        if self.runtime == Runtime::Automatic {
1213            let mark = self.unresolved_mark;
1214            self.inject_runtime(&mut script.body, |imports, src, stmts| {
1215                prepend_stmt(stmts, add_require(imports, src, mark))
1216            });
1217        }
1218    }
1219}
1220
1221// const { createElement } = require('react')
1222// const { jsx: jsx } = require('react/jsx-runtime')
1223fn add_require(imports: Vec<(Ident, IdentName)>, src: &str, unresolved_mark: Mark) -> Stmt {
1224    VarDecl {
1225        span: DUMMY_SP,
1226        kind: VarDeclKind::Const,
1227        declare: false,
1228        decls: vec![VarDeclarator {
1229            span: DUMMY_SP,
1230            name: Pat::Object(ObjectPat {
1231                span: DUMMY_SP,
1232                props: imports
1233                    .into_iter()
1234                    .map(|(local, imported)| {
1235                        if imported.sym != local.sym {
1236                            ObjectPatProp::KeyValue(KeyValuePatProp {
1237                                key: PropName::Ident(imported),
1238                                value: Box::new(Pat::Ident(local.into())),
1239                            })
1240                        } else {
1241                            ObjectPatProp::Assign(AssignPatProp {
1242                                span: DUMMY_SP,
1243                                key: local.into(),
1244                                value: None,
1245                            })
1246                        }
1247                    })
1248                    .collect(),
1249                optional: false,
1250                type_ann: None,
1251            }),
1252            // require('react')
1253            init: Some(Box::new(Expr::Call(CallExpr {
1254                span: DUMMY_SP,
1255                callee: Callee::Expr(Box::new(Expr::Ident(Ident {
1256                    ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1257                    sym: atom!("require"),
1258                    optional: false,
1259                    ..Default::default()
1260                }))),
1261                args: vec![ExprOrSpread {
1262                    spread: None,
1263                    expr: Box::new(Expr::Lit(Lit::Str(Str {
1264                        span: DUMMY_SP,
1265                        value: src.into(),
1266                        raw: None,
1267                    }))),
1268                }],
1269                ..Default::default()
1270            }))),
1271            definite: false,
1272        }],
1273        ..Default::default()
1274    }
1275    .into()
1276}
1277
1278impl<C> Jsx<C>
1279where
1280    C: Comments,
1281{
1282    fn jsx_name(&self, name: JSXElementName) -> Box<Expr> {
1283        let span = name.span();
1284        match name {
1285            JSXElementName::Ident(i) => {
1286                if i.sym == "this" {
1287                    return ThisExpr { span }.into();
1288                }
1289
1290                // If it starts with lowercase
1291                if i.as_ref().starts_with(|c: char| c.is_ascii_lowercase()) {
1292                    Lit::Str(Str {
1293                        span,
1294                        raw: None,
1295                        value: i.sym.into(),
1296                    })
1297                    .into()
1298                } else {
1299                    i.into()
1300                }
1301            }
1302            JSXElementName::JSXNamespacedName(JSXNamespacedName {
1303                ref ns, ref name, ..
1304            }) => {
1305                if self.throw_if_namespace && HANDLER.is_set() {
1306                    HANDLER.with(|handler| {
1307                        handler
1308                            .struct_span_err(
1309                                span,
1310                                "JSX Namespace is disabled by default because react does not \
1311                                 support it yet. You can specify \
1312                                 jsc.transform.react.throwIfNamespace to false to override \
1313                                 default behavior",
1314                            )
1315                            .emit()
1316                    });
1317                }
1318
1319                let value = format!("{}:{}", ns.sym, name.sym);
1320
1321                Lit::Str(Str {
1322                    span,
1323                    raw: None,
1324                    value: value.into(),
1325                })
1326                .into()
1327            }
1328            JSXElementName::JSXMemberExpr(JSXMemberExpr { obj, prop, .. }) => {
1329                fn convert_obj(obj: JSXObject) -> Box<Expr> {
1330                    let span = obj.span();
1331
1332                    (match obj {
1333                        JSXObject::Ident(i) => {
1334                            if i.sym == "this" {
1335                                Expr::This(ThisExpr { span })
1336                            } else {
1337                                i.into()
1338                            }
1339                        }
1340                        JSXObject::JSXMemberExpr(e) => MemberExpr {
1341                            span,
1342                            obj: convert_obj(e.obj),
1343                            prop: MemberProp::Ident(e.prop),
1344                        }
1345                        .into(),
1346                        #[cfg(swc_ast_unknown)]
1347                        _ => panic!("unable to access unknown nodes"),
1348                    })
1349                    .into()
1350                }
1351                MemberExpr {
1352                    span,
1353                    obj: convert_obj(obj),
1354                    prop: MemberProp::Ident(prop),
1355                }
1356                .into()
1357            }
1358            #[cfg(swc_ast_unknown)]
1359            _ => panic!("unable to access unknown nodes"),
1360        }
1361    }
1362}
1363
1364fn to_prop_name(n: JSXAttrName) -> PropName {
1365    let span = n.span();
1366
1367    match n {
1368        JSXAttrName::Ident(i) => {
1369            if i.sym.contains('-') {
1370                PropName::Str(Str {
1371                    span,
1372                    raw: None,
1373                    value: i.sym.into(),
1374                })
1375            } else {
1376                PropName::Ident(i)
1377            }
1378        }
1379        JSXAttrName::JSXNamespacedName(JSXNamespacedName { ns, name, .. }) => {
1380            let value = format!("{}:{}", ns.sym, name.sym);
1381
1382            PropName::Str(Str {
1383                span,
1384                raw: None,
1385                value: value.into(),
1386            })
1387        }
1388        #[cfg(swc_ast_unknown)]
1389        _ => panic!("unable to access unknown nodes"),
1390    }
1391}
1392
1393/// https://github.com/microsoft/TypeScript/blob/9e20e032effad965567d4a1e1c30d5433b0a3332/src/compiler/transformers/jsx.ts#L572-L608
1394///
1395/// JSX trims whitespace at the end and beginning of lines, except that the
1396/// start/end of a tag is considered a start/end of a line only if that line is
1397/// on the same line as the closing tag. See examples in
1398/// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
1399/// 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
1400///
1401/// An equivalent algorithm would be:
1402/// - If there is only one line, return it.
1403/// - If there is only whitespace (but multiple lines), return `undefined`.
1404/// - Split the text into lines.
1405/// - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
1406/// - Decode entities on each line (individually).
1407/// - Remove empty lines and join the rest with " ".
1408#[inline]
1409fn jsx_text_to_str<'a, T>(t: &'a T) -> Wtf8Atom
1410where
1411    &'a T: Into<&'a Wtf8>,
1412    T: ?Sized,
1413{
1414    let t = t.into();
1415    // Fast path: JSX text is almost always valid UTF-8
1416    if let Some(s) = t.as_str() {
1417        return jsx_text_to_str_impl(s).into();
1418    }
1419
1420    // Slow path: Handle Wtf8 with surrogates (extremely rare)
1421    jsx_text_to_str_wtf8_impl(t)
1422}
1423
1424/// Handle JSX text with surrogates
1425fn jsx_text_to_str_wtf8_impl(t: &Wtf8) -> Wtf8Atom {
1426    let mut acc: Option<Wtf8Buf> = None;
1427    let mut only_line: Option<(usize, usize)> = None; // (start, end) byte positions
1428    let mut first_non_whitespace: Option<usize> = Some(0);
1429    let mut last_non_whitespace: Option<usize> = None;
1430
1431    let mut byte_pos = 0;
1432    for cp in t.code_points() {
1433        let c = cp.to_char_lossy();
1434        let cp_value = cp.to_u32();
1435
1436        // Calculate byte length of this code point in WTF-8
1437        let cp_byte_len = if cp_value < 0x80 {
1438            1
1439        } else if cp_value < 0x800 {
1440            2
1441        } else if cp_value < 0x10000 {
1442            3
1443        } else {
1444            4
1445        };
1446
1447        if is_line_terminator(c) {
1448            if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
1449                add_line_of_jsx_text_wtf8(first, last, t, &mut acc, &mut only_line);
1450            }
1451            first_non_whitespace = None;
1452        } else if !is_white_space_single_line(c) {
1453            last_non_whitespace = Some(byte_pos + cp_byte_len);
1454            if first_non_whitespace.is_none() {
1455                first_non_whitespace.replace(byte_pos);
1456            }
1457        }
1458
1459        byte_pos += cp_byte_len;
1460    }
1461
1462    // Handle final line
1463    if let Some(first) = first_non_whitespace {
1464        add_line_of_jsx_text_wtf8(first, t.len(), t, &mut acc, &mut only_line);
1465    }
1466
1467    if let Some(acc) = acc {
1468        acc.into()
1469    } else if let Some((start, end)) = only_line {
1470        t.slice(start, end).into()
1471    } else {
1472        Wtf8Atom::default()
1473    }
1474}
1475
1476/// Helper for adding lines of JSX text when handling Wtf8 with surrogates
1477fn add_line_of_jsx_text_wtf8(
1478    line_start: usize,
1479    line_end: usize,
1480    source: &Wtf8,
1481    acc: &mut Option<Wtf8Buf>,
1482    only_line: &mut Option<(usize, usize)>,
1483) {
1484    if let Some((only_start, only_end)) = only_line.take() {
1485        // Second line - create accumulator
1486        let mut buffer = Wtf8Buf::with_capacity(source.len());
1487        buffer.push_wtf8(source.slice(only_start, only_end));
1488        buffer.push_str(" ");
1489        buffer.push_wtf8(source.slice(line_start, line_end));
1490        *acc = Some(buffer);
1491    } else if let Some(ref mut buffer) = acc {
1492        // Subsequent lines
1493        buffer.push_str(" ");
1494        buffer.push_wtf8(source.slice(line_start, line_end));
1495    } else {
1496        // First line
1497        *only_line = Some((line_start, line_end));
1498    }
1499}
1500
1501/// Internal implementation that works with &str
1502#[inline]
1503fn jsx_text_to_str_impl(t: &str) -> Atom {
1504    let mut acc: Option<String> = None;
1505    let mut only_line: Option<&str> = None;
1506    let mut first_non_whitespace: Option<usize> = Some(0);
1507    let mut last_non_whitespace: Option<usize> = None;
1508
1509    for (index, c) in t.char_indices() {
1510        if is_line_terminator(c) {
1511            if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
1512                let line_text = &t[first..last];
1513                add_line_of_jsx_text(line_text, &mut acc, &mut only_line);
1514            }
1515            first_non_whitespace = None;
1516        } else if !is_white_space_single_line(c) {
1517            last_non_whitespace = Some(index + c.len_utf8());
1518            if first_non_whitespace.is_none() {
1519                first_non_whitespace.replace(index);
1520            }
1521        }
1522    }
1523
1524    if let Some(first) = first_non_whitespace {
1525        let line_text = &t[first..];
1526        add_line_of_jsx_text(line_text, &mut acc, &mut only_line);
1527    }
1528
1529    if let Some(acc) = acc {
1530        acc.into()
1531    } else if let Some(only_line) = only_line {
1532        only_line.into()
1533    } else {
1534        "".into()
1535    }
1536}
1537
1538/// [TODO]: Re-validate this whitespace handling logic.
1539///
1540/// We cannot use [swc_ecma_utils::str::is_white_space_single_line] because
1541/// HTML entities (like `&nbsp;` → `\u{00a0}`) are pre-processed by the parser,
1542/// making it impossible to distinguish them from literal Unicode characters. We
1543/// should never trim HTML entities.
1544///
1545/// As a reference, Babel only trims regular spaces and tabs, so this is a
1546/// simplified implementation already in use.
1547/// https://github.com/babel/babel/blob/e5c8dc7330cb2f66c37637677609df90b31ff0de/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts#L28-L39
1548fn is_white_space_single_line(c: char) -> bool {
1549    matches!(c, ' ' | '\t')
1550}
1551
1552// less allocations trick from OXC
1553// https://github.com/oxc-project/oxc/blob/4c35f4abb6874bd741b84b34df7889637425e9ea/crates/oxc_transformer/src/jsx/jsx_impl.rs#L1061-L1091
1554fn add_line_of_jsx_text<'a>(
1555    trimmed_line: &'a str,
1556    acc: &mut Option<String>,
1557    only_line: &mut Option<&'a str>,
1558) {
1559    if let Some(buffer) = acc.as_mut() {
1560        // Already some text in accumulator. Push a space before this line is added to
1561        // `acc`.
1562        buffer.push(' ');
1563    } else if let Some(only_line_content) = only_line.take() {
1564        // This is the 2nd line containing text. Previous line did not contain any HTML
1565        // entities. Generate an accumulator containing previous line and a
1566        // trailing space. Current line will be added to the accumulator after
1567        // it.
1568        let mut buffer = String::with_capacity(trimmed_line.len() * 2); // rough estimate
1569        buffer.push_str(only_line_content);
1570        buffer.push(' ');
1571        *acc = Some(buffer);
1572    }
1573
1574    // [TODO]: Decode any HTML entities in this line
1575
1576    // For now, just use the trimmed line directly
1577    if let Some(buffer) = acc.as_mut() {
1578        buffer.push_str(trimmed_line);
1579    } else {
1580        // This is the first line containing text, and there are no HTML entities in
1581        // this line. Record this line in `only_line`.
1582        // If this turns out to be the only line, we won't need to construct a String,
1583        // so avoid all copying.
1584        *only_line = Some(trimmed_line);
1585    }
1586}
1587
1588fn jsx_attr_value_to_expr(v: JSXAttrValue) -> Option<Box<Expr>> {
1589    Some(match v {
1590        JSXAttrValue::Str(s) => {
1591            let value = transform_jsx_attr_str(&s.value);
1592
1593            Lit::Str(Str {
1594                span: s.span,
1595                raw: None,
1596                value: value.into(),
1597            })
1598            .into()
1599        }
1600        JSXAttrValue::JSXExprContainer(e) => match e.expr {
1601            JSXExpr::JSXEmptyExpr(_) => None?,
1602            JSXExpr::Expr(e) => e,
1603            #[cfg(swc_ast_unknown)]
1604            _ => panic!("unable to access unknown nodes"),
1605        },
1606        JSXAttrValue::JSXElement(e) => e.into(),
1607        JSXAttrValue::JSXFragment(f) => f.into(),
1608        #[cfg(swc_ast_unknown)]
1609        _ => panic!("unable to access unknown nodes"),
1610    })
1611}
1612
1613fn transform_jsx_attr_str(v: &Wtf8) -> Wtf8Buf {
1614    let single_quote = false;
1615    let mut buf = Wtf8Buf::with_capacity(v.len());
1616    let mut iter = v.code_points().peekable();
1617
1618    while let Some(code_point) = iter.next() {
1619        if let Some(c) = code_point.to_char() {
1620            match c {
1621                '\u{0008}' => buf.push_str("\\b"),
1622                '\u{000c}' => buf.push_str("\\f"),
1623                ' ' => buf.push_char(' '),
1624
1625                '\n' | '\r' | '\t' => {
1626                    buf.push_char(' ');
1627
1628                    while let Some(next) = iter.peek() {
1629                        if next.to_char() == Some(' ') {
1630                            iter.next();
1631                        } else {
1632                            break;
1633                        }
1634                    }
1635                }
1636                '\u{000b}' => buf.push_str("\\v"),
1637                '\0' => buf.push_str("\\x00"),
1638
1639                '\'' if single_quote => buf.push_str("\\'"),
1640                '"' if !single_quote => buf.push_char('"'),
1641
1642                '\x01'..='\x0f' | '\x10'..='\x1f' => {
1643                    buf.push_char(c);
1644                }
1645
1646                '\x20'..='\x7e' => {
1647                    //
1648                    buf.push_char(c);
1649                }
1650                '\u{7f}'..='\u{ff}' => {
1651                    buf.push_char(c);
1652                }
1653
1654                _ => {
1655                    buf.push_char(c);
1656                }
1657            }
1658        } else {
1659            buf.push(code_point);
1660        }
1661    }
1662
1663    buf
1664}