@herb-tools/formatter 0.4.1 → 0.4.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/dist/herb-format.js +213 -23
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +198 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +198 -11
- package/dist/index.esm.js.map +1 -1
- package/dist/types/printer.d.ts +14 -0
- package/package.json +2 -2
- package/src/cli.ts +11 -6
- package/src/printer.ts +240 -6
package/dist/index.esm.js
CHANGED
|
@@ -129,7 +129,7 @@ class Token {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
132
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-
|
|
132
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/errors.ts.erb
|
|
133
133
|
class HerbError {
|
|
134
134
|
type;
|
|
135
135
|
message;
|
|
@@ -579,7 +579,7 @@ function convertToUTF8(string) {
|
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
582
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-
|
|
582
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/nodes.ts.erb
|
|
583
583
|
class Node {
|
|
584
584
|
type;
|
|
585
585
|
location;
|
|
@@ -2574,7 +2574,7 @@ function fromSerializedNode(node) {
|
|
|
2574
2574
|
}
|
|
2575
2575
|
|
|
2576
2576
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
2577
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-
|
|
2577
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/visitor.ts.erb
|
|
2578
2578
|
class Visitor {
|
|
2579
2579
|
visit(node) {
|
|
2580
2580
|
if (!node)
|
|
@@ -2690,6 +2690,7 @@ class Printer extends Visitor {
|
|
|
2690
2690
|
lines = [];
|
|
2691
2691
|
indentLevel = 0;
|
|
2692
2692
|
inlineMode = false;
|
|
2693
|
+
isInComplexNesting = false;
|
|
2693
2694
|
constructor(source, options) {
|
|
2694
2695
|
super();
|
|
2695
2696
|
this.source = source;
|
|
@@ -2703,6 +2704,7 @@ class Printer extends Visitor {
|
|
|
2703
2704
|
const node = object;
|
|
2704
2705
|
this.lines = [];
|
|
2705
2706
|
this.indentLevel = indentLevel;
|
|
2707
|
+
this.isInComplexNesting = false; // Reset for each top-level element
|
|
2706
2708
|
if (typeof node.accept === 'function') {
|
|
2707
2709
|
node.accept(this);
|
|
2708
2710
|
}
|
|
@@ -2775,6 +2777,28 @@ class Printer extends Visitor {
|
|
|
2775
2777
|
}
|
|
2776
2778
|
return;
|
|
2777
2779
|
}
|
|
2780
|
+
if (children.length >= 1) {
|
|
2781
|
+
if (this.isInComplexNesting) {
|
|
2782
|
+
if (children.length === 1) {
|
|
2783
|
+
const child = children[0];
|
|
2784
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
2785
|
+
const textContent = child.content.trim();
|
|
2786
|
+
const singleLine = `<${tagName}>${textContent}</${tagName}>`;
|
|
2787
|
+
if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
|
|
2788
|
+
this.push(indent + singleLine);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
else {
|
|
2795
|
+
const inlineResult = this.tryRenderInline(children, tagName);
|
|
2796
|
+
if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
|
|
2797
|
+
this.push(indent + inlineResult);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2778
2802
|
this.push(indent + `<${tagName}>`);
|
|
2779
2803
|
this.withIndent(() => {
|
|
2780
2804
|
children.forEach(child => this.visit(child));
|
|
@@ -2806,13 +2830,18 @@ class Printer extends Visitor {
|
|
|
2806
2830
|
}
|
|
2807
2831
|
const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
|
|
2808
2832
|
const singleAttribute = attributes[0];
|
|
2809
|
-
|
|
2833
|
+
singleAttribute &&
|
|
2810
2834
|
(singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
2811
2835
|
singleAttribute.value?.children.length === 0;
|
|
2836
|
+
const hasERBControlFlow = inlineNodes.some(node => node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE' ||
|
|
2837
|
+
node instanceof ERBUnlessNode || node.type === 'AST_ERB_UNLESS_NODE' ||
|
|
2838
|
+
node instanceof ERBBlockNode || node.type === 'AST_ERB_BLOCK_NODE' ||
|
|
2839
|
+
node instanceof ERBCaseNode || node.type === 'AST_ERB_CASE_NODE' ||
|
|
2840
|
+
node instanceof ERBWhileNode || node.type === 'AST_ERB_WHILE_NODE' ||
|
|
2841
|
+
node instanceof ERBForNode || node.type === 'AST_ERB_FOR_NODE');
|
|
2812
2842
|
const shouldKeepInline = (attributes.length <= 3 &&
|
|
2813
|
-
!hasEmptyValue &&
|
|
2814
2843
|
inline.length + indent.length <= this.maxLineLength) ||
|
|
2815
|
-
inlineNodes.length > 0;
|
|
2844
|
+
(inlineNodes.length > 0 && !hasERBControlFlow);
|
|
2816
2845
|
if (shouldKeepInline) {
|
|
2817
2846
|
if (children.length === 0) {
|
|
2818
2847
|
if (isSelfClosing) {
|
|
@@ -2840,7 +2869,36 @@ class Printer extends Visitor {
|
|
|
2840
2869
|
}
|
|
2841
2870
|
return;
|
|
2842
2871
|
}
|
|
2843
|
-
if (inlineNodes.length > 0) {
|
|
2872
|
+
if (inlineNodes.length > 0 && hasERBControlFlow) {
|
|
2873
|
+
this.push(indent + `<${tagName}`);
|
|
2874
|
+
this.withIndent(() => {
|
|
2875
|
+
open.children.forEach(child => {
|
|
2876
|
+
if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
2877
|
+
this.push(this.indent() + this.renderAttribute(child));
|
|
2878
|
+
}
|
|
2879
|
+
else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
|
|
2880
|
+
this.visit(child);
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
});
|
|
2884
|
+
if (isSelfClosing) {
|
|
2885
|
+
this.push(indent + "/>");
|
|
2886
|
+
}
|
|
2887
|
+
else if (node.is_void) {
|
|
2888
|
+
this.push(indent + ">");
|
|
2889
|
+
}
|
|
2890
|
+
else if (children.length === 0) {
|
|
2891
|
+
this.push(indent + ">" + `</${tagName}>`);
|
|
2892
|
+
}
|
|
2893
|
+
else {
|
|
2894
|
+
this.push(indent + ">");
|
|
2895
|
+
this.withIndent(() => {
|
|
2896
|
+
children.forEach(child => this.visit(child));
|
|
2897
|
+
});
|
|
2898
|
+
this.push(indent + `</${tagName}>`);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
else if (inlineNodes.length > 0) {
|
|
2844
2902
|
this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
|
|
2845
2903
|
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
2846
2904
|
this.withIndent(() => {
|
|
@@ -2902,11 +2960,10 @@ class Printer extends Visitor {
|
|
|
2902
2960
|
const attributes = node.attributes.filter((attribute) => attribute instanceof HTMLAttributeNode || attribute.type === 'AST_HTML_ATTRIBUTE_NODE');
|
|
2903
2961
|
const inline = this.renderInlineOpen(tagName, attributes, true);
|
|
2904
2962
|
const singleAttribute = attributes[0];
|
|
2905
|
-
|
|
2963
|
+
singleAttribute &&
|
|
2906
2964
|
(singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
2907
2965
|
singleAttribute.value?.children.length === 0;
|
|
2908
2966
|
const shouldKeepInline = attributes.length <= 3 &&
|
|
2909
|
-
!hasEmptyValue &&
|
|
2910
2967
|
inline.length + indent.length <= this.maxLineLength;
|
|
2911
2968
|
if (shouldKeepInline) {
|
|
2912
2969
|
this.push(indent + inline);
|
|
@@ -3084,14 +3141,26 @@ class Printer extends Visitor {
|
|
|
3084
3141
|
const content = node.content?.value ?? "";
|
|
3085
3142
|
const close = node.tag_closing?.value ?? "";
|
|
3086
3143
|
this.lines.push(open + content + close);
|
|
3087
|
-
node.statements.
|
|
3144
|
+
if (node.statements.length > 0) {
|
|
3145
|
+
this.lines.push(" ");
|
|
3146
|
+
}
|
|
3147
|
+
node.statements.forEach((child, index) => {
|
|
3088
3148
|
if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
3089
|
-
this.lines.push(
|
|
3149
|
+
this.lines.push(this.renderAttribute(child));
|
|
3090
3150
|
}
|
|
3091
3151
|
else {
|
|
3092
3152
|
this.visit(child);
|
|
3093
3153
|
}
|
|
3154
|
+
if (index < node.statements.length - 1) {
|
|
3155
|
+
this.lines.push(" ");
|
|
3156
|
+
}
|
|
3094
3157
|
});
|
|
3158
|
+
if (node.statements.length > 0) {
|
|
3159
|
+
this.lines.push(" ");
|
|
3160
|
+
}
|
|
3161
|
+
if (node.subsequent) {
|
|
3162
|
+
this.visit(node.subsequent);
|
|
3163
|
+
}
|
|
3095
3164
|
if (node.end_node) {
|
|
3096
3165
|
const endNode = node.end_node;
|
|
3097
3166
|
const endOpen = endNode.tag_opening?.value ?? "";
|
|
@@ -3262,6 +3331,124 @@ class Printer extends Visitor {
|
|
|
3262
3331
|
}
|
|
3263
3332
|
return name + equals + value;
|
|
3264
3333
|
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Try to render children inline if they are simple enough.
|
|
3336
|
+
* Returns the inline string if possible, null otherwise.
|
|
3337
|
+
*/
|
|
3338
|
+
tryRenderInline(children, tagName, depth = 0) {
|
|
3339
|
+
if (children.length > 10) {
|
|
3340
|
+
return null;
|
|
3341
|
+
}
|
|
3342
|
+
const maxNestingDepth = this.getMaxNestingDepth(children, 0);
|
|
3343
|
+
if (maxNestingDepth > 1) {
|
|
3344
|
+
this.isInComplexNesting = true;
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
for (const child of children) {
|
|
3348
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
3349
|
+
const textContent = child.content;
|
|
3350
|
+
if (textContent.includes('\n')) {
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3355
|
+
const element = child;
|
|
3356
|
+
const openTag = element.open_tag;
|
|
3357
|
+
const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
|
|
3358
|
+
if (attributes.length > 0) {
|
|
3359
|
+
return null;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
|
|
3363
|
+
else {
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
const oldLines = this.lines;
|
|
3368
|
+
const oldInlineMode = this.inlineMode;
|
|
3369
|
+
try {
|
|
3370
|
+
this.lines = [];
|
|
3371
|
+
this.inlineMode = true;
|
|
3372
|
+
let content = '';
|
|
3373
|
+
for (const child of children) {
|
|
3374
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
3375
|
+
content += child.content;
|
|
3376
|
+
}
|
|
3377
|
+
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3378
|
+
const element = child;
|
|
3379
|
+
const openTag = element.open_tag;
|
|
3380
|
+
const childTagName = openTag?.tag_name?.value || '';
|
|
3381
|
+
const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
|
|
3382
|
+
const attributesString = attributes.length > 0
|
|
3383
|
+
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
3384
|
+
: '';
|
|
3385
|
+
const elementContent = this.renderElementInline(element);
|
|
3386
|
+
content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
|
|
3387
|
+
}
|
|
3388
|
+
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
|
|
3389
|
+
const erbNode = child;
|
|
3390
|
+
const open = erbNode.tag_opening?.value ?? "";
|
|
3391
|
+
const erbContent = erbNode.content?.value ?? "";
|
|
3392
|
+
const close = erbNode.tag_closing?.value ?? "";
|
|
3393
|
+
content += `${open} ${erbContent.trim()} ${close}`;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
content = content.replace(/\s+/g, ' ').trim();
|
|
3397
|
+
return `<${tagName}>${content}</${tagName}>`;
|
|
3398
|
+
}
|
|
3399
|
+
finally {
|
|
3400
|
+
this.lines = oldLines;
|
|
3401
|
+
this.inlineMode = oldInlineMode;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
3406
|
+
*/
|
|
3407
|
+
getMaxNestingDepth(children, currentDepth) {
|
|
3408
|
+
let maxDepth = currentDepth;
|
|
3409
|
+
for (const child of children) {
|
|
3410
|
+
if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3411
|
+
const element = child;
|
|
3412
|
+
const elementChildren = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
|
|
3413
|
+
!((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
|
|
3414
|
+
const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1);
|
|
3415
|
+
maxDepth = Math.max(maxDepth, childDepth);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return maxDepth;
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Render an HTML element's content inline (without the wrapping tags).
|
|
3422
|
+
*/
|
|
3423
|
+
renderElementInline(element) {
|
|
3424
|
+
const children = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
|
|
3425
|
+
!((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
|
|
3426
|
+
let content = '';
|
|
3427
|
+
for (const child of children) {
|
|
3428
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
3429
|
+
content += child.content;
|
|
3430
|
+
}
|
|
3431
|
+
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3432
|
+
const childElement = child;
|
|
3433
|
+
const openTag = childElement.open_tag;
|
|
3434
|
+
const childTagName = openTag?.tag_name?.value || '';
|
|
3435
|
+
const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
|
|
3436
|
+
const attributesString = attributes.length > 0
|
|
3437
|
+
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
3438
|
+
: '';
|
|
3439
|
+
const childContent = this.renderElementInline(childElement);
|
|
3440
|
+
content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
|
|
3441
|
+
}
|
|
3442
|
+
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
|
|
3443
|
+
const erbNode = child;
|
|
3444
|
+
const open = erbNode.tag_opening?.value ?? "";
|
|
3445
|
+
const erbContent = erbNode.content?.value ?? "";
|
|
3446
|
+
const close = erbNode.tag_closing?.value ?? "";
|
|
3447
|
+
content += `${open} ${erbContent.trim()} ${close}`;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
return content.replace(/\s+/g, ' ').trim();
|
|
3451
|
+
}
|
|
3265
3452
|
}
|
|
3266
3453
|
|
|
3267
3454
|
/**
|