@tsrx/prettier-plugin 0.3.72 → 0.3.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/index.js +798 -359
  3. package/src/index.test.js +5737 -14
package/src/index.js CHANGED
@@ -42,16 +42,16 @@ const { replaceEndOfLine, willBreak } = utils;
42
42
  /** @type {import('prettier').Plugin['languages']} */
43
43
  export const languages = [
44
44
  {
45
- name: 'ripple',
46
- parsers: ['ripple'],
45
+ name: 'tsrx',
46
+ parsers: ['tsrx'],
47
47
  extensions: ['.tsrx'],
48
- vscodeLanguageIds: ['ripple'],
48
+ vscodeLanguageIds: ['tsrx', 'ripple'],
49
49
  },
50
50
  ];
51
51
 
52
52
  /** @type {import('prettier').Plugin['parsers']} */
53
53
  export const parsers = {
54
- ripple: {
54
+ tsrx: {
55
55
  astFormat: 'ripple-ast',
56
56
  /**
57
57
  * @param {string} text
@@ -129,26 +129,6 @@ export const printers = {
129
129
  };
130
130
  }
131
131
 
132
- if (node.type === 'ScriptContent' && node.content) {
133
- return async (textToDoc) => {
134
- try {
135
- // Format JS/TS using Prettier's textToDoc
136
- const body = await textToDoc(node.content, {
137
- parser: 'babel-ts',
138
- });
139
-
140
- // Return complete element with tags
141
- // return ['<script>', indent([hardline, formattedContent]), hardline, '</script>'];
142
- return body;
143
- } catch (error) {
144
- // If JS/TS has syntax errors, return original unformatted content
145
- console.error('Error formatting JS/TS inside <script>:', error);
146
- return node.content;
147
- // return ['<script>', indent([hardline, node.content]), hardline, '</script>'];
148
- }
149
- };
150
- }
151
-
152
132
  return null;
153
133
  },
154
134
  /**
@@ -761,16 +741,9 @@ function printRippleNode(node, path, options, print, args) {
761
741
 
762
742
  const isInlineContext = args && args.isInlineContext;
763
743
  const suppressLeadingComments = args && args.suppressLeadingComments;
764
- const suppressExpressionLeadingComments = args && args.suppressExpressionLeadingComments;
765
- const parentNode = /** @type {AST.Node | null} */ (path.getParentNode());
766
-
767
- // For TSRXExpression and Text nodes, don't add leading comments here - they should be handled
768
- // as separate children within elements, not as part of the expression.
769
- const shouldSkipLeadingComments =
770
- parentNode?.type === 'Element' && (node.type === 'TSRXExpression' || node.type === 'Text');
771
744
 
772
745
  // Handle leading comments
773
- if (node.leadingComments && !shouldSkipLeadingComments && !suppressLeadingComments) {
746
+ if (node.leadingComments && !suppressLeadingComments) {
774
747
  for (let i = 0; i < node.leadingComments.length; i++) {
775
748
  const comment = node.leadingComments[i];
776
749
  const nextComment = node.leadingComments[i + 1];
@@ -786,7 +759,7 @@ function printRippleNode(node, path, options, print, args) {
786
759
  if (blankLinesBetween > 0) {
787
760
  parts.push(hardline);
788
761
  }
789
- } else if (isLastComment) {
762
+ } else if (isLastComment && node.type !== 'JSXText') {
790
763
  // Preserve a blank line between the last comment and the node if it existed
791
764
  const blankLinesBetween = getBlankLinesBetweenNodes(comment, node);
792
765
  if (blankLinesBetween > 0) {
@@ -902,10 +875,56 @@ function printRippleNode(node, path, options, print, args) {
902
875
  case 'IfStatement':
903
876
  nodeContent = printIfStatement(node, path, options, print);
904
877
  break;
878
+ case 'JSXIfExpression':
879
+ nodeContent = [
880
+ '@',
881
+ printIfStatement(
882
+ /** @type {AST.IfStatement} */ (/** @type {unknown} */ (node)),
883
+ path,
884
+ options,
885
+ print,
886
+ true,
887
+ ),
888
+ ];
889
+ break;
905
890
 
906
891
  case 'ForOfStatement':
907
892
  nodeContent = printForOfStatement(node, path, options, print);
908
893
  break;
894
+ case 'JSXForExpression':
895
+ if (node.statementType === 'ForInStatement') {
896
+ nodeContent = [
897
+ '@',
898
+ printForInStatement(
899
+ /** @type {AST.ForInStatement} */ (/** @type {unknown} */ (node)),
900
+ path,
901
+ options,
902
+ print,
903
+ ),
904
+ ];
905
+ } else if (node.statementType === 'ForStatement') {
906
+ nodeContent = [
907
+ '@',
908
+ printForStatement(
909
+ /** @type {AST.ForStatement} */ (/** @type {unknown} */ (node)),
910
+ path,
911
+ options,
912
+ print,
913
+ ),
914
+ ];
915
+ } else {
916
+ nodeContent = [
917
+ '@',
918
+ printForOfStatement(
919
+ /** @type {AST.ForOfStatement} */ (/** @type {unknown} */ (node)),
920
+ path,
921
+ options,
922
+ print,
923
+ true,
924
+ ),
925
+ ];
926
+ }
927
+ break;
909
928
 
910
929
  case 'ForStatement':
911
930
  nodeContent = printForStatement(node, path, options, print);
@@ -931,6 +950,18 @@ function printRippleNode(node, path, options, print, args) {
931
950
  case 'TryStatement':
932
951
  nodeContent = printTryStatement(node, path, options, print);
933
952
  break;
953
+ case 'JSXTryExpression':
954
+ nodeContent = [
955
+ '@',
956
+ printTryStatement(
957
+ /** @type {AST.TryStatement} */ (/** @type {unknown} */ (node)),
958
+ path,
959
+ options,
960
+ print,
961
+ true,
962
+ ),
963
+ ];
964
+ break;
934
965
 
935
966
  case 'ArrayExpression': {
936
967
  if (!node.elements || node.elements.length === 0) {
@@ -1497,9 +1528,10 @@ function printRippleNode(node, path, options, print, args) {
1497
1528
  (typePath) => print(typePath, { preferInlineSimpleUnionType: true }),
1498
1529
  'typeAnnotation',
1499
1530
  );
1500
- nodeContent = willBreak(typeAnnotation)
1501
- ? [path.call(print, 'expression'), ' as', indent([line, typeAnnotation])]
1502
- : [path.call(print, 'expression'), ' as ', typeAnnotation];
1531
+ nodeContent =
1532
+ node.typeAnnotation.type !== 'TSTypeLiteral' && willBreak(typeAnnotation)
1533
+ ? [path.call(print, 'expression'), ' as', indent([line, typeAnnotation])]
1534
+ : [path.call(print, 'expression'), ' as ', typeAnnotation];
1503
1535
  break;
1504
1536
  }
1505
1537
 
@@ -1508,14 +1540,19 @@ function printRippleNode(node, path, options, print, args) {
1508
1540
  (typePath) => print(typePath, { preferInlineSimpleUnionType: true }),
1509
1541
  'typeAnnotation',
1510
1542
  );
1511
- nodeContent = willBreak(typeAnnotation)
1512
- ? [path.call(print, 'expression'), ' satisfies', indent([line, typeAnnotation])]
1513
- : [path.call(print, 'expression'), ' satisfies ', typeAnnotation];
1543
+ nodeContent =
1544
+ node.typeAnnotation.type !== 'TSTypeLiteral' && willBreak(typeAnnotation)
1545
+ ? [path.call(print, 'expression'), ' satisfies', indent([line, typeAnnotation])]
1546
+ : [path.call(print, 'expression'), ' satisfies ', typeAnnotation];
1514
1547
  break;
1515
1548
  }
1516
1549
 
1517
1550
  case 'TSNonNullExpression': {
1518
- nodeContent = [path.call(print, 'expression'), '!'];
1551
+ const expression = path.call(print, 'expression');
1552
+ const needsParens =
1553
+ node.expression.type === 'TSAsExpression' ||
1554
+ node.expression.type === 'TSSatisfiesExpression';
1555
+ nodeContent = needsParens ? ['(', expression, ')!'] : [expression, '!'];
1519
1556
  break;
1520
1557
  }
1521
1558
 
@@ -1628,6 +1665,14 @@ function printRippleNode(node, path, options, print, args) {
1628
1665
  case 'SwitchStatement':
1629
1666
  nodeContent = printSwitchStatement(node, path, options, print);
1630
1667
  break;
1668
+ case 'JSXSwitchExpression':
1669
+ nodeContent = printJSXSwitchExpression(
1670
+ /** @type {AST.SwitchStatement} */ (/** @type {unknown} */ (node)),
1671
+ path,
1672
+ options,
1673
+ print,
1674
+ );
1675
+ break;
1631
1676
 
1632
1677
  case 'SwitchCase':
1633
1678
  nodeContent = printSwitchCase(node, path, options, print);
@@ -1685,27 +1730,14 @@ function printRippleNode(node, path, options, print, args) {
1685
1730
  }
1686
1731
  break;
1687
1732
  }
1688
- case 'SpreadAttribute': {
1689
- /** @type {Doc[]} */
1690
- const parts = ['{...', path.call(print, 'argument'), '}'];
1691
- nodeContent = parts;
1692
- break;
1693
- }
1694
-
1695
1733
  case 'Identifier': {
1696
1734
  // Simple case - just return the name directly like Prettier core
1697
- const trackedPrefix = node.tracked ? '@' : '';
1698
1735
  let identifierContent;
1699
1736
  if (node.typeAnnotation) {
1700
1737
  const optionalMarker = node.optional ? '?' : '';
1701
- identifierContent = [
1702
- trackedPrefix + node.name,
1703
- optionalMarker,
1704
- ': ',
1705
- path.call(print, 'typeAnnotation'),
1706
- ];
1738
+ identifierContent = [node.name, optionalMarker, ': ', path.call(print, 'typeAnnotation')];
1707
1739
  } else {
1708
- identifierContent = trackedPrefix + node.name;
1740
+ identifierContent = node.name;
1709
1741
  }
1710
1742
  // Preserve parentheses for type-cast identifiers, but only if:
1711
1743
  // 1. The identifier itself is marked as parenthesized
@@ -2120,6 +2152,10 @@ function printRippleNode(node, path, options, print, args) {
2120
2152
  nodeContent = printTSCallSignatureDeclaration(node, path, options, print);
2121
2153
  break;
2122
2154
 
2155
+ case 'TSConstructSignatureDeclaration':
2156
+ nodeContent = printTSConstructSignatureDeclaration(node, path, options, print);
2157
+ break;
2158
+
2123
2159
  case 'TSEnumMember':
2124
2160
  nodeContent = printTSEnumMember(node, path, options, print);
2125
2161
  break;
@@ -2245,16 +2281,17 @@ function printRippleNode(node, path, options, print, args) {
2245
2281
  break;
2246
2282
  }
2247
2283
 
2248
- case 'Element':
2249
- nodeContent = printElement(node, path, options, print);
2250
- break;
2251
-
2252
- case 'TsxCompat':
2253
- nodeContent = printTsxCompat(node, path, options, print);
2284
+ case 'JSXCodeBlock':
2285
+ nodeContent = printJSXCodeBlock(node, path, options, print);
2254
2286
  break;
2255
2287
 
2256
- case 'TsrxFragment':
2257
- nodeContent = printTsrx(node, path, options, print);
2288
+ case 'JSXStyleElement':
2289
+ nodeContent = printJSXElement(
2290
+ /** @type {ESTreeJSX.JSXElement} */ (/** @type {unknown} */ (node)),
2291
+ path,
2292
+ options,
2293
+ print,
2294
+ );
2258
2295
  break;
2259
2296
 
2260
2297
  case 'JSXElement':
@@ -2266,7 +2303,7 @@ function printRippleNode(node, path, options, print, args) {
2266
2303
  break;
2267
2304
 
2268
2305
  case 'JSXText':
2269
- nodeContent = node.value;
2306
+ nodeContent = printRawText(node.value);
2270
2307
  break;
2271
2308
 
2272
2309
  case 'JSXEmptyExpression':
@@ -2279,28 +2316,12 @@ function printRippleNode(node, path, options, print, args) {
2279
2316
  }
2280
2317
  break;
2281
2318
 
2282
- case 'Attribute':
2283
- nodeContent = printAttribute(node, path, options, print);
2284
- break;
2285
-
2286
- case 'TSRXExpression': {
2287
- const expressionDoc = suppressExpressionLeadingComments
2288
- ? path.call((exprPath) => print(exprPath, { suppressLeadingComments: true }), 'expression')
2289
- : path.call(print, 'expression');
2290
- nodeContent = ['{', expressionDoc, '}'];
2319
+ case 'JSXAttribute':
2320
+ nodeContent = printJSXAttribute(node, path, options, print);
2291
2321
  break;
2292
- }
2293
-
2294
- case 'Text': {
2295
- if (typeof node.raw === 'string') {
2296
- nodeContent = printRawText(node.raw);
2297
- break;
2298
- }
2299
2322
 
2300
- const expressionDoc = suppressExpressionLeadingComments
2301
- ? path.call((exprPath) => print(exprPath, { suppressLeadingComments: true }), 'expression')
2302
- : path.call(print, 'expression');
2303
- nodeContent = ['{', expressionDoc, '}'];
2323
+ case 'JSXSpreadAttribute': {
2324
+ nodeContent = ['{...', path.call(print, 'argument'), '}'];
2304
2325
  break;
2305
2326
  }
2306
2327
 
@@ -2527,7 +2548,10 @@ function printVariableDeclaration(node, path, options, print) {
2527
2548
  const isForLoopInit =
2528
2549
  (parentNode && parentNode.type === 'ForStatement' && parentNode.init === node) ||
2529
2550
  (parentNode && parentNode.type === 'ForOfStatement' && parentNode.left === node) ||
2530
- (parentNode && parentNode.type === 'ForInStatement' && parentNode.left === node);
2551
+ (parentNode && parentNode.type === 'ForInStatement' && parentNode.left === node) ||
2552
+ (parentNode &&
2553
+ parentNode.type === 'JSXForExpression' &&
2554
+ (parentNode.left === node || parentNode.init === node));
2531
2555
 
2532
2556
  const declarations = path.map(print, 'declarations');
2533
2557
  const declarationParts = join(', ', declarations);
@@ -2672,6 +2696,12 @@ function printArrowFunction(node, path, options, print, args) {
2672
2696
  if (shouldBreakBody) {
2673
2697
  parts.push(' =>', indent([hardline, bodyContent]));
2674
2698
  } else {
2699
+ if (isTemplateExpression(node.body)) {
2700
+ return conditionalGroup([
2701
+ group([...parts, ' => ', bodyContent]),
2702
+ group([...parts, ' =>', indent([hardline, bodyContent])]),
2703
+ ]);
2704
+ }
2675
2705
  parts.push(
2676
2706
  ' =>',
2677
2707
  group(indent(line), { id: groupId }),
@@ -2689,21 +2719,26 @@ function printArrowFunction(node, path, options, print, args) {
2689
2719
  * @returns {boolean}
2690
2720
  */
2691
2721
  function isTemplateExpression(node) {
2692
- return (
2693
- node.type === 'TsxCompat' ||
2694
- node.type === 'TsrxFragment' ||
2695
- node.type === 'JSXElement' ||
2696
- node.type === 'JSXFragment'
2697
- );
2722
+ return node.type === 'JSXElement' || node.type === 'JSXFragment';
2698
2723
  }
2699
2724
 
2700
2725
  /**
2701
2726
  * Check whether a braced attribute expression should close on its own line.
2702
2727
  * @param {AST.Node} node - The expression inside the attribute braces
2728
+ * @param {RippleFormatOptions} options
2729
+ * @param {AST.Node} [attributeNode]
2703
2730
  * @returns {boolean}
2704
2731
  */
2705
- function shouldBreakAttributeExpressionClosingBrace(node) {
2706
- return node.type === 'ArrowFunctionExpression' && node.body && isTemplateExpression(node.body);
2732
+ function shouldBreakAttributeExpressionClosingBrace(node, options, attributeNode = node) {
2733
+ return (
2734
+ node.type === 'ArrowFunctionExpression' &&
2735
+ node.body &&
2736
+ isTemplateExpression(node.body) &&
2737
+ sourceSpanExceedsPrintWidth(
2738
+ /** @type {AST.NodeWithLocation} */ (/** @type {unknown} */ (attributeNode ?? node)),
2739
+ options,
2740
+ )
2741
+ );
2707
2742
  }
2708
2743
 
2709
2744
  /**
@@ -2920,9 +2955,6 @@ function sourceSpanExceedsPrintWidth(node, options) {
2920
2955
  * @returns {boolean}
2921
2956
  */
2922
2957
  function shouldBreakArrowExpressionBody(node, options, args) {
2923
- if (args?.isInAttribute && isTemplateExpression(node)) {
2924
- return true;
2925
- }
2926
2958
  return (
2927
2959
  (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') &&
2928
2960
  sourceSpanExceedsPrintWidth(/** @type {AST.NodeWithLocation} */ (node), options)
@@ -3295,9 +3327,10 @@ function extractAndPrintLeadingComments(node) {
3295
3327
  * @param {AstPath<AST.IfStatement>} path - The AST path
3296
3328
  * @param {RippleFormatOptions} options - Prettier options
3297
3329
  * @param {PrintFn} print - Print callback
3330
+ * @param {boolean} [directive]
3298
3331
  * @returns {Doc[]}
3299
3332
  */
3300
- function printIfStatement(node, path, options, print) {
3333
+ function printIfStatement(node, path, options, print, directive = false) {
3301
3334
  // Extract leading comments from test node to print them before 'if' keyword
3302
3335
  const testNode = node.test;
3303
3336
 
@@ -3341,8 +3374,24 @@ function printIfStatement(node, path, options, print) {
3341
3374
  parts.push(' ');
3342
3375
  }
3343
3376
 
3344
- parts.push('else ');
3345
- parts.push(path.call(print, 'alternate'));
3377
+ parts.push(directive ? '@else ' : 'else ');
3378
+ if (directive && node.alternate.type === 'IfStatement') {
3379
+ parts.push(
3380
+ path.call(
3381
+ (alternatePath) =>
3382
+ printIfStatement(
3383
+ /** @type {AST.IfStatement} */ (alternatePath.node),
3384
+ /** @type {AstPath<AST.IfStatement>} */ (alternatePath),
3385
+ options,
3386
+ print,
3387
+ true,
3388
+ ),
3389
+ 'alternate',
3390
+ ),
3391
+ );
3392
+ } else {
3393
+ parts.push(path.call(print, 'alternate'));
3394
+ }
3346
3395
  }
3347
3396
 
3348
3397
  return parts;
@@ -3376,9 +3425,10 @@ function printForInStatement(node, path, options, print) {
3376
3425
  * @param {AstPath<AST.ForOfStatement>} path - The AST path
3377
3426
  * @param {RippleFormatOptions} options - Prettier options
3378
3427
  * @param {PrintFn} print - Print callback
3428
+ * @param {boolean} [directive]
3379
3429
  * @returns {Doc[]}
3380
3430
  */
3381
- function printForOfStatement(node, path, options, print) {
3431
+ function printForOfStatement(node, path, options, print, directive = false) {
3382
3432
  /** @type {Doc[]} */
3383
3433
  const parts = [];
3384
3434
  parts.push('for (');
@@ -3399,6 +3449,10 @@ function printForOfStatement(node, path, options, print) {
3399
3449
 
3400
3450
  parts.push(') ');
3401
3451
  parts.push(path.call(print, 'body'));
3452
+ if (node.empty) {
3453
+ parts.push(directive ? ' @empty ' : ' empty ');
3454
+ parts.push(path.call(print, 'empty'));
3455
+ }
3402
3456
 
3403
3457
  return parts;
3404
3458
  }
@@ -3715,9 +3769,10 @@ function printClassDeclaration(node, path, options, print) {
3715
3769
  * @param {AstPath<AST.TryStatement>} path - The AST path
3716
3770
  * @param {RippleFormatOptions} options - Prettier options
3717
3771
  * @param {PrintFn} print - Print callback
3772
+ * @param {boolean} [directive=false] - Whether this is a JSX @try expression.
3718
3773
  * @returns {Doc[]}
3719
3774
  */
3720
- function printTryStatement(node, path, options, print) {
3775
+ function printTryStatement(node, path, options, print, directive = false) {
3721
3776
  // Extract leading comments from block node to print them before 'try' keyword
3722
3777
  const blockNode = node.block;
3723
3778
 
@@ -3737,12 +3792,12 @@ function printTryStatement(node, path, options, print) {
3737
3792
  parts.push(block);
3738
3793
 
3739
3794
  if (node.pending) {
3740
- parts.push(' pending ');
3795
+ parts.push(directive ? ' @pending ' : ' pending ');
3741
3796
  parts.push(path.call(print, 'pending'));
3742
3797
  }
3743
3798
 
3744
3799
  if (node.handler) {
3745
- parts.push(' catch');
3800
+ parts.push(directive ? ' @catch' : ' catch');
3746
3801
  if (node.handler.param) {
3747
3802
  parts.push(' (');
3748
3803
  parts.push(path.call(print, 'handler', 'param'));
@@ -3949,7 +4004,6 @@ function printMemberExpression(node, path, options, print) {
3949
4004
 
3950
4005
  let result;
3951
4006
  if (node.computed) {
3952
- // Check if the MemberExpression itself is tracked to add @ symbol
3953
4007
  const openBracket = node.optional ? '?.[' : '[';
3954
4008
  result = [objectPart, openBracket, propertyPart, ']'];
3955
4009
  } else {
@@ -4483,6 +4537,76 @@ function printSwitchStatement(node, path, options, print) {
4483
4537
  return parts;
4484
4538
  }
4485
4539
 
4540
+ /**
4541
+ * Print a JSX switch expression. JSX switch cases use explicit template blocks:
4542
+ * `case value: { ... }`, unlike ordinary JavaScript switch cases.
4543
+ * @param {AST.SwitchStatement} node - The switch expression node
4544
+ * @param {AstPath<AST.SwitchStatement>} path - The AST path
4545
+ * @param {RippleFormatOptions} options - Prettier options
4546
+ * @param {PrintFn} print - Print callback
4547
+ * @returns {Doc[]}
4548
+ */
4549
+ function printJSXSwitchExpression(node, path, options, print) {
4550
+ const discriminant = path.call(
4551
+ (discriminantPath) => print(discriminantPath, { suppressLeadingComments: true }),
4552
+ 'discriminant',
4553
+ );
4554
+
4555
+ /** @type {Doc[]} */
4556
+ const cases = [];
4557
+ for (let i = 0; i < node.cases.length; i++) {
4558
+ const caseDoc = [printJSXSwitchCase(node.cases[i], path, options, print, i)];
4559
+ if (i < node.cases.length - 1 && isNextLineEmpty(node.cases[i], options)) {
4560
+ caseDoc.push(hardline);
4561
+ }
4562
+ cases.push(caseDoc);
4563
+ }
4564
+
4565
+ const bodyDoc =
4566
+ cases.length > 0 ? [indent([hardline, join(hardline, cases)]), hardline] : hardline;
4567
+
4568
+ const discriminantDoc = group(['@switch (', indent([softline, discriminant]), softline, ')']);
4569
+
4570
+ return [
4571
+ ...extractAndPrintLeadingComments(node.discriminant),
4572
+ discriminantDoc,
4573
+ ' {',
4574
+ bodyDoc,
4575
+ '}',
4576
+ ];
4577
+ }
4578
+
4579
+ /**
4580
+ * @param {AST.SwitchCase} node
4581
+ * @param {AstPath<AST.SwitchStatement>} path
4582
+ * @param {RippleFormatOptions} options
4583
+ * @param {PrintFn} print
4584
+ * @param {number} index
4585
+ * @returns {Doc[]}
4586
+ */
4587
+ function printJSXSwitchCase(node, path, options, print, index) {
4588
+ const header = node.test
4589
+ ? ['@case ', path.call(print, 'cases', index, 'test'), ':']
4590
+ : '@default:';
4591
+ const consequents = node.consequent || [];
4592
+ const printedConsequents = [];
4593
+
4594
+ for (let i = 0; i < consequents.length; i++) {
4595
+ const child = consequents[i];
4596
+ if (!child || child.type === 'EmptyStatement') {
4597
+ continue;
4598
+ }
4599
+ printedConsequents.push(path.call(print, 'cases', index, 'consequent', i));
4600
+ }
4601
+
4602
+ const bodyDoc =
4603
+ printedConsequents.length > 0
4604
+ ? [indent([hardline, join(hardline, printedConsequents)]), hardline]
4605
+ : hardline;
4606
+
4607
+ return [header, ' {', bodyDoc, '}'];
4608
+ }
4609
+
4486
4610
  /**
4487
4611
  * Print a switch case
4488
4612
  * @param {AST.SwitchCase} node - The switch case node
@@ -4650,6 +4774,26 @@ function getBlankLinesBetweenPositions(current_pos, next_pos) {
4650
4774
  * @param {AST.Node | AST.CSS.StyleSheet | AST.Comment} nextNode - Next node
4651
4775
  * @returns {number}
4652
4776
  */
4777
+ /**
4778
+ * The position to measure a leading blank line against: the first leading
4779
+ * comment if any (so the comment lines aren't miscounted as blank), else the
4780
+ * node itself.
4781
+ * @param {any} node
4782
+ * @returns {any}
4783
+ */
4784
+ function leadingAnchor(node) {
4785
+ const lead = node?.leadingComments;
4786
+ if (Array.isArray(lead) && lead.length > 0 && lead[0].loc) {
4787
+ return lead[0];
4788
+ }
4789
+ return node;
4790
+ }
4791
+
4792
+ /**
4793
+ * @param {any} currentNode
4794
+ * @param {any} nextNode
4795
+ * @returns {number}
4796
+ */
4653
4797
  function getBlankLinesBetweenNodes(currentNode, nextNode) {
4654
4798
  // Return the number of blank lines between two nodes based on their location
4655
4799
  if (
@@ -5022,6 +5166,16 @@ function printVariableDeclarator(node, path, options, print) {
5022
5166
  }
5023
5167
  }
5024
5168
 
5169
+ if (isTemplateExpression(node.init)) {
5170
+ const groupId = Symbol('declaration');
5171
+ return group([
5172
+ group(id),
5173
+ ' =',
5174
+ group(indent(line), { id: groupId }),
5175
+ indentIfBreak(init, { groupId }),
5176
+ ]);
5177
+ }
5178
+
5025
5179
  // Default: simple inline format with space
5026
5180
  // Use group to allow breaking if needed - but keep inline when it fits
5027
5181
  return group([id, ' = ', init]);
@@ -5191,6 +5345,45 @@ function printTSCallSignatureDeclaration(node, path, options, print) {
5191
5345
  return parts;
5192
5346
  }
5193
5347
 
5348
+ /**
5349
+ * Print a TypeScript construct signature in an interface or type literal
5350
+ * @param {AST.TSConstructSignatureDeclaration} node - The construct signature node
5351
+ * @param {AstPath<AST.TSConstructSignatureDeclaration>} path - The AST path
5352
+ * @param {RippleFormatOptions} options - Prettier options
5353
+ * @param {PrintFn} print - Print callback
5354
+ * @returns {Doc[]}
5355
+ */
5356
+ function printTSConstructSignatureDeclaration(node, path, options, print) {
5357
+ /** @type {Doc[]} */
5358
+ const parts = ['new '];
5359
+
5360
+ if (node.typeParameters) {
5361
+ const type_params = path.call(print, 'typeParameters');
5362
+ if (Array.isArray(type_params)) {
5363
+ parts.push(...type_params);
5364
+ } else {
5365
+ parts.push(type_params);
5366
+ }
5367
+ }
5368
+
5369
+ parts.push('(');
5370
+ if (node.parameters && node.parameters.length > 0) {
5371
+ const params = path.map(print, 'parameters');
5372
+ for (let i = 0; i < params.length; i++) {
5373
+ if (i > 0) parts.push(', ');
5374
+ parts.push(params[i]);
5375
+ }
5376
+ }
5377
+ parts.push(')');
5378
+
5379
+ if (node.typeAnnotation) {
5380
+ parts.push(': ');
5381
+ parts.push(path.call(print, 'typeAnnotation'));
5382
+ }
5383
+
5384
+ return parts;
5385
+ }
5386
+
5194
5387
  /**
5195
5388
  * Print a TypeScript type reference (e.g., Array<string>)
5196
5389
  * @param {AST.TSTypeReference} node - The type reference node
@@ -5465,6 +5658,36 @@ function printRawText(raw) {
5465
5658
  );
5466
5659
  }
5467
5660
 
5661
+ /**
5662
+ * @param {string} raw
5663
+ * @returns {Doc | Doc[] | string}
5664
+ */
5665
+ function printJSXTextChild(raw) {
5666
+ const text = raw.trim();
5667
+ if (!text) {
5668
+ return '';
5669
+ }
5670
+
5671
+ const lines = text
5672
+ .split(/\r\n|\r|\n/u)
5673
+ .map((line) => line.trim())
5674
+ .filter(Boolean);
5675
+ if (lines.length <= 1) {
5676
+ return lines[0] ?? '';
5677
+ }
5678
+
5679
+ return join(hardline, lines);
5680
+ }
5681
+
5682
+ /**
5683
+ * @param {string} raw
5684
+ * @returns {string}
5685
+ */
5686
+ function normalizeInlineJSXText(raw) {
5687
+ const text = raw.replace(/[^\S\r\n]+/gu, ' ');
5688
+ return text.trim() || !/[\r\n]/u.test(text) ? text : '';
5689
+ }
5690
+
5468
5691
  /**
5469
5692
  * @param {AST.Node} parentNode
5470
5693
  * @param {AST.Node} firstChild
@@ -5476,8 +5699,7 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5476
5699
  return false;
5477
5700
  }
5478
5701
 
5479
- // Always inline Text nodes — they are explicit text child forms.
5480
- if (firstChild.type === 'Text') {
5702
+ if (firstChild.type === 'JSXText') {
5481
5703
  return true;
5482
5704
  }
5483
5705
 
@@ -5487,7 +5709,7 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5487
5709
 
5488
5710
  // Inline JSX expressions if they fit, but respect original multi-line formatting
5489
5711
  // for non-literal expressions (e.g. {children} should stay multi-line if written that way)
5490
- if (firstChild.type === 'TSRXExpression' || firstChild.type === 'JSXExpressionContainer') {
5712
+ if (firstChild.type === 'JSXExpressionContainer') {
5491
5713
  if (wasOriginallySingleLine(parentNode)) {
5492
5714
  return true;
5493
5715
  }
@@ -5505,19 +5727,71 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5505
5727
  return false;
5506
5728
  }
5507
5729
 
5508
- if (firstChild.type === 'Element' && firstChild.selfClosing) {
5509
- return (
5510
- !(/** @type {AST.Element} */ (parentNode).attributes) ||
5511
- /** @type {AST.Element} */ (parentNode).attributes.length === 0
5512
- );
5730
+ if (firstChild.type === 'JSXElement' && firstChild.openingElement?.selfClosing) {
5731
+ const parent = /** @type {any} */ (parentNode);
5732
+ return !parent.openingElement?.attributes?.length;
5513
5733
  }
5514
5734
 
5515
5735
  return false;
5516
5736
  }
5517
5737
 
5738
+ /**
5739
+ * Check whether a child can participate in compact inline TSRX content.
5740
+ * @param {any} child
5741
+ * @returns {boolean}
5742
+ */
5743
+ function isInlineableTextOrExpressionChild(child) {
5744
+ if (!child || (child.type !== 'JSXText' && child.type !== 'JSXExpressionContainer')) {
5745
+ return false;
5746
+ }
5747
+
5748
+ if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
5749
+ return false;
5750
+ }
5751
+
5752
+ const expression = /** @type {{ expression?: AST.Node & AST.NodeWithMaybeComments }} */ (child)
5753
+ .expression;
5754
+ return !expression || !hasComment(expression);
5755
+ }
5756
+
5757
+ /**
5758
+ * @param {any} node
5759
+ * @returns {boolean}
5760
+ */
5761
+ function shouldTryInlineMultipleTextChildren(node) {
5762
+ return (
5763
+ wasOriginallySingleLine(node) &&
5764
+ Array.isArray(node.children) &&
5765
+ node.children.length > 1 &&
5766
+ node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
5767
+ node.children.every(isInlineableTextOrExpressionChild)
5768
+ );
5769
+ }
5770
+
5771
+ /**
5772
+ * @param {AST.Node} child
5773
+ * @returns {boolean}
5774
+ */
5775
+ function isSimpleJSXExpressionChild(child) {
5776
+ if (child?.type !== 'JSXExpressionContainer') {
5777
+ return false;
5778
+ }
5779
+
5780
+ const expression = child.expression;
5781
+ return (
5782
+ expression?.type === 'Identifier' ||
5783
+ expression?.type === 'Literal' ||
5784
+ expression?.type === 'TemplateLiteral' ||
5785
+ // Stock Prettier keeps a single `{expr}` child inline regardless of the
5786
+ // expression kind (member access, calls, etc.); only multiple children break.
5787
+ expression?.type === 'MemberExpression' ||
5788
+ expression?.type === 'CallExpression'
5789
+ );
5790
+ }
5791
+
5518
5792
  /**
5519
5793
  * Get leading comments from element metadata
5520
- * @param {AST.Element} node - The element node
5794
+ * @param {ESTreeJSX.JSXElement} node - The element node
5521
5795
  * @returns {AST.Comment[]}
5522
5796
  */
5523
5797
  function getElementLeadingComments(node) {
@@ -5577,142 +5851,10 @@ function createElementLevelCommentPartsTrimmed(comments) {
5577
5851
  return parts;
5578
5852
  }
5579
5853
 
5580
- /**
5581
- * Print a TsrxFragment node - renders native TSRX template children inside a fragment.
5582
- * @param {AST.TsrxFragment} node - The TsrxFragment node
5583
- * @param {AstPath<AST.TsrxFragment>} path - The AST path
5584
- * @param {RippleFormatOptions} options - Prettier options
5585
- * @param {PrintFn} print - Print callback
5586
- * @returns {Doc}
5587
- */
5588
- function printTsrx(node, path, options, print) {
5589
- const tagName = '<>';
5590
- const closingTagName = '</>';
5591
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
5592
-
5593
- if (!hasChildren) {
5594
- return [tagName, closingTagName];
5595
- }
5596
-
5597
- const printedChildren = [];
5598
-
5599
- for (let i = 0; i < node.children.length; i++) {
5600
- const child = node.children[i];
5601
-
5602
- if (child.type === 'JSXText') {
5603
- const text = child.value.trim();
5604
- if (!text) continue;
5605
- printedChildren.push(text);
5606
- } else {
5607
- const printedChild = path.call(print, 'children', i);
5608
- printedChildren.push(printedChild);
5609
- }
5610
- }
5611
-
5612
- if (printedChildren.length === 0) {
5613
- return [tagName, closingTagName];
5614
- }
5615
-
5616
- if (
5617
- printedChildren.length === 1 &&
5618
- ['Element', 'Text', 'TSRXExpression'].includes(node.children[0]?.type)
5619
- ) {
5620
- return group([tagName, indent([softline, printedChildren[0]]), softline, closingTagName]);
5621
- }
5622
-
5623
- return group([
5624
- tagName,
5625
- indent([hardline, join(hardline, printedChildren)]),
5626
- hardline,
5627
- closingTagName,
5628
- ]);
5629
- }
5630
-
5631
- /**
5632
- * Print a TSX compatibility node
5633
- * @param {AST.TsxCompat} node - The TSX compat node
5634
- * @param {AstPath<AST.TsxCompat>} path - The AST path
5635
- * @param {RippleFormatOptions} options - Prettier options
5636
- * @param {PrintFn} print - Print callback
5637
- * @returns {Doc}
5638
- */
5639
- function printTsxCompat(node, path, options, print) {
5640
- const tagName = `<tsx:${node.kind}>`;
5641
- const closingTagName = `</tsx:${node.kind}>`;
5642
-
5643
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
5644
-
5645
- if (!hasChildren) {
5646
- return [tagName, closingTagName];
5647
- }
5648
-
5649
- // Print JSXElement children - they remain as JSX
5650
- // Filter out whitespace-only JSXText nodes and merge adjacent text-like nodes
5651
- const finalChildren = [];
5652
- let accumulatedText = '';
5653
-
5654
- for (let i = 0; i < node.children.length; i++) {
5655
- const child = node.children[i];
5656
-
5657
- // Check if this is a text-like node (JSXText or Identifier in JSX context)
5658
- const isTextLike = child.type === 'JSXText';
5659
-
5660
- if (isTextLike) {
5661
- // Get the text content
5662
- let text;
5663
- if (child.type === 'JSXText') {
5664
- text = child.value.trim();
5665
- }
5666
-
5667
- if (text) {
5668
- if (accumulatedText) {
5669
- accumulatedText += ' ' + text;
5670
- } else {
5671
- accumulatedText = text;
5672
- }
5673
- }
5674
- } else {
5675
- // Before adding non-text node, flush accumulated text
5676
- if (accumulatedText) {
5677
- if (finalChildren.length > 0) {
5678
- finalChildren.push(hardline);
5679
- }
5680
- finalChildren.push(accumulatedText);
5681
- accumulatedText = '';
5682
- }
5683
-
5684
- if (finalChildren.length > 0) {
5685
- finalChildren.push(hardline);
5686
- }
5687
-
5688
- const printedChild = path.call(print, 'children', i);
5689
- finalChildren.push(printedChild);
5690
- }
5691
- }
5692
-
5693
- // Don't forget any remaining accumulated text
5694
- if (accumulatedText) {
5695
- if (finalChildren.length > 0) {
5696
- finalChildren.push(hardline);
5697
- }
5698
- finalChildren.push(accumulatedText);
5699
- }
5700
-
5701
- // Format the TsxCompat element
5702
- const elementOutput = group([
5703
- tagName,
5704
- indent([hardline, ...finalChildren]),
5705
- hardline,
5706
- closingTagName,
5707
- ]);
5708
-
5709
- return elementOutput;
5710
- }
5711
-
5712
5854
  /**
5713
5855
  * Print a JSX element
5714
- * @param {ESTreeJSX.JSXElement} node - The JSX element node
5715
- * @param {AstPath<ESTreeJSX.JSXElement>} path - The AST path
5856
+ * @param {AST.TSRXJSXElement} node - The JSX element node
5857
+ * @param {AstPath<any>} path - The AST path
5716
5858
  * @param {RippleFormatOptions} options - Prettier options
5717
5859
  * @param {PrintFn} print - Print callback
5718
5860
  * @returns {Doc | Doc[]}
@@ -5722,20 +5864,7 @@ function printJSXElement(node, path, options, print) {
5722
5864
  const openingElement = node.openingElement;
5723
5865
  const closingElement = node.closingElement;
5724
5866
 
5725
- /** @type {string} */
5726
- let tagName;
5727
- if (openingElement.name.type === 'JSXIdentifier') {
5728
- tagName = openingElement.name.name;
5729
- } else if (openingElement.name.type === 'JSXMemberExpression') {
5730
- // Handle Member expressions like React.Fragment
5731
- tagName = printJSXMemberExpression(openingElement.name);
5732
- } else if (openingElement.name.type === 'JSXNamespacedName') {
5733
- const namespace_name = openingElement.name.namespace.name;
5734
- const local_name = openingElement.name.name.name;
5735
- tagName = namespace_name + ':' + local_name;
5736
- } else {
5737
- tagName = 'Unknown';
5738
- }
5867
+ const tagName = printJSXElementName(openingElement.name);
5739
5868
 
5740
5869
  const isSelfClosing = openingElement.selfClosing;
5741
5870
  const hasAttributes = openingElement.attributes && openingElement.attributes.length > 0;
@@ -5747,6 +5876,12 @@ function printJSXElement(node, path, options, print) {
5747
5876
  typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
5748
5877
  }
5749
5878
 
5879
+ // Comments that sit inside the opening tag (before an attribute) are attached
5880
+ // by the parser to a body child; pull them out and key them by the attribute
5881
+ // they precede so they print in the opening tag, not jammed into the body.
5882
+ const openingTagCommentsByAttr = collectOpeningTagComments(node);
5883
+ const hasOpeningTagComments = openingTagCommentsByAttr.size > 0;
5884
+
5750
5885
  // Format attributes
5751
5886
  /** @type {Doc} */
5752
5887
  let attributesDoc = '';
@@ -5770,19 +5905,31 @@ function printJSXElement(node, path, options, print) {
5770
5905
  'attributes',
5771
5906
  i,
5772
5907
  );
5773
- } else if (attr.type === 'JSXSpreadAttribute' || attr.type === 'SpreadAttribute') {
5908
+ } else if (attr.type === 'JSXSpreadAttribute') {
5774
5909
  attrDoc = ['{...', path.call(print, 'openingElement', 'attributes', i, 'argument'), '}'];
5775
5910
  }
5776
5911
  if (!hasBreakingAttribute && attrDoc && willBreak(attrDoc)) {
5777
5912
  hasBreakingAttribute = true;
5778
5913
  }
5914
+ const lead = openingTagCommentsByAttr.get(i);
5915
+ if (lead) {
5916
+ /** @type {Doc[]} */
5917
+ const parts = [];
5918
+ for (const comment of lead) {
5919
+ parts.push(
5920
+ comment.type === 'Line' ? '//' + comment.value : '/*' + comment.value + '*/',
5921
+ );
5922
+ parts.push(hardline);
5923
+ }
5924
+ return [...parts, attrDoc];
5925
+ }
5779
5926
  return attrDoc;
5780
5927
  },
5781
5928
  );
5782
5929
  const attrLineBreak = options.singleAttributePerLine ? hardline : line;
5783
5930
  attributesDoc = indent([attrLineBreak, join(attrLineBreak, attrs)]);
5784
5931
  }
5785
- const shouldForceBreak = hasBreakingAttribute;
5932
+ const shouldForceBreak = hasBreakingAttribute || hasOpeningTagComments;
5786
5933
 
5787
5934
  if (isSelfClosing) {
5788
5935
  return group(['<', tagName, typeArgsDoc, attributesDoc, hasAttributes ? line : ' ', '/>'], {
@@ -5802,58 +5949,119 @@ function printJSXElement(node, path, options, print) {
5802
5949
  { shouldBreak: shouldForceBreak },
5803
5950
  );
5804
5951
 
5952
+ // Trailing comments after the last child are attached by the parser either to
5953
+ // the closing tag (`closingElement.leadingComments`) or, when the last child is
5954
+ // an `{expr}` container, to `metadata.elementLeadingComments` positioned inside
5955
+ // the body (start >= opening tag end). Emit both before `</tag>`.
5956
+ const openingTagEnd = /** @type {AST.NodeWithLocation} */ (openingElement).end;
5957
+ const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
5958
+ (/** @type {AST.Comment} */ comment) =>
5959
+ typeof comment.start === 'number' && comment.start >= openingTagEnd,
5960
+ );
5961
+ const trailingComments = [
5962
+ ...(node.closingElement?.leadingComments ?? []),
5963
+ ...bodyMetaComments,
5964
+ ].sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
5965
+ const lastMeaningfulChild = [...(node.children ?? [])]
5966
+ .reverse()
5967
+ .find((child) => child.type !== 'JSXText' || child.value.trim());
5968
+ const closingCommentDocs = printElementBodyLineComments(trailingComments, lastMeaningfulChild);
5969
+ const hasClosingComments = closingCommentDocs.length > 0;
5970
+ // A comment-only element has no children; its comments live in `innerComments`.
5971
+ const innerCommentDocs = printElementBodyLineComments(node.innerComments);
5972
+
5805
5973
  if (!hasChildren) {
5974
+ const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
5975
+ if (bodyComments.length > 0) {
5976
+ return group([openingTag, indent(bodyComments), hardline, '</', tagName, '>']);
5977
+ }
5806
5978
  return [openingTag, '</', tagName, '>'];
5807
5979
  }
5808
5980
 
5809
- // Format children - filter out empty text nodes and merge adjacent text nodes
5981
+ // A `@{ }` code block is the whole body and hugs the tags: `<div>@{ … }</div>`.
5982
+ if (node.children.length === 1 && node.children[0].type === 'JSXCodeBlock') {
5983
+ return group([openingTag, path.call(print, 'children', 0), '</', tagName, '>']);
5984
+ }
5985
+
5986
+ // Format children - filter out empty text nodes and merge adjacent text nodes.
5987
+ // childNodes tracks the source node behind each doc (a text run is a single
5988
+ // JSXText) so the join can preserve authored blank lines.
5810
5989
  const childrenDocs = [];
5990
+ const childNodes = [];
5811
5991
  let currentText = '';
5992
+ let currentTextNode = null;
5812
5993
 
5813
5994
  for (let i = 0; i < node.children.length; i++) {
5814
5995
  const child = node.children[i];
5815
5996
 
5816
5997
  if (child.type === 'JSXText') {
5817
- // Accumulate text content, preserving spaces between words
5818
- const trimmed = child.value.trim();
5819
- if (trimmed) {
5998
+ if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
5999
+ if (currentText) {
6000
+ childrenDocs.push(currentText);
6001
+ childNodes.push(currentTextNode);
6002
+ currentText = '';
6003
+ currentTextNode = null;
6004
+ }
6005
+ const printedChild = path.call(print, 'children', i);
6006
+ if (printedChild !== '') {
6007
+ childrenDocs.push(printedChild);
6008
+ childNodes.push(child);
6009
+ }
6010
+ continue;
6011
+ }
6012
+ // Accumulate text content, preserving meaningful boundary spaces.
6013
+ const text = normalizeInlineJSXText(child.value);
6014
+ if (text) {
5820
6015
  const nextChild = node.children[i + 1];
5821
6016
  const afterNextChild = node.children[i + 2];
5822
6017
  const nextText = afterNextChild?.type === 'JSXText' ? afterNextChild.value.trim() : '';
5823
6018
  if (
5824
6019
  tagName === 'tsrx' &&
5825
- trimmed.endsWith('=') &&
6020
+ text.trimEnd().endsWith('=') &&
5826
6021
  nextChild?.type === 'JSXElement' &&
5827
6022
  nextText === ';'
5828
6023
  ) {
5829
6024
  if (currentText) {
5830
6025
  childrenDocs.push(currentText);
6026
+ childNodes.push(currentTextNode);
5831
6027
  currentText = '';
6028
+ currentTextNode = null;
5832
6029
  }
5833
- childrenDocs.push([trimmed, ' ', path.call(print, 'children', i + 1), ';']);
6030
+ childrenDocs.push([text.trim(), ' ', path.call(print, 'children', i + 1), ';']);
6031
+ childNodes.push(child);
5834
6032
  i += 2;
5835
6033
  continue;
5836
6034
  }
5837
6035
 
5838
6036
  if (currentText) {
5839
- currentText += ' ' + trimmed;
6037
+ currentText += currentText.endsWith(' ') || text.startsWith(' ') ? text : ' ' + text;
5840
6038
  } else {
5841
- currentText = trimmed;
6039
+ currentText = text;
6040
+ currentTextNode = child;
5842
6041
  }
5843
6042
  }
5844
6043
  } else {
5845
6044
  // If we have accumulated text, push it before the non-text node
5846
6045
  if (currentText) {
5847
6046
  childrenDocs.push(currentText);
6047
+ childNodes.push(currentTextNode);
5848
6048
  currentText = '';
6049
+ currentTextNode = null;
5849
6050
  }
5850
6051
 
5851
6052
  if (child.type === 'JSXExpressionContainer') {
5852
6053
  // Handle JSX expression containers
5853
- childrenDocs.push(['{', path.call(print, 'children', i, 'expression'), '}']);
6054
+ childrenDocs.push([
6055
+ ...printTemplateChildLeadingComments(child),
6056
+ '{',
6057
+ path.call(print, 'children', i, 'expression'),
6058
+ '}',
6059
+ ]);
6060
+ childNodes.push(child);
5854
6061
  } else {
5855
6062
  // Handle nested JSX elements
5856
6063
  childrenDocs.push(path.call(print, 'children', i));
6064
+ childNodes.push(child);
5857
6065
  }
5858
6066
  }
5859
6067
  }
@@ -5861,37 +6069,71 @@ function printJSXElement(node, path, options, print) {
5861
6069
  // Don't forget any remaining text
5862
6070
  if (currentText) {
5863
6071
  childrenDocs.push(currentText);
6072
+ childNodes.push(currentTextNode);
5864
6073
  }
5865
6074
 
5866
- // Check if content can be inlined (single text node or single expression)
5867
- if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
5868
- return group([openingTag, childrenDocs[0], '</', tagName, '>']);
6075
+ // A child with leading comments must break onto its own line, so the comment
6076
+ // reads above the child rather than being jammed onto the opening tag.
6077
+ const hasChildLeadingComments = node.children.some((child) => {
6078
+ const leadingComments = /** @type {AST.NodeWithMaybeComments} */ (child).leadingComments;
6079
+ return Array.isArray(leadingComments) && leadingComments.length > 0;
6080
+ });
6081
+ const forceMultiline = hasClosingComments || hasChildLeadingComments;
6082
+
6083
+ // Check if content can be inlined (single text node or single expression).
6084
+ // Trailing or child-leading comments force the multi-line layout. A single
6085
+ // text child stays inline when it fits and otherwise fills/wraps to printWidth.
6086
+ if (!forceMultiline && childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
6087
+ // The open tag breaks for attributes independently; the text+closing get
6088
+ // their own group so the text only drops to its own (filled) lines when it
6089
+ // itself overflows — otherwise it hugs `>text</tag>`.
6090
+ return [
6091
+ openingTag,
6092
+ group([indent([softline, printRawText(childrenDocs[0])]), softline, '</', tagName, '>']),
6093
+ ];
5869
6094
  }
5870
6095
  const meaningfulChildren = node.children.filter(
5871
- (child) => child.type !== 'JSXText' || child.value.trim(),
6096
+ (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim(),
5872
6097
  );
5873
6098
  const singleMeaningfulChild = meaningfulChildren.length === 1 ? meaningfulChildren[0] : null;
5874
6099
  if (
6100
+ !forceMultiline &&
5875
6101
  childrenDocs.length === 1 &&
5876
6102
  singleMeaningfulChild?.type === 'JSXExpressionContainer' &&
5877
- singleMeaningfulChild.expression.type === 'Identifier'
6103
+ isSimpleJSXExpressionChild(/** @type {AST.Node} */ (singleMeaningfulChild))
5878
6104
  ) {
5879
6105
  return group([openingTag, childrenDocs[0], '</', tagName, '>']);
5880
6106
  }
6107
+ if (
6108
+ !forceMultiline &&
6109
+ childrenDocs.length > 1 &&
6110
+ wasOriginallySingleLine(node) &&
6111
+ node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
6112
+ node.children.every(
6113
+ (/** @type {any} */ child) =>
6114
+ child.type === 'JSXText' || isSimpleJSXExpressionChild(/** @type {AST.Node} */ (child)),
6115
+ )
6116
+ ) {
6117
+ return group([openingTag, ...childrenDocs, '</', tagName, '>']);
6118
+ }
5881
6119
 
5882
- // Multiple children or complex children - format with line breaks
6120
+ // Multiple children or complex children - format with line breaks. Text runs
6121
+ // fill/wrap to printWidth.
5883
6122
  const formattedChildren = [];
5884
6123
  for (let i = 0; i < childrenDocs.length; i++) {
5885
- formattedChildren.push(childrenDocs[i]);
6124
+ const childDoc = childrenDocs[i];
6125
+ formattedChildren.push(typeof childDoc === 'string' ? printRawText(childDoc) : childDoc);
5886
6126
  if (i < childrenDocs.length - 1) {
5887
- formattedChildren.push(hardline);
6127
+ // Preserve a single authored blank line between children (2+ collapse to 1).
6128
+ const blank = getBlankLinesBetweenNodes(childNodes[i], leadingAnchor(childNodes[i + 1])) > 0;
6129
+ formattedChildren.push(blank ? [hardline, hardline] : hardline);
5888
6130
  }
5889
6131
  }
5890
6132
 
5891
6133
  // Build the final element
5892
6134
  return group([
5893
6135
  openingTag,
5894
- indent([hardline, ...formattedChildren]),
6136
+ indent([hardline, ...formattedChildren, ...closingCommentDocs]),
5895
6137
  hardline,
5896
6138
  '</',
5897
6139
  tagName,
@@ -5914,23 +6156,46 @@ function printJSXFragment(node, path, options, print) {
5914
6156
  return '<></>';
5915
6157
  }
5916
6158
 
5917
- // Format children - filter out empty text nodes
6159
+ // A `@{ }` code block is the whole body and hugs the tags: `<>@{ … }</>`.
6160
+ if (node.children.length === 1 && /** @type {any} */ (node.children[0]).type === 'JSXCodeBlock') {
6161
+ return group(['<>', path.call(print, 'children', 0), '</>']);
6162
+ }
6163
+
6164
+ // Format children - filter out empty text nodes. childNodes tracks the source
6165
+ // node behind each doc so the join can preserve authored blank lines.
5918
6166
  const childrenDocs = [];
6167
+ const childNodes = [];
5919
6168
  for (let i = 0; i < node.children.length; i++) {
5920
6169
  const child = node.children[i];
5921
6170
 
5922
6171
  if (child.type === 'JSXText') {
6172
+ if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
6173
+ const printedChild = path.call(print, 'children', i);
6174
+ if (printedChild !== '') {
6175
+ childrenDocs.push(printedChild);
6176
+ childNodes.push(child);
6177
+ }
6178
+ continue;
6179
+ }
5923
6180
  // Handle JSX text nodes - trim whitespace and only include if not empty
5924
- const text = child.value.trim();
6181
+ const text = printJSXTextChild(child.value);
5925
6182
  if (text) {
5926
6183
  childrenDocs.push(text);
6184
+ childNodes.push(child);
5927
6185
  }
5928
6186
  } else if (child.type === 'JSXExpressionContainer') {
5929
6187
  // Handle JSX expression containers
5930
- childrenDocs.push(['{', path.call(print, 'children', i, 'expression'), '}']);
6188
+ childrenDocs.push([
6189
+ ...printTemplateChildLeadingComments(child),
6190
+ '{',
6191
+ path.call(print, 'children', i, 'expression'),
6192
+ '}',
6193
+ ]);
6194
+ childNodes.push(child);
5931
6195
  } else {
5932
6196
  // Handle nested JSX elements and fragments
5933
6197
  childrenDocs.push(path.call(print, 'children', i));
6198
+ childNodes.push(child);
5934
6199
  }
5935
6200
  }
5936
6201
 
@@ -5938,13 +6203,33 @@ function printJSXFragment(node, path, options, print) {
5938
6203
  if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
5939
6204
  return ['<>', childrenDocs[0], '</>'];
5940
6205
  }
6206
+ const meaningfulChildren = node.children.filter(
6207
+ (child) => child.type !== 'JSXText' || child.value.trim(),
6208
+ );
6209
+ if (
6210
+ childrenDocs.length === 1 &&
6211
+ meaningfulChildren.length === 1 &&
6212
+ meaningfulChildren[0].type === 'JSXElement' &&
6213
+ wasOriginallySingleLine(node) &&
6214
+ !willBreak(childrenDocs[0])
6215
+ ) {
6216
+ // Keep the fragment inline when it fits; otherwise expand `<>` onto its own
6217
+ // lines so a breaking single child reads as `<>\n <Child …/>\n</>` rather than
6218
+ // `<><Child` with only the child's attributes broken.
6219
+ return conditionalGroup([
6220
+ ['<>', childrenDocs[0], '</>'],
6221
+ group(['<>', indent([hardline, childrenDocs[0]]), hardline, '</>']),
6222
+ ]);
6223
+ }
5941
6224
 
5942
6225
  // Multiple children or complex children - format with line breaks
5943
6226
  const formattedChildren = [];
5944
6227
  for (let i = 0; i < childrenDocs.length; i++) {
5945
6228
  formattedChildren.push(childrenDocs[i]);
5946
6229
  if (i < childrenDocs.length - 1) {
5947
- formattedChildren.push(hardline);
6230
+ // Preserve a single authored blank line between children (2+ collapse to 1).
6231
+ const blank = getBlankLinesBetweenNodes(childNodes[i], leadingAnchor(childNodes[i + 1])) > 0;
6232
+ formattedChildren.push(blank ? [hardline, hardline] : hardline);
5948
6233
  }
5949
6234
  }
5950
6235
 
@@ -5952,6 +6237,169 @@ function printJSXFragment(node, path, options, print) {
5952
6237
  return group(['<>', indent([hardline, ...formattedChildren]), hardline, '</>']);
5953
6238
  }
5954
6239
 
6240
+ /**
6241
+ * Comments written inside an opening tag, before an attribute, are attached by
6242
+ * the parser to the next visited body child (positionally they sort before the
6243
+ * opening tag's end, but the child is visited first). Pull those out of the
6244
+ * children and return a map from attribute index to the comments that precede it,
6245
+ * so the element printer can render them in the opening tag instead of the body.
6246
+ * @param {AST.TSRXJSXElement} node
6247
+ * @returns {Map<number, AST.Comment[]>}
6248
+ */
6249
+ function collectOpeningTagComments(node) {
6250
+ /** @type {Map<number, AST.Comment[]>} */
6251
+ const byAttr = new Map();
6252
+ const openingElement = /** @type {AST.NodeWithLocation} */ (node.openingElement);
6253
+ const attributes = /** @type {any[]} */ (node.openingElement?.attributes) ?? [];
6254
+ if (!openingElement || attributes.length === 0 || !Array.isArray(node.children)) {
6255
+ return byAttr;
6256
+ }
6257
+ const openingEnd = openingElement.end;
6258
+ /** @type {AST.Comment[]} */
6259
+ const collected = [];
6260
+ for (const child of node.children) {
6261
+ const lead = /** @type {AST.NodeWithMaybeComments} */ (child).leadingComments;
6262
+ if (!Array.isArray(lead) || lead.length === 0) continue;
6263
+ const keep = [];
6264
+ for (const comment of lead) {
6265
+ if (typeof comment.start === 'number' && comment.start < openingEnd) {
6266
+ collected.push(comment);
6267
+ } else {
6268
+ keep.push(comment);
6269
+ }
6270
+ }
6271
+ if (keep.length !== lead.length) {
6272
+ /** @type {any} */ (child).leadingComments = keep;
6273
+ }
6274
+ }
6275
+ if (collected.length === 0) return byAttr;
6276
+ collected.sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
6277
+ let ci = 0;
6278
+ for (let ai = 0; ai < attributes.length; ai++) {
6279
+ const attrStart = /** @type {AST.NodeWithLocation} */ (attributes[ai]).start;
6280
+ /** @type {AST.Comment[]} */
6281
+ const forAttr = [];
6282
+ while (ci < collected.length && /** @type {number} */ (collected[ci].start) < attrStart) {
6283
+ forAttr.push(collected[ci]);
6284
+ ci++;
6285
+ }
6286
+ if (forAttr.length > 0) byAttr.set(ai, forAttr);
6287
+ }
6288
+ return byAttr;
6289
+ }
6290
+
6291
+ /**
6292
+ * Build doc parts for a template child's leading comments (each on its own line).
6293
+ * Used for `{expr}` children, whose `{ … }` form is printed inline by the JSX
6294
+ * printers and so would otherwise skip the node's attached leading comments.
6295
+ * @param {AST.Node & AST.NodeWithMaybeComments} child
6296
+ * @returns {Doc[]}
6297
+ */
6298
+ function printTemplateChildLeadingComments(child) {
6299
+ const comments = child.leadingComments;
6300
+ if (!comments || comments.length === 0) {
6301
+ return [];
6302
+ }
6303
+ /** @type {Doc[]} */
6304
+ const parts = [];
6305
+ for (let i = 0; i < comments.length; i++) {
6306
+ const comment = comments[i];
6307
+ if (comment.type === 'Line') {
6308
+ parts.push('//' + comment.value);
6309
+ } else if (comment.type === 'Block') {
6310
+ parts.push('/*' + comment.value + '*/');
6311
+ }
6312
+ parts.push(hardline);
6313
+ const next = comments[i + 1];
6314
+ if (next && getBlankLinesBetweenNodes(comment, next) > 0) {
6315
+ parts.push(hardline);
6316
+ }
6317
+ }
6318
+ return parts;
6319
+ }
6320
+
6321
+ /**
6322
+ * Build doc parts for `//` line comments attached to an element body — trailing
6323
+ * comments before `</tag>` (`closingElement.leadingComments`) or the comments of a
6324
+ * comment-only element (`innerComments`). Block comments are intentionally skipped:
6325
+ * they survive in the adjacent JSXText value and are already rendered as text, so
6326
+ * emitting them here would duplicate them. Each comment is emitted on its own line
6327
+ * at the children indent.
6328
+ * @param {AST.Comment[] | null | undefined} commentList
6329
+ * @param {any} [previousNode]
6330
+ * @returns {Doc[]}
6331
+ */
6332
+ function printElementBodyLineComments(commentList, previousNode = null) {
6333
+ const comments = (commentList ?? []).filter((comment) => comment.type === 'Line');
6334
+ if (comments.length === 0) {
6335
+ return [];
6336
+ }
6337
+ /** @type {Doc[]} */
6338
+ const parts = [];
6339
+ /** @type {AST.Node | AST.Comment | null | undefined} */
6340
+ let prev = previousNode;
6341
+ for (let i = 0; i < comments.length; i++) {
6342
+ parts.push(hardline);
6343
+ // Preserve a blank line before this comment if one existed in source.
6344
+ if (prev && getBlankLinesBetweenNodes(prev, comments[i]) > 0) {
6345
+ parts.push(hardline);
6346
+ }
6347
+ parts.push('//' + comments[i].value);
6348
+ prev = comments[i];
6349
+ }
6350
+ return parts;
6351
+ }
6352
+
6353
+ /**
6354
+ * Print a TSRX code block: setup statements then the single render output.
6355
+ * Callers in element/fragment body position hug it to the surrounding tags;
6356
+ * on its own as an arrow body it stands alone.
6357
+ * @param {AST.JSXCodeBlock} node
6358
+ * @param {AstPath<AST.JSXCodeBlock>} path
6359
+ * @param {RippleFormatOptions} options
6360
+ * @param {PrintFn} print
6361
+ * @returns {Doc}
6362
+ */
6363
+ function printJSXCodeBlock(node, path, options, print) {
6364
+ /** @type {Doc[]} */
6365
+ const parts = [];
6366
+ for (let i = 0; i < node.body.length; i++) {
6367
+ parts.push(path.call(print, 'body', i));
6368
+ if (i < node.body.length - 1) {
6369
+ parts.push(
6370
+ shouldAddBlankLine(node.body[i], node.body[i + 1]) ? [hardline, hardline] : hardline,
6371
+ );
6372
+ }
6373
+ }
6374
+ if (node.render) {
6375
+ if (node.body.length > 0) {
6376
+ // Preserve a blank line between the last setup statement and the render
6377
+ // output (measured to the render's leading comment, if any).
6378
+ const last = node.body[node.body.length - 1];
6379
+ const renderStart =
6380
+ /** @type {AST.NodeWithMaybeComments} */ (node.render).leadingComments?.[0] ?? node.render;
6381
+ parts.push(
6382
+ getBlankLinesBetweenNodes(last, renderStart) > 0 ? [hardline, hardline] : hardline,
6383
+ );
6384
+ }
6385
+ parts.push(path.call(print, 'render'));
6386
+ }
6387
+ // Trailing comments after the last statement/render inside the block.
6388
+ const innerCommentDocs = printElementBodyLineComments(node.innerComments);
6389
+ if (innerCommentDocs.length > 0) {
6390
+ const lastNode = node.render ?? node.body[node.body.length - 1];
6391
+ const firstComment = (node.innerComments ?? []).find((c) => c.type === 'Line');
6392
+ if (lastNode && firstComment && getBlankLinesBetweenNodes(lastNode, firstComment) > 0) {
6393
+ parts.push(hardline);
6394
+ }
6395
+ parts.push(...innerCommentDocs);
6396
+ }
6397
+ if (parts.length === 0) {
6398
+ return '@{}';
6399
+ }
6400
+ return group(['@{', indent([hardline, ...parts]), hardline, '}']);
6401
+ }
6402
+
5955
6403
  /**
5956
6404
  * Print a JSX attribute
5957
6405
  * @param {ESTreeJSX.JSXAttribute} attr - The JSX attribute node
@@ -5963,6 +6411,10 @@ function printJSXFragment(node, path, options, print) {
5963
6411
  function printJSXAttribute(attr, path, options, print) {
5964
6412
  const name = /** @type {ESTreeJSX.JSXIdentifier} */ (attr.name).name;
5965
6413
 
6414
+ if (attr.shorthand) {
6415
+ return ['{', name, '}'];
6416
+ }
6417
+
5966
6418
  if (!attr.value) {
5967
6419
  return name;
5968
6420
  }
@@ -5980,12 +6432,16 @@ function printJSXAttribute(attr, path, options, print) {
5980
6432
 
5981
6433
  if (attr.value.type === 'JSXExpressionContainer') {
5982
6434
  const expression = attr.value.expression;
6435
+ if (expression.type === 'Literal' && typeof expression.value === 'string') {
6436
+ const quote = options.jsxSingleQuote ? "'" : '"';
6437
+ return [name, '=', quote, /** @type {string} */ (expression.value), quote];
6438
+ }
5983
6439
  const exprDoc = path.call(
5984
6440
  (valuePath) => print(valuePath, { isInAttribute: true }),
5985
6441
  'value',
5986
6442
  'expression',
5987
6443
  );
5988
- if (shouldBreakAttributeExpressionClosingBrace(expression)) {
6444
+ if (shouldBreakAttributeExpressionClosingBrace(expression, options, attr)) {
5989
6445
  return [name, '={', exprDoc, hardline, '}'];
5990
6446
  }
5991
6447
  return [name, '={', exprDoc, '}'];
@@ -5995,16 +6451,21 @@ function printJSXAttribute(attr, path, options, print) {
5995
6451
  }
5996
6452
 
5997
6453
  /**
5998
- * Print a JSX member expression (e.g., React.Fragment)
5999
- * @param {AST.Node} node - The JSX member expression or identifier
6454
+ * Print a JSX element name.
6455
+ * @param {AST.Node} node - The JSX element name node
6000
6456
  * @returns {string}
6001
6457
  */
6002
- function printJSXMemberExpression(node) {
6458
+ function printJSXElementName(node) {
6003
6459
  if (node.type === 'JSXIdentifier') {
6004
6460
  return node.name;
6005
6461
  }
6006
6462
  if (node.type === 'JSXMemberExpression') {
6007
- return printJSXMemberExpression(node.object) + '.' + printJSXMemberExpression(node.property);
6463
+ return printJSXElementName(node.object) + '.' + printJSXElementName(node.property);
6464
+ }
6465
+ if (node.type === 'JSXNamespacedName') {
6466
+ const namespace_name = node.namespace.name;
6467
+ const local_name = node.name.name;
6468
+ return namespace_name + ':' + local_name;
6008
6469
  }
6009
6470
  return 'Unknown';
6010
6471
  }
@@ -6017,8 +6478,24 @@ function printJSXMemberExpression(node) {
6017
6478
  * @returns {string}
6018
6479
  */
6019
6480
  function printMemberExpressionSimple(node, options, computed = false) {
6481
+ if (node.type === 'JSXIdentifier') {
6482
+ return node.name;
6483
+ }
6484
+
6485
+ if (node.type === 'JSXMemberExpression') {
6486
+ return (
6487
+ printMemberExpressionSimple(node.object, options) +
6488
+ '.' +
6489
+ printMemberExpressionSimple(node.property, options, true)
6490
+ );
6491
+ }
6492
+
6493
+ if (node.type === 'JSXNamespacedName') {
6494
+ return node.namespace.name + ':' + node.name.name;
6495
+ }
6496
+
6020
6497
  if (node.type === 'Identifier') {
6021
- return (computed ? '' : node.tracked ? '@' : '') + node.name;
6498
+ return node.name;
6022
6499
  }
6023
6500
 
6024
6501
  if (node.type === 'MemberExpression') {
@@ -6064,7 +6541,7 @@ function is_attribute_value_breakable(value, is_nested_in_object = false) {
6064
6541
  }
6065
6542
 
6066
6543
  /**
6067
- * Print a Ripple Element node
6544
+ * Print a JSX element node
6068
6545
  * @param {AST.Element} element - The element node
6069
6546
  * @param {AstPath<AST.Element>} path - The AST path
6070
6547
  * @param {RippleFormatOptions} options - Prettier options
@@ -6072,7 +6549,7 @@ function is_attribute_value_breakable(value, is_nested_in_object = false) {
6072
6549
  * @returns {Doc}
6073
6550
  */
6074
6551
  function printElement(element, path, options, print) {
6075
- const node = /** @type {AST.Element & AST.NodeWithLocation} */ (element);
6552
+ const node = /** @type {any} */ (element);
6076
6553
  const tagName = printMemberExpressionSimple(node.id, options);
6077
6554
  const openingElement = /** @type {any} */ (node.openingElement);
6078
6555
  /** @type {Doc} */
@@ -6080,7 +6557,7 @@ function printElement(element, path, options, print) {
6080
6557
  if (openingElement?.typeArguments) {
6081
6558
  typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
6082
6559
  }
6083
- const elementLeadingComments = getElementLeadingComments(node);
6560
+ const elementLeadingComments = getElementLeadingComments(/** @type {any} */ (node));
6084
6561
 
6085
6562
  // `metadata.elementLeadingComments` may include comments that actually appear *inside* the element
6086
6563
  // body (after the opening tag). Those must not be hoisted before the element.
@@ -6115,7 +6592,7 @@ function printElement(element, path, options, print) {
6115
6592
  const openingEnd = /** @type {AST.NodeWithLocation} */ (node.openingElement).end;
6116
6593
  for (const child of node.children) {
6117
6594
  if (
6118
- (child.type === 'TSRXExpression' || child.type === 'Text') &&
6595
+ (child.type === 'JSXExpressionContainer' || child.type === 'JSXText') &&
6119
6596
  Array.isArray(child.leadingComments)
6120
6597
  ) {
6121
6598
  for (const comment of child.leadingComments) {
@@ -6192,11 +6669,14 @@ function printElement(element, path, options, print) {
6192
6669
  parts.push(attrLineBreak);
6193
6670
  const attrDoc = print(attrPath);
6194
6671
  parts.push(attrDoc);
6195
- const attr_node = /** @type {AST.Attribute | AST.SpreadAttribute} */ (attrPath.node);
6672
+ const attr_node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
6673
+ /** @type {unknown} */ (attrPath.node)
6674
+ );
6196
6675
  if (
6197
6676
  !hasBreakingAttribute &&
6198
6677
  (willBreak(attrDoc) ||
6199
- (attr_node.type === 'Attribute' && is_attribute_value_breakable(attr_node.value)))
6678
+ (attr_node.type === 'JSXAttribute' &&
6679
+ is_attribute_value_breakable(/** @type {any} */ (attr_node.value))))
6200
6680
  ) {
6201
6681
  hasBreakingAttribute = true;
6202
6682
  }
@@ -6304,15 +6784,17 @@ function printElement(element, path, options, print) {
6304
6784
  }
6305
6785
  }
6306
6786
 
6307
- const isTextLikeChild = currentChild.type === 'TSRXExpression' || currentChild.type === 'Text';
6787
+ const isTextLikeChild =
6788
+ currentChild.type === 'JSXExpressionContainer' || currentChild.type === 'JSXText';
6308
6789
  const hasTextLeadingComments =
6309
6790
  shouldLiftTextLevelComments &&
6310
6791
  isTextLikeChild &&
6311
6792
  Array.isArray(currentChild.leadingComments) &&
6312
6793
  currentChild.leadingComments.length > 0;
6794
+ const currentChildAny = /** @type {any} */ (currentChild);
6313
6795
  const rawExpressionLeadingComments =
6314
- isTextLikeChild && Array.isArray(currentChild.expression?.leadingComments)
6315
- ? currentChild.expression.leadingComments
6796
+ isTextLikeChild && Array.isArray(currentChildAny.expression?.leadingComments)
6797
+ ? currentChildAny.expression.leadingComments
6316
6798
  : null;
6317
6799
  const elementBodyLeadingComments =
6318
6800
  hasTextLeadingComments && node.openingElement
@@ -6418,10 +6900,10 @@ function printElement(element, path, options, print) {
6418
6900
  : nextChild;
6419
6901
  const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, whitespaceTarget);
6420
6902
  const isTextOrExpressionChild =
6421
- currentChild.type === 'TSRXExpression' ||
6422
- currentChild.type === 'Text' ||
6423
- nextChild.type === 'TSRXExpression' ||
6424
- nextChild.type === 'Text';
6903
+ currentChild.type === 'JSXExpressionContainer' ||
6904
+ currentChild.type === 'JSXText' ||
6905
+ nextChild.type === 'JSXExpressionContainer' ||
6906
+ nextChild.type === 'JSXText';
6425
6907
 
6426
6908
  if (whitespaceLinesCount > 0) {
6427
6909
  finalChildren.push(hardline);
@@ -6472,10 +6954,15 @@ function printElement(element, path, options, print) {
6472
6954
 
6473
6955
  const closingTag = ['</', tagName, '>'];
6474
6956
  let elementOutput;
6957
+ const shouldTryInlineMultipleChildren =
6958
+ !openingTagAlwaysBreaks &&
6959
+ fallbackCommentParts.length === 0 &&
6960
+ closingElementComments.length === 0 &&
6961
+ shouldTryInlineMultipleTextChildren(node);
6475
6962
 
6476
6963
  if (finalChildren.length === 1) {
6477
6964
  const child = finalChildren[0];
6478
- const firstChild = node.children[0];
6965
+ const firstChild = /** @type {any} */ (node.children[0]);
6479
6966
  const isNonSelfClosingElement =
6480
6967
  firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
6481
6968
  const isElementChild = firstChild && firstChild.type === 'Element';
@@ -6506,63 +6993,15 @@ function printElement(element, path, options, print) {
6506
6993
  } else {
6507
6994
  elementOutput = [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag];
6508
6995
  }
6996
+ } else if (shouldTryInlineMultipleChildren) {
6997
+ const inlineChildren = path.map(print, 'children');
6998
+ elementOutput = conditionalGroup([
6999
+ group([openingTag, ...inlineChildren, closingTag]),
7000
+ [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag],
7001
+ ]);
6509
7002
  } else {
6510
7003
  elementOutput = group([openingTag, indent([hardline, ...finalChildren]), hardline, closingTag]);
6511
7004
  }
6512
7005
 
6513
7006
  return leadingCommentParts.length > 0 ? [...leadingCommentParts, elementOutput] : elementOutput;
6514
7007
  }
6515
-
6516
- /**
6517
- * Print a Ripple attribute node
6518
- * @param {AST.Attribute} node - The attribute node
6519
- * @param {AstPath<AST.Attribute>} path - The AST path
6520
- * @param {RippleFormatOptions} options - Prettier options
6521
- * @param {PrintFn} print - Print callback
6522
- * @returns {Doc[]}
6523
- */
6524
- function printAttribute(node, path, options, print) {
6525
- /** @type {Doc[]} */
6526
- const parts = [];
6527
-
6528
- // Handle shorthand syntax: {id} instead of id={id}
6529
- // Check if either node.shorthand is true, OR if the value is an Identifier with the same name
6530
- const isShorthand =
6531
- node.shorthand ||
6532
- (node.value && node.value.type === 'Identifier' && node.value.name === node.name.name);
6533
-
6534
- if (isShorthand) {
6535
- parts.push('{');
6536
- parts.push(node.name.name);
6537
- parts.push('}');
6538
- return parts;
6539
- }
6540
-
6541
- parts.push(node.name.name);
6542
-
6543
- if (node.value) {
6544
- if (node.value.type === 'Literal' && typeof node.value.value === 'string') {
6545
- // String literals don't need curly braces
6546
- // Use jsxSingleQuote option if available, otherwise use double quotes
6547
- parts.push('=');
6548
- const useJsxSingleQuote = options.jsxSingleQuote === true;
6549
- parts.push(
6550
- formatStringLiteral(node.value.value, {
6551
- ...options,
6552
- singleQuote: useJsxSingleQuote,
6553
- }),
6554
- );
6555
- } else {
6556
- // All other values need curly braces: numbers, booleans, null, expressions, etc.
6557
- parts.push('={');
6558
- // Pass inline context for attribute values (keep objects compact)
6559
- parts.push(path.call((attrPath) => print(attrPath, { isInAttribute: true }), 'value'));
6560
- if (shouldBreakAttributeExpressionClosingBrace(node.value)) {
6561
- parts.push(hardline);
6562
- }
6563
- parts.push('}');
6564
- }
6565
- }
6566
-
6567
- return parts;
6568
- }