@herb-tools/formatter 0.8.1 → 0.8.2
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/README.md +38 -0
- package/dist/herb-format.js +393 -188
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +372 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +372 -176
- package/dist/index.esm.js.map +1 -1
- package/dist/types/format-helpers.d.ts +0 -3
- package/dist/types/format-ignore.d.ts +9 -0
- package/dist/types/format-printer.d.ts +23 -0
- package/package.json +5 -5
- package/src/format-helpers.ts +0 -11
- package/src/format-ignore.ts +45 -0
- package/src/format-printer.ts +339 -170
- package/src/formatter.ts +2 -1
package/dist/index.cjs
CHANGED
|
@@ -243,7 +243,7 @@ class Token {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
246
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.
|
|
246
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.2/templates/javascript/packages/core/src/errors.ts.erb
|
|
247
247
|
class HerbError {
|
|
248
248
|
type;
|
|
249
249
|
message;
|
|
@@ -773,7 +773,7 @@ function convertToUTF8(string) {
|
|
|
773
773
|
}
|
|
774
774
|
|
|
775
775
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
776
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.
|
|
776
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.2/templates/javascript/packages/core/src/nodes.ts.erb
|
|
777
777
|
class Node {
|
|
778
778
|
type;
|
|
779
779
|
location;
|
|
@@ -3007,7 +3007,7 @@ class ParseResult extends Result {
|
|
|
3007
3007
|
}
|
|
3008
3008
|
|
|
3009
3009
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
3010
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.
|
|
3010
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.2/templates/javascript/packages/core/src/node-type-guards.ts.erb
|
|
3011
3011
|
/**
|
|
3012
3012
|
* Type guard functions for AST nodes.
|
|
3013
3013
|
* These functions provide type checking by combining both instanceof
|
|
@@ -3398,6 +3398,16 @@ function isERBOutputNode(node) {
|
|
|
3398
3398
|
return false;
|
|
3399
3399
|
return ["<%=", "<%=="].includes(node.tag_opening?.value);
|
|
3400
3400
|
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Checks if a node is a ERB comment node (control flow: <%# %>)
|
|
3403
|
+
*/
|
|
3404
|
+
function isERBCommentNode(node) {
|
|
3405
|
+
if (!isERBNode(node))
|
|
3406
|
+
return false;
|
|
3407
|
+
if (!node.tag_opening?.value)
|
|
3408
|
+
return false;
|
|
3409
|
+
return node.tag_opening?.value === "<%#" || (node.tag_opening?.value !== "<%#" && (node.content?.value || "").trimStart().startsWith("#"));
|
|
3410
|
+
}
|
|
3401
3411
|
/**
|
|
3402
3412
|
* Checks if a node is a non-output ERB node (control flow: <% %>)
|
|
3403
3413
|
*/
|
|
@@ -3452,7 +3462,7 @@ function getTagName(node) {
|
|
|
3452
3462
|
* Check if a node is a comment (HTML comment or ERB comment)
|
|
3453
3463
|
*/
|
|
3454
3464
|
function isCommentNode(node) {
|
|
3455
|
-
return
|
|
3465
|
+
return isHTMLCommentNode(node) || isERBCommentNode(node);
|
|
3456
3466
|
}
|
|
3457
3467
|
/**
|
|
3458
3468
|
* Compares two positions to determine if the first comes before the second
|
|
@@ -3494,7 +3504,7 @@ function getNodesAfterPosition(nodes, position, inclusive = true) {
|
|
|
3494
3504
|
}
|
|
3495
3505
|
|
|
3496
3506
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
3497
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.
|
|
3507
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.8.2/templates/javascript/packages/core/src/visitor.ts.erb
|
|
3498
3508
|
class Visitor {
|
|
3499
3509
|
visit(node) {
|
|
3500
3510
|
if (!node)
|
|
@@ -4121,14 +4131,6 @@ const SPACEABLE_CONTAINERS = new Set([
|
|
|
4121
4131
|
'div', 'section', 'article', 'main', 'header', 'footer', 'aside',
|
|
4122
4132
|
'figure', 'details', 'summary', 'dialog', 'fieldset'
|
|
4123
4133
|
]);
|
|
4124
|
-
const TIGHT_GROUP_PARENTS = new Set([
|
|
4125
|
-
'ul', 'ol', 'nav', 'select', 'datalist', 'optgroup', 'tr', 'thead',
|
|
4126
|
-
'tbody', 'tfoot'
|
|
4127
|
-
]);
|
|
4128
|
-
const TIGHT_GROUP_CHILDREN = new Set([
|
|
4129
|
-
'li', 'option', 'td', 'th', 'dt', 'dd'
|
|
4130
|
-
]);
|
|
4131
|
-
const SPACING_THRESHOLD = 3;
|
|
4132
4134
|
/**
|
|
4133
4135
|
* Token list attributes that contain space-separated values and benefit from
|
|
4134
4136
|
* spacing around ERB content for readability
|
|
@@ -4515,6 +4517,10 @@ class FormatPrinter extends Printer {
|
|
|
4515
4517
|
currentAttributeName = null;
|
|
4516
4518
|
elementStack = [];
|
|
4517
4519
|
elementFormattingAnalysis = new Map();
|
|
4520
|
+
nodeIsMultiline = new Map();
|
|
4521
|
+
stringLineCount = 0;
|
|
4522
|
+
tagGroupsCache = new Map();
|
|
4523
|
+
allSingleLineCache = new Map();
|
|
4518
4524
|
source;
|
|
4519
4525
|
constructor(source, options) {
|
|
4520
4526
|
super();
|
|
@@ -4529,6 +4535,10 @@ class FormatPrinter extends Printer {
|
|
|
4529
4535
|
// TODO: refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
4530
4536
|
this.lines = [];
|
|
4531
4537
|
this.indentLevel = 0;
|
|
4538
|
+
this.stringLineCount = 0;
|
|
4539
|
+
this.nodeIsMultiline.clear();
|
|
4540
|
+
this.tagGroupsCache.clear();
|
|
4541
|
+
this.allSingleLineCache.clear();
|
|
4532
4542
|
this.visit(node);
|
|
4533
4543
|
return this.lines.join("\n");
|
|
4534
4544
|
}
|
|
@@ -4562,7 +4572,9 @@ class FormatPrinter extends Printer {
|
|
|
4562
4572
|
capture(callback) {
|
|
4563
4573
|
const previousLines = this.lines;
|
|
4564
4574
|
const previousInlineMode = this.inlineMode;
|
|
4575
|
+
const previousStringLineCount = this.stringLineCount;
|
|
4565
4576
|
this.lines = [];
|
|
4577
|
+
this.stringLineCount = 0;
|
|
4566
4578
|
try {
|
|
4567
4579
|
callback();
|
|
4568
4580
|
return this.lines;
|
|
@@ -4570,8 +4582,18 @@ class FormatPrinter extends Printer {
|
|
|
4570
4582
|
finally {
|
|
4571
4583
|
this.lines = previousLines;
|
|
4572
4584
|
this.inlineMode = previousInlineMode;
|
|
4585
|
+
this.stringLineCount = previousStringLineCount;
|
|
4573
4586
|
}
|
|
4574
4587
|
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Track a boundary node's multiline status by comparing line count before/after rendering.
|
|
4590
|
+
*/
|
|
4591
|
+
trackBoundary(node, callback) {
|
|
4592
|
+
const startLineCount = this.stringLineCount;
|
|
4593
|
+
callback();
|
|
4594
|
+
const endLineCount = this.stringLineCount;
|
|
4595
|
+
this.nodeIsMultiline.set(node, (endLineCount - startLineCount) > 1);
|
|
4596
|
+
}
|
|
4575
4597
|
/**
|
|
4576
4598
|
* Capture all nodes that would be visited during a callback
|
|
4577
4599
|
* Returns a flat list of all nodes without generating any output
|
|
@@ -4607,6 +4629,7 @@ class FormatPrinter extends Printer {
|
|
|
4607
4629
|
*/
|
|
4608
4630
|
push(line) {
|
|
4609
4631
|
this.lines.push(line);
|
|
4632
|
+
this.stringLineCount++;
|
|
4610
4633
|
}
|
|
4611
4634
|
/**
|
|
4612
4635
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
@@ -4651,6 +4674,79 @@ class FormatPrinter extends Printer {
|
|
|
4651
4674
|
extractInlineNodes(nodes) {
|
|
4652
4675
|
return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode));
|
|
4653
4676
|
}
|
|
4677
|
+
/**
|
|
4678
|
+
* Check if a node will render as multiple lines when formatted.
|
|
4679
|
+
*/
|
|
4680
|
+
isMultilineElement(node) {
|
|
4681
|
+
if (isNode(node, ERBContentNode)) {
|
|
4682
|
+
return (node.content?.value || "").includes("\n");
|
|
4683
|
+
}
|
|
4684
|
+
if (isNode(node, HTMLElementNode) && isContentPreserving(node)) {
|
|
4685
|
+
return true;
|
|
4686
|
+
}
|
|
4687
|
+
const tracked = this.nodeIsMultiline.get(node);
|
|
4688
|
+
if (tracked !== undefined) {
|
|
4689
|
+
return tracked;
|
|
4690
|
+
}
|
|
4691
|
+
return false;
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
4694
|
+
* Get a grouping key for a node (tag name for HTML, ERB type for ERB)
|
|
4695
|
+
*/
|
|
4696
|
+
getGroupingKey(node) {
|
|
4697
|
+
if (isNode(node, HTMLElementNode)) {
|
|
4698
|
+
return getTagName(node);
|
|
4699
|
+
}
|
|
4700
|
+
if (isERBOutputNode(node))
|
|
4701
|
+
return "erb-output";
|
|
4702
|
+
if (isERBCommentNode(node))
|
|
4703
|
+
return "erb-comment";
|
|
4704
|
+
if (isERBNode(node))
|
|
4705
|
+
return "erb-code";
|
|
4706
|
+
return null;
|
|
4707
|
+
}
|
|
4708
|
+
/**
|
|
4709
|
+
* Detect groups of consecutive same-tag/same-type single-line elements
|
|
4710
|
+
* Returns a map of index -> group info for efficient lookup
|
|
4711
|
+
*/
|
|
4712
|
+
detectTagGroups(siblings) {
|
|
4713
|
+
const cached = this.tagGroupsCache.get(siblings);
|
|
4714
|
+
if (cached)
|
|
4715
|
+
return cached;
|
|
4716
|
+
const groupMap = new Map();
|
|
4717
|
+
const meaningfulNodes = [];
|
|
4718
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
4719
|
+
const node = siblings[i];
|
|
4720
|
+
if (!this.isMultilineElement(node)) {
|
|
4721
|
+
const groupKey = this.getGroupingKey(node);
|
|
4722
|
+
if (groupKey) {
|
|
4723
|
+
meaningfulNodes.push({ index: i, groupKey });
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
let groupStart = 0;
|
|
4728
|
+
while (groupStart < meaningfulNodes.length) {
|
|
4729
|
+
const startGroupKey = meaningfulNodes[groupStart].groupKey;
|
|
4730
|
+
let groupEnd = groupStart;
|
|
4731
|
+
while (groupEnd + 1 < meaningfulNodes.length && meaningfulNodes[groupEnd + 1].groupKey === startGroupKey) {
|
|
4732
|
+
groupEnd++;
|
|
4733
|
+
}
|
|
4734
|
+
if (groupEnd > groupStart) {
|
|
4735
|
+
const groupStartIndex = meaningfulNodes[groupStart].index;
|
|
4736
|
+
const groupEndIndex = meaningfulNodes[groupEnd].index;
|
|
4737
|
+
for (let i = groupStart; i <= groupEnd; i++) {
|
|
4738
|
+
groupMap.set(meaningfulNodes[i].index, {
|
|
4739
|
+
tagName: startGroupKey,
|
|
4740
|
+
groupStart: groupStartIndex,
|
|
4741
|
+
groupEnd: groupEndIndex
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
groupStart = groupEnd + 1;
|
|
4746
|
+
}
|
|
4747
|
+
this.tagGroupsCache.set(siblings, groupMap);
|
|
4748
|
+
return groupMap;
|
|
4749
|
+
}
|
|
4654
4750
|
/**
|
|
4655
4751
|
* Determine if spacing should be added between sibling elements
|
|
4656
4752
|
*
|
|
@@ -4666,48 +4762,59 @@ class FormatPrinter extends Printer {
|
|
|
4666
4762
|
* @param hasExistingSpacing - Whether user-added spacing already exists
|
|
4667
4763
|
* @returns true if spacing should be added before the current element
|
|
4668
4764
|
*/
|
|
4669
|
-
shouldAddSpacingBetweenSiblings(parentElement, siblings, currentIndex
|
|
4670
|
-
|
|
4765
|
+
shouldAddSpacingBetweenSiblings(parentElement, siblings, currentIndex) {
|
|
4766
|
+
const currentNode = siblings[currentIndex];
|
|
4767
|
+
const previousMeaningfulIndex = findPreviousMeaningfulSibling(siblings, currentIndex);
|
|
4768
|
+
const previousNode = previousMeaningfulIndex !== -1 ? siblings[previousMeaningfulIndex] : null;
|
|
4769
|
+
if (previousNode && (isNode(previousNode, XMLDeclarationNode) || isNode(previousNode, HTMLDoctypeNode))) {
|
|
4671
4770
|
return true;
|
|
4672
4771
|
}
|
|
4673
4772
|
const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "");
|
|
4674
|
-
if (hasMixedContent)
|
|
4773
|
+
if (hasMixedContent)
|
|
4675
4774
|
return false;
|
|
4775
|
+
const isCurrentComment = isCommentNode(currentNode);
|
|
4776
|
+
const isPreviousComment = previousNode ? isCommentNode(previousNode) : false;
|
|
4777
|
+
const isCurrentMultiline = this.isMultilineElement(currentNode);
|
|
4778
|
+
const isPreviousMultiline = previousNode ? this.isMultilineElement(previousNode) : false;
|
|
4779
|
+
if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
|
|
4780
|
+
return isPreviousMultiline && isCurrentMultiline;
|
|
4676
4781
|
}
|
|
4677
|
-
|
|
4678
|
-
if (meaningfulSiblings.length < SPACING_THRESHOLD) {
|
|
4782
|
+
if (isPreviousComment && isCurrentComment) {
|
|
4679
4783
|
return false;
|
|
4680
4784
|
}
|
|
4785
|
+
if (isCurrentMultiline || isPreviousMultiline) {
|
|
4786
|
+
return true;
|
|
4787
|
+
}
|
|
4788
|
+
const meaningfulSiblings = siblings.filter(child => isNonWhitespaceNode(child));
|
|
4681
4789
|
const parentTagName = parentElement ? getTagName(parentElement) : null;
|
|
4682
|
-
|
|
4683
|
-
|
|
4790
|
+
const isSpaceableContainer = !parentTagName || SPACEABLE_CONTAINERS.has(parentTagName);
|
|
4791
|
+
const tagGroups = this.detectTagGroups(siblings);
|
|
4792
|
+
const cached = this.allSingleLineCache.get(siblings);
|
|
4793
|
+
let allSingleLineHTMLElements;
|
|
4794
|
+
if (cached !== undefined) {
|
|
4795
|
+
allSingleLineHTMLElements = cached;
|
|
4796
|
+
}
|
|
4797
|
+
else {
|
|
4798
|
+
allSingleLineHTMLElements = meaningfulSiblings.every(node => isNode(node, HTMLElementNode) && !this.isMultilineElement(node));
|
|
4799
|
+
this.allSingleLineCache.set(siblings, allSingleLineHTMLElements);
|
|
4684
4800
|
}
|
|
4685
|
-
const isSpaceableContainer = !parentTagName || (parentTagName && SPACEABLE_CONTAINERS.has(parentTagName));
|
|
4686
4801
|
if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
|
|
4687
4802
|
return false;
|
|
4688
4803
|
}
|
|
4689
|
-
const
|
|
4690
|
-
const
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
return false;
|
|
4700
|
-
}
|
|
4804
|
+
const currentGroup = tagGroups.get(currentIndex);
|
|
4805
|
+
const previousGroup = previousNode ? tagGroups.get(previousMeaningfulIndex) : undefined;
|
|
4806
|
+
if (currentGroup && previousGroup && currentGroup.groupStart === previousGroup.groupStart && currentGroup.groupEnd === previousGroup.groupEnd) {
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
if (previousGroup && previousGroup.groupEnd === previousMeaningfulIndex) {
|
|
4810
|
+
return true;
|
|
4811
|
+
}
|
|
4812
|
+
if (allSingleLineHTMLElements && tagGroups.size === 0) {
|
|
4813
|
+
return false;
|
|
4701
4814
|
}
|
|
4702
4815
|
if (isNode(currentNode, HTMLElementNode)) {
|
|
4703
4816
|
const currentTagName = getTagName(currentNode);
|
|
4704
|
-
if (INLINE_ELEMENTS.has(currentTagName)) {
|
|
4705
|
-
return false;
|
|
4706
|
-
}
|
|
4707
|
-
if (TIGHT_GROUP_CHILDREN.has(currentTagName)) {
|
|
4708
|
-
return false;
|
|
4709
|
-
}
|
|
4710
|
-
if (currentTagName === 'a' && parentTagName === 'nav') {
|
|
4817
|
+
if (currentTagName && INLINE_ELEMENTS.has(currentTagName)) {
|
|
4711
4818
|
return false;
|
|
4712
4819
|
}
|
|
4713
4820
|
}
|
|
@@ -4928,7 +5035,7 @@ class FormatPrinter extends Printer {
|
|
|
4928
5035
|
this.inlineMode = wasInlineMode;
|
|
4929
5036
|
return;
|
|
4930
5037
|
}
|
|
4931
|
-
let
|
|
5038
|
+
let lastMeaningfulNode = null;
|
|
4932
5039
|
let hasHandledSpacing = false;
|
|
4933
5040
|
for (let i = 0; i < children.length; i++) {
|
|
4934
5041
|
const child = children[i];
|
|
@@ -4942,36 +5049,42 @@ class FormatPrinter extends Printer {
|
|
|
4942
5049
|
}
|
|
4943
5050
|
if (shouldAppendToLastLine(child, children, i)) {
|
|
4944
5051
|
this.appendChildToLastLine(child, children, i);
|
|
4945
|
-
|
|
5052
|
+
lastMeaningfulNode = child;
|
|
4946
5053
|
hasHandledSpacing = false;
|
|
4947
5054
|
continue;
|
|
4948
5055
|
}
|
|
4949
|
-
if (isNonWhitespaceNode(child)
|
|
4950
|
-
|
|
4951
|
-
|
|
5056
|
+
if (!isNonWhitespaceNode(child))
|
|
5057
|
+
continue;
|
|
5058
|
+
const childStartLine = this.stringLineCount;
|
|
4952
5059
|
this.visit(child);
|
|
4953
|
-
if (
|
|
4954
|
-
|
|
4955
|
-
|
|
5060
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
5061
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(null, children, i);
|
|
5062
|
+
if (shouldAddSpacing) {
|
|
5063
|
+
this.lines.splice(childStartLine, 0, "");
|
|
5064
|
+
this.stringLineCount++;
|
|
5065
|
+
}
|
|
4956
5066
|
}
|
|
5067
|
+
lastMeaningfulNode = child;
|
|
5068
|
+
hasHandledSpacing = false;
|
|
4957
5069
|
}
|
|
4958
5070
|
}
|
|
4959
5071
|
visitHTMLElementNode(node) {
|
|
4960
5072
|
this.elementStack.push(node);
|
|
4961
5073
|
this.elementFormattingAnalysis.set(node, this.analyzeElementFormatting(node));
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5074
|
+
this.trackBoundary(node, () => {
|
|
5075
|
+
if (this.inlineMode && node.is_void && this.indentLevel === 0) {
|
|
5076
|
+
const openTag = this.capture(() => this.visit(node.open_tag)).join('');
|
|
5077
|
+
this.pushToLastLine(openTag);
|
|
5078
|
+
return;
|
|
5079
|
+
}
|
|
5080
|
+
this.visit(node.open_tag);
|
|
5081
|
+
if (node.body.length > 0) {
|
|
5082
|
+
this.visitHTMLElementBody(node.body, node);
|
|
5083
|
+
}
|
|
5084
|
+
if (node.close_tag) {
|
|
5085
|
+
this.visit(node.close_tag);
|
|
5086
|
+
}
|
|
5087
|
+
});
|
|
4975
5088
|
this.elementStack.pop();
|
|
4976
5089
|
}
|
|
4977
5090
|
visitHTMLElementBody(body, element) {
|
|
@@ -5118,17 +5231,19 @@ class FormatPrinter extends Printer {
|
|
|
5118
5231
|
}
|
|
5119
5232
|
/**
|
|
5120
5233
|
* Visit element children with intelligent spacing logic
|
|
5234
|
+
*
|
|
5235
|
+
* Tracks line positions and immediately splices blank lines after rendering each child.
|
|
5121
5236
|
*/
|
|
5122
5237
|
visitElementChildren(body, parentElement) {
|
|
5123
|
-
let
|
|
5238
|
+
let lastMeaningfulNode = null;
|
|
5124
5239
|
let hasHandledSpacing = false;
|
|
5125
|
-
for (let
|
|
5126
|
-
const child = body[
|
|
5240
|
+
for (let index = 0; index < body.length; index++) {
|
|
5241
|
+
const child = body[index];
|
|
5127
5242
|
if (isNode(child, HTMLTextNode)) {
|
|
5128
5243
|
const isWhitespaceOnly = child.content.trim() === "";
|
|
5129
5244
|
if (isWhitespaceOnly) {
|
|
5130
|
-
const hasPreviousNonWhitespace =
|
|
5131
|
-
const hasNextNonWhitespace =
|
|
5245
|
+
const hasPreviousNonWhitespace = index > 0 && isNonWhitespaceNode(body[index - 1]);
|
|
5246
|
+
const hasNextNonWhitespace = index < body.length - 1 && isNonWhitespaceNode(body[index + 1]);
|
|
5132
5247
|
const hasMultipleNewlines = child.content.includes('\n\n');
|
|
5133
5248
|
if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
|
|
5134
5249
|
this.push("");
|
|
@@ -5137,24 +5252,26 @@ class FormatPrinter extends Printer {
|
|
|
5137
5252
|
continue;
|
|
5138
5253
|
}
|
|
5139
5254
|
}
|
|
5140
|
-
if (isNonWhitespaceNode(child)
|
|
5141
|
-
|
|
5142
|
-
const hasExistingSpacing = i > 0 && isNode(element, HTMLTextNode) && element.content.trim() === "" && (element.content.includes('\n\n') || element.content.split('\n').length > 2);
|
|
5143
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, i, hasExistingSpacing);
|
|
5144
|
-
if (shouldAddSpacing) {
|
|
5145
|
-
this.push("");
|
|
5146
|
-
}
|
|
5147
|
-
}
|
|
5255
|
+
if (!isNonWhitespaceNode(child))
|
|
5256
|
+
continue;
|
|
5148
5257
|
let hasTrailingHerbDisable = false;
|
|
5149
5258
|
if (isNode(child, HTMLElementNode) && child.close_tag) {
|
|
5150
|
-
for (let j =
|
|
5259
|
+
for (let j = index + 1; j < body.length; j++) {
|
|
5151
5260
|
const nextChild = body[j];
|
|
5152
5261
|
if (isNode(nextChild, WhitespaceNode) || isPureWhitespaceNode(nextChild)) {
|
|
5153
5262
|
continue;
|
|
5154
5263
|
}
|
|
5155
5264
|
if (isNode(nextChild, ERBContentNode) && isHerbDisableComment(nextChild)) {
|
|
5156
5265
|
hasTrailingHerbDisable = true;
|
|
5266
|
+
const childStartLine = this.stringLineCount;
|
|
5157
5267
|
this.visit(child);
|
|
5268
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
5269
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index);
|
|
5270
|
+
if (shouldAddSpacing) {
|
|
5271
|
+
this.lines.splice(childStartLine, 0, "");
|
|
5272
|
+
this.stringLineCount++;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5158
5275
|
const herbDisableString = this.capture(() => {
|
|
5159
5276
|
const savedIndentLevel = this.indentLevel;
|
|
5160
5277
|
this.indentLevel = 0;
|
|
@@ -5164,17 +5281,25 @@ class FormatPrinter extends Printer {
|
|
|
5164
5281
|
this.indentLevel = savedIndentLevel;
|
|
5165
5282
|
}).join("");
|
|
5166
5283
|
this.pushToLastLine(' ' + herbDisableString);
|
|
5167
|
-
|
|
5284
|
+
index = j;
|
|
5285
|
+
lastMeaningfulNode = child;
|
|
5286
|
+
hasHandledSpacing = false;
|
|
5168
5287
|
break;
|
|
5169
5288
|
}
|
|
5170
5289
|
break;
|
|
5171
5290
|
}
|
|
5172
5291
|
}
|
|
5173
5292
|
if (!hasTrailingHerbDisable) {
|
|
5293
|
+
const childStartLine = this.stringLineCount;
|
|
5174
5294
|
this.visit(child);
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5295
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
5296
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index);
|
|
5297
|
+
if (shouldAddSpacing) {
|
|
5298
|
+
this.lines.splice(childStartLine, 0, "");
|
|
5299
|
+
this.stringLineCount++;
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
lastMeaningfulNode = child;
|
|
5178
5303
|
hasHandledSpacing = false;
|
|
5179
5304
|
}
|
|
5180
5305
|
}
|
|
@@ -5270,6 +5395,11 @@ class FormatPrinter extends Printer {
|
|
|
5270
5395
|
return "";
|
|
5271
5396
|
}
|
|
5272
5397
|
}).join("");
|
|
5398
|
+
const trimmedInner = inner.trim();
|
|
5399
|
+
if (trimmedInner.startsWith('[if ') && trimmedInner.endsWith('<![endif]')) {
|
|
5400
|
+
this.pushWithIndent(open + inner + close);
|
|
5401
|
+
return;
|
|
5402
|
+
}
|
|
5273
5403
|
const hasNewlines = inner.includes('\n');
|
|
5274
5404
|
if (hasNewlines) {
|
|
5275
5405
|
const lines = inner.split('\n');
|
|
@@ -5352,8 +5482,7 @@ class FormatPrinter extends Printer {
|
|
|
5352
5482
|
this.pushWithIndent(IdentityPrinter.print(node));
|
|
5353
5483
|
}
|
|
5354
5484
|
visitERBContentNode(node) {
|
|
5355
|
-
|
|
5356
|
-
if (node.tag_opening?.value === "<%#") {
|
|
5485
|
+
if (isERBCommentNode(node)) {
|
|
5357
5486
|
this.visitERBCommentNode(node);
|
|
5358
5487
|
}
|
|
5359
5488
|
else {
|
|
@@ -5364,83 +5493,92 @@ class FormatPrinter extends Printer {
|
|
|
5364
5493
|
this.printERBNode(node);
|
|
5365
5494
|
}
|
|
5366
5495
|
visitERBYieldNode(node) {
|
|
5367
|
-
this.
|
|
5496
|
+
this.trackBoundary(node, () => {
|
|
5497
|
+
this.printERBNode(node);
|
|
5498
|
+
});
|
|
5368
5499
|
}
|
|
5369
5500
|
visitERBInNode(node) {
|
|
5370
|
-
this.
|
|
5371
|
-
|
|
5501
|
+
this.trackBoundary(node, () => {
|
|
5502
|
+
this.printERBNode(node);
|
|
5503
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5504
|
+
});
|
|
5372
5505
|
}
|
|
5373
5506
|
visitERBCaseMatchNode(node) {
|
|
5374
|
-
this.
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
visitERBBlockNode(node) {
|
|
5383
|
-
this.printERBNode(node);
|
|
5384
|
-
this.withIndent(() => {
|
|
5385
|
-
const hasTextFlow = this.isInTextFlowContext(null, node.body);
|
|
5386
|
-
if (hasTextFlow) {
|
|
5387
|
-
this.visitTextFlowChildren(node.body);
|
|
5388
|
-
}
|
|
5389
|
-
else {
|
|
5390
|
-
this.visitElementChildren(node.body, null);
|
|
5391
|
-
}
|
|
5507
|
+
this.trackBoundary(node, () => {
|
|
5508
|
+
this.printERBNode(node);
|
|
5509
|
+
this.withIndent(() => this.visitAll(node.children));
|
|
5510
|
+
this.visitAll(node.conditions);
|
|
5511
|
+
if (node.else_clause)
|
|
5512
|
+
this.visit(node.else_clause);
|
|
5513
|
+
if (node.end_node)
|
|
5514
|
+
this.visit(node.end_node);
|
|
5392
5515
|
});
|
|
5393
|
-
if (node.end_node)
|
|
5394
|
-
this.visit(node.end_node);
|
|
5395
5516
|
}
|
|
5396
|
-
|
|
5397
|
-
|
|
5517
|
+
visitERBBlockNode(node) {
|
|
5518
|
+
this.trackBoundary(node, () => {
|
|
5398
5519
|
this.printERBNode(node);
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
this.
|
|
5520
|
+
this.withIndent(() => {
|
|
5521
|
+
const hasTextFlow = this.isInTextFlowContext(null, node.body);
|
|
5522
|
+
if (hasTextFlow) {
|
|
5523
|
+
this.visitTextFlowChildren(node.body);
|
|
5403
5524
|
}
|
|
5404
5525
|
else {
|
|
5405
|
-
|
|
5406
|
-
|
|
5526
|
+
this.visitElementChildren(node.body, null);
|
|
5527
|
+
}
|
|
5528
|
+
});
|
|
5529
|
+
if (node.end_node)
|
|
5530
|
+
this.visit(node.end_node);
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
visitERBIfNode(node) {
|
|
5534
|
+
this.trackBoundary(node, () => {
|
|
5535
|
+
if (this.inlineMode) {
|
|
5536
|
+
this.printERBNode(node);
|
|
5537
|
+
node.statements.forEach(child => {
|
|
5538
|
+
if (isNode(child, HTMLAttributeNode)) {
|
|
5407
5539
|
this.lines.push(" ");
|
|
5540
|
+
this.lines.push(this.renderAttribute(child));
|
|
5408
5541
|
}
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5542
|
+
else {
|
|
5543
|
+
const shouldAddSpaces = this.isInTokenListAttribute;
|
|
5544
|
+
if (shouldAddSpaces) {
|
|
5545
|
+
this.lines.push(" ");
|
|
5546
|
+
}
|
|
5547
|
+
this.visit(child);
|
|
5548
|
+
if (shouldAddSpaces) {
|
|
5549
|
+
this.lines.push(" ");
|
|
5550
|
+
}
|
|
5412
5551
|
}
|
|
5552
|
+
});
|
|
5553
|
+
const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode));
|
|
5554
|
+
const isTokenList = this.isInTokenListAttribute;
|
|
5555
|
+
if ((hasHTMLAttributes || isTokenList) && node.end_node) {
|
|
5556
|
+
this.lines.push(" ");
|
|
5413
5557
|
}
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
this.lines.push(" ");
|
|
5558
|
+
if (node.subsequent)
|
|
5559
|
+
this.visit(node.subsequent);
|
|
5560
|
+
if (node.end_node)
|
|
5561
|
+
this.visit(node.end_node);
|
|
5419
5562
|
}
|
|
5420
|
-
|
|
5421
|
-
this.
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
}
|
|
5430
|
-
|
|
5431
|
-
this.visit(node.subsequent);
|
|
5432
|
-
if (node.end_node)
|
|
5433
|
-
this.visit(node.end_node);
|
|
5434
|
-
}
|
|
5563
|
+
else {
|
|
5564
|
+
this.printERBNode(node);
|
|
5565
|
+
this.withIndent(() => {
|
|
5566
|
+
node.statements.forEach(child => this.visit(child));
|
|
5567
|
+
});
|
|
5568
|
+
if (node.subsequent)
|
|
5569
|
+
this.visit(node.subsequent);
|
|
5570
|
+
if (node.end_node)
|
|
5571
|
+
this.visit(node.end_node);
|
|
5572
|
+
}
|
|
5573
|
+
});
|
|
5435
5574
|
}
|
|
5436
5575
|
visitERBElseNode(node) {
|
|
5576
|
+
this.printERBNode(node);
|
|
5437
5577
|
if (this.inlineMode) {
|
|
5438
|
-
this.
|
|
5439
|
-
node.statements.forEach(statement => this.visit(statement));
|
|
5578
|
+
this.visitAll(node.statements);
|
|
5440
5579
|
}
|
|
5441
5580
|
else {
|
|
5442
|
-
this.
|
|
5443
|
-
this.withIndent(() => node.statements.forEach(statement => this.visit(statement)));
|
|
5581
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5444
5582
|
}
|
|
5445
5583
|
}
|
|
5446
5584
|
visitERBWhenNode(node) {
|
|
@@ -5448,43 +5586,53 @@ class FormatPrinter extends Printer {
|
|
|
5448
5586
|
this.withIndent(() => this.visitAll(node.statements));
|
|
5449
5587
|
}
|
|
5450
5588
|
visitERBCaseNode(node) {
|
|
5451
|
-
this.
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5589
|
+
this.trackBoundary(node, () => {
|
|
5590
|
+
this.printERBNode(node);
|
|
5591
|
+
this.withIndent(() => this.visitAll(node.children));
|
|
5592
|
+
this.visitAll(node.conditions);
|
|
5593
|
+
if (node.else_clause)
|
|
5594
|
+
this.visit(node.else_clause);
|
|
5595
|
+
if (node.end_node)
|
|
5596
|
+
this.visit(node.end_node);
|
|
5597
|
+
});
|
|
5458
5598
|
}
|
|
5459
5599
|
visitERBBeginNode(node) {
|
|
5460
|
-
this.
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5600
|
+
this.trackBoundary(node, () => {
|
|
5601
|
+
this.printERBNode(node);
|
|
5602
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5603
|
+
if (node.rescue_clause)
|
|
5604
|
+
this.visit(node.rescue_clause);
|
|
5605
|
+
if (node.else_clause)
|
|
5606
|
+
this.visit(node.else_clause);
|
|
5607
|
+
if (node.ensure_clause)
|
|
5608
|
+
this.visit(node.ensure_clause);
|
|
5609
|
+
if (node.end_node)
|
|
5610
|
+
this.visit(node.end_node);
|
|
5611
|
+
});
|
|
5470
5612
|
}
|
|
5471
5613
|
visitERBWhileNode(node) {
|
|
5472
|
-
this.
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5614
|
+
this.trackBoundary(node, () => {
|
|
5615
|
+
this.printERBNode(node);
|
|
5616
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5617
|
+
if (node.end_node)
|
|
5618
|
+
this.visit(node.end_node);
|
|
5619
|
+
});
|
|
5476
5620
|
}
|
|
5477
5621
|
visitERBUntilNode(node) {
|
|
5478
|
-
this.
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5622
|
+
this.trackBoundary(node, () => {
|
|
5623
|
+
this.printERBNode(node);
|
|
5624
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5625
|
+
if (node.end_node)
|
|
5626
|
+
this.visit(node.end_node);
|
|
5627
|
+
});
|
|
5482
5628
|
}
|
|
5483
5629
|
visitERBForNode(node) {
|
|
5484
|
-
this.
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5630
|
+
this.trackBoundary(node, () => {
|
|
5631
|
+
this.printERBNode(node);
|
|
5632
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5633
|
+
if (node.end_node)
|
|
5634
|
+
this.visit(node.end_node);
|
|
5635
|
+
});
|
|
5488
5636
|
}
|
|
5489
5637
|
visitERBRescueNode(node) {
|
|
5490
5638
|
this.printERBNode(node);
|
|
@@ -5495,12 +5643,14 @@ class FormatPrinter extends Printer {
|
|
|
5495
5643
|
this.withIndent(() => this.visitAll(node.statements));
|
|
5496
5644
|
}
|
|
5497
5645
|
visitERBUnlessNode(node) {
|
|
5498
|
-
this.
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5646
|
+
this.trackBoundary(node, () => {
|
|
5647
|
+
this.printERBNode(node);
|
|
5648
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
5649
|
+
if (node.else_clause)
|
|
5650
|
+
this.visit(node.else_clause);
|
|
5651
|
+
if (node.end_node)
|
|
5652
|
+
this.visit(node.end_node);
|
|
5653
|
+
});
|
|
5504
5654
|
}
|
|
5505
5655
|
// --- Element Formatting Analysis Helpers ---
|
|
5506
5656
|
/**
|
|
@@ -5545,6 +5695,14 @@ class FormatPrinter extends Printer {
|
|
|
5545
5695
|
return false;
|
|
5546
5696
|
if (children.length === 0)
|
|
5547
5697
|
return true;
|
|
5698
|
+
const hasNonInlineChildElements = children.some(child => {
|
|
5699
|
+
if (isNode(child, HTMLElementNode)) {
|
|
5700
|
+
return !this.shouldRenderElementContentInline(child);
|
|
5701
|
+
}
|
|
5702
|
+
return false;
|
|
5703
|
+
});
|
|
5704
|
+
if (hasNonInlineChildElements)
|
|
5705
|
+
return false;
|
|
5548
5706
|
let hasLeadingHerbDisable = false;
|
|
5549
5707
|
for (const child of node.body) {
|
|
5550
5708
|
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
@@ -5562,7 +5720,7 @@ class FormatPrinter extends Printer {
|
|
|
5562
5720
|
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), node.body);
|
|
5563
5721
|
if (fullInlineResult) {
|
|
5564
5722
|
const totalLength = this.indent.length + fullInlineResult.length;
|
|
5565
|
-
return totalLength <= this.maxLineLength
|
|
5723
|
+
return totalLength <= this.maxLineLength;
|
|
5566
5724
|
}
|
|
5567
5725
|
return false;
|
|
5568
5726
|
}
|
|
@@ -5583,7 +5741,8 @@ class FormatPrinter extends Printer {
|
|
|
5583
5741
|
const openTagResult = this.renderInlineOpen(tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), false, [], node.open_tag?.children || []);
|
|
5584
5742
|
const childrenContent = this.renderChildrenInline(children);
|
|
5585
5743
|
const fullLine = openTagResult + childrenContent + `</${tagName}>`;
|
|
5586
|
-
|
|
5744
|
+
const totalLength = this.indent.length + fullLine.length;
|
|
5745
|
+
if (totalLength <= this.maxLineLength) {
|
|
5587
5746
|
return true;
|
|
5588
5747
|
}
|
|
5589
5748
|
}
|
|
@@ -6455,6 +6614,41 @@ function resolveFormatOptions(options = {}) {
|
|
|
6455
6614
|
};
|
|
6456
6615
|
}
|
|
6457
6616
|
|
|
6617
|
+
const HERB_FORMATTER_PREFIX = "herb:formatter";
|
|
6618
|
+
const HERB_FORMATTER_IGNORE_PREFIX = `${HERB_FORMATTER_PREFIX} ignore`;
|
|
6619
|
+
/**
|
|
6620
|
+
* Check if an ERB content node is a herb:formatter ignore comment
|
|
6621
|
+
*/
|
|
6622
|
+
function isHerbFormatterIgnoreComment(node) {
|
|
6623
|
+
if (!isERBCommentNode(node))
|
|
6624
|
+
return false;
|
|
6625
|
+
const content = node?.content?.value || "";
|
|
6626
|
+
return content.trim() === HERB_FORMATTER_IGNORE_PREFIX;
|
|
6627
|
+
}
|
|
6628
|
+
/**
|
|
6629
|
+
* Check if the document contains a herb:formatter ignore directive anywhere.
|
|
6630
|
+
*/
|
|
6631
|
+
function hasFormatterIgnoreDirective(node) {
|
|
6632
|
+
const detector = new FormatterIgnoreDetector();
|
|
6633
|
+
detector.visit(node);
|
|
6634
|
+
return detector.hasIgnoreDirective;
|
|
6635
|
+
}
|
|
6636
|
+
/**
|
|
6637
|
+
* Visitor that detects if the AST contains a herb:formatter ignore directive.
|
|
6638
|
+
*/
|
|
6639
|
+
class FormatterIgnoreDetector extends Visitor {
|
|
6640
|
+
hasIgnoreDirective = false;
|
|
6641
|
+
visitERBContentNode(node) {
|
|
6642
|
+
if (isHerbFormatterIgnoreComment(node)) {
|
|
6643
|
+
this.hasIgnoreDirective = true;
|
|
6644
|
+
return;
|
|
6645
|
+
}
|
|
6646
|
+
if (this.hasIgnoreDirective)
|
|
6647
|
+
return;
|
|
6648
|
+
this.visitChildNodes(node);
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
|
|
6458
6652
|
/**
|
|
6459
6653
|
* Formatter uses a Herb Backend to parse the source and then
|
|
6460
6654
|
* formats the resulting AST into a well-indented, wrapped string.
|
|
@@ -6499,6 +6693,8 @@ class Formatter {
|
|
|
6499
6693
|
return source;
|
|
6500
6694
|
if (isScaffoldTemplate(result))
|
|
6501
6695
|
return source;
|
|
6696
|
+
if (hasFormatterIgnoreDirective(result.value))
|
|
6697
|
+
return source;
|
|
6502
6698
|
const resolvedOptions = resolveFormatOptions({ ...this.options, ...options });
|
|
6503
6699
|
let node = result.value;
|
|
6504
6700
|
if (resolvedOptions.preRewriters.length > 0) {
|