@tsrx/prettier-plugin 0.3.78 → 0.3.80
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 +117 -727
- package/src/index.test.js +122 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsrx/prettier-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.80",
|
|
4
4
|
"description": "Ripple plugin for Prettier",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prettier": "^3.8.3"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@tsrx/core": "0.1.
|
|
30
|
+
"@tsrx/core": "0.1.28"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src/"
|
package/src/index.js
CHANGED
|
@@ -2292,20 +2292,20 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
2292
2292
|
break;
|
|
2293
2293
|
|
|
2294
2294
|
case 'JSXStyleElement':
|
|
2295
|
-
nodeContent = printJSXElement(
|
|
2296
|
-
/** @type {ESTreeJSX.JSXElement} */ (/** @type {unknown} */ (node)),
|
|
2297
|
-
path,
|
|
2298
|
-
options,
|
|
2299
|
-
print,
|
|
2300
|
-
);
|
|
2295
|
+
nodeContent = printJSXElement(node, path, options, print);
|
|
2301
2296
|
break;
|
|
2302
2297
|
|
|
2303
2298
|
case 'JSXElement':
|
|
2304
|
-
nodeContent = printJSXElement(node, path, options, print);
|
|
2299
|
+
nodeContent = printJSXElement(/** @type {AST.TSRXJSXElement} */ (node), path, options, print);
|
|
2305
2300
|
break;
|
|
2306
2301
|
|
|
2307
2302
|
case 'JSXFragment':
|
|
2308
|
-
nodeContent = printJSXFragment(
|
|
2303
|
+
nodeContent = printJSXFragment(
|
|
2304
|
+
/** @type {AST.TSRXJSXFragment} */ (node),
|
|
2305
|
+
path,
|
|
2306
|
+
options,
|
|
2307
|
+
print,
|
|
2308
|
+
);
|
|
2309
2309
|
break;
|
|
2310
2310
|
|
|
2311
2311
|
case 'JSXText':
|
|
@@ -5694,86 +5694,6 @@ function normalizeInlineJSXText(raw) {
|
|
|
5694
5694
|
return text.trim() || !/[\r\n]/u.test(text) ? text : '';
|
|
5695
5695
|
}
|
|
5696
5696
|
|
|
5697
|
-
/**
|
|
5698
|
-
* @param {AST.Node} parentNode
|
|
5699
|
-
* @param {AST.Node} firstChild
|
|
5700
|
-
* @param {Doc} childDoc
|
|
5701
|
-
* @returns {boolean}
|
|
5702
|
-
*/
|
|
5703
|
-
function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
|
|
5704
|
-
if (!firstChild || childDoc == null) {
|
|
5705
|
-
return false;
|
|
5706
|
-
}
|
|
5707
|
-
|
|
5708
|
-
if (firstChild.type === 'JSXText') {
|
|
5709
|
-
return true;
|
|
5710
|
-
}
|
|
5711
|
-
|
|
5712
|
-
if (typeof childDoc === 'string') {
|
|
5713
|
-
return childDoc.length <= 20 && !childDoc.includes('\n');
|
|
5714
|
-
}
|
|
5715
|
-
|
|
5716
|
-
// Inline JSX expressions if they fit, but respect original multi-line formatting
|
|
5717
|
-
// for non-literal expressions (e.g. {children} should stay multi-line if written that way)
|
|
5718
|
-
if (firstChild.type === 'JSXExpressionContainer') {
|
|
5719
|
-
if (wasOriginallySingleLine(parentNode)) {
|
|
5720
|
-
return true;
|
|
5721
|
-
}
|
|
5722
|
-
// For multi-line parents, only inline if the expression is a simple literal
|
|
5723
|
-
const expr = firstChild.expression;
|
|
5724
|
-
if (expr && (expr.type === 'Literal' || expr.type === 'TemplateLiteral')) {
|
|
5725
|
-
return true;
|
|
5726
|
-
}
|
|
5727
|
-
return false;
|
|
5728
|
-
}
|
|
5729
|
-
|
|
5730
|
-
// Respect original formatting for elements: if parent was originally multi-line, keep it multi-line
|
|
5731
|
-
// This follows Prettier's philosophy for decorators and objects
|
|
5732
|
-
if (!wasOriginallySingleLine(parentNode)) {
|
|
5733
|
-
return false;
|
|
5734
|
-
}
|
|
5735
|
-
|
|
5736
|
-
if (firstChild.type === 'JSXElement' && firstChild.openingElement?.selfClosing) {
|
|
5737
|
-
const parent = /** @type {any} */ (parentNode);
|
|
5738
|
-
return !parent.openingElement?.attributes?.length;
|
|
5739
|
-
}
|
|
5740
|
-
|
|
5741
|
-
return false;
|
|
5742
|
-
}
|
|
5743
|
-
|
|
5744
|
-
/**
|
|
5745
|
-
* Check whether a child can participate in compact inline TSRX content.
|
|
5746
|
-
* @param {any} child
|
|
5747
|
-
* @returns {boolean}
|
|
5748
|
-
*/
|
|
5749
|
-
function isInlineableTextOrExpressionChild(child) {
|
|
5750
|
-
if (!child || (child.type !== 'JSXText' && child.type !== 'JSXExpressionContainer')) {
|
|
5751
|
-
return false;
|
|
5752
|
-
}
|
|
5753
|
-
|
|
5754
|
-
if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
|
|
5755
|
-
return false;
|
|
5756
|
-
}
|
|
5757
|
-
|
|
5758
|
-
const expression = /** @type {{ expression?: AST.Node & AST.NodeWithMaybeComments }} */ (child)
|
|
5759
|
-
.expression;
|
|
5760
|
-
return !expression || !hasComment(expression);
|
|
5761
|
-
}
|
|
5762
|
-
|
|
5763
|
-
/**
|
|
5764
|
-
* @param {any} node
|
|
5765
|
-
* @returns {boolean}
|
|
5766
|
-
*/
|
|
5767
|
-
function shouldTryInlineMultipleTextChildren(node) {
|
|
5768
|
-
return (
|
|
5769
|
-
wasOriginallySingleLine(node) &&
|
|
5770
|
-
Array.isArray(node.children) &&
|
|
5771
|
-
node.children.length > 1 &&
|
|
5772
|
-
node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
|
|
5773
|
-
node.children.every(isInlineableTextOrExpressionChild)
|
|
5774
|
-
);
|
|
5775
|
-
}
|
|
5776
|
-
|
|
5777
5697
|
/**
|
|
5778
5698
|
* @param {AST.Node} child
|
|
5779
5699
|
* @returns {boolean}
|
|
@@ -5795,72 +5715,10 @@ function isSimpleJSXExpressionChild(child) {
|
|
|
5795
5715
|
);
|
|
5796
5716
|
}
|
|
5797
5717
|
|
|
5798
|
-
/**
|
|
5799
|
-
* Get leading comments from element metadata
|
|
5800
|
-
* @param {ESTreeJSX.JSXElement} node - The element node
|
|
5801
|
-
* @returns {AST.Comment[]}
|
|
5802
|
-
*/
|
|
5803
|
-
function getElementLeadingComments(node) {
|
|
5804
|
-
const fromMetadata = node?.metadata?.elementLeadingComments;
|
|
5805
|
-
if (Array.isArray(fromMetadata)) {
|
|
5806
|
-
return fromMetadata;
|
|
5807
|
-
}
|
|
5808
|
-
return [];
|
|
5809
|
-
}
|
|
5810
|
-
|
|
5811
|
-
/**
|
|
5812
|
-
* Create doc parts for element-level comments
|
|
5813
|
-
* @param {AST.Comment[]} comments - Array of comments
|
|
5814
|
-
* @returns {Doc[]}
|
|
5815
|
-
*/
|
|
5816
|
-
function createElementLevelCommentParts(comments) {
|
|
5817
|
-
if (!comments || comments.length === 0) {
|
|
5818
|
-
return [];
|
|
5819
|
-
}
|
|
5820
|
-
|
|
5821
|
-
/** @type {Doc[]} */
|
|
5822
|
-
const parts = [];
|
|
5823
|
-
|
|
5824
|
-
for (let i = 0; i < comments.length; i++) {
|
|
5825
|
-
const comment = comments[i];
|
|
5826
|
-
const nextComment = comments[i + 1];
|
|
5827
|
-
|
|
5828
|
-
if (comment.type === 'Line') {
|
|
5829
|
-
parts.push('//' + comment.value);
|
|
5830
|
-
parts.push(hardline);
|
|
5831
|
-
} else if (comment.type === 'Block') {
|
|
5832
|
-
parts.push('/*' + comment.value + '*/');
|
|
5833
|
-
parts.push(hardline);
|
|
5834
|
-
}
|
|
5835
|
-
|
|
5836
|
-
if (nextComment) {
|
|
5837
|
-
const blankLinesBetween = getBlankLinesBetweenNodes(comment, nextComment);
|
|
5838
|
-
if (blankLinesBetween > 0) {
|
|
5839
|
-
parts.push(hardline);
|
|
5840
|
-
}
|
|
5841
|
-
}
|
|
5842
|
-
}
|
|
5843
|
-
|
|
5844
|
-
return parts;
|
|
5845
|
-
}
|
|
5846
|
-
|
|
5847
|
-
/**
|
|
5848
|
-
* Create element-level comment parts with trailing hardline trimmed
|
|
5849
|
-
* @param {AST.Comment[]} comments - Array of comments
|
|
5850
|
-
* @returns {Doc[]}
|
|
5851
|
-
*/
|
|
5852
|
-
function createElementLevelCommentPartsTrimmed(comments) {
|
|
5853
|
-
const parts = createElementLevelCommentParts(comments);
|
|
5854
|
-
if (parts.length > 0 && parts[parts.length - 1] === hardline) {
|
|
5855
|
-
parts.pop();
|
|
5856
|
-
}
|
|
5857
|
-
return parts;
|
|
5858
|
-
}
|
|
5859
|
-
|
|
5860
5718
|
/**
|
|
5861
5719
|
* Print a JSX element
|
|
5862
|
-
* @param {AST.TSRXJSXElement} node - The JSX element node
|
|
5863
|
-
* @param {AstPath<
|
|
5720
|
+
* @param {AST.TSRXJSXElement | AST.JSXStyleElement} node - The JSX element node
|
|
5721
|
+
* @param {AstPath<AST.TSRXJSXElement>} path - The AST path
|
|
5864
5722
|
* @param {RippleFormatOptions} options - Prettier options
|
|
5865
5723
|
* @param {PrintFn} print - Print callback
|
|
5866
5724
|
* @returns {Doc | Doc[]}
|
|
@@ -5873,7 +5731,7 @@ function printJSXElement(node, path, options, print) {
|
|
|
5873
5731
|
// Dynamic tags (`<{expr}>`) print the opening expression for both tags so
|
|
5874
5732
|
// they stay textually identical; static names print as plain strings.
|
|
5875
5733
|
const tagName =
|
|
5876
|
-
|
|
5734
|
+
openingElement.name.type === 'JSXExpressionContainer'
|
|
5877
5735
|
? ['{', path.call(print, 'openingElement', 'name', 'expression'), '}']
|
|
5878
5736
|
: printJSXElementName(openingElement.name);
|
|
5879
5737
|
|
|
@@ -5960,26 +5818,13 @@ function printJSXElement(node, path, options, print) {
|
|
|
5960
5818
|
{ shouldBreak: shouldForceBreak },
|
|
5961
5819
|
);
|
|
5962
5820
|
|
|
5963
|
-
//
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
|
|
5969
|
-
(/** @type {AST.Comment} */ comment) =>
|
|
5970
|
-
typeof comment.start === 'number' && comment.start >= openingTagEnd,
|
|
5821
|
+
// Comments before `</tag>` and the comments of a comment-only element.
|
|
5822
|
+
const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
|
|
5823
|
+
/** @type {AST.TSRXJSXElement} */ (node),
|
|
5824
|
+
openingElement,
|
|
5825
|
+
node.closingElement,
|
|
5971
5826
|
);
|
|
5972
|
-
const trailingComments = [
|
|
5973
|
-
...(node.closingElement?.leadingComments ?? []),
|
|
5974
|
-
...bodyMetaComments,
|
|
5975
|
-
].sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
|
|
5976
|
-
const lastMeaningfulChild = [...(node.children ?? [])]
|
|
5977
|
-
.reverse()
|
|
5978
|
-
.find((child) => child.type !== 'JSXText' || child.value.trim());
|
|
5979
|
-
const closingCommentDocs = printElementBodyLineComments(trailingComments, lastMeaningfulChild);
|
|
5980
5827
|
const hasClosingComments = closingCommentDocs.length > 0;
|
|
5981
|
-
// A comment-only element has no children; its comments live in `innerComments`.
|
|
5982
|
-
const innerCommentDocs = printElementBodyLineComments(node.innerComments);
|
|
5983
5828
|
|
|
5984
5829
|
if (!hasChildren) {
|
|
5985
5830
|
const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
|
|
@@ -6067,6 +5912,7 @@ function printJSXElement(node, path, options, print) {
|
|
|
6067
5912
|
'{',
|
|
6068
5913
|
path.call(print, 'children', i, 'expression'),
|
|
6069
5914
|
'}',
|
|
5915
|
+
...printTemplateChildTrailingComments(child),
|
|
6070
5916
|
]);
|
|
6071
5917
|
childNodes.push(child);
|
|
6072
5918
|
} else {
|
|
@@ -6112,7 +5958,7 @@ function printJSXElement(node, path, options, print) {
|
|
|
6112
5958
|
];
|
|
6113
5959
|
}
|
|
6114
5960
|
const meaningfulChildren = node.children.filter(
|
|
6115
|
-
(
|
|
5961
|
+
(child) => child.type !== 'JSXText' || child.value.trim(),
|
|
6116
5962
|
);
|
|
6117
5963
|
const singleMeaningfulChild = meaningfulChildren.length === 1 ? meaningfulChildren[0] : null;
|
|
6118
5964
|
if (
|
|
@@ -6127,9 +5973,9 @@ function printJSXElement(node, path, options, print) {
|
|
|
6127
5973
|
!forceMultiline &&
|
|
6128
5974
|
childrenDocs.length > 1 &&
|
|
6129
5975
|
wasOriginallySingleLine(node) &&
|
|
6130
|
-
node.children.some((
|
|
5976
|
+
node.children.some((child) => child.type === 'JSXText') &&
|
|
6131
5977
|
node.children.every(
|
|
6132
|
-
(
|
|
5978
|
+
(child) =>
|
|
6133
5979
|
child.type === 'JSXText' || isSimpleJSXExpressionChild(/** @type {AST.Node} */ (child)),
|
|
6134
5980
|
)
|
|
6135
5981
|
) {
|
|
@@ -6162,8 +6008,8 @@ function printJSXElement(node, path, options, print) {
|
|
|
6162
6008
|
|
|
6163
6009
|
/**
|
|
6164
6010
|
* Print a JSX fragment (<>...</>)
|
|
6165
|
-
* @param {
|
|
6166
|
-
* @param {AstPath<
|
|
6011
|
+
* @param {AST.TSRXJSXFragment} node - The JSX fragment node
|
|
6012
|
+
* @param {AstPath<AST.TSRXJSXFragment>} path - The AST path
|
|
6167
6013
|
* @param {RippleFormatOptions} options - Prettier options
|
|
6168
6014
|
* @param {PrintFn} print - Print callback
|
|
6169
6015
|
* @returns {Doc}
|
|
@@ -6171,12 +6017,23 @@ function printJSXElement(node, path, options, print) {
|
|
|
6171
6017
|
function printJSXFragment(node, path, options, print) {
|
|
6172
6018
|
const hasChildren = node.children && node.children.length > 0;
|
|
6173
6019
|
|
|
6020
|
+
// Comments before `</>` and the comments of a comment-only fragment.
|
|
6021
|
+
const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
|
|
6022
|
+
node,
|
|
6023
|
+
node.openingFragment,
|
|
6024
|
+
node.closingFragment,
|
|
6025
|
+
);
|
|
6026
|
+
|
|
6174
6027
|
if (!hasChildren) {
|
|
6028
|
+
const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
|
|
6029
|
+
if (bodyComments.length > 0) {
|
|
6030
|
+
return group(['<>', indent(bodyComments), hardline, '</>']);
|
|
6031
|
+
}
|
|
6175
6032
|
return '<></>';
|
|
6176
6033
|
}
|
|
6177
6034
|
|
|
6178
6035
|
// A `@{ … }` code block is the whole body and hugs the tags: `<>@{ … }</>`.
|
|
6179
|
-
if (node.children.length === 1 &&
|
|
6036
|
+
if (node.children.length === 1 && node.children[0].type === 'JSXCodeBlock') {
|
|
6180
6037
|
return group(['<>', path.call(print, 'children', 0), '</>']);
|
|
6181
6038
|
}
|
|
6182
6039
|
|
|
@@ -6209,6 +6066,7 @@ function printJSXFragment(node, path, options, print) {
|
|
|
6209
6066
|
'{',
|
|
6210
6067
|
path.call(print, 'children', i, 'expression'),
|
|
6211
6068
|
'}',
|
|
6069
|
+
...printTemplateChildTrailingComments(child),
|
|
6212
6070
|
]);
|
|
6213
6071
|
childNodes.push(child);
|
|
6214
6072
|
} else {
|
|
@@ -6219,7 +6077,11 @@ function printJSXFragment(node, path, options, print) {
|
|
|
6219
6077
|
}
|
|
6220
6078
|
|
|
6221
6079
|
// Check if content can be inlined (single text node or single expression)
|
|
6222
|
-
if (
|
|
6080
|
+
if (
|
|
6081
|
+
childrenDocs.length === 1 &&
|
|
6082
|
+
typeof childrenDocs[0] === 'string' &&
|
|
6083
|
+
closingCommentDocs.length === 0
|
|
6084
|
+
) {
|
|
6223
6085
|
return ['<>', childrenDocs[0], '</>'];
|
|
6224
6086
|
}
|
|
6225
6087
|
const meaningfulChildren = node.children.filter(
|
|
@@ -6230,6 +6092,7 @@ function printJSXFragment(node, path, options, print) {
|
|
|
6230
6092
|
meaningfulChildren.length === 1 &&
|
|
6231
6093
|
meaningfulChildren[0].type === 'JSXElement' &&
|
|
6232
6094
|
wasOriginallySingleLine(node) &&
|
|
6095
|
+
closingCommentDocs.length === 0 &&
|
|
6233
6096
|
!willBreak(childrenDocs[0])
|
|
6234
6097
|
) {
|
|
6235
6098
|
// Keep the fragment inline when it fits; otherwise expand `<>` onto its own
|
|
@@ -6253,7 +6116,12 @@ function printJSXFragment(node, path, options, print) {
|
|
|
6253
6116
|
}
|
|
6254
6117
|
|
|
6255
6118
|
// Build the final fragment
|
|
6256
|
-
return group([
|
|
6119
|
+
return group([
|
|
6120
|
+
'<>',
|
|
6121
|
+
indent([hardline, ...formattedChildren, ...closingCommentDocs]),
|
|
6122
|
+
hardline,
|
|
6123
|
+
'</>',
|
|
6124
|
+
]);
|
|
6257
6125
|
}
|
|
6258
6126
|
|
|
6259
6127
|
/**
|
|
@@ -6262,14 +6130,14 @@ function printJSXFragment(node, path, options, print) {
|
|
|
6262
6130
|
* opening tag's end, but the child is visited first). Pull those out of the
|
|
6263
6131
|
* children and return a map from attribute index to the comments that precede it,
|
|
6264
6132
|
* so the element printer can render them in the opening tag instead of the body.
|
|
6265
|
-
* @param {AST.TSRXJSXElement} node
|
|
6133
|
+
* @param {AST.TSRXJSXElement | AST.JSXStyleElement} node
|
|
6266
6134
|
* @returns {Map<number, AST.Comment[]>}
|
|
6267
6135
|
*/
|
|
6268
6136
|
function collectOpeningTagComments(node) {
|
|
6269
6137
|
/** @type {Map<number, AST.Comment[]>} */
|
|
6270
6138
|
const byAttr = new Map();
|
|
6271
6139
|
const openingElement = /** @type {AST.NodeWithLocation} */ (node.openingElement);
|
|
6272
|
-
const attributes =
|
|
6140
|
+
const attributes = node.openingElement?.attributes ?? [];
|
|
6273
6141
|
if (!openingElement || attributes.length === 0 || !Array.isArray(node.children)) {
|
|
6274
6142
|
return byAttr;
|
|
6275
6143
|
}
|
|
@@ -6288,7 +6156,7 @@ function collectOpeningTagComments(node) {
|
|
|
6288
6156
|
}
|
|
6289
6157
|
}
|
|
6290
6158
|
if (keep.length !== lead.length) {
|
|
6291
|
-
|
|
6159
|
+
child.leadingComments = keep;
|
|
6292
6160
|
}
|
|
6293
6161
|
}
|
|
6294
6162
|
if (collected.length === 0) return byAttr;
|
|
@@ -6338,18 +6206,74 @@ function printTemplateChildLeadingComments(child) {
|
|
|
6338
6206
|
}
|
|
6339
6207
|
|
|
6340
6208
|
/**
|
|
6341
|
-
* Build doc parts for
|
|
6209
|
+
* Build doc parts for a template child's trailing comments (kept on the same
|
|
6210
|
+
* line as the child). Used for `{expr}` children, whose `{ … }` form is printed
|
|
6211
|
+
* inline by the JSX printers and so would otherwise skip the node's attached
|
|
6212
|
+
* trailing comments.
|
|
6213
|
+
* @param {AST.Node & AST.NodeWithMaybeComments} child
|
|
6214
|
+
* @returns {Doc[]}
|
|
6215
|
+
*/
|
|
6216
|
+
function printTemplateChildTrailingComments(child) {
|
|
6217
|
+
const comments = child.trailingComments;
|
|
6218
|
+
if (!comments || comments.length === 0) {
|
|
6219
|
+
return [];
|
|
6220
|
+
}
|
|
6221
|
+
/** @type {Doc[]} */
|
|
6222
|
+
const parts = [];
|
|
6223
|
+
for (const comment of comments) {
|
|
6224
|
+
if (comment.type === 'Line') {
|
|
6225
|
+
parts.push(lineSuffix([' ', '//' + comment.value]));
|
|
6226
|
+
parts.push(breakParent);
|
|
6227
|
+
} else if (comment.type === 'Block') {
|
|
6228
|
+
parts.push(' /*' + comment.value + '*/');
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
return parts;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
/**
|
|
6235
|
+
* Collect and print the comments that belong to an element/fragment body:
|
|
6236
|
+
* trailing comments after the last child (attached by the parser to the closing
|
|
6237
|
+
* tag's `leadingComments` or, when the last child is an `{expr}` container, to
|
|
6238
|
+
* `metadata.elementLeadingComments` positioned inside the body) and the comments
|
|
6239
|
+
* of a comment-only body (`innerComments`).
|
|
6240
|
+
* @param {AST.TSRXJSXElement | AST.TSRXJSXFragment} node
|
|
6241
|
+
* @param {AST.TSRXJSXElement['openingElement'] | AST.TSRXJSXFragment['openingFragment']} openingNode
|
|
6242
|
+
* @param {AST.TSRXJSXElement['closingElement'] | AST.TSRXJSXFragment['closingFragment']} closingNode
|
|
6243
|
+
* @returns {{ closingCommentDocs: Doc[], innerCommentDocs: Doc[] }}
|
|
6244
|
+
*/
|
|
6245
|
+
function collectElementBodyCommentDocs(node, openingNode, closingNode) {
|
|
6246
|
+
const openingEnd = openingNode?.end;
|
|
6247
|
+
const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
|
|
6248
|
+
(/** @type {AST.Comment} */ comment) =>
|
|
6249
|
+
typeof comment.start === 'number' &&
|
|
6250
|
+
typeof openingEnd === 'number' &&
|
|
6251
|
+
comment.start >= openingEnd,
|
|
6252
|
+
);
|
|
6253
|
+
const trailingComments = [...(closingNode?.leadingComments ?? []), ...bodyMetaComments].sort(
|
|
6254
|
+
(/** @type {AST.Comment} */ a, /** @type {AST.Comment} */ b) =>
|
|
6255
|
+
/** @type {number} */ (a.start) - /** @type {number} */ (b.start),
|
|
6256
|
+
);
|
|
6257
|
+
const lastMeaningfulChild = [...(node.children ?? [])]
|
|
6258
|
+
.reverse()
|
|
6259
|
+
.find((child) => child.type !== 'JSXText' || child.value.trim());
|
|
6260
|
+
return {
|
|
6261
|
+
closingCommentDocs: printElementBodyComments(trailingComments, lastMeaningfulChild),
|
|
6262
|
+
innerCommentDocs: printElementBodyComments(node.innerComments),
|
|
6263
|
+
};
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
/**
|
|
6267
|
+
* Build doc parts for comments attached to an element body — trailing
|
|
6342
6268
|
* comments before `</tag>` (`closingElement.leadingComments`) or the comments of a
|
|
6343
|
-
* comment-only element (`innerComments`).
|
|
6344
|
-
* they survive in the adjacent JSXText value and are already rendered as text, so
|
|
6345
|
-
* emitting them here would duplicate them. Each comment is emitted on its own line
|
|
6269
|
+
* comment-only element (`innerComments`). Each comment is emitted on its own line
|
|
6346
6270
|
* at the children indent.
|
|
6347
6271
|
* @param {AST.Comment[] | null | undefined} commentList
|
|
6348
6272
|
* @param {any} [previousNode]
|
|
6349
6273
|
* @returns {Doc[]}
|
|
6350
6274
|
*/
|
|
6351
|
-
function
|
|
6352
|
-
const comments =
|
|
6275
|
+
function printElementBodyComments(commentList, previousNode = null) {
|
|
6276
|
+
const comments = commentList ?? [];
|
|
6353
6277
|
if (comments.length === 0) {
|
|
6354
6278
|
return [];
|
|
6355
6279
|
}
|
|
@@ -6363,7 +6287,9 @@ function printElementBodyLineComments(commentList, previousNode = null) {
|
|
|
6363
6287
|
if (prev && getBlankLinesBetweenNodes(prev, comments[i]) > 0) {
|
|
6364
6288
|
parts.push(hardline);
|
|
6365
6289
|
}
|
|
6366
|
-
parts.push(
|
|
6290
|
+
parts.push(
|
|
6291
|
+
comments[i].type === 'Line' ? '//' + comments[i].value : '/*' + comments[i].value + '*/',
|
|
6292
|
+
);
|
|
6367
6293
|
prev = comments[i];
|
|
6368
6294
|
}
|
|
6369
6295
|
return parts;
|
|
@@ -6404,10 +6330,10 @@ function printJSXCodeBlock(node, path, options, print) {
|
|
|
6404
6330
|
parts.push(path.call(print, 'render'));
|
|
6405
6331
|
}
|
|
6406
6332
|
// Trailing comments after the last statement/render inside the block.
|
|
6407
|
-
const innerCommentDocs =
|
|
6333
|
+
const innerCommentDocs = printElementBodyComments(node.innerComments);
|
|
6408
6334
|
if (innerCommentDocs.length > 0) {
|
|
6409
6335
|
const lastNode = node.render ?? node.body[node.body.length - 1];
|
|
6410
|
-
const firstComment = (node.innerComments ?? [])
|
|
6336
|
+
const firstComment = (node.innerComments ?? [])[0];
|
|
6411
6337
|
if (lastNode && firstComment && getBlankLinesBetweenNodes(lastNode, firstComment) > 0) {
|
|
6412
6338
|
parts.push(hardline);
|
|
6413
6339
|
}
|
|
@@ -6488,539 +6414,3 @@ function printJSXElementName(node) {
|
|
|
6488
6414
|
}
|
|
6489
6415
|
return 'Unknown';
|
|
6490
6416
|
}
|
|
6491
|
-
|
|
6492
|
-
/**
|
|
6493
|
-
* Print a member expression as simple string (for element tag names)
|
|
6494
|
-
* @param {AST.Node} node - The node to print
|
|
6495
|
-
* @param {RippleFormatOptions} options - Prettier options
|
|
6496
|
-
* @param {boolean} [computed=false] - Whether the property is computed
|
|
6497
|
-
* @returns {string}
|
|
6498
|
-
*/
|
|
6499
|
-
function printMemberExpressionSimple(node, options, computed = false) {
|
|
6500
|
-
if (node.type === 'JSXIdentifier') {
|
|
6501
|
-
return node.name;
|
|
6502
|
-
}
|
|
6503
|
-
|
|
6504
|
-
if (node.type === 'JSXMemberExpression') {
|
|
6505
|
-
return (
|
|
6506
|
-
printMemberExpressionSimple(node.object, options) +
|
|
6507
|
-
'.' +
|
|
6508
|
-
printMemberExpressionSimple(node.property, options, true)
|
|
6509
|
-
);
|
|
6510
|
-
}
|
|
6511
|
-
|
|
6512
|
-
if (node.type === 'JSXNamespacedName') {
|
|
6513
|
-
return node.namespace.name + ':' + node.name.name;
|
|
6514
|
-
}
|
|
6515
|
-
|
|
6516
|
-
if (node.type === 'Identifier') {
|
|
6517
|
-
return node.name;
|
|
6518
|
-
}
|
|
6519
|
-
|
|
6520
|
-
if (node.type === 'MemberExpression') {
|
|
6521
|
-
const obj = printMemberExpressionSimple(node.object, options);
|
|
6522
|
-
let prop;
|
|
6523
|
-
if (node.computed) {
|
|
6524
|
-
prop = '[' + printMemberExpressionSimple(node.property, options, true) + ']';
|
|
6525
|
-
} else {
|
|
6526
|
-
prop = '.' + printMemberExpressionSimple(node.property, options, true);
|
|
6527
|
-
}
|
|
6528
|
-
return obj + prop;
|
|
6529
|
-
}
|
|
6530
|
-
|
|
6531
|
-
if (node.type === 'Literal') {
|
|
6532
|
-
return computed ? formatStringLiteral(node.value, options) : JSON.stringify(node.value);
|
|
6533
|
-
}
|
|
6534
|
-
return '';
|
|
6535
|
-
}
|
|
6536
|
-
|
|
6537
|
-
/**
|
|
6538
|
-
* Check whether an attribute value can expand into multiline content.
|
|
6539
|
-
* @param {AST.Expression | null | undefined} value - The attribute value node
|
|
6540
|
-
* @param {boolean} [is_nested_in_object=false] - Whether this value is nested within an object literal
|
|
6541
|
-
* @returns {boolean}
|
|
6542
|
-
*/
|
|
6543
|
-
function is_attribute_value_breakable(value, is_nested_in_object = false) {
|
|
6544
|
-
if (!value) return false;
|
|
6545
|
-
|
|
6546
|
-
switch (value.type) {
|
|
6547
|
-
case 'ConditionalExpression':
|
|
6548
|
-
// Keep simple top-level ternary attributes inline when they fit.
|
|
6549
|
-
// We only force-break when a conditional is nested in an object literal value.
|
|
6550
|
-
return is_nested_in_object;
|
|
6551
|
-
case 'ObjectExpression':
|
|
6552
|
-
return value.properties.some(
|
|
6553
|
-
(property) =>
|
|
6554
|
-
property.type === 'Property' &&
|
|
6555
|
-
is_attribute_value_breakable(/** @type {AST.Expression} */ (property.value), true),
|
|
6556
|
-
);
|
|
6557
|
-
default:
|
|
6558
|
-
return false;
|
|
6559
|
-
}
|
|
6560
|
-
}
|
|
6561
|
-
|
|
6562
|
-
/**
|
|
6563
|
-
* Print a JSX element node
|
|
6564
|
-
* @param {AST.Element} element - The element node
|
|
6565
|
-
* @param {AstPath<AST.Element>} path - The AST path
|
|
6566
|
-
* @param {RippleFormatOptions} options - Prettier options
|
|
6567
|
-
* @param {PrintFn} print - Print callback
|
|
6568
|
-
* @returns {Doc}
|
|
6569
|
-
*/
|
|
6570
|
-
function printElement(element, path, options, print) {
|
|
6571
|
-
const node = /** @type {any} */ (element);
|
|
6572
|
-
const tagName = printMemberExpressionSimple(node.id, options);
|
|
6573
|
-
const openingElement = /** @type {any} */ (node.openingElement);
|
|
6574
|
-
/** @type {Doc} */
|
|
6575
|
-
let typeArgsDoc = '';
|
|
6576
|
-
if (openingElement?.typeArguments) {
|
|
6577
|
-
typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
|
|
6578
|
-
}
|
|
6579
|
-
const elementLeadingComments = getElementLeadingComments(/** @type {any} */ (node));
|
|
6580
|
-
|
|
6581
|
-
// `metadata.elementLeadingComments` may include comments that actually appear *inside* the element
|
|
6582
|
-
// body (after the opening tag). Those must not be hoisted before the element.
|
|
6583
|
-
const outerElementLeadingComments = elementLeadingComments.filter(
|
|
6584
|
-
(/** @type {AST.Comment} */ comment) =>
|
|
6585
|
-
typeof comment.start !== 'number' || comment.start < node.start,
|
|
6586
|
-
);
|
|
6587
|
-
const innerElementBodyComments = elementLeadingComments.filter(
|
|
6588
|
-
(/** @type {AST.Comment} */ comment) =>
|
|
6589
|
-
typeof comment.start === 'number' &&
|
|
6590
|
-
comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
|
|
6591
|
-
comment.start < node.end,
|
|
6592
|
-
);
|
|
6593
|
-
const metadataCommentParts =
|
|
6594
|
-
outerElementLeadingComments.length > 0
|
|
6595
|
-
? createElementLevelCommentParts(outerElementLeadingComments)
|
|
6596
|
-
: [];
|
|
6597
|
-
const fallbackElementComments = [];
|
|
6598
|
-
const shouldLiftTextLevelComments = outerElementLeadingComments.length === 0;
|
|
6599
|
-
|
|
6600
|
-
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
6601
|
-
const hasInnerComments = Array.isArray(node.innerComments) && node.innerComments.length > 0;
|
|
6602
|
-
const isSelfClosing = !!node.selfClosing;
|
|
6603
|
-
const hasAttributes = Array.isArray(node.attributes) && node.attributes.length > 0;
|
|
6604
|
-
|
|
6605
|
-
// Collect comments that the parser attached to children but actually belong inside
|
|
6606
|
-
// the opening tag (positionally before openingElement.end). These should be printed
|
|
6607
|
-
// as leading comments before the appropriate attribute, not lifted to element-level.
|
|
6608
|
-
/** @type {Set<AST.Comment>} */
|
|
6609
|
-
const openingTagCommentsSet = new Set();
|
|
6610
|
-
if (hasChildren && node.openingElement) {
|
|
6611
|
-
const openingEnd = /** @type {AST.NodeWithLocation} */ (node.openingElement).end;
|
|
6612
|
-
for (const child of node.children) {
|
|
6613
|
-
if (
|
|
6614
|
-
(child.type === 'JSXExpressionContainer' || child.type === 'JSXText') &&
|
|
6615
|
-
Array.isArray(child.leadingComments)
|
|
6616
|
-
) {
|
|
6617
|
-
for (const comment of child.leadingComments) {
|
|
6618
|
-
if (
|
|
6619
|
-
typeof comment.start === 'number' &&
|
|
6620
|
-
comment.start >= node.start &&
|
|
6621
|
-
comment.start < openingEnd
|
|
6622
|
-
) {
|
|
6623
|
-
openingTagCommentsSet.add(comment);
|
|
6624
|
-
}
|
|
6625
|
-
}
|
|
6626
|
-
}
|
|
6627
|
-
}
|
|
6628
|
-
}
|
|
6629
|
-
|
|
6630
|
-
// Build a map from attribute index to comments that should precede that attribute
|
|
6631
|
-
const openingTagCommentsByAttrIndex = new Map();
|
|
6632
|
-
if (openingTagCommentsSet.size > 0 && hasAttributes) {
|
|
6633
|
-
const sortedOTC = [...openingTagCommentsSet].sort(
|
|
6634
|
-
(a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start),
|
|
6635
|
-
);
|
|
6636
|
-
let commentIdx = 0;
|
|
6637
|
-
for (let attrIdx = 0; attrIdx < node.attributes.length; attrIdx++) {
|
|
6638
|
-
const attr = node.attributes[attrIdx];
|
|
6639
|
-
const commentsForAttr = [];
|
|
6640
|
-
while (
|
|
6641
|
-
commentIdx < sortedOTC.length &&
|
|
6642
|
-
/** @type {number} */ (sortedOTC[commentIdx].start) <
|
|
6643
|
-
/** @type {AST.NodeWithLocation} */ (attr).start
|
|
6644
|
-
) {
|
|
6645
|
-
commentsForAttr.push(sortedOTC[commentIdx]);
|
|
6646
|
-
commentIdx++;
|
|
6647
|
-
}
|
|
6648
|
-
if (commentsForAttr.length > 0) {
|
|
6649
|
-
openingTagCommentsByAttrIndex.set(attrIdx, commentsForAttr);
|
|
6650
|
-
}
|
|
6651
|
-
}
|
|
6652
|
-
}
|
|
6653
|
-
|
|
6654
|
-
if (isSelfClosing && !hasInnerComments && !hasAttributes) {
|
|
6655
|
-
const elementDoc = group(['<', tagName, typeArgsDoc, ' />']);
|
|
6656
|
-
return metadataCommentParts.length > 0 ? [...metadataCommentParts, elementDoc] : elementDoc;
|
|
6657
|
-
}
|
|
6658
|
-
|
|
6659
|
-
// Determine the line break type for attributes
|
|
6660
|
-
// When singleAttributePerLine is true, force each attribute on its own line with hardline
|
|
6661
|
-
// Otherwise, use line to allow collapsing when it fits
|
|
6662
|
-
const attrLineBreak = options.singleAttributePerLine ? hardline : line;
|
|
6663
|
-
|
|
6664
|
-
const shouldUseSelfClosingSyntax = isSelfClosing || (!hasChildren && !hasInnerComments);
|
|
6665
|
-
|
|
6666
|
-
const hasOpeningTagComments = openingTagCommentsSet.size > 0;
|
|
6667
|
-
let attrIndex = 0;
|
|
6668
|
-
let hasBreakingAttribute = false;
|
|
6669
|
-
const attrDocs = hasAttributes
|
|
6670
|
-
? path.map((attrPath) => {
|
|
6671
|
-
const idx = attrIndex++;
|
|
6672
|
-
const commentsForAttr = openingTagCommentsByAttrIndex.get(idx);
|
|
6673
|
-
/** @type {Doc[]} */
|
|
6674
|
-
const parts = [];
|
|
6675
|
-
if (commentsForAttr) {
|
|
6676
|
-
for (const comment of commentsForAttr) {
|
|
6677
|
-
// Line comments (//) consume the rest of the line, so they must
|
|
6678
|
-
// use hardline to force a break. Block comments can use normal breaks.
|
|
6679
|
-
if (comment.type === 'Line') {
|
|
6680
|
-
parts.push(hardline);
|
|
6681
|
-
parts.push('//' + comment.value);
|
|
6682
|
-
} else if (comment.type === 'Block') {
|
|
6683
|
-
parts.push(attrLineBreak);
|
|
6684
|
-
parts.push('/*' + comment.value + '*/');
|
|
6685
|
-
}
|
|
6686
|
-
}
|
|
6687
|
-
}
|
|
6688
|
-
parts.push(attrLineBreak);
|
|
6689
|
-
const attrDoc = print(attrPath);
|
|
6690
|
-
parts.push(attrDoc);
|
|
6691
|
-
const attr_node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
|
|
6692
|
-
/** @type {unknown} */ (attrPath.node)
|
|
6693
|
-
);
|
|
6694
|
-
if (
|
|
6695
|
-
!hasBreakingAttribute &&
|
|
6696
|
-
(willBreak(attrDoc) ||
|
|
6697
|
-
(attr_node.type === 'JSXAttribute' &&
|
|
6698
|
-
is_attribute_value_breakable(/** @type {any} */ (attr_node.value))))
|
|
6699
|
-
) {
|
|
6700
|
-
hasBreakingAttribute = true;
|
|
6701
|
-
}
|
|
6702
|
-
return parts;
|
|
6703
|
-
}, 'attributes')
|
|
6704
|
-
: [];
|
|
6705
|
-
const shouldForceBreak = hasOpeningTagComments || hasBreakingAttribute;
|
|
6706
|
-
const openingTagAlwaysBreaks =
|
|
6707
|
-
(hasAttributes && options.singleAttributePerLine) || shouldForceBreak;
|
|
6708
|
-
const openingTag = group([
|
|
6709
|
-
'<',
|
|
6710
|
-
tagName,
|
|
6711
|
-
typeArgsDoc,
|
|
6712
|
-
hasAttributes
|
|
6713
|
-
? indent([
|
|
6714
|
-
...attrDocs,
|
|
6715
|
-
// Force the group to break when there are line comments in the opening tag,
|
|
6716
|
-
// or when any attribute value would break (e.g. multiline objects, ternaries).
|
|
6717
|
-
// This ensures attributes are broken onto separate lines rather than breaking
|
|
6718
|
-
// expression values inline on the same line as the tag name.
|
|
6719
|
-
...(shouldForceBreak ? [breakParent] : []),
|
|
6720
|
-
])
|
|
6721
|
-
: '',
|
|
6722
|
-
// Add line break opportunity before > or />
|
|
6723
|
-
// Use line for self-closing (keeps space), softline for non-self-closing when attributes present
|
|
6724
|
-
// When bracketSameLine is true, don't add line break for non-self-closing elements
|
|
6725
|
-
shouldUseSelfClosingSyntax
|
|
6726
|
-
? hasAttributes
|
|
6727
|
-
? line
|
|
6728
|
-
: ''
|
|
6729
|
-
: hasAttributes && !options.bracketSameLine
|
|
6730
|
-
? softline
|
|
6731
|
-
: '',
|
|
6732
|
-
shouldUseSelfClosingSyntax ? (hasAttributes ? '/>' : ' />') : '>',
|
|
6733
|
-
]);
|
|
6734
|
-
|
|
6735
|
-
if (!hasChildren) {
|
|
6736
|
-
if (!hasInnerComments) {
|
|
6737
|
-
return metadataCommentParts.length > 0 ? [...metadataCommentParts, openingTag] : openingTag;
|
|
6738
|
-
}
|
|
6739
|
-
|
|
6740
|
-
/** @type {Doc[]} */
|
|
6741
|
-
const innerParts = [];
|
|
6742
|
-
for (const comment of node.innerComments ?? []) {
|
|
6743
|
-
if (comment.type === 'Line') {
|
|
6744
|
-
innerParts.push('//' + comment.value);
|
|
6745
|
-
innerParts.push(hardline);
|
|
6746
|
-
} else if (comment.type === 'Block') {
|
|
6747
|
-
innerParts.push('/*' + comment.value + '*/');
|
|
6748
|
-
innerParts.push(hardline);
|
|
6749
|
-
}
|
|
6750
|
-
}
|
|
6751
|
-
|
|
6752
|
-
if (innerParts.length > 0 && innerParts[innerParts.length - 1] === hardline) {
|
|
6753
|
-
innerParts.pop();
|
|
6754
|
-
}
|
|
6755
|
-
|
|
6756
|
-
const closingTag = ['</', tagName, '>'];
|
|
6757
|
-
const elementOutput = group([
|
|
6758
|
-
openingTag,
|
|
6759
|
-
indent([hardline, ...innerParts]),
|
|
6760
|
-
hardline,
|
|
6761
|
-
closingTag,
|
|
6762
|
-
]);
|
|
6763
|
-
return metadataCommentParts.length > 0
|
|
6764
|
-
? [...metadataCommentParts, elementOutput]
|
|
6765
|
-
: elementOutput;
|
|
6766
|
-
}
|
|
6767
|
-
|
|
6768
|
-
// Has children - use unified children processing
|
|
6769
|
-
// Build children with whitespace preservation
|
|
6770
|
-
/** @type {Doc[]} */
|
|
6771
|
-
const finalChildren = [];
|
|
6772
|
-
const sortedInnerElementBodyComments =
|
|
6773
|
-
innerElementBodyComments.length > 0
|
|
6774
|
-
? innerElementBodyComments.slice().sort((a, b) => (a.start ?? 0) - (b.start ?? 0))
|
|
6775
|
-
: [];
|
|
6776
|
-
let innerElementBodyCommentIndex = 0;
|
|
6777
|
-
|
|
6778
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
6779
|
-
const currentChild = node.children[i];
|
|
6780
|
-
const nextChild = node.children[i + 1];
|
|
6781
|
-
|
|
6782
|
-
// Insert any element-body comments that appear before this child.
|
|
6783
|
-
if (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
|
|
6784
|
-
const currentChildStart = typeof currentChild.start === 'number' ? currentChild.start : null;
|
|
6785
|
-
if (currentChildStart != null) {
|
|
6786
|
-
const commentsBefore = [];
|
|
6787
|
-
while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
|
|
6788
|
-
/** @type {AST.Comment} */
|
|
6789
|
-
const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
|
|
6790
|
-
if (typeof comment.start !== 'number' || comment.start >= currentChildStart) {
|
|
6791
|
-
break;
|
|
6792
|
-
}
|
|
6793
|
-
commentsBefore.push(comment);
|
|
6794
|
-
innerElementBodyCommentIndex++;
|
|
6795
|
-
}
|
|
6796
|
-
if (commentsBefore.length > 0) {
|
|
6797
|
-
if (finalChildren.length > 0) {
|
|
6798
|
-
finalChildren.push(hardline);
|
|
6799
|
-
}
|
|
6800
|
-
finalChildren.push(...createElementLevelCommentPartsTrimmed(commentsBefore));
|
|
6801
|
-
finalChildren.push(hardline);
|
|
6802
|
-
}
|
|
6803
|
-
}
|
|
6804
|
-
}
|
|
6805
|
-
|
|
6806
|
-
const isTextLikeChild =
|
|
6807
|
-
currentChild.type === 'JSXExpressionContainer' || currentChild.type === 'JSXText';
|
|
6808
|
-
const hasTextLeadingComments =
|
|
6809
|
-
shouldLiftTextLevelComments &&
|
|
6810
|
-
isTextLikeChild &&
|
|
6811
|
-
Array.isArray(currentChild.leadingComments) &&
|
|
6812
|
-
currentChild.leadingComments.length > 0;
|
|
6813
|
-
const currentChildAny = /** @type {any} */ (currentChild);
|
|
6814
|
-
const rawExpressionLeadingComments =
|
|
6815
|
-
isTextLikeChild && Array.isArray(currentChildAny.expression?.leadingComments)
|
|
6816
|
-
? currentChildAny.expression.leadingComments
|
|
6817
|
-
: null;
|
|
6818
|
-
const elementBodyLeadingComments =
|
|
6819
|
-
hasTextLeadingComments && node.openingElement
|
|
6820
|
-
? /** @type {AST.Comment[]} */ (currentChild.leadingComments).filter(
|
|
6821
|
-
(comment) =>
|
|
6822
|
-
comment.context?.containerId === node.metadata?.commentContainerId &&
|
|
6823
|
-
comment.context?.beforeMeaningfulChild &&
|
|
6824
|
-
typeof comment.start === 'number' &&
|
|
6825
|
-
comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
|
|
6826
|
-
comment.start < /** @type {AST.NodeWithLocation} */ (currentChild).start,
|
|
6827
|
-
)
|
|
6828
|
-
: [];
|
|
6829
|
-
|
|
6830
|
-
if (hasTextLeadingComments) {
|
|
6831
|
-
for (let j = 0; j < /** @type {AST.Comment[]} */ (currentChild.leadingComments).length; j++) {
|
|
6832
|
-
const comment = /** @type {AST.Comment[]} */ (currentChild.leadingComments)[j];
|
|
6833
|
-
// Don't lift comments that belong inside the opening tag (handled in attribute section)
|
|
6834
|
-
if (!openingTagCommentsSet.has(comment) && !elementBodyLeadingComments.includes(comment)) {
|
|
6835
|
-
fallbackElementComments.push(comment);
|
|
6836
|
-
}
|
|
6837
|
-
}
|
|
6838
|
-
}
|
|
6839
|
-
|
|
6840
|
-
const childPrintArgs = /** @type {PrintArgs} */ ({});
|
|
6841
|
-
if (hasTextLeadingComments) {
|
|
6842
|
-
childPrintArgs.suppressLeadingComments = true;
|
|
6843
|
-
}
|
|
6844
|
-
if (rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0) {
|
|
6845
|
-
childPrintArgs.suppressExpressionLeadingComments = true;
|
|
6846
|
-
}
|
|
6847
|
-
|
|
6848
|
-
const printedChild =
|
|
6849
|
-
Object.keys(childPrintArgs).length > 0
|
|
6850
|
-
? path.call((childPath) => print(childPath, childPrintArgs), 'children', i)
|
|
6851
|
-
: path.call(print, 'children', i);
|
|
6852
|
-
|
|
6853
|
-
const childLeadingCommentParts =
|
|
6854
|
-
elementBodyLeadingComments.length > 0
|
|
6855
|
-
? createElementLevelCommentParts(elementBodyLeadingComments)
|
|
6856
|
-
: rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
|
|
6857
|
-
? createElementLevelCommentParts(rawExpressionLeadingComments)
|
|
6858
|
-
: null;
|
|
6859
|
-
const childDoc = childLeadingCommentParts
|
|
6860
|
-
? [...childLeadingCommentParts, printedChild]
|
|
6861
|
-
: printedChild;
|
|
6862
|
-
finalChildren.push(childDoc);
|
|
6863
|
-
|
|
6864
|
-
// Insert element-body comments that fall between this child and the next child (or the closing tag).
|
|
6865
|
-
let insertedBodyCommentsBetween = false;
|
|
6866
|
-
if (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
|
|
6867
|
-
const currentChildEnd = /** @type {AST.NodeWithLocation} */ (currentChild).end;
|
|
6868
|
-
const nextChildStart =
|
|
6869
|
-
nextChild && typeof nextChild.start === 'number' ? nextChild.start : null;
|
|
6870
|
-
const commentsBetween = [];
|
|
6871
|
-
while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
|
|
6872
|
-
/** @type {AST.Comment} */
|
|
6873
|
-
const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
|
|
6874
|
-
if (typeof comment.start !== 'number') {
|
|
6875
|
-
innerElementBodyCommentIndex++;
|
|
6876
|
-
continue;
|
|
6877
|
-
}
|
|
6878
|
-
if (comment.start < currentChildEnd) {
|
|
6879
|
-
break;
|
|
6880
|
-
}
|
|
6881
|
-
if (nextChildStart != null && comment.start >= nextChildStart) {
|
|
6882
|
-
break;
|
|
6883
|
-
}
|
|
6884
|
-
commentsBetween.push(comment);
|
|
6885
|
-
innerElementBodyCommentIndex++;
|
|
6886
|
-
}
|
|
6887
|
-
if (commentsBetween.length > 0) {
|
|
6888
|
-
const firstComment = commentsBetween[0];
|
|
6889
|
-
const lastComment = commentsBetween[commentsBetween.length - 1];
|
|
6890
|
-
|
|
6891
|
-
// Preserve any blank line(s) that existed between the previous child and the comment block.
|
|
6892
|
-
const blankLinesBefore = getBlankLinesBetweenNodes(currentChild, firstComment);
|
|
6893
|
-
finalChildren.push(hardline);
|
|
6894
|
-
if (blankLinesBefore > 0) {
|
|
6895
|
-
finalChildren.push(hardline);
|
|
6896
|
-
}
|
|
6897
|
-
|
|
6898
|
-
finalChildren.push(...createElementLevelCommentPartsTrimmed(commentsBetween));
|
|
6899
|
-
|
|
6900
|
-
if (nextChild) {
|
|
6901
|
-
// Preserve any blank line(s) that existed between the comment block and the next child.
|
|
6902
|
-
const blankLinesAfter = getBlankLinesBetweenNodes(lastComment, nextChild);
|
|
6903
|
-
finalChildren.push(hardline);
|
|
6904
|
-
if (blankLinesAfter > 0) {
|
|
6905
|
-
finalChildren.push(hardline);
|
|
6906
|
-
}
|
|
6907
|
-
}
|
|
6908
|
-
insertedBodyCommentsBetween = true;
|
|
6909
|
-
}
|
|
6910
|
-
}
|
|
6911
|
-
|
|
6912
|
-
if (nextChild) {
|
|
6913
|
-
if (insertedBodyCommentsBetween) {
|
|
6914
|
-
continue;
|
|
6915
|
-
}
|
|
6916
|
-
const whitespaceTarget =
|
|
6917
|
-
nextChild.leadingComments && nextChild.leadingComments.length > 0
|
|
6918
|
-
? nextChild.leadingComments[0]
|
|
6919
|
-
: nextChild;
|
|
6920
|
-
const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, whitespaceTarget);
|
|
6921
|
-
const isTextOrExpressionChild =
|
|
6922
|
-
currentChild.type === 'JSXExpressionContainer' ||
|
|
6923
|
-
currentChild.type === 'JSXText' ||
|
|
6924
|
-
nextChild.type === 'JSXExpressionContainer' ||
|
|
6925
|
-
nextChild.type === 'JSXText';
|
|
6926
|
-
|
|
6927
|
-
if (whitespaceLinesCount > 0) {
|
|
6928
|
-
finalChildren.push(hardline);
|
|
6929
|
-
finalChildren.push(hardline);
|
|
6930
|
-
} else if (!isTextOrExpressionChild && shouldAddBlankLine(currentChild, nextChild)) {
|
|
6931
|
-
finalChildren.push(hardline);
|
|
6932
|
-
finalChildren.push(hardline);
|
|
6933
|
-
} else {
|
|
6934
|
-
finalChildren.push(hardline);
|
|
6935
|
-
}
|
|
6936
|
-
}
|
|
6937
|
-
}
|
|
6938
|
-
|
|
6939
|
-
// Collect comments attached to the closing element (comments that appear after the last child
|
|
6940
|
-
// but before the closing tag, e.g. `</div>`). The parser attaches these as leadingComments
|
|
6941
|
-
// on either the closingElement node or the closingElement.name node depending on context.
|
|
6942
|
-
const closingElementComments = [
|
|
6943
|
-
...(node.closingElement && Array.isArray(node.closingElement.leadingComments)
|
|
6944
|
-
? node.closingElement.leadingComments
|
|
6945
|
-
: []),
|
|
6946
|
-
...(node.closingElement &&
|
|
6947
|
-
node.closingElement.name &&
|
|
6948
|
-
Array.isArray(node.closingElement.name.leadingComments)
|
|
6949
|
-
? node.closingElement.name.leadingComments
|
|
6950
|
-
: []),
|
|
6951
|
-
];
|
|
6952
|
-
|
|
6953
|
-
if (closingElementComments.length > 0) {
|
|
6954
|
-
const lastChild = node.children[node.children.length - 1];
|
|
6955
|
-
if (lastChild) {
|
|
6956
|
-
const blankLinesBefore = getBlankLinesBetweenNodes(lastChild, closingElementComments[0]);
|
|
6957
|
-
finalChildren.push(hardline);
|
|
6958
|
-
if (blankLinesBefore > 0) {
|
|
6959
|
-
finalChildren.push(hardline);
|
|
6960
|
-
}
|
|
6961
|
-
}
|
|
6962
|
-
finalChildren.push(...createElementLevelCommentPartsTrimmed(closingElementComments));
|
|
6963
|
-
}
|
|
6964
|
-
|
|
6965
|
-
const fallbackCommentParts =
|
|
6966
|
-
fallbackElementComments.length > 0
|
|
6967
|
-
? createElementLevelCommentParts(fallbackElementComments)
|
|
6968
|
-
: [];
|
|
6969
|
-
const leadingCommentParts =
|
|
6970
|
-
metadataCommentParts.length > 0
|
|
6971
|
-
? [...metadataCommentParts, ...fallbackCommentParts]
|
|
6972
|
-
: fallbackCommentParts;
|
|
6973
|
-
|
|
6974
|
-
const closingTag = ['</', tagName, '>'];
|
|
6975
|
-
let elementOutput;
|
|
6976
|
-
const shouldTryInlineMultipleChildren =
|
|
6977
|
-
!openingTagAlwaysBreaks &&
|
|
6978
|
-
fallbackCommentParts.length === 0 &&
|
|
6979
|
-
closingElementComments.length === 0 &&
|
|
6980
|
-
shouldTryInlineMultipleTextChildren(node);
|
|
6981
|
-
|
|
6982
|
-
if (finalChildren.length === 1) {
|
|
6983
|
-
const child = finalChildren[0];
|
|
6984
|
-
const firstChild = /** @type {any} */ (node.children[0]);
|
|
6985
|
-
const isNonSelfClosingElement =
|
|
6986
|
-
firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
|
|
6987
|
-
const isElementChild = firstChild && firstChild.type === 'Element';
|
|
6988
|
-
const isRawTextChild =
|
|
6989
|
-
firstChild && firstChild.type === 'Text' && typeof firstChild.raw === 'string';
|
|
6990
|
-
|
|
6991
|
-
if (
|
|
6992
|
-
(typeof child === 'string' || isRawTextChild) &&
|
|
6993
|
-
shouldInlineSingleChild(node, firstChild, child)
|
|
6994
|
-
) {
|
|
6995
|
-
elementOutput = openingTagAlwaysBreaks
|
|
6996
|
-
? [openingTag, indent([hardline, child]), hardline, closingTag]
|
|
6997
|
-
: conditionalGroup([
|
|
6998
|
-
group([openingTag, child, closingTag]),
|
|
6999
|
-
[openingTag, indent([hardline, child]), hardline, closingTag],
|
|
7000
|
-
]);
|
|
7001
|
-
} else if (
|
|
7002
|
-
child &&
|
|
7003
|
-
typeof child === 'object' &&
|
|
7004
|
-
!isNonSelfClosingElement &&
|
|
7005
|
-
shouldInlineSingleChild(node, firstChild, child)
|
|
7006
|
-
) {
|
|
7007
|
-
if (isElementChild && hasAttributes) {
|
|
7008
|
-
elementOutput = [openingTag, indent([hardline, child]), hardline, closingTag];
|
|
7009
|
-
} else {
|
|
7010
|
-
elementOutput = group([openingTag, indent([softline, child]), softline, closingTag]);
|
|
7011
|
-
}
|
|
7012
|
-
} else {
|
|
7013
|
-
elementOutput = [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag];
|
|
7014
|
-
}
|
|
7015
|
-
} else if (shouldTryInlineMultipleChildren) {
|
|
7016
|
-
const inlineChildren = path.map(print, 'children');
|
|
7017
|
-
elementOutput = conditionalGroup([
|
|
7018
|
-
group([openingTag, ...inlineChildren, closingTag]),
|
|
7019
|
-
[openingTag, indent([hardline, ...finalChildren]), hardline, closingTag],
|
|
7020
|
-
]);
|
|
7021
|
-
} else {
|
|
7022
|
-
elementOutput = group([openingTag, indent([hardline, ...finalChildren]), hardline, closingTag]);
|
|
7023
|
-
}
|
|
7024
|
-
|
|
7025
|
-
return leadingCommentParts.length > 0 ? [...leadingCommentParts, elementOutput] : elementOutput;
|
|
7026
|
-
}
|
package/src/index.test.js
CHANGED
|
@@ -2753,6 +2753,128 @@ function test() {
|
|
|
2753
2753
|
expect(result).toBeWithNewline(expected);
|
|
2754
2754
|
});
|
|
2755
2755
|
|
|
2756
|
+
it('should preserve template comments and blank lines from unformatted input', async () => {
|
|
2757
|
+
const input = `function TodoList() @{
|
|
2758
|
+
<>
|
|
2759
|
+
/* world 0 */
|
|
2760
|
+
// hello
|
|
2761
|
+
/* world 1 */
|
|
2762
|
+
<ul>
|
|
2763
|
+
// hello
|
|
2764
|
+
/* world 2 */
|
|
2765
|
+
|
|
2766
|
+
</ul>
|
|
2767
|
+
|
|
2768
|
+
<ul>
|
|
2769
|
+
// hello
|
|
2770
|
+
/* world 3 */
|
|
2771
|
+
// hello
|
|
2772
|
+
</ul>
|
|
2773
|
+
/* world 4 */
|
|
2774
|
+
</>
|
|
2775
|
+
}`;
|
|
2776
|
+
|
|
2777
|
+
const expected = `function TodoList() @{
|
|
2778
|
+
<>
|
|
2779
|
+
/* world 0 */
|
|
2780
|
+
// hello
|
|
2781
|
+
/* world 1 */
|
|
2782
|
+
<ul>
|
|
2783
|
+
// hello
|
|
2784
|
+
/* world 2 */
|
|
2785
|
+
</ul>
|
|
2786
|
+
|
|
2787
|
+
<ul>
|
|
2788
|
+
// hello
|
|
2789
|
+
/* world 3 */
|
|
2790
|
+
// hello
|
|
2791
|
+
</ul>
|
|
2792
|
+
/* world 4 */
|
|
2793
|
+
</>
|
|
2794
|
+
}`;
|
|
2795
|
+
|
|
2796
|
+
const result = await format(input, { singleQuote: true });
|
|
2797
|
+
expect(result).toBeWithNewline(expected);
|
|
2798
|
+
// Reformatting the output must be stable.
|
|
2799
|
+
expect(await format(result, { singleQuote: true })).toBeWithNewline(expected);
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
it('should preserve block comments before a closing fragment', async () => {
|
|
2803
|
+
const expected = `function App() @{
|
|
2804
|
+
<>
|
|
2805
|
+
<span>{'child'}</span>
|
|
2806
|
+
|
|
2807
|
+
/* block comment */
|
|
2808
|
+
</>
|
|
2809
|
+
}`;
|
|
2810
|
+
|
|
2811
|
+
const result = await format(expected, { singleQuote: true });
|
|
2812
|
+
expect(result).toBeWithNewline(expected);
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
it('should preserve a blank line before a trailing block comment in elements', async () => {
|
|
2816
|
+
const expected = `function App() {
|
|
2817
|
+
<div>
|
|
2818
|
+
<span>{'child'}</span>
|
|
2819
|
+
|
|
2820
|
+
/* block comment */
|
|
2821
|
+
</div>
|
|
2822
|
+
}`;
|
|
2823
|
+
|
|
2824
|
+
const result = await format(expected, { singleQuote: true });
|
|
2825
|
+
expect(result).toBeWithNewline(expected);
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
it('should preserve a comment-only fragment body', async () => {
|
|
2829
|
+
const expected = `function App() @{
|
|
2830
|
+
<>
|
|
2831
|
+
/* only */
|
|
2832
|
+
</>
|
|
2833
|
+
}`;
|
|
2834
|
+
|
|
2835
|
+
const result = await format(expected, { singleQuote: true });
|
|
2836
|
+
expect(result).toBeWithNewline(expected);
|
|
2837
|
+
});
|
|
2838
|
+
|
|
2839
|
+
it('should keep blank lines around comments between template siblings', async () => {
|
|
2840
|
+
const expected = `function App() @{
|
|
2841
|
+
<>
|
|
2842
|
+
<ul></ul>
|
|
2843
|
+
|
|
2844
|
+
/* between */
|
|
2845
|
+
|
|
2846
|
+
<ul></ul>
|
|
2847
|
+
</>
|
|
2848
|
+
}`;
|
|
2849
|
+
|
|
2850
|
+
const result = await format(expected, { singleQuote: true });
|
|
2851
|
+
expect(result).toBeWithNewline(expected);
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
it('should keep a trailing line comment after an expression container child', async () => {
|
|
2855
|
+
const expected = `function App() @{
|
|
2856
|
+
<>
|
|
2857
|
+
{q} // hey
|
|
2858
|
+
// hello
|
|
2859
|
+
</>
|
|
2860
|
+
}`;
|
|
2861
|
+
|
|
2862
|
+
const result = await format(expected, { singleQuote: true });
|
|
2863
|
+
expect(result).toBeWithNewline(expected);
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
it('should keep a trailing block comment after an expression container child', async () => {
|
|
2867
|
+
const expected = `function App() {
|
|
2868
|
+
<div>
|
|
2869
|
+
{x} /* note */
|
|
2870
|
+
<span>{'tail'}</span>
|
|
2871
|
+
</div>
|
|
2872
|
+
}`;
|
|
2873
|
+
|
|
2874
|
+
const result = await format(expected, { singleQuote: true });
|
|
2875
|
+
expect(result).toBeWithNewline(expected);
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2756
2878
|
it('should preserve trailing comments in function parameters', async () => {
|
|
2757
2879
|
const expected = `function test(
|
|
2758
2880
|
// comment in params
|