@tsrx/prettier-plugin 0.3.72 → 0.3.74

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 +800 -346
  3. package/src/index.test.js +5781 -6
package/src/index.js CHANGED
@@ -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,13 +1730,6 @@ 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
1735
  const trackedPrefix = node.tracked ? '@' : '';
@@ -2120,6 +2158,10 @@ function printRippleNode(node, path, options, print, args) {
2120
2158
  nodeContent = printTSCallSignatureDeclaration(node, path, options, print);
2121
2159
  break;
2122
2160
 
2161
+ case 'TSConstructSignatureDeclaration':
2162
+ nodeContent = printTSConstructSignatureDeclaration(node, path, options, print);
2163
+ break;
2164
+
2123
2165
  case 'TSEnumMember':
2124
2166
  nodeContent = printTSEnumMember(node, path, options, print);
2125
2167
  break;
@@ -2245,16 +2287,17 @@ function printRippleNode(node, path, options, print, args) {
2245
2287
  break;
2246
2288
  }
2247
2289
 
2248
- case 'Element':
2249
- nodeContent = printElement(node, path, options, print);
2250
- break;
2251
-
2252
- case 'TsxCompat':
2253
- nodeContent = printTsxCompat(node, path, options, print);
2290
+ case 'JSXCodeBlock':
2291
+ nodeContent = printJSXCodeBlock(node, path, options, print);
2254
2292
  break;
2255
2293
 
2256
- case 'TsrxFragment':
2257
- nodeContent = printTsrx(node, path, options, print);
2294
+ case 'JSXStyleElement':
2295
+ nodeContent = printJSXElement(
2296
+ /** @type {ESTreeJSX.JSXElement} */ (/** @type {unknown} */ (node)),
2297
+ path,
2298
+ options,
2299
+ print,
2300
+ );
2258
2301
  break;
2259
2302
 
2260
2303
  case 'JSXElement':
@@ -2266,7 +2309,7 @@ function printRippleNode(node, path, options, print, args) {
2266
2309
  break;
2267
2310
 
2268
2311
  case 'JSXText':
2269
- nodeContent = node.value;
2312
+ nodeContent = printRawText(node.value);
2270
2313
  break;
2271
2314
 
2272
2315
  case 'JSXEmptyExpression':
@@ -2279,28 +2322,12 @@ function printRippleNode(node, path, options, print, args) {
2279
2322
  }
2280
2323
  break;
2281
2324
 
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, '}'];
2325
+ case 'JSXAttribute':
2326
+ nodeContent = printJSXAttribute(node, path, options, print);
2291
2327
  break;
2292
- }
2293
-
2294
- case 'Text': {
2295
- if (typeof node.raw === 'string') {
2296
- nodeContent = printRawText(node.raw);
2297
- break;
2298
- }
2299
2328
 
2300
- const expressionDoc = suppressExpressionLeadingComments
2301
- ? path.call((exprPath) => print(exprPath, { suppressLeadingComments: true }), 'expression')
2302
- : path.call(print, 'expression');
2303
- nodeContent = ['{', expressionDoc, '}'];
2329
+ case 'JSXSpreadAttribute': {
2330
+ nodeContent = ['{...', path.call(print, 'argument'), '}'];
2304
2331
  break;
2305
2332
  }
2306
2333
 
@@ -2527,7 +2554,10 @@ function printVariableDeclaration(node, path, options, print) {
2527
2554
  const isForLoopInit =
2528
2555
  (parentNode && parentNode.type === 'ForStatement' && parentNode.init === node) ||
2529
2556
  (parentNode && parentNode.type === 'ForOfStatement' && parentNode.left === node) ||
2530
- (parentNode && parentNode.type === 'ForInStatement' && parentNode.left === node);
2557
+ (parentNode && parentNode.type === 'ForInStatement' && parentNode.left === node) ||
2558
+ (parentNode &&
2559
+ parentNode.type === 'JSXForExpression' &&
2560
+ (parentNode.left === node || parentNode.init === node));
2531
2561
 
2532
2562
  const declarations = path.map(print, 'declarations');
2533
2563
  const declarationParts = join(', ', declarations);
@@ -2672,6 +2702,12 @@ function printArrowFunction(node, path, options, print, args) {
2672
2702
  if (shouldBreakBody) {
2673
2703
  parts.push(' =>', indent([hardline, bodyContent]));
2674
2704
  } else {
2705
+ if (isTemplateExpression(node.body)) {
2706
+ return conditionalGroup([
2707
+ group([...parts, ' => ', bodyContent]),
2708
+ group([...parts, ' =>', indent([hardline, bodyContent])]),
2709
+ ]);
2710
+ }
2675
2711
  parts.push(
2676
2712
  ' =>',
2677
2713
  group(indent(line), { id: groupId }),
@@ -2689,21 +2725,26 @@ function printArrowFunction(node, path, options, print, args) {
2689
2725
  * @returns {boolean}
2690
2726
  */
2691
2727
  function isTemplateExpression(node) {
2692
- return (
2693
- node.type === 'TsxCompat' ||
2694
- node.type === 'TsrxFragment' ||
2695
- node.type === 'JSXElement' ||
2696
- node.type === 'JSXFragment'
2697
- );
2728
+ return node.type === 'JSXElement' || node.type === 'JSXFragment';
2698
2729
  }
2699
2730
 
2700
2731
  /**
2701
2732
  * Check whether a braced attribute expression should close on its own line.
2702
2733
  * @param {AST.Node} node - The expression inside the attribute braces
2734
+ * @param {RippleFormatOptions} options
2735
+ * @param {AST.Node} [attributeNode]
2703
2736
  * @returns {boolean}
2704
2737
  */
2705
- function shouldBreakAttributeExpressionClosingBrace(node) {
2706
- return node.type === 'ArrowFunctionExpression' && node.body && isTemplateExpression(node.body);
2738
+ function shouldBreakAttributeExpressionClosingBrace(node, options, attributeNode = node) {
2739
+ return (
2740
+ node.type === 'ArrowFunctionExpression' &&
2741
+ node.body &&
2742
+ isTemplateExpression(node.body) &&
2743
+ sourceSpanExceedsPrintWidth(
2744
+ /** @type {AST.NodeWithLocation} */ (/** @type {unknown} */ (attributeNode ?? node)),
2745
+ options,
2746
+ )
2747
+ );
2707
2748
  }
2708
2749
 
2709
2750
  /**
@@ -2920,9 +2961,6 @@ function sourceSpanExceedsPrintWidth(node, options) {
2920
2961
  * @returns {boolean}
2921
2962
  */
2922
2963
  function shouldBreakArrowExpressionBody(node, options, args) {
2923
- if (args?.isInAttribute && isTemplateExpression(node)) {
2924
- return true;
2925
- }
2926
2964
  return (
2927
2965
  (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') &&
2928
2966
  sourceSpanExceedsPrintWidth(/** @type {AST.NodeWithLocation} */ (node), options)
@@ -3295,9 +3333,10 @@ function extractAndPrintLeadingComments(node) {
3295
3333
  * @param {AstPath<AST.IfStatement>} path - The AST path
3296
3334
  * @param {RippleFormatOptions} options - Prettier options
3297
3335
  * @param {PrintFn} print - Print callback
3336
+ * @param {boolean} [directive]
3298
3337
  * @returns {Doc[]}
3299
3338
  */
3300
- function printIfStatement(node, path, options, print) {
3339
+ function printIfStatement(node, path, options, print, directive = false) {
3301
3340
  // Extract leading comments from test node to print them before 'if' keyword
3302
3341
  const testNode = node.test;
3303
3342
 
@@ -3341,8 +3380,24 @@ function printIfStatement(node, path, options, print) {
3341
3380
  parts.push(' ');
3342
3381
  }
3343
3382
 
3344
- parts.push('else ');
3345
- parts.push(path.call(print, 'alternate'));
3383
+ parts.push(directive ? '@else ' : 'else ');
3384
+ if (directive && node.alternate.type === 'IfStatement') {
3385
+ parts.push(
3386
+ path.call(
3387
+ (alternatePath) =>
3388
+ printIfStatement(
3389
+ /** @type {AST.IfStatement} */ (alternatePath.node),
3390
+ /** @type {AstPath<AST.IfStatement>} */ (alternatePath),
3391
+ options,
3392
+ print,
3393
+ true,
3394
+ ),
3395
+ 'alternate',
3396
+ ),
3397
+ );
3398
+ } else {
3399
+ parts.push(path.call(print, 'alternate'));
3400
+ }
3346
3401
  }
3347
3402
 
3348
3403
  return parts;
@@ -3376,9 +3431,10 @@ function printForInStatement(node, path, options, print) {
3376
3431
  * @param {AstPath<AST.ForOfStatement>} path - The AST path
3377
3432
  * @param {RippleFormatOptions} options - Prettier options
3378
3433
  * @param {PrintFn} print - Print callback
3434
+ * @param {boolean} [directive]
3379
3435
  * @returns {Doc[]}
3380
3436
  */
3381
- function printForOfStatement(node, path, options, print) {
3437
+ function printForOfStatement(node, path, options, print, directive = false) {
3382
3438
  /** @type {Doc[]} */
3383
3439
  const parts = [];
3384
3440
  parts.push('for (');
@@ -3399,6 +3455,10 @@ function printForOfStatement(node, path, options, print) {
3399
3455
 
3400
3456
  parts.push(') ');
3401
3457
  parts.push(path.call(print, 'body'));
3458
+ if (node.empty) {
3459
+ parts.push(directive ? ' @empty ' : ' empty ');
3460
+ parts.push(path.call(print, 'empty'));
3461
+ }
3402
3462
 
3403
3463
  return parts;
3404
3464
  }
@@ -3715,9 +3775,10 @@ function printClassDeclaration(node, path, options, print) {
3715
3775
  * @param {AstPath<AST.TryStatement>} path - The AST path
3716
3776
  * @param {RippleFormatOptions} options - Prettier options
3717
3777
  * @param {PrintFn} print - Print callback
3778
+ * @param {boolean} [directive=false] - Whether this is a JSX @try expression.
3718
3779
  * @returns {Doc[]}
3719
3780
  */
3720
- function printTryStatement(node, path, options, print) {
3781
+ function printTryStatement(node, path, options, print, directive = false) {
3721
3782
  // Extract leading comments from block node to print them before 'try' keyword
3722
3783
  const blockNode = node.block;
3723
3784
 
@@ -3737,12 +3798,12 @@ function printTryStatement(node, path, options, print) {
3737
3798
  parts.push(block);
3738
3799
 
3739
3800
  if (node.pending) {
3740
- parts.push(' pending ');
3801
+ parts.push(directive ? ' @pending ' : ' pending ');
3741
3802
  parts.push(path.call(print, 'pending'));
3742
3803
  }
3743
3804
 
3744
3805
  if (node.handler) {
3745
- parts.push(' catch');
3806
+ parts.push(directive ? ' @catch' : ' catch');
3746
3807
  if (node.handler.param) {
3747
3808
  parts.push(' (');
3748
3809
  parts.push(path.call(print, 'handler', 'param'));
@@ -4483,6 +4544,76 @@ function printSwitchStatement(node, path, options, print) {
4483
4544
  return parts;
4484
4545
  }
4485
4546
 
4547
+ /**
4548
+ * Print a JSX switch expression. JSX switch cases use explicit template blocks:
4549
+ * `case value: { ... }`, unlike ordinary JavaScript switch cases.
4550
+ * @param {AST.SwitchStatement} node - The switch expression node
4551
+ * @param {AstPath<AST.SwitchStatement>} path - The AST path
4552
+ * @param {RippleFormatOptions} options - Prettier options
4553
+ * @param {PrintFn} print - Print callback
4554
+ * @returns {Doc[]}
4555
+ */
4556
+ function printJSXSwitchExpression(node, path, options, print) {
4557
+ const discriminant = path.call(
4558
+ (discriminantPath) => print(discriminantPath, { suppressLeadingComments: true }),
4559
+ 'discriminant',
4560
+ );
4561
+
4562
+ /** @type {Doc[]} */
4563
+ const cases = [];
4564
+ for (let i = 0; i < node.cases.length; i++) {
4565
+ const caseDoc = [printJSXSwitchCase(node.cases[i], path, options, print, i)];
4566
+ if (i < node.cases.length - 1 && isNextLineEmpty(node.cases[i], options)) {
4567
+ caseDoc.push(hardline);
4568
+ }
4569
+ cases.push(caseDoc);
4570
+ }
4571
+
4572
+ const bodyDoc =
4573
+ cases.length > 0 ? [indent([hardline, join(hardline, cases)]), hardline] : hardline;
4574
+
4575
+ const discriminantDoc = group(['@switch (', indent([softline, discriminant]), softline, ')']);
4576
+
4577
+ return [
4578
+ ...extractAndPrintLeadingComments(node.discriminant),
4579
+ discriminantDoc,
4580
+ ' {',
4581
+ bodyDoc,
4582
+ '}',
4583
+ ];
4584
+ }
4585
+
4586
+ /**
4587
+ * @param {AST.SwitchCase} node
4588
+ * @param {AstPath<AST.SwitchStatement>} path
4589
+ * @param {RippleFormatOptions} options
4590
+ * @param {PrintFn} print
4591
+ * @param {number} index
4592
+ * @returns {Doc[]}
4593
+ */
4594
+ function printJSXSwitchCase(node, path, options, print, index) {
4595
+ const header = node.test
4596
+ ? ['@case ', path.call(print, 'cases', index, 'test'), ':']
4597
+ : '@default:';
4598
+ const consequents = node.consequent || [];
4599
+ const printedConsequents = [];
4600
+
4601
+ for (let i = 0; i < consequents.length; i++) {
4602
+ const child = consequents[i];
4603
+ if (!child || child.type === 'EmptyStatement') {
4604
+ continue;
4605
+ }
4606
+ printedConsequents.push(path.call(print, 'cases', index, 'consequent', i));
4607
+ }
4608
+
4609
+ const bodyDoc =
4610
+ printedConsequents.length > 0
4611
+ ? [indent([hardline, join(hardline, printedConsequents)]), hardline]
4612
+ : hardline;
4613
+
4614
+ return [header, ' {', bodyDoc, '}'];
4615
+ }
4616
+
4486
4617
  /**
4487
4618
  * Print a switch case
4488
4619
  * @param {AST.SwitchCase} node - The switch case node
@@ -4650,6 +4781,26 @@ function getBlankLinesBetweenPositions(current_pos, next_pos) {
4650
4781
  * @param {AST.Node | AST.CSS.StyleSheet | AST.Comment} nextNode - Next node
4651
4782
  * @returns {number}
4652
4783
  */
4784
+ /**
4785
+ * The position to measure a leading blank line against: the first leading
4786
+ * comment if any (so the comment lines aren't miscounted as blank), else the
4787
+ * node itself.
4788
+ * @param {any} node
4789
+ * @returns {any}
4790
+ */
4791
+ function leadingAnchor(node) {
4792
+ const lead = node?.leadingComments;
4793
+ if (Array.isArray(lead) && lead.length > 0 && lead[0].loc) {
4794
+ return lead[0];
4795
+ }
4796
+ return node;
4797
+ }
4798
+
4799
+ /**
4800
+ * @param {any} currentNode
4801
+ * @param {any} nextNode
4802
+ * @returns {number}
4803
+ */
4653
4804
  function getBlankLinesBetweenNodes(currentNode, nextNode) {
4654
4805
  // Return the number of blank lines between two nodes based on their location
4655
4806
  if (
@@ -5022,6 +5173,16 @@ function printVariableDeclarator(node, path, options, print) {
5022
5173
  }
5023
5174
  }
5024
5175
 
5176
+ if (isTemplateExpression(node.init)) {
5177
+ const groupId = Symbol('declaration');
5178
+ return group([
5179
+ group(id),
5180
+ ' =',
5181
+ group(indent(line), { id: groupId }),
5182
+ indentIfBreak(init, { groupId }),
5183
+ ]);
5184
+ }
5185
+
5025
5186
  // Default: simple inline format with space
5026
5187
  // Use group to allow breaking if needed - but keep inline when it fits
5027
5188
  return group([id, ' = ', init]);
@@ -5191,6 +5352,45 @@ function printTSCallSignatureDeclaration(node, path, options, print) {
5191
5352
  return parts;
5192
5353
  }
5193
5354
 
5355
+ /**
5356
+ * Print a TypeScript construct signature in an interface or type literal
5357
+ * @param {AST.TSConstructSignatureDeclaration} node - The construct signature node
5358
+ * @param {AstPath<AST.TSConstructSignatureDeclaration>} path - The AST path
5359
+ * @param {RippleFormatOptions} options - Prettier options
5360
+ * @param {PrintFn} print - Print callback
5361
+ * @returns {Doc[]}
5362
+ */
5363
+ function printTSConstructSignatureDeclaration(node, path, options, print) {
5364
+ /** @type {Doc[]} */
5365
+ const parts = ['new '];
5366
+
5367
+ if (node.typeParameters) {
5368
+ const type_params = path.call(print, 'typeParameters');
5369
+ if (Array.isArray(type_params)) {
5370
+ parts.push(...type_params);
5371
+ } else {
5372
+ parts.push(type_params);
5373
+ }
5374
+ }
5375
+
5376
+ parts.push('(');
5377
+ if (node.parameters && node.parameters.length > 0) {
5378
+ const params = path.map(print, 'parameters');
5379
+ for (let i = 0; i < params.length; i++) {
5380
+ if (i > 0) parts.push(', ');
5381
+ parts.push(params[i]);
5382
+ }
5383
+ }
5384
+ parts.push(')');
5385
+
5386
+ if (node.typeAnnotation) {
5387
+ parts.push(': ');
5388
+ parts.push(path.call(print, 'typeAnnotation'));
5389
+ }
5390
+
5391
+ return parts;
5392
+ }
5393
+
5194
5394
  /**
5195
5395
  * Print a TypeScript type reference (e.g., Array<string>)
5196
5396
  * @param {AST.TSTypeReference} node - The type reference node
@@ -5465,6 +5665,36 @@ function printRawText(raw) {
5465
5665
  );
5466
5666
  }
5467
5667
 
5668
+ /**
5669
+ * @param {string} raw
5670
+ * @returns {Doc | Doc[] | string}
5671
+ */
5672
+ function printJSXTextChild(raw) {
5673
+ const text = raw.trim();
5674
+ if (!text) {
5675
+ return '';
5676
+ }
5677
+
5678
+ const lines = text
5679
+ .split(/\r\n|\r|\n/u)
5680
+ .map((line) => line.trim())
5681
+ .filter(Boolean);
5682
+ if (lines.length <= 1) {
5683
+ return lines[0] ?? '';
5684
+ }
5685
+
5686
+ return join(hardline, lines);
5687
+ }
5688
+
5689
+ /**
5690
+ * @param {string} raw
5691
+ * @returns {string}
5692
+ */
5693
+ function normalizeInlineJSXText(raw) {
5694
+ const text = raw.replace(/[^\S\r\n]+/gu, ' ');
5695
+ return text.trim() || !/[\r\n]/u.test(text) ? text : '';
5696
+ }
5697
+
5468
5698
  /**
5469
5699
  * @param {AST.Node} parentNode
5470
5700
  * @param {AST.Node} firstChild
@@ -5476,8 +5706,7 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5476
5706
  return false;
5477
5707
  }
5478
5708
 
5479
- // Always inline Text nodes — they are explicit text child forms.
5480
- if (firstChild.type === 'Text') {
5709
+ if (firstChild.type === 'JSXText') {
5481
5710
  return true;
5482
5711
  }
5483
5712
 
@@ -5487,7 +5716,7 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5487
5716
 
5488
5717
  // Inline JSX expressions if they fit, but respect original multi-line formatting
5489
5718
  // for non-literal expressions (e.g. {children} should stay multi-line if written that way)
5490
- if (firstChild.type === 'TSRXExpression' || firstChild.type === 'JSXExpressionContainer') {
5719
+ if (firstChild.type === 'JSXExpressionContainer') {
5491
5720
  if (wasOriginallySingleLine(parentNode)) {
5492
5721
  return true;
5493
5722
  }
@@ -5505,19 +5734,71 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5505
5734
  return false;
5506
5735
  }
5507
5736
 
5508
- if (firstChild.type === 'Element' && firstChild.selfClosing) {
5509
- return (
5510
- !(/** @type {AST.Element} */ (parentNode).attributes) ||
5511
- /** @type {AST.Element} */ (parentNode).attributes.length === 0
5512
- );
5737
+ if (firstChild.type === 'JSXElement' && firstChild.openingElement?.selfClosing) {
5738
+ const parent = /** @type {any} */ (parentNode);
5739
+ return !parent.openingElement?.attributes?.length;
5513
5740
  }
5514
5741
 
5515
5742
  return false;
5516
5743
  }
5517
5744
 
5745
+ /**
5746
+ * Check whether a child can participate in compact inline TSRX content.
5747
+ * @param {any} child
5748
+ * @returns {boolean}
5749
+ */
5750
+ function isInlineableTextOrExpressionChild(child) {
5751
+ if (!child || (child.type !== 'JSXText' && child.type !== 'JSXExpressionContainer')) {
5752
+ return false;
5753
+ }
5754
+
5755
+ if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
5756
+ return false;
5757
+ }
5758
+
5759
+ const expression = /** @type {{ expression?: AST.Node & AST.NodeWithMaybeComments }} */ (child)
5760
+ .expression;
5761
+ return !expression || !hasComment(expression);
5762
+ }
5763
+
5764
+ /**
5765
+ * @param {any} node
5766
+ * @returns {boolean}
5767
+ */
5768
+ function shouldTryInlineMultipleTextChildren(node) {
5769
+ return (
5770
+ wasOriginallySingleLine(node) &&
5771
+ Array.isArray(node.children) &&
5772
+ node.children.length > 1 &&
5773
+ node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
5774
+ node.children.every(isInlineableTextOrExpressionChild)
5775
+ );
5776
+ }
5777
+
5778
+ /**
5779
+ * @param {AST.Node} child
5780
+ * @returns {boolean}
5781
+ */
5782
+ function isSimpleJSXExpressionChild(child) {
5783
+ if (child?.type !== 'JSXExpressionContainer') {
5784
+ return false;
5785
+ }
5786
+
5787
+ const expression = child.expression;
5788
+ return (
5789
+ expression?.type === 'Identifier' ||
5790
+ expression?.type === 'Literal' ||
5791
+ expression?.type === 'TemplateLiteral' ||
5792
+ // Stock Prettier keeps a single `{expr}` child inline regardless of the
5793
+ // expression kind (member access, calls, etc.); only multiple children break.
5794
+ expression?.type === 'MemberExpression' ||
5795
+ expression?.type === 'CallExpression'
5796
+ );
5797
+ }
5798
+
5518
5799
  /**
5519
5800
  * Get leading comments from element metadata
5520
- * @param {AST.Element} node - The element node
5801
+ * @param {ESTreeJSX.JSXElement} node - The element node
5521
5802
  * @returns {AST.Comment[]}
5522
5803
  */
5523
5804
  function getElementLeadingComments(node) {
@@ -5577,142 +5858,10 @@ function createElementLevelCommentPartsTrimmed(comments) {
5577
5858
  return parts;
5578
5859
  }
5579
5860
 
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
5861
  /**
5713
5862
  * Print a JSX element
5714
- * @param {ESTreeJSX.JSXElement} node - The JSX element node
5715
- * @param {AstPath<ESTreeJSX.JSXElement>} path - The AST path
5863
+ * @param {AST.TSRXJSXElement} node - The JSX element node
5864
+ * @param {AstPath<any>} path - The AST path
5716
5865
  * @param {RippleFormatOptions} options - Prettier options
5717
5866
  * @param {PrintFn} print - Print callback
5718
5867
  * @returns {Doc | Doc[]}
@@ -5722,20 +5871,7 @@ function printJSXElement(node, path, options, print) {
5722
5871
  const openingElement = node.openingElement;
5723
5872
  const closingElement = node.closingElement;
5724
5873
 
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
- }
5874
+ const tagName = printJSXElementName(openingElement.name);
5739
5875
 
5740
5876
  const isSelfClosing = openingElement.selfClosing;
5741
5877
  const hasAttributes = openingElement.attributes && openingElement.attributes.length > 0;
@@ -5747,6 +5883,12 @@ function printJSXElement(node, path, options, print) {
5747
5883
  typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
5748
5884
  }
5749
5885
 
5886
+ // Comments that sit inside the opening tag (before an attribute) are attached
5887
+ // by the parser to a body child; pull them out and key them by the attribute
5888
+ // they precede so they print in the opening tag, not jammed into the body.
5889
+ const openingTagCommentsByAttr = collectOpeningTagComments(node);
5890
+ const hasOpeningTagComments = openingTagCommentsByAttr.size > 0;
5891
+
5750
5892
  // Format attributes
5751
5893
  /** @type {Doc} */
5752
5894
  let attributesDoc = '';
@@ -5770,19 +5912,31 @@ function printJSXElement(node, path, options, print) {
5770
5912
  'attributes',
5771
5913
  i,
5772
5914
  );
5773
- } else if (attr.type === 'JSXSpreadAttribute' || attr.type === 'SpreadAttribute') {
5915
+ } else if (attr.type === 'JSXSpreadAttribute') {
5774
5916
  attrDoc = ['{...', path.call(print, 'openingElement', 'attributes', i, 'argument'), '}'];
5775
5917
  }
5776
5918
  if (!hasBreakingAttribute && attrDoc && willBreak(attrDoc)) {
5777
5919
  hasBreakingAttribute = true;
5778
5920
  }
5921
+ const lead = openingTagCommentsByAttr.get(i);
5922
+ if (lead) {
5923
+ /** @type {Doc[]} */
5924
+ const parts = [];
5925
+ for (const comment of lead) {
5926
+ parts.push(
5927
+ comment.type === 'Line' ? '//' + comment.value : '/*' + comment.value + '*/',
5928
+ );
5929
+ parts.push(hardline);
5930
+ }
5931
+ return [...parts, attrDoc];
5932
+ }
5779
5933
  return attrDoc;
5780
5934
  },
5781
5935
  );
5782
5936
  const attrLineBreak = options.singleAttributePerLine ? hardline : line;
5783
5937
  attributesDoc = indent([attrLineBreak, join(attrLineBreak, attrs)]);
5784
5938
  }
5785
- const shouldForceBreak = hasBreakingAttribute;
5939
+ const shouldForceBreak = hasBreakingAttribute || hasOpeningTagComments;
5786
5940
 
5787
5941
  if (isSelfClosing) {
5788
5942
  return group(['<', tagName, typeArgsDoc, attributesDoc, hasAttributes ? line : ' ', '/>'], {
@@ -5802,58 +5956,119 @@ function printJSXElement(node, path, options, print) {
5802
5956
  { shouldBreak: shouldForceBreak },
5803
5957
  );
5804
5958
 
5959
+ // Trailing comments after the last child are attached by the parser either to
5960
+ // the closing tag (`closingElement.leadingComments`) or, when the last child is
5961
+ // an `{expr}` container, to `metadata.elementLeadingComments` positioned inside
5962
+ // the body (start >= opening tag end). Emit both before `</tag>`.
5963
+ const openingTagEnd = /** @type {AST.NodeWithLocation} */ (openingElement).end;
5964
+ const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
5965
+ (/** @type {AST.Comment} */ comment) =>
5966
+ typeof comment.start === 'number' && comment.start >= openingTagEnd,
5967
+ );
5968
+ const trailingComments = [
5969
+ ...(node.closingElement?.leadingComments ?? []),
5970
+ ...bodyMetaComments,
5971
+ ].sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
5972
+ const lastMeaningfulChild = [...(node.children ?? [])]
5973
+ .reverse()
5974
+ .find((child) => child.type !== 'JSXText' || child.value.trim());
5975
+ const closingCommentDocs = printElementBodyLineComments(trailingComments, lastMeaningfulChild);
5976
+ const hasClosingComments = closingCommentDocs.length > 0;
5977
+ // A comment-only element has no children; its comments live in `innerComments`.
5978
+ const innerCommentDocs = printElementBodyLineComments(node.innerComments);
5979
+
5805
5980
  if (!hasChildren) {
5981
+ const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
5982
+ if (bodyComments.length > 0) {
5983
+ return group([openingTag, indent(bodyComments), hardline, '</', tagName, '>']);
5984
+ }
5806
5985
  return [openingTag, '</', tagName, '>'];
5807
5986
  }
5808
5987
 
5809
- // Format children - filter out empty text nodes and merge adjacent text nodes
5988
+ // A `@{ }` code block is the whole body and hugs the tags: `<div>@{ … }</div>`.
5989
+ if (node.children.length === 1 && node.children[0].type === 'JSXCodeBlock') {
5990
+ return group([openingTag, path.call(print, 'children', 0), '</', tagName, '>']);
5991
+ }
5992
+
5993
+ // Format children - filter out empty text nodes and merge adjacent text nodes.
5994
+ // childNodes tracks the source node behind each doc (a text run is a single
5995
+ // JSXText) so the join can preserve authored blank lines.
5810
5996
  const childrenDocs = [];
5997
+ const childNodes = [];
5811
5998
  let currentText = '';
5999
+ let currentTextNode = null;
5812
6000
 
5813
6001
  for (let i = 0; i < node.children.length; i++) {
5814
6002
  const child = node.children[i];
5815
6003
 
5816
6004
  if (child.type === 'JSXText') {
5817
- // Accumulate text content, preserving spaces between words
5818
- const trimmed = child.value.trim();
5819
- if (trimmed) {
6005
+ if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
6006
+ if (currentText) {
6007
+ childrenDocs.push(currentText);
6008
+ childNodes.push(currentTextNode);
6009
+ currentText = '';
6010
+ currentTextNode = null;
6011
+ }
6012
+ const printedChild = path.call(print, 'children', i);
6013
+ if (printedChild !== '') {
6014
+ childrenDocs.push(printedChild);
6015
+ childNodes.push(child);
6016
+ }
6017
+ continue;
6018
+ }
6019
+ // Accumulate text content, preserving meaningful boundary spaces.
6020
+ const text = normalizeInlineJSXText(child.value);
6021
+ if (text) {
5820
6022
  const nextChild = node.children[i + 1];
5821
6023
  const afterNextChild = node.children[i + 2];
5822
6024
  const nextText = afterNextChild?.type === 'JSXText' ? afterNextChild.value.trim() : '';
5823
6025
  if (
5824
6026
  tagName === 'tsrx' &&
5825
- trimmed.endsWith('=') &&
6027
+ text.trimEnd().endsWith('=') &&
5826
6028
  nextChild?.type === 'JSXElement' &&
5827
6029
  nextText === ';'
5828
6030
  ) {
5829
6031
  if (currentText) {
5830
6032
  childrenDocs.push(currentText);
6033
+ childNodes.push(currentTextNode);
5831
6034
  currentText = '';
6035
+ currentTextNode = null;
5832
6036
  }
5833
- childrenDocs.push([trimmed, ' ', path.call(print, 'children', i + 1), ';']);
6037
+ childrenDocs.push([text.trim(), ' ', path.call(print, 'children', i + 1), ';']);
6038
+ childNodes.push(child);
5834
6039
  i += 2;
5835
6040
  continue;
5836
6041
  }
5837
6042
 
5838
6043
  if (currentText) {
5839
- currentText += ' ' + trimmed;
6044
+ currentText += currentText.endsWith(' ') || text.startsWith(' ') ? text : ' ' + text;
5840
6045
  } else {
5841
- currentText = trimmed;
6046
+ currentText = text;
6047
+ currentTextNode = child;
5842
6048
  }
5843
6049
  }
5844
6050
  } else {
5845
6051
  // If we have accumulated text, push it before the non-text node
5846
6052
  if (currentText) {
5847
6053
  childrenDocs.push(currentText);
6054
+ childNodes.push(currentTextNode);
5848
6055
  currentText = '';
6056
+ currentTextNode = null;
5849
6057
  }
5850
6058
 
5851
6059
  if (child.type === 'JSXExpressionContainer') {
5852
6060
  // Handle JSX expression containers
5853
- childrenDocs.push(['{', path.call(print, 'children', i, 'expression'), '}']);
6061
+ childrenDocs.push([
6062
+ ...printTemplateChildLeadingComments(child),
6063
+ '{',
6064
+ path.call(print, 'children', i, 'expression'),
6065
+ '}',
6066
+ ]);
6067
+ childNodes.push(child);
5854
6068
  } else {
5855
6069
  // Handle nested JSX elements
5856
6070
  childrenDocs.push(path.call(print, 'children', i));
6071
+ childNodes.push(child);
5857
6072
  }
5858
6073
  }
5859
6074
  }
@@ -5861,37 +6076,71 @@ function printJSXElement(node, path, options, print) {
5861
6076
  // Don't forget any remaining text
5862
6077
  if (currentText) {
5863
6078
  childrenDocs.push(currentText);
6079
+ childNodes.push(currentTextNode);
5864
6080
  }
5865
6081
 
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, '>']);
6082
+ // A child with leading comments must break onto its own line, so the comment
6083
+ // reads above the child rather than being jammed onto the opening tag.
6084
+ const hasChildLeadingComments = node.children.some((child) => {
6085
+ const leadingComments = /** @type {AST.NodeWithMaybeComments} */ (child).leadingComments;
6086
+ return Array.isArray(leadingComments) && leadingComments.length > 0;
6087
+ });
6088
+ const forceMultiline = hasClosingComments || hasChildLeadingComments;
6089
+
6090
+ // Check if content can be inlined (single text node or single expression).
6091
+ // Trailing or child-leading comments force the multi-line layout. A single
6092
+ // text child stays inline when it fits and otherwise fills/wraps to printWidth.
6093
+ if (!forceMultiline && childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
6094
+ // The open tag breaks for attributes independently; the text+closing get
6095
+ // their own group so the text only drops to its own (filled) lines when it
6096
+ // itself overflows — otherwise it hugs `>text</tag>`.
6097
+ return [
6098
+ openingTag,
6099
+ group([indent([softline, printRawText(childrenDocs[0])]), softline, '</', tagName, '>']),
6100
+ ];
5869
6101
  }
5870
6102
  const meaningfulChildren = node.children.filter(
5871
- (child) => child.type !== 'JSXText' || child.value.trim(),
6103
+ (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim(),
5872
6104
  );
5873
6105
  const singleMeaningfulChild = meaningfulChildren.length === 1 ? meaningfulChildren[0] : null;
5874
6106
  if (
6107
+ !forceMultiline &&
5875
6108
  childrenDocs.length === 1 &&
5876
6109
  singleMeaningfulChild?.type === 'JSXExpressionContainer' &&
5877
- singleMeaningfulChild.expression.type === 'Identifier'
6110
+ isSimpleJSXExpressionChild(/** @type {AST.Node} */ (singleMeaningfulChild))
5878
6111
  ) {
5879
6112
  return group([openingTag, childrenDocs[0], '</', tagName, '>']);
5880
6113
  }
6114
+ if (
6115
+ !forceMultiline &&
6116
+ childrenDocs.length > 1 &&
6117
+ wasOriginallySingleLine(node) &&
6118
+ node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
6119
+ node.children.every(
6120
+ (/** @type {any} */ child) =>
6121
+ child.type === 'JSXText' || isSimpleJSXExpressionChild(/** @type {AST.Node} */ (child)),
6122
+ )
6123
+ ) {
6124
+ return group([openingTag, ...childrenDocs, '</', tagName, '>']);
6125
+ }
5881
6126
 
5882
- // Multiple children or complex children - format with line breaks
6127
+ // Multiple children or complex children - format with line breaks. Text runs
6128
+ // fill/wrap to printWidth.
5883
6129
  const formattedChildren = [];
5884
6130
  for (let i = 0; i < childrenDocs.length; i++) {
5885
- formattedChildren.push(childrenDocs[i]);
6131
+ const childDoc = childrenDocs[i];
6132
+ formattedChildren.push(typeof childDoc === 'string' ? printRawText(childDoc) : childDoc);
5886
6133
  if (i < childrenDocs.length - 1) {
5887
- formattedChildren.push(hardline);
6134
+ // Preserve a single authored blank line between children (2+ collapse to 1).
6135
+ const blank = getBlankLinesBetweenNodes(childNodes[i], leadingAnchor(childNodes[i + 1])) > 0;
6136
+ formattedChildren.push(blank ? [hardline, hardline] : hardline);
5888
6137
  }
5889
6138
  }
5890
6139
 
5891
6140
  // Build the final element
5892
6141
  return group([
5893
6142
  openingTag,
5894
- indent([hardline, ...formattedChildren]),
6143
+ indent([hardline, ...formattedChildren, ...closingCommentDocs]),
5895
6144
  hardline,
5896
6145
  '</',
5897
6146
  tagName,
@@ -5914,23 +6163,46 @@ function printJSXFragment(node, path, options, print) {
5914
6163
  return '<></>';
5915
6164
  }
5916
6165
 
5917
- // Format children - filter out empty text nodes
6166
+ // A `@{ }` code block is the whole body and hugs the tags: `<>@{ … }</>`.
6167
+ if (node.children.length === 1 && /** @type {any} */ (node.children[0]).type === 'JSXCodeBlock') {
6168
+ return group(['<>', path.call(print, 'children', 0), '</>']);
6169
+ }
6170
+
6171
+ // Format children - filter out empty text nodes. childNodes tracks the source
6172
+ // node behind each doc so the join can preserve authored blank lines.
5918
6173
  const childrenDocs = [];
6174
+ const childNodes = [];
5919
6175
  for (let i = 0; i < node.children.length; i++) {
5920
6176
  const child = node.children[i];
5921
6177
 
5922
6178
  if (child.type === 'JSXText') {
6179
+ if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
6180
+ const printedChild = path.call(print, 'children', i);
6181
+ if (printedChild !== '') {
6182
+ childrenDocs.push(printedChild);
6183
+ childNodes.push(child);
6184
+ }
6185
+ continue;
6186
+ }
5923
6187
  // Handle JSX text nodes - trim whitespace and only include if not empty
5924
- const text = child.value.trim();
6188
+ const text = printJSXTextChild(child.value);
5925
6189
  if (text) {
5926
6190
  childrenDocs.push(text);
6191
+ childNodes.push(child);
5927
6192
  }
5928
6193
  } else if (child.type === 'JSXExpressionContainer') {
5929
6194
  // Handle JSX expression containers
5930
- childrenDocs.push(['{', path.call(print, 'children', i, 'expression'), '}']);
6195
+ childrenDocs.push([
6196
+ ...printTemplateChildLeadingComments(child),
6197
+ '{',
6198
+ path.call(print, 'children', i, 'expression'),
6199
+ '}',
6200
+ ]);
6201
+ childNodes.push(child);
5931
6202
  } else {
5932
6203
  // Handle nested JSX elements and fragments
5933
6204
  childrenDocs.push(path.call(print, 'children', i));
6205
+ childNodes.push(child);
5934
6206
  }
5935
6207
  }
5936
6208
 
@@ -5938,13 +6210,33 @@ function printJSXFragment(node, path, options, print) {
5938
6210
  if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
5939
6211
  return ['<>', childrenDocs[0], '</>'];
5940
6212
  }
6213
+ const meaningfulChildren = node.children.filter(
6214
+ (child) => child.type !== 'JSXText' || child.value.trim(),
6215
+ );
6216
+ if (
6217
+ childrenDocs.length === 1 &&
6218
+ meaningfulChildren.length === 1 &&
6219
+ meaningfulChildren[0].type === 'JSXElement' &&
6220
+ wasOriginallySingleLine(node) &&
6221
+ !willBreak(childrenDocs[0])
6222
+ ) {
6223
+ // Keep the fragment inline when it fits; otherwise expand `<>` onto its own
6224
+ // lines so a breaking single child reads as `<>\n <Child …/>\n</>` rather than
6225
+ // `<><Child` with only the child's attributes broken.
6226
+ return conditionalGroup([
6227
+ ['<>', childrenDocs[0], '</>'],
6228
+ group(['<>', indent([hardline, childrenDocs[0]]), hardline, '</>']),
6229
+ ]);
6230
+ }
5941
6231
 
5942
6232
  // Multiple children or complex children - format with line breaks
5943
6233
  const formattedChildren = [];
5944
6234
  for (let i = 0; i < childrenDocs.length; i++) {
5945
6235
  formattedChildren.push(childrenDocs[i]);
5946
6236
  if (i < childrenDocs.length - 1) {
5947
- formattedChildren.push(hardline);
6237
+ // Preserve a single authored blank line between children (2+ collapse to 1).
6238
+ const blank = getBlankLinesBetweenNodes(childNodes[i], leadingAnchor(childNodes[i + 1])) > 0;
6239
+ formattedChildren.push(blank ? [hardline, hardline] : hardline);
5948
6240
  }
5949
6241
  }
5950
6242
 
@@ -5952,6 +6244,169 @@ function printJSXFragment(node, path, options, print) {
5952
6244
  return group(['<>', indent([hardline, ...formattedChildren]), hardline, '</>']);
5953
6245
  }
5954
6246
 
6247
+ /**
6248
+ * Comments written inside an opening tag, before an attribute, are attached by
6249
+ * the parser to the next visited body child (positionally they sort before the
6250
+ * opening tag's end, but the child is visited first). Pull those out of the
6251
+ * children and return a map from attribute index to the comments that precede it,
6252
+ * so the element printer can render them in the opening tag instead of the body.
6253
+ * @param {AST.TSRXJSXElement} node
6254
+ * @returns {Map<number, AST.Comment[]>}
6255
+ */
6256
+ function collectOpeningTagComments(node) {
6257
+ /** @type {Map<number, AST.Comment[]>} */
6258
+ const byAttr = new Map();
6259
+ const openingElement = /** @type {AST.NodeWithLocation} */ (node.openingElement);
6260
+ const attributes = /** @type {any[]} */ (node.openingElement?.attributes) ?? [];
6261
+ if (!openingElement || attributes.length === 0 || !Array.isArray(node.children)) {
6262
+ return byAttr;
6263
+ }
6264
+ const openingEnd = openingElement.end;
6265
+ /** @type {AST.Comment[]} */
6266
+ const collected = [];
6267
+ for (const child of node.children) {
6268
+ const lead = /** @type {AST.NodeWithMaybeComments} */ (child).leadingComments;
6269
+ if (!Array.isArray(lead) || lead.length === 0) continue;
6270
+ const keep = [];
6271
+ for (const comment of lead) {
6272
+ if (typeof comment.start === 'number' && comment.start < openingEnd) {
6273
+ collected.push(comment);
6274
+ } else {
6275
+ keep.push(comment);
6276
+ }
6277
+ }
6278
+ if (keep.length !== lead.length) {
6279
+ /** @type {any} */ (child).leadingComments = keep;
6280
+ }
6281
+ }
6282
+ if (collected.length === 0) return byAttr;
6283
+ collected.sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
6284
+ let ci = 0;
6285
+ for (let ai = 0; ai < attributes.length; ai++) {
6286
+ const attrStart = /** @type {AST.NodeWithLocation} */ (attributes[ai]).start;
6287
+ /** @type {AST.Comment[]} */
6288
+ const forAttr = [];
6289
+ while (ci < collected.length && /** @type {number} */ (collected[ci].start) < attrStart) {
6290
+ forAttr.push(collected[ci]);
6291
+ ci++;
6292
+ }
6293
+ if (forAttr.length > 0) byAttr.set(ai, forAttr);
6294
+ }
6295
+ return byAttr;
6296
+ }
6297
+
6298
+ /**
6299
+ * Build doc parts for a template child's leading comments (each on its own line).
6300
+ * Used for `{expr}` children, whose `{ … }` form is printed inline by the JSX
6301
+ * printers and so would otherwise skip the node's attached leading comments.
6302
+ * @param {AST.Node & AST.NodeWithMaybeComments} child
6303
+ * @returns {Doc[]}
6304
+ */
6305
+ function printTemplateChildLeadingComments(child) {
6306
+ const comments = child.leadingComments;
6307
+ if (!comments || comments.length === 0) {
6308
+ return [];
6309
+ }
6310
+ /** @type {Doc[]} */
6311
+ const parts = [];
6312
+ for (let i = 0; i < comments.length; i++) {
6313
+ const comment = comments[i];
6314
+ if (comment.type === 'Line') {
6315
+ parts.push('//' + comment.value);
6316
+ } else if (comment.type === 'Block') {
6317
+ parts.push('/*' + comment.value + '*/');
6318
+ }
6319
+ parts.push(hardline);
6320
+ const next = comments[i + 1];
6321
+ if (next && getBlankLinesBetweenNodes(comment, next) > 0) {
6322
+ parts.push(hardline);
6323
+ }
6324
+ }
6325
+ return parts;
6326
+ }
6327
+
6328
+ /**
6329
+ * Build doc parts for `//` line comments attached to an element body — trailing
6330
+ * comments before `</tag>` (`closingElement.leadingComments`) or the comments of a
6331
+ * comment-only element (`innerComments`). Block comments are intentionally skipped:
6332
+ * they survive in the adjacent JSXText value and are already rendered as text, so
6333
+ * emitting them here would duplicate them. Each comment is emitted on its own line
6334
+ * at the children indent.
6335
+ * @param {AST.Comment[] | null | undefined} commentList
6336
+ * @param {any} [previousNode]
6337
+ * @returns {Doc[]}
6338
+ */
6339
+ function printElementBodyLineComments(commentList, previousNode = null) {
6340
+ const comments = (commentList ?? []).filter((comment) => comment.type === 'Line');
6341
+ if (comments.length === 0) {
6342
+ return [];
6343
+ }
6344
+ /** @type {Doc[]} */
6345
+ const parts = [];
6346
+ /** @type {AST.Node | AST.Comment | null | undefined} */
6347
+ let prev = previousNode;
6348
+ for (let i = 0; i < comments.length; i++) {
6349
+ parts.push(hardline);
6350
+ // Preserve a blank line before this comment if one existed in source.
6351
+ if (prev && getBlankLinesBetweenNodes(prev, comments[i]) > 0) {
6352
+ parts.push(hardline);
6353
+ }
6354
+ parts.push('//' + comments[i].value);
6355
+ prev = comments[i];
6356
+ }
6357
+ return parts;
6358
+ }
6359
+
6360
+ /**
6361
+ * Print a TSRX code block: setup statements then the single render output.
6362
+ * Callers in element/fragment body position hug it to the surrounding tags;
6363
+ * on its own as an arrow body it stands alone.
6364
+ * @param {AST.JSXCodeBlock} node
6365
+ * @param {AstPath<AST.JSXCodeBlock>} path
6366
+ * @param {RippleFormatOptions} options
6367
+ * @param {PrintFn} print
6368
+ * @returns {Doc}
6369
+ */
6370
+ function printJSXCodeBlock(node, path, options, print) {
6371
+ /** @type {Doc[]} */
6372
+ const parts = [];
6373
+ for (let i = 0; i < node.body.length; i++) {
6374
+ parts.push(path.call(print, 'body', i));
6375
+ if (i < node.body.length - 1) {
6376
+ parts.push(
6377
+ shouldAddBlankLine(node.body[i], node.body[i + 1]) ? [hardline, hardline] : hardline,
6378
+ );
6379
+ }
6380
+ }
6381
+ if (node.render) {
6382
+ if (node.body.length > 0) {
6383
+ // Preserve a blank line between the last setup statement and the render
6384
+ // output (measured to the render's leading comment, if any).
6385
+ const last = node.body[node.body.length - 1];
6386
+ const renderStart =
6387
+ /** @type {AST.NodeWithMaybeComments} */ (node.render).leadingComments?.[0] ?? node.render;
6388
+ parts.push(
6389
+ getBlankLinesBetweenNodes(last, renderStart) > 0 ? [hardline, hardline] : hardline,
6390
+ );
6391
+ }
6392
+ parts.push(path.call(print, 'render'));
6393
+ }
6394
+ // Trailing comments after the last statement/render inside the block.
6395
+ const innerCommentDocs = printElementBodyLineComments(node.innerComments);
6396
+ if (innerCommentDocs.length > 0) {
6397
+ const lastNode = node.render ?? node.body[node.body.length - 1];
6398
+ const firstComment = (node.innerComments ?? []).find((c) => c.type === 'Line');
6399
+ if (lastNode && firstComment && getBlankLinesBetweenNodes(lastNode, firstComment) > 0) {
6400
+ parts.push(hardline);
6401
+ }
6402
+ parts.push(...innerCommentDocs);
6403
+ }
6404
+ if (parts.length === 0) {
6405
+ return '@{}';
6406
+ }
6407
+ return group(['@{', indent([hardline, ...parts]), hardline, '}']);
6408
+ }
6409
+
5955
6410
  /**
5956
6411
  * Print a JSX attribute
5957
6412
  * @param {ESTreeJSX.JSXAttribute} attr - The JSX attribute node
@@ -5963,6 +6418,10 @@ function printJSXFragment(node, path, options, print) {
5963
6418
  function printJSXAttribute(attr, path, options, print) {
5964
6419
  const name = /** @type {ESTreeJSX.JSXIdentifier} */ (attr.name).name;
5965
6420
 
6421
+ if (attr.shorthand) {
6422
+ return ['{', name, '}'];
6423
+ }
6424
+
5966
6425
  if (!attr.value) {
5967
6426
  return name;
5968
6427
  }
@@ -5980,12 +6439,16 @@ function printJSXAttribute(attr, path, options, print) {
5980
6439
 
5981
6440
  if (attr.value.type === 'JSXExpressionContainer') {
5982
6441
  const expression = attr.value.expression;
6442
+ if (expression.type === 'Literal' && typeof expression.value === 'string') {
6443
+ const quote = options.jsxSingleQuote ? "'" : '"';
6444
+ return [name, '=', quote, /** @type {string} */ (expression.value), quote];
6445
+ }
5983
6446
  const exprDoc = path.call(
5984
6447
  (valuePath) => print(valuePath, { isInAttribute: true }),
5985
6448
  'value',
5986
6449
  'expression',
5987
6450
  );
5988
- if (shouldBreakAttributeExpressionClosingBrace(expression)) {
6451
+ if (shouldBreakAttributeExpressionClosingBrace(expression, options, attr)) {
5989
6452
  return [name, '={', exprDoc, hardline, '}'];
5990
6453
  }
5991
6454
  return [name, '={', exprDoc, '}'];
@@ -5995,20 +6458,33 @@ function printJSXAttribute(attr, path, options, print) {
5995
6458
  }
5996
6459
 
5997
6460
  /**
5998
- * Print a JSX member expression (e.g., React.Fragment)
5999
- * @param {AST.Node} node - The JSX member expression or identifier
6461
+ * Print a JSX element name.
6462
+ * @param {AST.Node} node - The JSX element name node
6000
6463
  * @returns {string}
6001
6464
  */
6002
- function printJSXMemberExpression(node) {
6465
+ function printJSXElementName(node) {
6003
6466
  if (node.type === 'JSXIdentifier') {
6004
- return node.name;
6467
+ return (isDynamicJSXIdentifier(node) ? '@' : '') + node.name;
6005
6468
  }
6006
6469
  if (node.type === 'JSXMemberExpression') {
6007
- return printJSXMemberExpression(node.object) + '.' + printJSXMemberExpression(node.property);
6470
+ return printJSXElementName(node.object) + '.' + printJSXElementName(node.property);
6471
+ }
6472
+ if (node.type === 'JSXNamespacedName') {
6473
+ const namespace_name = node.namespace.name;
6474
+ const local_name = node.name.name;
6475
+ return namespace_name + ':' + local_name;
6008
6476
  }
6009
6477
  return 'Unknown';
6010
6478
  }
6011
6479
 
6480
+ /**
6481
+ * @param {ESTreeJSX.JSXIdentifier} node
6482
+ * @returns {boolean}
6483
+ */
6484
+ function isDynamicJSXIdentifier(node) {
6485
+ return /** @type {{ dynamic?: boolean }} */ (node).dynamic === true;
6486
+ }
6487
+
6012
6488
  /**
6013
6489
  * Print a member expression as simple string (for element tag names)
6014
6490
  * @param {AST.Node} node - The node to print
@@ -6017,6 +6493,22 @@ function printJSXMemberExpression(node) {
6017
6493
  * @returns {string}
6018
6494
  */
6019
6495
  function printMemberExpressionSimple(node, options, computed = false) {
6496
+ if (node.type === 'JSXIdentifier') {
6497
+ return (isDynamicJSXIdentifier(node) ? '@' : '') + node.name;
6498
+ }
6499
+
6500
+ if (node.type === 'JSXMemberExpression') {
6501
+ return (
6502
+ printMemberExpressionSimple(node.object, options) +
6503
+ '.' +
6504
+ printMemberExpressionSimple(node.property, options, true)
6505
+ );
6506
+ }
6507
+
6508
+ if (node.type === 'JSXNamespacedName') {
6509
+ return node.namespace.name + ':' + node.name.name;
6510
+ }
6511
+
6020
6512
  if (node.type === 'Identifier') {
6021
6513
  return (computed ? '' : node.tracked ? '@' : '') + node.name;
6022
6514
  }
@@ -6064,7 +6556,7 @@ function is_attribute_value_breakable(value, is_nested_in_object = false) {
6064
6556
  }
6065
6557
 
6066
6558
  /**
6067
- * Print a Ripple Element node
6559
+ * Print a JSX element node
6068
6560
  * @param {AST.Element} element - The element node
6069
6561
  * @param {AstPath<AST.Element>} path - The AST path
6070
6562
  * @param {RippleFormatOptions} options - Prettier options
@@ -6072,7 +6564,7 @@ function is_attribute_value_breakable(value, is_nested_in_object = false) {
6072
6564
  * @returns {Doc}
6073
6565
  */
6074
6566
  function printElement(element, path, options, print) {
6075
- const node = /** @type {AST.Element & AST.NodeWithLocation} */ (element);
6567
+ const node = /** @type {any} */ (element);
6076
6568
  const tagName = printMemberExpressionSimple(node.id, options);
6077
6569
  const openingElement = /** @type {any} */ (node.openingElement);
6078
6570
  /** @type {Doc} */
@@ -6080,7 +6572,7 @@ function printElement(element, path, options, print) {
6080
6572
  if (openingElement?.typeArguments) {
6081
6573
  typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
6082
6574
  }
6083
- const elementLeadingComments = getElementLeadingComments(node);
6575
+ const elementLeadingComments = getElementLeadingComments(/** @type {any} */ (node));
6084
6576
 
6085
6577
  // `metadata.elementLeadingComments` may include comments that actually appear *inside* the element
6086
6578
  // body (after the opening tag). Those must not be hoisted before the element.
@@ -6115,7 +6607,7 @@ function printElement(element, path, options, print) {
6115
6607
  const openingEnd = /** @type {AST.NodeWithLocation} */ (node.openingElement).end;
6116
6608
  for (const child of node.children) {
6117
6609
  if (
6118
- (child.type === 'TSRXExpression' || child.type === 'Text') &&
6610
+ (child.type === 'JSXExpressionContainer' || child.type === 'JSXText') &&
6119
6611
  Array.isArray(child.leadingComments)
6120
6612
  ) {
6121
6613
  for (const comment of child.leadingComments) {
@@ -6192,11 +6684,14 @@ function printElement(element, path, options, print) {
6192
6684
  parts.push(attrLineBreak);
6193
6685
  const attrDoc = print(attrPath);
6194
6686
  parts.push(attrDoc);
6195
- const attr_node = /** @type {AST.Attribute | AST.SpreadAttribute} */ (attrPath.node);
6687
+ const attr_node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
6688
+ /** @type {unknown} */ (attrPath.node)
6689
+ );
6196
6690
  if (
6197
6691
  !hasBreakingAttribute &&
6198
6692
  (willBreak(attrDoc) ||
6199
- (attr_node.type === 'Attribute' && is_attribute_value_breakable(attr_node.value)))
6693
+ (attr_node.type === 'JSXAttribute' &&
6694
+ is_attribute_value_breakable(/** @type {any} */ (attr_node.value))))
6200
6695
  ) {
6201
6696
  hasBreakingAttribute = true;
6202
6697
  }
@@ -6304,15 +6799,17 @@ function printElement(element, path, options, print) {
6304
6799
  }
6305
6800
  }
6306
6801
 
6307
- const isTextLikeChild = currentChild.type === 'TSRXExpression' || currentChild.type === 'Text';
6802
+ const isTextLikeChild =
6803
+ currentChild.type === 'JSXExpressionContainer' || currentChild.type === 'JSXText';
6308
6804
  const hasTextLeadingComments =
6309
6805
  shouldLiftTextLevelComments &&
6310
6806
  isTextLikeChild &&
6311
6807
  Array.isArray(currentChild.leadingComments) &&
6312
6808
  currentChild.leadingComments.length > 0;
6809
+ const currentChildAny = /** @type {any} */ (currentChild);
6313
6810
  const rawExpressionLeadingComments =
6314
- isTextLikeChild && Array.isArray(currentChild.expression?.leadingComments)
6315
- ? currentChild.expression.leadingComments
6811
+ isTextLikeChild && Array.isArray(currentChildAny.expression?.leadingComments)
6812
+ ? currentChildAny.expression.leadingComments
6316
6813
  : null;
6317
6814
  const elementBodyLeadingComments =
6318
6815
  hasTextLeadingComments && node.openingElement
@@ -6418,10 +6915,10 @@ function printElement(element, path, options, print) {
6418
6915
  : nextChild;
6419
6916
  const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, whitespaceTarget);
6420
6917
  const isTextOrExpressionChild =
6421
- currentChild.type === 'TSRXExpression' ||
6422
- currentChild.type === 'Text' ||
6423
- nextChild.type === 'TSRXExpression' ||
6424
- nextChild.type === 'Text';
6918
+ currentChild.type === 'JSXExpressionContainer' ||
6919
+ currentChild.type === 'JSXText' ||
6920
+ nextChild.type === 'JSXExpressionContainer' ||
6921
+ nextChild.type === 'JSXText';
6425
6922
 
6426
6923
  if (whitespaceLinesCount > 0) {
6427
6924
  finalChildren.push(hardline);
@@ -6472,10 +6969,15 @@ function printElement(element, path, options, print) {
6472
6969
 
6473
6970
  const closingTag = ['</', tagName, '>'];
6474
6971
  let elementOutput;
6972
+ const shouldTryInlineMultipleChildren =
6973
+ !openingTagAlwaysBreaks &&
6974
+ fallbackCommentParts.length === 0 &&
6975
+ closingElementComments.length === 0 &&
6976
+ shouldTryInlineMultipleTextChildren(node);
6475
6977
 
6476
6978
  if (finalChildren.length === 1) {
6477
6979
  const child = finalChildren[0];
6478
- const firstChild = node.children[0];
6980
+ const firstChild = /** @type {any} */ (node.children[0]);
6479
6981
  const isNonSelfClosingElement =
6480
6982
  firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
6481
6983
  const isElementChild = firstChild && firstChild.type === 'Element';
@@ -6506,63 +7008,15 @@ function printElement(element, path, options, print) {
6506
7008
  } else {
6507
7009
  elementOutput = [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag];
6508
7010
  }
7011
+ } else if (shouldTryInlineMultipleChildren) {
7012
+ const inlineChildren = path.map(print, 'children');
7013
+ elementOutput = conditionalGroup([
7014
+ group([openingTag, ...inlineChildren, closingTag]),
7015
+ [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag],
7016
+ ]);
6509
7017
  } else {
6510
7018
  elementOutput = group([openingTag, indent([hardline, ...finalChildren]), hardline, closingTag]);
6511
7019
  }
6512
7020
 
6513
7021
  return leadingCommentParts.length > 0 ? [...leadingCommentParts, elementOutput] : elementOutput;
6514
7022
  }
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
- }