diff --git a/analysis/src/Utils.ml b/analysis/src/Utils.ml index 863598dc568..21fc887fe77 100644 --- a/analysis/src/Utils.ml +++ b/analysis/src/Utils.ml @@ -112,6 +112,7 @@ let identifyPexp pexp = | Pexp_open _ -> "Pexp_open" | Pexp_await _ -> "Pexp_await" | Pexp_jsx_element _ -> "Pexp_jsx_element" + | Pexp_jsx_text _ -> "Pexp_jsx_text" let identifyPpat pat = match pat with diff --git a/compiler/frontend/bs_ast_mapper.ml b/compiler/frontend/bs_ast_mapper.ml index 292e199b5ae..b549a6dec72 100644 --- a/compiler/frontend/bs_ast_mapper.ml +++ b/compiler/frontend/bs_ast_mapper.ml @@ -406,6 +406,10 @@ module E = struct jsx_container_element ~loc ~attrs name (map_jsx_props sub props) ote (map_jsx_children sub children) closing_tag + | Pexp_jsx_text + {jsx_text_content; jsx_text_leading_space; jsx_text_trailing_space} -> + jsx_text ~loc ~attrs ~leading_space:jsx_text_leading_space + ~trailing_space:jsx_text_trailing_space jsx_text_content end module P = struct diff --git a/compiler/ml/ast_helper.ml b/compiler/ml/ast_helper.ml index 2fae640eb07..3b787736cb7 100644 --- a/compiler/ml/ast_helper.ml +++ b/compiler/ml/ast_helper.ml @@ -217,6 +217,16 @@ module Exp = struct jsx_container_element_closing_tag = e; })) + let jsx_text ?loc ?attrs ?(leading_space = false) ?(trailing_space = false) + text = + mk ?loc ?attrs + (Pexp_jsx_text + { + jsx_text_content = text; + jsx_text_leading_space = leading_space; + jsx_text_trailing_space = trailing_space; + }) + let case ?bar lhs ?guard rhs = {pc_bar = bar; pc_lhs = lhs; pc_guard = guard; pc_rhs = rhs} diff --git a/compiler/ml/ast_helper.mli b/compiler/ml/ast_helper.mli index 11227b903ad..fb67ef26965 100644 --- a/compiler/ml/ast_helper.mli +++ b/compiler/ml/ast_helper.mli @@ -225,6 +225,14 @@ module Exp : sig Parsetree.jsx_closing_container_tag option -> expression + val jsx_text : + ?loc:loc -> + ?attrs:attrs -> + ?leading_space:bool -> + ?trailing_space:bool -> + string -> + expression + val case : ?bar:Lexing.position -> pattern -> ?guard:expression -> expression -> case val await : ?loc:loc -> ?attrs:attrs -> expression -> expression diff --git a/compiler/ml/ast_iterator.ml b/compiler/ml/ast_iterator.ml index a430bb0b7bd..ec216c5e282 100644 --- a/compiler/ml/ast_iterator.ml +++ b/compiler/ml/ast_iterator.ml @@ -377,6 +377,7 @@ module E = struct iter_loc sub name; iter_jsx_props sub props; iter_jsx_children sub children + | Pexp_jsx_text _ -> () end module P = struct diff --git a/compiler/ml/ast_mapper.ml b/compiler/ml/ast_mapper.ml index 673465477bd..2be0ada2c0f 100644 --- a/compiler/ml/ast_mapper.ml +++ b/compiler/ml/ast_mapper.ml @@ -370,6 +370,10 @@ module E = struct (map_jsx_props sub props) ote (map_jsx_children sub children) closing_tag + | Pexp_jsx_text + {jsx_text_content; jsx_text_leading_space; jsx_text_trailing_space} -> + jsx_text ~loc ~attrs ~leading_space:jsx_text_leading_space + ~trailing_space:jsx_text_trailing_space jsx_text_content end module P = struct diff --git a/compiler/ml/ast_mapper_from0.ml b/compiler/ml/ast_mapper_from0.ml index 3f91d6ac1ee..1a2698cbee0 100644 --- a/compiler/ml/ast_mapper_from0.ml +++ b/compiler/ml/ast_mapper_from0.ml @@ -310,24 +310,44 @@ module E = struct | _ -> true) attrs + let try_map_jsx_text (sub : mapper) (e : expression) : Pt.expression option = + match e.pexp_desc with + | Pexp_apply + ( { + pexp_desc = + Pexp_ident {txt = Longident.Ldot (Lident "React", "string")}; + }, + [ + ( Asttypes.Noloc.Nolabel, + {pexp_desc = Pexp_constant (Pconst_string (text, None))} ); + ] ) -> + let loc = sub.location sub e.pexp_loc in + Some (Ast_helper.Exp.jsx_text ~loc text) + | _ -> None + let map_jsx_children sub (e : expression) : Pt.jsx_children = + let map_jsx_child (e : expression) : Pt.expression = + match try_map_jsx_text sub e with + | Some jsx_text -> jsx_text + | None -> sub.expr sub e + in let rec visit (e : expression) : Pt.expression list = match e.pexp_desc with | Pexp_construct ({txt = Longident.Lident "::"}, Some {pexp_desc = Pexp_tuple [e1; e2]}) -> - sub.expr sub e1 :: visit e2 + map_jsx_child e1 :: visit e2 | Pexp_construct ({txt = Longident.Lident "[]"}, ext_opt) -> ( match ext_opt with | None -> [] | Some e -> visit e) - | _ -> [sub.expr sub e] + | _ -> [map_jsx_child e] in match e.pexp_desc with | Pexp_construct ({txt = Longident.Lident "[]" | Longident.Lident "::"}, _) -> visit e - | _ -> [sub.expr sub e] + | _ -> [map_jsx_child e] let try_map_jsx_prop (sub : mapper) (lbl : Asttypes.Noloc.arg_label) (e : expression) : Parsetree.jsx_prop option = diff --git a/compiler/ml/ast_mapper_to0.ml b/compiler/ml/ast_mapper_to0.ml index d0ac43d737a..94d4a176835 100644 --- a/compiler/ml/ast_mapper_to0.ml +++ b/compiler/ml/ast_mapper_to0.ml @@ -539,6 +539,17 @@ module E = struct (Asttypes.Noloc.Labelled "children", children_expr); (Asttypes.Noloc.Nolabel, jsx_unit_expr); ]) + | Pexp_jsx_text text -> + (* Transform JSX text to React.string("text") *) + let react_string_ident = + {loc; txt = Longident.Ldot (Lident "React", "string")} + in + let string_const = + Ast_helper0.Exp.constant ~loc + (Pconst_string (text.jsx_text_content, None)) + in + apply ~loc ~attrs (ident react_string_ident) + [(Asttypes.Noloc.Nolabel, string_const)] end module P = struct diff --git a/compiler/ml/depend.ml b/compiler/ml/depend.ml index e5e39eb4b55..243def59cc2 100644 --- a/compiler/ml/depend.ml +++ b/compiler/ml/depend.ml @@ -311,6 +311,7 @@ let rec add_expr bv exp = | JsxQualifiedLowerTag {path; _} | JsxUpperTag path -> add_path bv path); and_jsx_props bv props; add_jsx_children bv children + | Pexp_jsx_text _ -> () and add_jsx_children bv xs = List.iter (add_expr bv) xs diff --git a/compiler/ml/experimental_features.ml b/compiler/ml/experimental_features.ml index 662157b1efb..6ab2f918a3d 100644 --- a/compiler/ml/experimental_features.ml +++ b/compiler/ml/experimental_features.ml @@ -1,12 +1,14 @@ -type feature = LetUnwrap +type feature = LetUnwrap | JsxText let to_string (f : feature) : string = match f with | LetUnwrap -> "LetUnwrap" + | JsxText -> "JsxText" let from_string (s : string) : feature option = match s with | "LetUnwrap" -> Some LetUnwrap + | "JsxText" -> Some JsxText | _ -> None module FeatureSet = Set.Make (struct diff --git a/compiler/ml/experimental_features.mli b/compiler/ml/experimental_features.mli index 5c28eca2ba2..1cca1ab132a 100644 --- a/compiler/ml/experimental_features.mli +++ b/compiler/ml/experimental_features.mli @@ -1,4 +1,4 @@ -type feature = LetUnwrap +type feature = LetUnwrap | JsxText val enable_from_string : string -> unit val is_enabled : feature -> bool diff --git a/compiler/ml/parsetree.ml b/compiler/ml/parsetree.ml index 78c9899f744..1831e7269e7 100644 --- a/compiler/ml/parsetree.ml +++ b/compiler/ml/parsetree.ml @@ -316,6 +316,13 @@ and expression_desc = (* . *) | Pexp_await of expression | Pexp_jsx_element of jsx_element + | Pexp_jsx_text of jsx_text + +and jsx_text = { + jsx_text_content: string; (* The trimmed text content *) + jsx_text_leading_space: bool; (* Had whitespace before the text *) + jsx_text_trailing_space: bool; (* Had whitespace after the text *) +} (* an element of a record pattern or expression *) and 'a record_element = {lid: Longident.t loc; x: 'a; opt: bool (* optional *)} diff --git a/compiler/ml/printast.ml b/compiler/ml/printast.ml index 44d699eb388..6ed6836b6ed 100644 --- a/compiler/ml/printast.ml +++ b/compiler/ml/printast.ml @@ -380,6 +380,10 @@ and expression i ppf x = | Some closing_tag -> line i ppf "closing_tag =%a\n" fmt_jsx_tag_name closing_tag.jsx_closing_container_tag_name) + | Pexp_jsx_text + {jsx_text_content; jsx_text_leading_space; jsx_text_trailing_space} -> + line i ppf "Pexp_jsx_text %S (leading=%b, trailing=%b)\n" jsx_text_content + jsx_text_leading_space jsx_text_trailing_space and jsx_children i ppf children = line i ppf "jsx_children =\n"; diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index ee304dff7d7..a29d0e15a16 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -195,7 +195,7 @@ let iter_expression f e = module_expr me | Pexp_pack me -> module_expr me | Pexp_await _ -> assert false (* should be handled earlier *) - | Pexp_jsx_element _ -> + | Pexp_jsx_element _ | Pexp_jsx_text _ -> raise (Error (e.pexp_loc, Env.empty, Jsx_not_enabled)) and case {pc_lhs = _; pc_guard; pc_rhs} = may expr pc_guard; @@ -3256,7 +3256,7 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected) | Pexp_extension ext -> raise (Error_forward (Builtin_attributes.error_of_extension ext)) | Pexp_await _ -> (* should be handled earlier *) assert false - | Pexp_jsx_element _ -> + | Pexp_jsx_element _ | Pexp_jsx_text _ -> raise (Error (sexp.pexp_loc, Env.empty, Jsx_not_enabled)) and type_function ?in_function ~arity ~async loc attrs env ty_expected_ l diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 4cf9c9a0dd1..b6a8f40c089 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -1274,7 +1274,7 @@ let mk_react_jsx (config : Jsx_common.jsx_config) mapper loc attrs let args = [(nolabel, elementTag); (nolabel, props_record)] @ key_and_unit in Exp.apply ~loc ~attrs ~transformed_jsx:true jsx_expr args -(* In most situations, the component name is the make function from a module. +(* In most situations, the component name is the make function from a module. However, if the name contains a lowercase letter, it means it probably an external component. In this case, we use the name as is. See tests/syntax_tests/data/ppx/react/externalWithCustomName.res @@ -1348,6 +1348,17 @@ let expr ~(config : Jsx_common.jsx_config) mapper expression = | JsxTagInvalid name -> Jsx_common.raise_error ~loc "JSX: element name is neither upper- or lowercase, got \"%s\"" name)) + | { + pexp_desc = Pexp_jsx_text {jsx_text_content = text}; + pexp_loc = loc; + pexp_attributes = attrs; + } -> + (* Transform JSX text to React.string("text") *) + let react_string_ident = + Exp.ident ~loc {loc; txt = module_access_name config "string"} + in + let string_const = Exp.constant ~loc (Pconst_string (text, None)) in + Exp.apply ~loc ~attrs react_string_ident [(Nolabel, string_const)] | e -> default_mapper.expr mapper e let module_binding ~(config : Jsx_common.jsx_config) mapper module_binding = diff --git a/compiler/syntax/src/res_ast_debugger.ml b/compiler/syntax/src/res_ast_debugger.ml index 52a57b89c55..707105fbb7a 100644 --- a/compiler/syntax/src/res_ast_debugger.ml +++ b/compiler/syntax/src/res_ast_debugger.ml @@ -731,6 +731,15 @@ module SexpAst = struct Sexp.list (map_empty ~f:jsx_prop props); Sexp.list (map_empty ~f:expression xs); ] + | Pexp_jsx_text + {jsx_text_content; jsx_text_leading_space; jsx_text_trailing_space} -> + Sexp.list + [ + Sexp.atom "Pexp_jsx_text"; + Sexp.atom jsx_text_content; + Sexp.atom (Printf.sprintf "leading=%b" jsx_text_leading_space); + Sexp.atom (Printf.sprintf "trailing=%b" jsx_text_trailing_space); + ] in Sexp.list [Sexp.atom "expression"; desc] diff --git a/compiler/syntax/src/res_comments_table.ml b/compiler/syntax/src/res_comments_table.ml index 6774b2bc2b1..43554f775b0 100644 --- a/compiler/syntax/src/res_comments_table.ml +++ b/compiler/syntax/src/res_comments_table.ml @@ -100,9 +100,9 @@ let attach tbl loc comments = * * When splitting around the location of `x = 5`: * - leading: [comment1] - * - inside: [comment2] + * - inside: [comment2] * - trailing: [comment3] - * + * * This is the primary comment partitioning function used for associating comments * with AST nodes during the tree traversal. * @@ -1800,13 +1800,14 @@ and walk_expression expr t comments = let children_nodes = List.map (fun e -> Expression e) children in walk_list children_nodes t comments_for_children - (* It is less likely that there are comments inside the closing tag, + (* It is less likely that there are comments inside the closing tag, so we don't process them right now, - if you ever need this, feel free to update process _rest. + if you ever need this, feel free to update process _rest. Comments after the closing tag will already be taking into account by the parent node. *) ) | Pexp_await expr -> walk_expression expr t comments | Pexp_send _ -> () + | Pexp_jsx_text _ -> () and walk_expr_parameter (_attrs, _argLbl, expr_opt, pattern) t comments = let leading, inside, trailing = partition_by_loc comments pattern.ppat_loc in diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 0d7ef55e960..43bebaead0c 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -7,6 +7,9 @@ module ResPrinter = Res_printer module Scanner = Res_scanner module Parser = Res_parser +let is_jsx_text_enabled () = + Experimental_features.is_enabled Experimental_features.JsxText + let mk_loc start_loc end_loc = Location.{loc_start = start_loc; loc_end = end_loc; loc_ghost = false} @@ -3014,34 +3017,123 @@ and parse_jsx_props p : Parsetree.jsx_prop list = parse_region ~grammar:Grammar.JsxAttribute ~f:parse_jsx_prop p and parse_jsx_children p : Parsetree.jsx_children = + if is_jsx_text_enabled () then + (* JSX text RFC: https://github.com/rescript-lang/rescript/blob/master/docs/jsx-text-rfc.md *) + parse_jsx_children_2 p + else + let rec loop p children = + match p.Parser.token with + | Token.Eof -> children + | LessThan when Scanner.peekSlash p.scanner -> children + | LessThan -> + (* Imagine:
Hello
` instead of `{React.string(\"Hello\")}
`." } }, "additionalProperties": false diff --git a/docs/jsx-text-rfc.md b/docs/jsx-text-rfc.md new file mode 100644 index 00000000000..0d936d9b822 --- /dev/null +++ b/docs/jsx-text-rfc.md @@ -0,0 +1,285 @@ +# RFC: JSX Text Support + +## Summary + +This RFC proposes adding support for bare text content inside JSX elements, allowing developers to write `Hello
` instead of `{React.string("Hello")}
`. + +## Motivation + +Currently, ReScript requires explicit `React.string()` calls for text content in JSX: + +```rescript +// Current syntax (verbose) +{React.string("Hello world")}
+Hello world
+A B C
` parses and prints with correct spacing. + +### Type Checking (ast_mapper_to0.ml) + +During the mapping from new AST to old AST (before type checking), `Pexp_jsx_text` is transformed to `React.string("text")`: + +```ocaml +| Pexp_jsx_text text -> + (* Transform JSX text to React.string("text") *) + let react_string_ident = {loc; txt = Longident.Ldot (Lident "React", "string")} in + let string_const = Ast_helper0.Exp.constant ~loc (Pconst_string (text.jsx_text_content, None)) in + apply ~loc ~attrs (ident react_string_ident) [(Asttypes.Noloc.Nolabel, string_const)] +``` + +This means: + +- The type system sees `React.string("text")` and type-checks it normally +- No changes needed to the type checker +- The generated JavaScript is identical to explicit `React.string()` calls + +### Reverse Mapping (ast_mapper_from0.ml) + +When converting from old AST to new AST (e.g., for PPX output), the mapper detects `React.string("literal")` patterns in JSX children and converts them back to `Pexp_jsx_text`: + +```ocaml +| Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Longident.Ldot (Lident "React", "string")}}, + [(Nolabel, {pexp_desc = Pexp_constant (Pconst_string (text, None))})] ) -> + Some (Ast_helper.Exp.jsx_text ~loc text) +``` + +## Whitespace Handling + +The implementation tracks whitespace metadata for accurate round-tripping: + +- **Content**: The actual text is trimmed (leading/trailing whitespace removed) +- **Leading space flag**: Records if whitespace existed before the trimmed text +- **Trailing space flag**: Records if whitespace existed after the trimmed text + +This allows the printer to reconstruct proper spacing between elements: + +```rescript +// Input +Hello {name}!
` or `{React.string(`Hello {name}!`)}
`. + +3. **Forbidden characters**: `>` and `}` inside text produce parser errors with helpful suggestions. + +## Examples + +### Before (current syntax) + +```rescript +{React.string("This is a paragraph with ")}{React.string("bold")}{React.string(" text.")}
+This is a paragraph with bold text.
+Hello {userName}, you have {unreadCount->Int.toString} messages.
+``` + +## Development Commands + +All development commands require the `-enable-experimental JsxText` flag. + +### Parse and view AST + +To see how JSX text is parsed, including whitespace metadata: + +```bash +dune exec bsc -- -enable-experimental JsxText -dparsetree path/to/file.res -only-parse +``` + +Example output: + +``` +Pexp_jsx_text "Hello" (leading=false, trailing=true) +``` + +### Parse and reprint (round-trip test) + +To verify that parsing and reprinting preserves the original formatting: + +```bash +dune exec bsc -- -enable-experimental JsxText -only-parse -reprint-source path/to/file.res +``` + +### View transformed AST (with React.string) + +To see how the JSX text transforms to `React.string()` calls: + +```bash +dune exec bsc -- -enable-experimental JsxText -bs-jsx 4 -dparsetree path/to/file.res +``` + +### Full compilation + +To compile and see the generated JavaScript: + +```bash +dune exec bsc -- -enable-experimental JsxText path/to/file.res +``` + +## Implementation Checklist + +- [x] Add `Pexp_jsx_text` to `parsetree.ml` with whitespace metadata +- [x] Add `Ast_helper.Exp.jsx_text` helper with `leading_space` and `trailing_space` params +- [x] Update parser (`res_core.ml`) to parse JSX text with whitespace tracking +- [x] Update printer (`res_printer.ml`) to print bare text with correct spacing +- [x] Update `res_parens.ml` to not wrap JSX text in braces +- [x] Update `ast_mapper_to0.ml` to transform to `React.string()` +- [x] Update `ast_mapper_from0.ml` to recognize `React.string()` patterns +- [x] Update all pattern matches (`ast_mapper.ml`, `printast.ml`, `ast_iterator.ml`, `depend.ml`, `res_comments_table.ml`, `typecore.ml`, `res_ast_debugger.ml`, `bs_ast_mapper.ml`, `analysis/src/Utils.ml`) +- [ ] Add syntax tests +- [ ] Add integration tests +- [ ] Filter out empty text nodes +- [ ] Consider whitespace normalization (collapse multiple spaces/newlines) + +## Files Modified + +| File | Purpose | +| ------------------------------------------- | ----------------------------------------------- | +| `compiler/ml/parsetree.ml` | AST definition with `jsx_text` record type | +| `compiler/ml/ast_helper.ml` | Helper function to create `Pexp_jsx_text` nodes | +| `compiler/ml/ast_helper.mli` | Interface for the helper function | +| `compiler/ml/experimental_features.ml` | Added `JsxText` experimental feature | +| `compiler/ml/experimental_features.mli` | Interface for experimental features | +| `compiler/syntax/src/res_core.ml` | Parser implementation with whitespace tracking | +| `compiler/syntax/src/res_printer.ml` | Printer with whitespace-aware separators | +| `compiler/syntax/src/res_parens.ml` | Parenthesization rules | +| `compiler/ml/ast_mapper_to0.ml` | Transform to `React.string()` for type checking | +| `compiler/ml/ast_mapper_from0.ml` | Reverse transform from `React.string()` | +| `compiler/ml/ast_mapper.ml` | AST mapper support | +| `compiler/ml/ast_iterator.ml` | AST iterator support | +| `compiler/ml/printast.ml` | Debug AST printing | +| `compiler/ml/depend.ml` | Dependency analysis | +| `compiler/ml/typecore.ml` | Type checking error for direct use | +| `compiler/syntax/src/res_comments_table.ml` | Comment attachment | +| `compiler/syntax/src/res_ast_debugger.ml` | S-expression debug output | +| `compiler/frontend/bs_ast_mapper.ml` | Frontend AST mapper | +| `analysis/src/Utils.ml` | Analysis utilities | +| `rewatch/src/config.rs` | Added `JsxText` to rewatch config parsing | +| `docs/docson/build-schema.json` | JSON schema for `rescript.json` | + +## Future Considerations + +1. **HTML entities**: Could add `&`, `<`, `>`, `"` decoding +2. **Whitespace modes**: Could add options for whitespace handling (preserve, collapse, trim) +3. **Promotion to stable**: Once the feature is proven stable, it could be enabled by default diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index fdf65a93fd1..55b1a69ae35 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -235,6 +235,7 @@ pub enum DeprecationWarning { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum ExperimentalFeature { LetUnwrap, + JsxText, } impl<'de> serde::Deserialize<'de> for ExperimentalFeature { @@ -254,8 +255,9 @@ impl<'de> serde::Deserialize<'de> for ExperimentalFeature { { match v { "LetUnwrap" => Ok(ExperimentalFeature::LetUnwrap), + "JsxText" => Ok(ExperimentalFeature::JsxText), other => { - let available = ["LetUnwrap"].join(", "); + let available = ["LetUnwrap", "JsxText"].join(", "); Err(DeError::custom(format!( "Unknown experimental feature '{other}'. Available features: {available}", ))) @@ -571,6 +573,7 @@ impl Config { "-enable-experimental".to_string(), match feature { ExperimentalFeature::LetUnwrap => "LetUnwrap", + ExperimentalFeature::JsxText => "JsxText", } .to_string(), ] diff --git a/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected b/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected index 46b4d181884..60800d6029c 100644 --- a/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected +++ b/tests/build_tests/super_errors/expected/jsx_custom_component_children.res.expected @@ -1,10 +1,10 @@ [1;31mWe've found a bug for you![0m - [36m/.../fixtures/jsx_custom_component_children.res[0m:[2m24:28-29[0m + [36m/.../fixtures/jsx_custom_component_children.res[0m:[2m24:27-28[0m 22 [2m│[0m } 23 [2m│[0m - [1;31m24[0m [2m│[0m let x ={React.string("moo")}
+{React.string("moo")}
// c1 -{React.string("moo")}
+{React.string("moo")}
// c2 // c3 -{React.string("moo")}
+{React.string("moo")}
// c4 -{React.string("moo")}
+{React.string("moo")}
{React.string("moo")}
+{React.string("moo")}
// c1 -{React.string("moo")}
+{React.string("moo")}
// c2 // c3 -{React.string("moo")}
+{React.string("moo")}
// c4 -{React.string("moo")}
+{React.string("moo")}
> -let arrow_with_fragment = el => <> - {t(nbsp ++ "(")} - el - {t(")")} -> +let arrow_with_fragment = el => <> {t(nbsp ++ "(")} el {t(")")} > -let arrow_with_container_tag = el => -