@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.
- package/package.json +2 -2
- package/src/index.js +800 -346
- 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 && !
|
|
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 =
|
|
1501
|
-
|
|
1502
|
-
|
|
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 =
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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 '
|
|
2249
|
-
nodeContent =
|
|
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 '
|
|
2257
|
-
nodeContent =
|
|
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 '
|
|
2283
|
-
nodeContent =
|
|
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
|
-
|
|
2301
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 === '
|
|
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 === '
|
|
5509
|
-
|
|
5510
|
-
|
|
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 {
|
|
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 {
|
|
5715
|
-
* @param {AstPath<
|
|
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
|
-
|
|
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'
|
|
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
|
-
//
|
|
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
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
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
|
-
|
|
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([
|
|
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 += ' ' +
|
|
6044
|
+
currentText += currentText.endsWith(' ') || text.startsWith(' ') ? text : ' ' + text;
|
|
5840
6045
|
} else {
|
|
5841
|
-
currentText =
|
|
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([
|
|
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
|
-
//
|
|
5867
|
-
|
|
5868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6131
|
+
const childDoc = childrenDocs[i];
|
|
6132
|
+
formattedChildren.push(typeof childDoc === 'string' ? printRawText(childDoc) : childDoc);
|
|
5886
6133
|
if (i < childrenDocs.length - 1) {
|
|
5887
|
-
|
|
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
|
-
//
|
|
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
|
|
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([
|
|
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
|
-
|
|
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
|
|
5999
|
-
* @param {AST.Node} node - The JSX
|
|
6461
|
+
* Print a JSX element name.
|
|
6462
|
+
* @param {AST.Node} node - The JSX element name node
|
|
6000
6463
|
* @returns {string}
|
|
6001
6464
|
*/
|
|
6002
|
-
function
|
|
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
|
|
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
|
|
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 {
|
|
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 === '
|
|
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 {
|
|
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 === '
|
|
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 =
|
|
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(
|
|
6315
|
-
?
|
|
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 === '
|
|
6422
|
-
currentChild.type === '
|
|
6423
|
-
nextChild.type === '
|
|
6424
|
-
nextChild.type === '
|
|
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
|
-
}
|