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