From ff26d86081ba28167ce80c5f109c059bd02dbc87 Mon Sep 17 00:00:00 2001 From: nojaf Date: Sat, 2 Aug 2025 19:04:24 +0200 Subject: [PATCH 1/6] jsx text --- compiler/syntax/src/res_core.ml | 134 +++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 0d7ef55e96..cb158791e8 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -7,6 +7,8 @@ module ResPrinter = Res_printer module Scanner = Res_scanner module Parser = Res_parser +let is_jsx_2 = true + let mk_loc start_loc end_loc = Location.{loc_start = start_loc; loc_end = end_loc; loc_ghost = false} @@ -3014,34 +3016,128 @@ 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_2 then + (* New RFC proposal *) + 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:
< + * is `<` the start of a jsx-child?
+ * reconsiderLessThan peeks at the next token and + * determines the correct token to disambiguate *) + let child = + parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p + in + loop p (child :: children) + | token when Grammar.is_jsx_child_start token -> + let child = + parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p + in + loop p (child :: children) + | _ -> children + in + let children = + match p.Parser.token with + | DotDotDot -> + Parser.next p; + let expr = + parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p + in + Parsetree.JSXChildrenSpreading expr + | _ -> + let children = List.rev (loop p []) in + Parsetree.JSXChildrenItems children + in + children + +and _nojaf_dump_token prefix p = + let start_line = p.Parser.start_pos.Lexing.pos_lnum in + let start_col = + p.Parser.start_pos.Lexing.pos_cnum - p.Parser.start_pos.Lexing.pos_bol + 1 + in + let end_line = p.Parser.end_pos.Lexing.pos_lnum in + let end_col = + p.Parser.end_pos.Lexing.pos_cnum - p.Parser.end_pos.Lexing.pos_bol + 1 + in + + Format.printf "%s %s (%d,%d-%d,%d)\n" prefix + (Res_token.to_string p.token) + start_line start_col end_line end_col + +and parse_jsx_children_2 (p : Parser.t) : Parsetree.jsx_children = let rec loop p children = match p.Parser.token with | Token.Eof -> children - | LessThan when Scanner.peekSlash p.scanner -> children + (* next jsx child start or closing of current element *) | LessThan -> - (* Imagine:
< - * is `<` the start of a jsx-child?
- * reconsiderLessThan peeks at the next token and - * determines the correct token to disambiguate *) + if Scanner.peekSlash p.scanner then children + else + let child = parse_jsx p in + child :: loop p children + (* Expression hole *) + | Lbrace -> + print_endline "jsx child lbrace"; let child = parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p in - loop p (child :: children) - | token when Grammar.is_jsx_child_start token -> - let child = - parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p + child :: loop p children + | Rbrace -> failwith "Nested children not supported" + | _text_token -> + Printf.printf "text_token: %s\n" (Res_token.to_string p.token); + let text_expr = parse_jsx_text p in + text_expr :: loop p children + in + let children = loop p [] in + Parsetree.JSXChildrenItems children + +(** + Parse tokens as regular string inside a jsx element. + See https://facebook.github.io/jsx/#prod-JSXText + *) +and parse_jsx_text p : Parsetree.expression = + let start_pos = p.Parser.start_pos in + let rec visit p = + match p.Parser.token with + | GreaterThan -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message + "Unexpected token. Did you mean `{\">\"}` or `>`?"); + Parser.next p; + Recover.default_expr () + | Rbrace -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message + "Unexpected token. Did you mean `{\"}\"}` or `}`?"); + Parser.next p; + Recover.default_expr () + (* Nested children or the closing of the current element, so we can return the text as child. *) + | LessThan | Lbrace -> + let end_pos = p.Parser.start_pos in + let loc = mk_loc start_pos end_pos in + let text = + String.sub p.scanner.src start_pos.pos_cnum + (end_pos.pos_cnum - start_pos.pos_cnum) + |> String.trim in - loop p (child :: children) - | _ -> children + (* TODO: not sure what *j is *) + let string_expr = + Ast_helper.Exp.constant ~loc (Parsetree.Pconst_string (text, Some "*j")) + in + let react_string_ident = Longident.Ldot (Lident "React", "string") in + let attrs = [make_braces_attr loc] in + Ast_helper.Exp.apply ~loc ~attrs + (Ast_helper.Exp.ident ~loc (Location.mkloc react_string_ident loc)) + [(Nolabel, string_expr)] + | _text_token -> + Parser.next p; + visit p in - match p.Parser.token with - | DotDotDot -> - Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p - (Diagnostics.message ErrorMessages.spread_children_no_longer_supported); - Parser.next p; - [parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p] - | _ -> List.rev (loop p []) + visit p and parse_braced_or_record_expr p = let start_pos = p.Parser.start_pos in From 4c05cd7a74a31d76194d9419c2c0e6faf952569a Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 20 Jan 2026 09:05:03 +0100 Subject: [PATCH 2/6] Fix parser --- compiler/syntax/src/res_core.ml | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index cb158791e8..5c3ded7cec 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -3041,19 +3041,13 @@ and parse_jsx_children p : Parsetree.jsx_children = loop p (child :: children) | _ -> children in - let children = - match p.Parser.token with - | DotDotDot -> - Parser.next p; - let expr = - parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p - in - Parsetree.JSXChildrenSpreading expr - | _ -> - let children = List.rev (loop p []) in - Parsetree.JSXChildrenItems children - in - children + match p.Parser.token with + | DotDotDot -> + Parser.err ~start_pos:p.start_pos ~end_pos:p.end_pos p + (Diagnostics.message ErrorMessages.spread_children_no_longer_supported); + Parser.next p; + [parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p] + | _ -> List.rev (loop p []) and _nojaf_dump_token prefix p = let start_line = p.Parser.start_pos.Lexing.pos_lnum in @@ -3092,8 +3086,7 @@ and parse_jsx_children_2 (p : Parser.t) : Parsetree.jsx_children = let text_expr = parse_jsx_text p in text_expr :: loop p children in - let children = loop p [] in - Parsetree.JSXChildrenItems children + loop p [] (** Parse tokens as regular string inside a jsx element. From fff7d0af1860ff046ba491397cc260602bd0ce96 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 20 Jan 2026 09:42:47 +0100 Subject: [PATCH 3/6] Improve parser and introduce Pexp_jsx_text ast node --- analysis/src/Utils.ml | 1 + compiler/frontend/bs_ast_mapper.ml | 4 + compiler/ml/ast_helper.ml | 10 + compiler/ml/ast_helper.mli | 8 + compiler/ml/ast_iterator.ml | 1 + compiler/ml/ast_mapper.ml | 4 + compiler/ml/ast_mapper_from0.ml | 26 ++- compiler/ml/ast_mapper_to0.ml | 11 + compiler/ml/depend.ml | 1 + compiler/ml/parsetree.ml | 7 + compiler/ml/printast.ml | 4 + compiler/ml/typecore.ml | 4 +- compiler/syntax/src/res_ast_debugger.ml | 9 + compiler/syntax/src/res_comments_table.ml | 9 +- compiler/syntax/src/res_core.ml | 30 +-- compiler/syntax/src/res_parens.ml | 5 +- compiler/syntax/src/res_printer.ml | 71 ++++-- docs/jsx-text-rfc.md | 249 ++++++++++++++++++++++ 18 files changed, 414 insertions(+), 40 deletions(-) create mode 100644 docs/jsx-text-rfc.md diff --git a/analysis/src/Utils.ml b/analysis/src/Utils.ml index 863598dc56..21fc887fe7 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 292e199b5a..b549a6dec7 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 2fae640eb0..3b787736cb 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 11227b903a..fb67ef2696 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 a430bb0b7b..ec216c5e28 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 673465477b..2be0ada2c0 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 3f91d6ac1e..1a2698cbee 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 d0ac43d737..94d4a17683 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 e5e39eb4b5..243def59cc 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/parsetree.ml b/compiler/ml/parsetree.ml index 78c9899f74..1831e7269e 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 44d699eb38..6ed6836b6e 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 ee304dff7d..a29d0e15a1 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/res_ast_debugger.ml b/compiler/syntax/src/res_ast_debugger.ml index 52a57b89c5..707105fbb7 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 6774b2bc2b..43554f775b 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 5c3ded7cec..5a40897fc8 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -3075,14 +3075,12 @@ and parse_jsx_children_2 (p : Parser.t) : Parsetree.jsx_children = child :: loop p children (* Expression hole *) | Lbrace -> - print_endline "jsx child lbrace"; let child = parse_primary_expr ~operand:(parse_atomic_expr p) ~no_call:true p in child :: loop p children | Rbrace -> failwith "Nested children not supported" | _text_token -> - Printf.printf "text_token: %s\n" (Res_token.to_string p.token); let text_expr = parse_jsx_text p in text_expr :: loop p children in @@ -3093,6 +3091,8 @@ and parse_jsx_children_2 (p : Parser.t) : Parsetree.jsx_children = See https://facebook.github.io/jsx/#prod-JSXText *) and parse_jsx_text p : Parsetree.expression = + (* Use prev_end_pos to capture any leading whitespace that the scanner skipped *) + let raw_start_pos = p.Parser.prev_end_pos in let start_pos = p.Parser.start_pos in let rec visit p = match p.Parser.token with @@ -3112,20 +3112,22 @@ and parse_jsx_text p : Parsetree.expression = | LessThan | Lbrace -> let end_pos = p.Parser.start_pos in let loc = mk_loc start_pos end_pos in - let text = - String.sub p.scanner.src start_pos.pos_cnum - (end_pos.pos_cnum - start_pos.pos_cnum) - |> String.trim + let raw_text = + String.sub p.scanner.src raw_start_pos.pos_cnum + (end_pos.pos_cnum - raw_start_pos.pos_cnum) in - (* TODO: not sure what *j is *) - let string_expr = - Ast_helper.Exp.constant ~loc (Parsetree.Pconst_string (text, Some "*j")) + let text = String.trim raw_text in + let leading_space = + String.length raw_text > 0 + && (raw_text.[0] = ' ' || raw_text.[0] = '\t' || raw_text.[0] = '\n') in - let react_string_ident = Longident.Ldot (Lident "React", "string") in - let attrs = [make_braces_attr loc] in - Ast_helper.Exp.apply ~loc ~attrs - (Ast_helper.Exp.ident ~loc (Location.mkloc react_string_ident loc)) - [(Nolabel, string_expr)] + let trailing_space = + String.length raw_text > 0 + && + let last = raw_text.[String.length raw_text - 1] in + last = ' ' || last = '\t' || last = '\n' + in + Ast_helper.Exp.jsx_text ~loc ~leading_space ~trailing_space text | _text_token -> Parser.next p; visit p diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 8d8aeef140..297be044f7 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -375,7 +375,8 @@ let jsx_child_expr expr = ( Pexp_ident _ | Pexp_constant _ | Pexp_field _ | Pexp_construct _ | Pexp_variant _ | Pexp_array _ | Pexp_pack _ | Pexp_record _ | Pexp_extension _ | Pexp_letmodule _ | Pexp_letexception _ - | Pexp_open _ | Pexp_sequence _ | Pexp_let _ | Pexp_jsx_element _ ); + | Pexp_open _ | Pexp_sequence _ | Pexp_let _ | Pexp_jsx_element _ + | Pexp_jsx_text _ ); pexp_attributes = []; } -> Nothing @@ -386,7 +387,7 @@ let jsx_child_expr expr = pexp_attributes = []; } -> Nothing - | {pexp_desc = Pexp_jsx_element _} -> Nothing + | {pexp_desc = Pexp_jsx_element _ | Pexp_jsx_text _} -> Nothing | _ -> Parenthesized)) let binary_expr expr = diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 2010d23f6d..0409cf1509 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -1073,7 +1073,7 @@ and print_include_declaration ~state Doc.text "include "; (let include_doc = match include_declaration.pincl_mod.pmod_desc with - (* + (* include Module.Name({ type t = t }) try as oneliner if there is a single type alias declaration *) @@ -2981,6 +2981,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = }) -> print_jsx_container_tag ~state tag_name opening_greater_than props children closing_tag e.pexp_loc cmt_tbl + | Pexp_jsx_text {jsx_text_content = text} -> Doc.text text | Pexp_construct ({txt = Longident.Lident "()"}, _) -> Doc.text "()" | Pexp_construct ({txt = Longident.Lident "[]"}, _) -> Doc.concat @@ -4572,13 +4573,31 @@ and print_jsx_container_tag ~state tag_name | [] -> false in let line_sep = get_line_sep_for_jsx_children children in + (* Check if all children are simple (text or braced expressions, no nested JSX elements) *) + let all_children_simple = + List.for_all + (function + | {Parsetree.pexp_desc = Pexp_jsx_element _} -> false + | _ -> true) + children + in let print_children children = - Doc.concat - [ - Doc.indent - (Doc.concat [Doc.line; print_jsx_children ~state children cmt_tbl]); - line_sep; - ] + if all_children_simple then + (* For simple children, try to keep them on the same line as the tags *) + Doc.concat + [ + Doc.soft_line; + print_jsx_children ~state children cmt_tbl; + Doc.soft_line; + ] + else + (* For complex children with nested elements, use indentation and line breaks *) + Doc.concat + [ + Doc.indent + (Doc.concat [Doc.line; print_jsx_children ~state children cmt_tbl]); + line_sep; + ] in (* comments between the opening and closing tag *) @@ -4673,13 +4692,15 @@ and print_jsx_fragment ~state (opening_greater_than : Lexing.position) ]) and get_line_sep_for_jsx_children (children : Parsetree.jsx_children) = + (* Use hard_line only when there are nested JSX elements. + For simple children like text and expressions, use soft line + so they can be grouped on the same line when they fit. *) if - List.length children > 1 - || List.exists - (function - | {Parsetree.pexp_desc = Pexp_jsx_element _} -> true - | _ -> false) - children + List.exists + (function + | {Parsetree.pexp_desc = Pexp_jsx_element _} -> true + | _ -> false) + children then Doc.hard_line else Doc.line @@ -4720,6 +4741,25 @@ and print_jsx_children ~state (children : Parsetree.jsx_children) cmt_tbl = match children with | [] -> Doc.nil | children -> + (* Determine the separator between two adjacent children based on their types + and whitespace information from JSX text nodes *) + let get_separator_between x y = + match (x.Parsetree.pexp_desc, y.Parsetree.pexp_desc) with + (* Between two JSX text nodes: use trailing space of x or leading space of y *) + | ( Pexp_jsx_text {jsx_text_trailing_space = trailing}, + Pexp_jsx_text {jsx_text_leading_space = leading} ) -> + if trailing || leading then Doc.space else Doc.nil + (* Text followed by expression: use trailing space of text *) + | Pexp_jsx_text {jsx_text_trailing_space = trailing}, _ -> + if trailing then Doc.space else Doc.nil + (* Expression followed by text: use leading space of text *) + | _, Pexp_jsx_text {jsx_text_leading_space = leading} -> + if leading then Doc.space else Doc.nil + (* Between JSX elements: line break *) + | Pexp_jsx_element _, _ | _, Pexp_jsx_element _ -> sep + (* Default: use the computed separator *) + | _ -> sep + in let rec visit acc children = match children with | [] -> acc @@ -4737,16 +4777,17 @@ and print_jsx_children ~state (children : Parsetree.jsx_children) cmt_tbl = let leading_single_line_comments = get_leading_line_comment_count cmt_tbl (get_loc y) in + let child_sep = get_separator_between x y in (* If there are lines between the jsx elements, we preserve at least one line *) if (* Unless they are all comments *) (* The edge case of comment followed by blank line is not caught here *) lines_between > 0 && not (lines_between = leading_single_line_comments) then - let doc = Doc.concat [print_expr x; sep; Doc.hard_line] in + let doc = Doc.concat [print_expr x; child_sep; Doc.hard_line] in visit (Doc.concat [acc; doc]) rest else - let doc = Doc.concat [print_expr x; sep] in + let doc = Doc.concat [print_expr x; child_sep] in visit (Doc.concat [acc; doc]) rest in visit Doc.nil children diff --git a/docs/jsx-text-rfc.md b/docs/jsx-text-rfc.md new file mode 100644 index 0000000000..7f979ec595 --- /dev/null +++ b/docs/jsx-text-rfc.md @@ -0,0 +1,249 @@ +# 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")}

+
{React.string("Welcome to ")}{name}{React.string("!")}
+``` + +This is verbose compared to standard JSX in JavaScript/TypeScript: + +```jsx +// Standard JSX (what we want) +

Hello world

+
Welcome to {name}!
+``` + +## Design + +### New AST Node + +A new expression type `Pexp_jsx_text` is added to `parsetree.ml`: + +```ocaml +| 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 *) +} +``` + +This node represents literal text content inside JSX elements, with whitespace metadata for accurate round-tripping. + +### Parsing (res_core.ml) + +The parser's `parse_jsx_children_2` function recognizes text tokens inside JSX elements. When tokens don't match known JSX child starters (`<`, `{`, etc.), they are accumulated as text until a delimiter is reached. + +The `parse_jsx_text` function: + +1. Captures `prev_end_pos` to include any leading whitespace the scanner skipped +2. Accumulates tokens until it hits `<` (new element/closing tag) or `{` (expression) +3. Extracts the raw source text between positions +4. Trims whitespace but records whether leading/trailing whitespace existed +5. Creates a `Pexp_jsx_text` node with whitespace metadata + +Forbidden characters `>` and `}` produce error messages suggesting alternatives. + +### Printing (res_printer.ml) + +The printer outputs `Pexp_jsx_text` as bare text without any wrapping: + +- No braces `{}` +- No `React.string()` call +- Just the literal text content + +The printer uses the whitespace metadata to determine separators between JSX children: + +```ocaml +let get_separator_between x y = + match (x.Parsetree.pexp_desc, y.Parsetree.pexp_desc) with + | ( Pexp_jsx_text {jsx_text_trailing_space = trailing}, + Pexp_jsx_text {jsx_text_leading_space = leading} ) -> + if trailing || leading then Doc.space else Doc.nil + | Pexp_jsx_text {jsx_text_trailing_space = trailing}, _ -> + if trailing then Doc.space else Doc.nil + | _, Pexp_jsx_text {jsx_text_leading_space = leading} -> + if leading then Doc.space else Doc.nil + | _ -> (* default separator *) +``` + +This ensures round-trip preservation: `

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 world today
+ +// Parsed as: +// - "Hello" with trailing_space=true +// - world +// - "today" with leading_space=true + +// Reprints correctly as: +
Hello world today
+``` + +## Limitations + +1. **No HTML entity decoding**: Text like `>` remains as `>`, not `>`. Users must use `{">"}` for literal special characters. + +2. **No string interpolation**: Text is treated as a plain string. For dynamic content, use expression syntax: `

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("Welcome")}

+

{React.string("This is a paragraph with ")}{React.string("bold")}{React.string(" text.")}

+
+``` + +### After (with this RFC) + +```rescript +
+

Welcome

+

This is a paragraph with bold text.

+
+``` + +### Mixed content + +```rescript +

Hello {userName}, you have {unreadCount->Int.toString} messages.

+``` + +## Development Commands + +### Parse and view AST + +To see how JSX text is parsed, including whitespace metadata: + +```bash +dune exec bsc -- -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 -- -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 -- -bs-jsx 4 -dparsetree path/to/file.res +``` + +### Full compilation + +To compile and see the generated JavaScript: + +```bash +dune exec bsc -- 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/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 | + +## Future Considerations + +1. **HTML entities**: Could add `&`, `<`, `>`, `"` decoding +2. **Whitespace modes**: Could add options for whitespace handling (preserve, collapse, trim) +3. **Opt-in/opt-out**: Could be gated behind a flag during initial rollout From 069954ad4330740e190be57bb50f3cb27f6b34b9 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 20 Jan 2026 09:59:13 +0100 Subject: [PATCH 4/6] Add experimental feature --- compiler/ml/experimental_features.ml | 4 +- compiler/ml/experimental_features.mli | 2 +- compiler/syntax/src/res_core.ml | 7 +-- docs/docson/build-schema.json | 4 ++ docs/jsx-text-rfc.md | 46 +++++++++++++++++-- rewatch/src/config.rs | 5 +- ...jsx_custom_component_children.res.expected | 4 +- .../missing_required_prop.res.expected | 4 +- ...g_required_prop_when_children.res.expected | 6 +-- ...quired_prop_when_single_child.res.expected | 4 +- .../jsx_custom_component_children.res | 2 +- .../fixtures/missing_required_prop.res | 4 +- .../missing_required_prop_when_children.res | 6 +-- ...issing_required_prop_when_single_child.res | 4 +- .../typescript-react-example/src/Hooks.res | 37 +++++++-------- tests/tests/src/async_jsx.res | 2 +- tests/tests/src/jsx_preserve_test.res | 26 +++++------ 17 files changed, 103 insertions(+), 64 deletions(-) diff --git a/compiler/ml/experimental_features.ml b/compiler/ml/experimental_features.ml index 662157b1ef..6ab2f918a3 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 5c28eca2ba..1cca1ab132 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/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 5a40897fc8..43bebaead0 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -7,7 +7,8 @@ module ResPrinter = Res_printer module Scanner = Res_scanner module Parser = Res_parser -let is_jsx_2 = true +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} @@ -3016,8 +3017,8 @@ 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_2 then - (* New RFC proposal *) + 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 = diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index cc8e27955f..bfbfd953bd 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -473,6 +473,10 @@ "LetUnwrap": { "type": "boolean", "description": "Enable `let?` syntax." + }, + "JsxText": { + "type": "boolean", + "description": "Enable bare text content in JSX elements, e.g. `

Hello

` instead of `

{React.string(\"Hello\")}

`." } }, "additionalProperties": false diff --git a/docs/jsx-text-rfc.md b/docs/jsx-text-rfc.md index 7f979ec595..0d936d9b82 100644 --- a/docs/jsx-text-rfc.md +++ b/docs/jsx-text-rfc.md @@ -22,6 +22,36 @@ This is verbose compared to standard JSX in JavaScript/TypeScript:
Welcome to {name}!
``` +## Enabling the Feature + +JSX text support is an experimental feature that must be explicitly enabled. + +### Via rescript.json + +Add to your `rescript.json`: + +```json +{ + "experimental-features": { + "JsxText": true + } +} +``` + +### Via compiler flag + +Pass the flag directly to the compiler: + +```bash +bsc -enable-experimental JsxText myfile.res +``` + +For development/testing with dune: + +```bash +dune exec bsc -- -enable-experimental JsxText myfile.res +``` + ## Design ### New AST Node @@ -167,12 +197,14 @@ This allows the printer to reconstruct proper spacing between elements: ## 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 -- -dparsetree path/to/file.res -only-parse +dune exec bsc -- -enable-experimental JsxText -dparsetree path/to/file.res -only-parse ``` Example output: @@ -186,7 +218,7 @@ Pexp_jsx_text "Hello" (leading=false, trailing=true) To verify that parsing and reprinting preserves the original formatting: ```bash -dune exec bsc -- -only-parse -reprint-source path/to/file.res +dune exec bsc -- -enable-experimental JsxText -only-parse -reprint-source path/to/file.res ``` ### View transformed AST (with React.string) @@ -194,7 +226,7 @@ dune exec bsc -- -only-parse -reprint-source path/to/file.res To see how the JSX text transforms to `React.string()` calls: ```bash -dune exec bsc -- -bs-jsx 4 -dparsetree path/to/file.res +dune exec bsc -- -enable-experimental JsxText -bs-jsx 4 -dparsetree path/to/file.res ``` ### Full compilation @@ -202,7 +234,7 @@ dune exec bsc -- -bs-jsx 4 -dparsetree path/to/file.res To compile and see the generated JavaScript: ```bash -dune exec bsc -- path/to/file.res +dune exec bsc -- -enable-experimental JsxText path/to/file.res ``` ## Implementation Checklist @@ -227,6 +259,8 @@ dune exec bsc -- path/to/file.res | `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 | @@ -241,9 +275,11 @@ dune exec bsc -- path/to/file.res | `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. **Opt-in/opt-out**: Could be gated behind a flag during initial rollout +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 fdf65a93fd..55b1a69ae3 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 46b4d18188..60800d6029 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 @@ We've found a bug for you! - /.../fixtures/jsx_custom_component_children.res:24:28-29 + /.../fixtures/jsx_custom_component_children.res:24:27-28 22 │ } 23 │ - 24 │ let x = {1.} + 24 │ let x = {1.} 25 │ This has type: float diff --git a/tests/build_tests/super_errors/expected/missing_required_prop.res.expected b/tests/build_tests/super_errors/expected/missing_required_prop.res.expected index 98e2403974..b969d40ef2 100644 --- a/tests/build_tests/super_errors/expected/missing_required_prop.res.expected +++ b/tests/build_tests/super_errors/expected/missing_required_prop.res.expected @@ -1,10 +1,10 @@ We've found a bug for you! - /.../fixtures/missing_required_prop.res:28:7-37 + /.../fixtures/missing_required_prop.res:28:7-35 26 ┆ let make = () => { 27 ┆ - 28 ┆ 
{""->React.string}
 + 28 ┆ 
{""->React.string}
 29 ┆
30 ┆ } diff --git a/tests/build_tests/super_errors/expected/missing_required_prop_when_children.res.expected b/tests/build_tests/super_errors/expected/missing_required_prop_when_children.res.expected index 154d54ffb4..ad47150c41 100644 --- a/tests/build_tests/super_errors/expected/missing_required_prop_when_children.res.expected +++ b/tests/build_tests/super_errors/expected/missing_required_prop_when_children.res.expected @@ -1,11 +1,11 @@ We've found a bug for you! - /.../fixtures/missing_required_prop_when_children.res:31:7-32:37 + /.../fixtures/missing_required_prop_when_children.res:31:7-32:35 29 ┆ let make = () => { 30 ┆ - 31 ┆  - 32 ┆ 
{""->React.string}
 + 31 ┆  + 32 ┆ 
{""->React.string}
 33 ┆
34 ┆ } diff --git a/tests/build_tests/super_errors/expected/missing_required_prop_when_single_child.res.expected b/tests/build_tests/super_errors/expected/missing_required_prop_when_single_child.res.expected index e8e32f6ce5..b700a9b3cf 100644 --- a/tests/build_tests/super_errors/expected/missing_required_prop_when_single_child.res.expected +++ b/tests/build_tests/super_errors/expected/missing_required_prop_when_single_child.res.expected @@ -1,10 +1,10 @@ We've found a bug for you! - /.../fixtures/missing_required_prop_when_single_child.res:28:7-37 + /.../fixtures/missing_required_prop_when_single_child.res:28:7-35 26 ┆ let make = () => { 27 ┆ - 28 ┆ 
{""->React.string}
 + 28 ┆ 
{""->React.string}
 29 ┆
30 ┆ } diff --git a/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res b/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res index b4059e242b..cf7510529c 100644 --- a/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res +++ b/tests/build_tests/super_errors/fixtures/jsx_custom_component_children.res @@ -21,4 +21,4 @@ module CustomComponent = { } } -let x = {1.} +let x = {1.} diff --git a/tests/build_tests/super_errors/fixtures/missing_required_prop.res b/tests/build_tests/super_errors/fixtures/missing_required_prop.res index 3faa09aaa6..a3d2496609 100644 --- a/tests/build_tests/super_errors/fixtures/missing_required_prop.res +++ b/tests/build_tests/super_errors/fixtures/missing_required_prop.res @@ -17,7 +17,7 @@ module ReactDOM = { module Wrapper = { @react.component let make = (~value: 'value) => { -
{React.null}
+
{React.null}
} } @@ -25,7 +25,7 @@ module SomeComponent = { @react.component let make = () => { -
{""->React.string}
+
{""->React.string}
} } diff --git a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res index ab468eda49..a7b32a0cfd 100644 --- a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res +++ b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_children.res @@ -20,7 +20,7 @@ module ReactDOM = { module Wrapper = { @react.component let make = (~value: 'value, ~children: React.element) => { -
{children}
+
{children}
} } @@ -28,8 +28,8 @@ module SomeComponent = { @react.component let make = () => { - -
{""->React.string}
+ +
{""->React.string}
} } diff --git a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res index 97b0438f3f..40fa7fced9 100644 --- a/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res +++ b/tests/build_tests/super_errors/fixtures/missing_required_prop_when_single_child.res @@ -17,7 +17,7 @@ module ReactDOM = { module Wrapper = { @react.component let make = (~value: 'value, ~children: React.element) => { -
{children}
+
{children}
} } @@ -25,7 +25,7 @@ module SomeComponent = { @react.component let make = () => { -
{""->React.string}
+
{""->React.string}
} } diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res b/tests/gentype_tests/typescript-react-example/src/Hooks.res index 05f93d0f25..16b8111d94 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.res +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res @@ -8,22 +8,20 @@ let make = (~vehicle) => {

- {React.string( - "Hooks example " ++ - (vehicle.name ++ - (" clicked " ++ (Belt.Int.toString(count) ++ " times"))), - )} + {React.string( + "Hooks example " ++ (vehicle.name ++ (" clicked " ++ (Belt.Int.toString(count) ++ " times"))), + )}

- + React.string(x["randomString"])}> - {React.string("child1")} - {React.string("child2")} + {React.string("child1")} + {React.string("child2")} React.string(x["randomString"])} > - {React.string("child1")} - {React.string("child2")} + {React.string("child1")} + {React.string("child2")}
} @@ -35,32 +33,31 @@ module Another = { @genType @react.component let anotherComponent = (~vehicle, ~callback: unit => unit) => { callback() -
{React.string("Another Hook " ++ vehicle.name)}
+
{React.string("Another Hook " ++ vehicle.name)}
} } module Inner = { @genType @react.component - let make = (~vehicle) =>
{React.string("Another Hook " ++ vehicle.name)}
+ let make = (~vehicle) =>
{React.string("Another Hook " ++ vehicle.name)}
module Another = { @genType @react.component - let anotherComponent = (~vehicle) => -
{React.string("Another Hook " ++ vehicle.name)}
+ let anotherComponent = (~vehicle) =>
{React.string("Another Hook " ++ vehicle.name)}
} module Inner2 = { @genType @react.component - let make = (~vehicle) =>
{React.string("Another Hook " ++ vehicle.name)}
+ let make = (~vehicle) =>
{React.string("Another Hook " ++ vehicle.name)}
module Another = { @genType @react.component let anotherComponent = (~vehicle) => -
{React.string("Another Hook " ++ vehicle.name)}
+
{React.string("Another Hook " ++ vehicle.name)}
} } } module NoProps = { @genType @react.component - let make = () =>
React.null
+ let make = () =>
React.null
} type cb = (~_to: vehicle) => unit @@ -84,7 +81,7 @@ module WithRef = { let make = React.forwardRef((~vehicle, ref) => { let _ = 34 switch ref->Js.Nullable.toOption { - | Some(ref) => + | Some(ref) => | None => React.null } }) @@ -94,7 +91,7 @@ type r = {x: string} module ForwardRef = { @genType - let input = React.forwardRef((r, ref) =>
{React.string(r.x)}
) + let input = React.forwardRef((r, ref) =>
{React.string(r.x)}
) } @genType type callback<'input, 'output> = 'input => 'output @@ -130,7 +127,7 @@ module WithChildren = { let aComponentWithChildren = (~vehicle, ~children) =>
{React.string("Another Hook " ++ vehicle.name)} -
children
+
children
} diff --git a/tests/tests/src/async_jsx.res b/tests/tests/src/async_jsx.res index 277e357164..ec0401b3f4 100644 --- a/tests/tests/src/async_jsx.res +++ b/tests/tests/src/async_jsx.res @@ -15,7 +15,7 @@ module Foo = { let make = async () => { let now = await getNow()
-

{React.string(now->Date.toLocaleString)}

+

{React.string(now->Date.toLocaleString)}

} } diff --git a/tests/tests/src/jsx_preserve_test.res b/tests/tests/src/jsx_preserve_test.res index aeeaad1531..5c7328d3c2 100644 --- a/tests/tests/src/jsx_preserve_test.res +++ b/tests/tests/src/jsx_preserve_test.res @@ -11,12 +11,12 @@ module Icon = { let _single_element_child =
-

{React.string("Hello, world!")}

+

{React.string("Hello, world!")}

let _multiple_element_children =
-

{React.string("Hello, world!")}

+

{React.string("Hello, world!")}

@@ -34,7 +34,7 @@ let _multiple_element_fragment = let _unary_element_with_props = let _container_element_with_props_and_children = -
{React.string("Hello, world!")}
+
{React.string("Hello, world!")}
let baseProps: JsxDOM.domProps = { title: "foo", @@ -50,8 +50,8 @@ let _container_with_spread_props =
let baseChildren = React.array([ - {React.string("Hello, world!")} , - {React.string("Hello, world!")} , + {React.string("Hello, world!")}, + {React.string("Hello, world!")}, ]) let _unary_element_with_spread_props_keyed = @@ -77,7 +77,7 @@ module A = { module B = { @react.component let make = () => { -

{React.string("Hello, world!")}

+

{React.string("Hello, world!")}

} } @@ -91,11 +91,7 @@ module MyWeirdComponent = { type props = {\"MyWeirdProp": string} @react.componentWithProps - let make = props => -

- {React.string("foo")} - {React.string(props.\"MyWeirdProp")} -

+ let make = props =>

{React.string("foo")} {React.string(props.\"MyWeirdProp")}

} let _escaped_jsx_prop = @@ -103,12 +99,12 @@ let _escaped_jsx_prop = let _large_component =
()} onMouseDown={_ => ()}>

()} onMouseDown={_ => ()}> - {React.string("Hello, world!")} + {React.string("Hello, world!")}

()} onMouseDown={_ => ()}> - {React.string("Hello, world!")} + {React.string("Hello, world!")} -

{React.int(5)}

+

{React.int(5)}

module ComponentWithOptionalProps = { @@ -166,5 +162,5 @@ module ContextProvider = { @react.component let make = (~children) => { - {children} + {children} } From a3ef2678bd954b7ca4473bd4293f0c1f771cd541 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 20 Jan 2026 10:25:20 +0100 Subject: [PATCH 5/6] Fix jsx_ppx and indentation for children in tag --- compiler/syntax/src/jsx_v4.ml | 13 ++++- compiler/syntax/src/res_printer.ml | 5 +- .../typescript-react-example/src/Hooks.res | 16 +++--- .../ast-mapping/expected/JSXElements.res.txt | 10 ++-- .../ast-mapping/expected/JSXFragments.res.txt | 10 ++-- .../reason/expected/bracedJsx.res.txt | 2 +- .../conversion/reason/expected/braces.res.txt | 2 +- .../reason/expected/jsxProps.res.txt | 2 +- .../printer/comments/expected/jsx.res.txt | 2 +- .../printer/expr/expected/asyncAwait.res.txt | 2 +- .../data/printer/expr/expected/braced.res.txt | 2 +- .../printer/expr/expected/callback.res.txt | 4 +- .../printer/expr/expected/exoticIdent.res.txt | 6 +-- .../data/printer/expr/expected/jsx.res.txt | 49 +++++++------------ .../data/printer/expr/expected/switch.res.txt | 8 +-- .../expr/expected/underscoreApply.res.txt | 2 +- .../printer/other/expected/fatSlider.res.txt | 2 +- .../data/printer/other/expected/home.res.txt | 4 +- .../other/expected/signaturePicker.res.txt | 2 +- tests/tests/src/jsx_preserve_test.res | 4 +- 20 files changed, 72 insertions(+), 75 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 4cf9c9a0dd..b6a8f40c08 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_printer.ml b/compiler/syntax/src/res_printer.ml index 0409cf1509..9f3a5a7ca2 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -4586,8 +4586,9 @@ and print_jsx_container_tag ~state tag_name (* For simple children, try to keep them on the same line as the tags *) Doc.concat [ - Doc.soft_line; - print_jsx_children ~state children cmt_tbl; + Doc.indent + (Doc.concat + [Doc.soft_line; print_jsx_children ~state children cmt_tbl]); Doc.soft_line; ] else diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res b/tests/gentype_tests/typescript-react-example/src/Hooks.res index 16b8111d94..6e6787ffcc 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.res +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res @@ -8,20 +8,22 @@ let make = (~vehicle) => {

- {React.string( - "Hooks example " ++ (vehicle.name ++ (" clicked " ++ (Belt.Int.toString(count) ++ " times"))), - )} + {React.string( + "Hooks example " ++ + (vehicle.name ++ + (" clicked " ++ (Belt.Int.toString(count) ++ " times"))), + )}

React.string(x["randomString"])}> - {React.string("child1")} - {React.string("child2")} + {React.string("child1")} + {React.string("child2")} React.string(x["randomString"])} > - {React.string("child1")} - {React.string("child2")} + {React.string("child1")} + {React.string("child2")}
} diff --git a/tests/syntax_tests/data/ast-mapping/expected/JSXElements.res.txt b/tests/syntax_tests/data/ast-mapping/expected/JSXElements.res.txt index f151681291..4ad4530b72 100644 --- a/tests/syntax_tests/data/ast-mapping/expected/JSXElements.res.txt +++ b/tests/syntax_tests/data/ast-mapping/expected/JSXElements.res.txt @@ -10,8 +10,8 @@ let elementWithChildren = ReactDOM.jsxs( "div", { children: React.array([ - ReactDOM.jsx("h1", {children: ?ReactDOM.someElement({React.string("Hi")})}), - ReactDOM.jsx("p", {children: ?ReactDOM.someElement({React.string("Hello")})}), + ReactDOM.jsx("h1", {children: ?ReactDOM.someElement(React.string("Hi"))}), + ReactDOM.jsx("p", {children: ?ReactDOM.someElement(React.string("Hello"))}), ]), }, ) @@ -21,8 +21,8 @@ let elementWithChildrenAndAttributes = ReactDOM.jsxs( { className: "container", children: React.array([ - ReactDOM.jsx("h1", {children: ?ReactDOM.someElement({React.string("Hi")})}), - ReactDOM.jsx("p", {children: ?ReactDOM.someElement({React.string("Hello")})}), + ReactDOM.jsx("h1", {children: ?ReactDOM.someElement(React.string("Hi"))}), + ReactDOM.jsx("p", {children: ?ReactDOM.someElement(React.string("Hello"))}), ]), }, ) @@ -32,7 +32,7 @@ let elementWithConditionalChildren = ReactDOM.jsx( { children: ?ReactDOM.someElement({ if true { - ReactDOM.jsx("h1", {children: ?ReactDOM.someElement({React.string("Hi")})}) + ReactDOM.jsx("h1", {children: ?ReactDOM.someElement(React.string("Hi"))}) } else { React.null } diff --git a/tests/syntax_tests/data/ast-mapping/expected/JSXFragments.res.txt b/tests/syntax_tests/data/ast-mapping/expected/JSXFragments.res.txt index 6ed9d65c47..39f5f67acf 100644 --- a/tests/syntax_tests/data/ast-mapping/expected/JSXFragments.res.txt +++ b/tests/syntax_tests/data/ast-mapping/expected/JSXFragments.res.txt @@ -6,8 +6,8 @@ let fragmentWithJSXElements = React.jsxs( React.jsxFragment, { children: React.array([ - ReactDOM.jsx("h1", {children: ?ReactDOM.someElement({React.string("Hi")})}), - ReactDOM.jsx("p", {children: ?ReactDOM.someElement({React.string("Hello")})}), + ReactDOM.jsx("h1", {children: ?ReactDOM.someElement(React.string("Hi"))}), + ReactDOM.jsx("p", {children: ?ReactDOM.someElement(React.string("Hello"))}), ]), }, ) @@ -16,9 +16,9 @@ let nestedFragments = React.jsxs( React.jsxFragment, { children: React.array([ - ReactDOM.jsx("h1", {children: ?ReactDOM.someElement({React.string("Hi")})}), - ReactDOM.jsx("p", {children: ?ReactDOM.someElement({React.string("Hello")})}), - React.jsx(React.jsxFragment, {children: {React.string("Bye")}}), + ReactDOM.jsx("h1", {children: ?ReactDOM.someElement(React.string("Hi"))}), + ReactDOM.jsx("p", {children: ?ReactDOM.someElement(React.string("Hello"))}), + React.jsx(React.jsxFragment, {children: React.string("Bye")}), ]), }, ) diff --git a/tests/syntax_tests/data/conversion/reason/expected/bracedJsx.res.txt b/tests/syntax_tests/data/conversion/reason/expected/bracedJsx.res.txt index 17d75e3d9a..8a787d9f95 100644 --- a/tests/syntax_tests/data/conversion/reason/expected/bracedJsx.res.txt +++ b/tests/syntax_tests/data/conversion/reason/expected/bracedJsx.res.txt @@ -107,7 +107,7 @@ let make = () => { let userPrefix = "~ " -
{"Erreur"->ReasonReact.string}
+
{"Erreur"->ReasonReact.string}
(event->ReactEvent.Mouse.target)["querySelector"]("input")["focus"]()} diff --git a/tests/syntax_tests/data/conversion/reason/expected/braces.res.txt b/tests/syntax_tests/data/conversion/reason/expected/braces.res.txt index 0a3e335d63..9b215dce3d 100644 --- a/tests/syntax_tests/data/conversion/reason/expected/braces.res.txt +++ b/tests/syntax_tests/data/conversion/reason/expected/braces.res.txt @@ -4,7 +4,7 @@ let f = () => id if isArray(children) { // Scenario 1 let code = children->asStringArray->Js.Array2.joinWith("") - {code->s} + {code->s} } else if isObject(children) { // Scenario 2 children->asElement diff --git a/tests/syntax_tests/data/conversion/reason/expected/jsxProps.res.txt b/tests/syntax_tests/data/conversion/reason/expected/jsxProps.res.txt index 3d98360833..2b63f6d6ac 100644 --- a/tests/syntax_tests/data/conversion/reason/expected/jsxProps.res.txt +++ b/tests/syntax_tests/data/conversion/reason/expected/jsxProps.res.txt @@ -6,4 +6,4 @@ let handleClick = (href, event) => @react.component let make = (~href, ~className="", ~children) => - handleClick(href, event)}> children + handleClick(href, event)}>children diff --git a/tests/syntax_tests/data/printer/comments/expected/jsx.res.txt b/tests/syntax_tests/data/printer/comments/expected/jsx.res.txt index 664018ce3b..896d338700 100644 --- a/tests/syntax_tests/data/printer/comments/expected/jsx.res.txt +++ b/tests/syntax_tests/data/printer/comments/expected/jsx.res.txt @@ -3,7 +3,7 @@ module Cite = { let make = (~author: option, ~children) => { // For semantics, check out // https://css-tricks.com/quoting-in-html-quotations-citations-and-blockquotes/ -
foo
+
foo
} } diff --git a/tests/syntax_tests/data/printer/expr/expected/asyncAwait.res.txt b/tests/syntax_tests/data/printer/expr/expected/asyncAwait.res.txt index cb61de0fa2..15013af502 100644 --- a/tests/syntax_tests/data/printer/expr/expected/asyncAwait.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/asyncAwait.res.txt @@ -27,7 +27,7 @@ assert(await f()) user.data = await fetch() - {await weirdReactSuspenseApi} +{await weirdReactSuspenseApi} let inBinaryExpression = (await x->Js.Promise.resolve) + 1 let inBinaryExpression = (await x->Js.Promise.resolve) + (await y->Js.Promise.resolve) diff --git a/tests/syntax_tests/data/printer/expr/expected/braced.res.txt b/tests/syntax_tests/data/printer/expr/expected/braced.res.txt index 80ea824389..a005d0e955 100644 --- a/tests/syntax_tests/data/printer/expr/expected/braced.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/braced.res.txt @@ -282,7 +282,7 @@ apply({ a }) -let x = {
child
} +let x = {
child
} // not valid jsx let x = {@JSX child} diff --git a/tests/syntax_tests/data/printer/expr/expected/callback.res.txt b/tests/syntax_tests/data/printer/expr/expected/callback.res.txt index 1328629ebe..c06c927259 100644 --- a/tests/syntax_tests/data/printer/expr/expected/callback.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/callback.res.txt @@ -204,12 +204,12 @@ let f = () => {
    {users ->Array.map(user => { -
  • {user.username->React.string}
  • +
  • {user.username->React.string}
  • }) ->React.array}
{reloadableUser.last->AsyncData.isLoading ? "Loading next page"->React.string : React.null} - + }}
diff --git a/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt b/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt index 9ba0e0c3d3..3b907ff94e 100644 --- a/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/exoticIdent.res.txt @@ -59,11 +59,7 @@ assert(\"let") @let let x = 1 -let x = -
- \"module" - \"let" -
+let x =
\"module" \"let"
type dict = { key: int, diff --git a/tests/syntax_tests/data/printer/expr/expected/jsx.res.txt b/tests/syntax_tests/data/printer/expr/expected/jsx.res.txt index b6364fa397..4d7f731e6b 100644 --- a/tests/syntax_tests/data/printer/expr/expected/jsx.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/jsx.res.txt @@ -31,12 +31,8 @@ let x = {children} -let x = {a} -let x = - - {a} - {b} - +let x = {a} +let x = {a} {b} let x = {a} @@ -286,7 +282,7 @@ let x = {@attr ident}
-let x = test
} nav={} /> +let x = test
} nav={} />
{possibleGradeValues @@ -301,13 +297,13 @@ let x = test
} nav={} /> // https://github.com/rescript-lang/syntax/issues/113 -
{Js.log(a <= 10)}
+
{Js.log(a <= 10)}
-
{Js.log(a <= 10)}
+
{Js.log(a <= 10)}
Js.log(a <= 10)}> -
{Js.log(a <= 10)}
+
{Js.log(a <= 10)}
@@ -378,7 +374,7 @@ let x = { } let x = { - let _ =
{children}
+ let _ =
{children}
msg->React.string } @@ -393,7 +389,7 @@ let x = { } let x = { - let _ = {children} + let _ = {children} msg->React.string } @@ -464,44 +460,35 @@ let x = let 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 fragmented_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 => -
- {t(nbsp ++ "(")} - el - {t(")")} -
+let arrow_with_container_tag = el =>
{t(nbsp ++ "(")} el {t(")")}
// div tag stays after > -
{React.string("First A div")}
-
{React.string("Second A div")}
+
{React.string("First A div")}
+
{React.string("Second A div")}
| B => <> // fragment tag stays after <> -
{React.string("First B div")}
-
{React.string("Second B div")}
+
{React.string("First B div")}
+
{React.string("Second B div")}
} diff --git a/tests/syntax_tests/data/printer/expr/expected/underscoreApply.res.txt b/tests/syntax_tests/data/printer/expr/expected/underscoreApply.res.txt index 43357ac6ed..f99e372394 100644 --- a/tests/syntax_tests/data/printer/expr/expected/underscoreApply.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/underscoreApply.res.txt @@ -45,7 +45,7 @@ getDirector(a, b, _).name f(a, b, _) ? g(x, y, _) : h(alpha, beta, _)
-
{f(a, b, _)}
+
{f(a, b, _)}
f(a, b, _)[ix] f(a, b, _)[ix] = 2 diff --git a/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt b/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt index 6e065f0ac4..c9e529e486 100644 --- a/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt +++ b/tests/syntax_tests/data/printer/other/expected/fatSlider.res.txt @@ -34,7 +34,7 @@ let make = (~min=50, ~max=250, ~meterSuffix=?) => { values onChange={v => values_set(_ => v)} renderTrack={({props, children}) => { - let element =
children
+ let element =
children
ReasonReact.cloneElement(element, ~props=Obj.magic(props), [children]) }} diff --git a/tests/syntax_tests/data/printer/other/expected/home.res.txt b/tests/syntax_tests/data/printer/other/expected/home.res.txt index 326697c0d0..605bde0aed 100644 --- a/tests/syntax_tests/data/printer/other/expected/home.res.txt +++ b/tests/syntax_tests/data/printer/other/expected/home.res.txt @@ -10,11 +10,11 @@ let make = () => { // Template
-
{"Other stuff here"->string}
+
{"Other stuff here"->string}
- {" BPM"->string} } /> + {" BPM"->string}} />
diff --git a/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt b/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt index fe32b18e3c..424fa5944e 100644 --- a/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt +++ b/tests/syntax_tests/data/printer/other/expected/signaturePicker.res.txt @@ -25,7 +25,7 @@ let make = () => { }