@herb-tools/formatter 0.4.0 → 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/index.cjs CHANGED
@@ -131,7 +131,7 @@ class Token {
131
131
  }
132
132
 
133
133
  // NOTE: This file is generated by the templates/template.rb script and should not
134
- // be modified manually. See /Users/marcoroth/Development/herb-release/templates/javascript/packages/core/src/errors.ts.erb
134
+ // be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/errors.ts.erb
135
135
  class HerbError {
136
136
  type;
137
137
  message;
@@ -581,7 +581,7 @@ function convertToUTF8(string) {
581
581
  }
582
582
 
583
583
  // NOTE: This file is generated by the templates/template.rb script and should not
584
- // be modified manually. See /Users/marcoroth/Development/herb-release/templates/javascript/packages/core/src/nodes.ts.erb
584
+ // be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/nodes.ts.erb
585
585
  class Node {
586
586
  type;
587
587
  location;
@@ -2576,7 +2576,7 @@ function fromSerializedNode(node) {
2576
2576
  }
2577
2577
 
2578
2578
  // NOTE: This file is generated by the templates/template.rb script and should not
2579
- // be modified manually. See /Users/marcoroth/Development/herb-release/templates/javascript/packages/core/src/visitor.ts.erb
2579
+ // be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/visitor.ts.erb
2580
2580
  class Visitor {
2581
2581
  visit(node) {
2582
2582
  if (!node)
@@ -2691,6 +2691,8 @@ class Printer extends Visitor {
2691
2691
  source;
2692
2692
  lines = [];
2693
2693
  indentLevel = 0;
2694
+ inlineMode = false;
2695
+ isInComplexNesting = false;
2694
2696
  constructor(source, options) {
2695
2697
  super();
2696
2698
  this.source = source;
@@ -2704,6 +2706,7 @@ class Printer extends Visitor {
2704
2706
  const node = object;
2705
2707
  this.lines = [];
2706
2708
  this.indentLevel = indentLevel;
2709
+ this.isInComplexNesting = false; // Reset for each top-level element
2707
2710
  if (typeof node.accept === 'function') {
2708
2711
  node.accept(this);
2709
2712
  }
@@ -2753,6 +2756,8 @@ class Printer extends Visitor {
2753
2756
  const tagName = open.tag_name?.value ?? "";
2754
2757
  const indent = this.indent();
2755
2758
  const attributes = open.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
2759
+ const inlineNodes = open.children.filter(child => !(child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') &&
2760
+ !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE'));
2756
2761
  const children = node.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
2757
2762
  !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
2758
2763
  const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
@@ -2761,7 +2766,7 @@ class Printer extends Visitor {
2761
2766
  this.push(indent + `<${tagName}`);
2762
2767
  return;
2763
2768
  }
2764
- if (attributes.length === 0) {
2769
+ if (attributes.length === 0 && inlineNodes.length === 0) {
2765
2770
  if (children.length === 0) {
2766
2771
  if (isSelfClosing) {
2767
2772
  this.push(indent + `<${tagName} />`);
@@ -2774,6 +2779,28 @@ class Printer extends Visitor {
2774
2779
  }
2775
2780
  return;
2776
2781
  }
2782
+ if (children.length >= 1) {
2783
+ if (this.isInComplexNesting) {
2784
+ if (children.length === 1) {
2785
+ const child = children[0];
2786
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2787
+ const textContent = child.content.trim();
2788
+ const singleLine = `<${tagName}>${textContent}</${tagName}>`;
2789
+ if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
2790
+ this.push(indent + singleLine);
2791
+ return;
2792
+ }
2793
+ }
2794
+ }
2795
+ }
2796
+ else {
2797
+ const inlineResult = this.tryRenderInline(children, tagName);
2798
+ if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
2799
+ this.push(indent + inlineResult);
2800
+ return;
2801
+ }
2802
+ }
2803
+ }
2777
2804
  this.push(indent + `<${tagName}>`);
2778
2805
  this.withIndent(() => {
2779
2806
  children.forEach(child => this.visit(child));
@@ -2783,14 +2810,40 @@ class Printer extends Visitor {
2783
2810
  }
2784
2811
  return;
2785
2812
  }
2786
- const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing);
2813
+ if (attributes.length === 0 && inlineNodes.length > 0) {
2814
+ const inline = this.renderInlineOpen(tagName, [], isSelfClosing, inlineNodes, open.children);
2815
+ if (children.length === 0) {
2816
+ if (isSelfClosing || node.is_void) {
2817
+ this.push(indent + inline);
2818
+ }
2819
+ else {
2820
+ this.push(indent + inline + `</${tagName}>`);
2821
+ }
2822
+ return;
2823
+ }
2824
+ this.push(indent + inline);
2825
+ this.withIndent(() => {
2826
+ children.forEach(child => this.visit(child));
2827
+ });
2828
+ if (!node.is_void && !isSelfClosing) {
2829
+ this.push(indent + `</${tagName}>`);
2830
+ }
2831
+ return;
2832
+ }
2833
+ const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
2787
2834
  const singleAttribute = attributes[0];
2788
- const hasEmptyValue = singleAttribute &&
2835
+ singleAttribute &&
2789
2836
  (singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
2790
2837
  singleAttribute.value?.children.length === 0;
2791
- const shouldKeepInline = attributes.length <= 3 &&
2792
- !hasEmptyValue &&
2793
- inline.length + indent.length <= this.maxLineLength;
2838
+ const hasERBControlFlow = inlineNodes.some(node => node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE' ||
2839
+ node instanceof ERBUnlessNode || node.type === 'AST_ERB_UNLESS_NODE' ||
2840
+ node instanceof ERBBlockNode || node.type === 'AST_ERB_BLOCK_NODE' ||
2841
+ node instanceof ERBCaseNode || node.type === 'AST_ERB_CASE_NODE' ||
2842
+ node instanceof ERBWhileNode || node.type === 'AST_ERB_WHILE_NODE' ||
2843
+ node instanceof ERBForNode || node.type === 'AST_ERB_FOR_NODE');
2844
+ const shouldKeepInline = (attributes.length <= 3 &&
2845
+ inline.length + indent.length <= this.maxLineLength) ||
2846
+ (inlineNodes.length > 0 && !hasERBControlFlow);
2794
2847
  if (shouldKeepInline) {
2795
2848
  if (children.length === 0) {
2796
2849
  if (isSelfClosing) {
@@ -2818,27 +2871,67 @@ class Printer extends Visitor {
2818
2871
  }
2819
2872
  return;
2820
2873
  }
2821
- this.push(indent + `<${tagName}`);
2822
- this.withIndent(() => {
2823
- attributes.forEach(attribute => {
2824
- this.push(this.indent() + this.renderAttribute(attribute));
2874
+ if (inlineNodes.length > 0 && hasERBControlFlow) {
2875
+ this.push(indent + `<${tagName}`);
2876
+ this.withIndent(() => {
2877
+ open.children.forEach(child => {
2878
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
2879
+ this.push(this.indent() + this.renderAttribute(child));
2880
+ }
2881
+ else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
2882
+ this.visit(child);
2883
+ }
2884
+ });
2825
2885
  });
2826
- });
2827
- if (isSelfClosing) {
2828
- this.push(indent + "/>");
2829
- }
2830
- else if (node.is_void) {
2831
- this.push(indent + ">");
2886
+ if (isSelfClosing) {
2887
+ this.push(indent + "/>");
2888
+ }
2889
+ else if (node.is_void) {
2890
+ this.push(indent + ">");
2891
+ }
2892
+ else if (children.length === 0) {
2893
+ this.push(indent + ">" + `</${tagName}>`);
2894
+ }
2895
+ else {
2896
+ this.push(indent + ">");
2897
+ this.withIndent(() => {
2898
+ children.forEach(child => this.visit(child));
2899
+ });
2900
+ this.push(indent + `</${tagName}>`);
2901
+ }
2832
2902
  }
2833
- else if (children.length === 0) {
2834
- this.push(indent + ">" + `</${tagName}>`);
2903
+ else if (inlineNodes.length > 0) {
2904
+ this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
2905
+ if (!isSelfClosing && !node.is_void && children.length > 0) {
2906
+ this.withIndent(() => {
2907
+ children.forEach(child => this.visit(child));
2908
+ });
2909
+ this.push(indent + `</${tagName}>`);
2910
+ }
2835
2911
  }
2836
2912
  else {
2837
- this.push(indent + ">");
2913
+ this.push(indent + `<${tagName}`);
2838
2914
  this.withIndent(() => {
2839
- children.forEach(child => this.visit(child));
2915
+ attributes.forEach(attribute => {
2916
+ this.push(this.indent() + this.renderAttribute(attribute));
2917
+ });
2840
2918
  });
2841
- this.push(indent + `</${tagName}>`);
2919
+ if (isSelfClosing) {
2920
+ this.push(indent + "/>");
2921
+ }
2922
+ else if (node.is_void) {
2923
+ this.push(indent + ">");
2924
+ }
2925
+ else if (children.length === 0) {
2926
+ this.push(indent + ">" + `</${tagName}>`);
2927
+ }
2928
+ else {
2929
+ this.push(indent + ">");
2930
+ this.withIndent(() => {
2931
+ children.forEach(child => this.visit(child));
2932
+ });
2933
+ this.push(indent + `</${tagName}>`);
2934
+ }
2842
2935
  }
2843
2936
  }
2844
2937
  visitHTMLOpenTagNode(node) {
@@ -2869,11 +2962,10 @@ class Printer extends Visitor {
2869
2962
  const attributes = node.attributes.filter((attribute) => attribute instanceof HTMLAttributeNode || attribute.type === 'AST_HTML_ATTRIBUTE_NODE');
2870
2963
  const inline = this.renderInlineOpen(tagName, attributes, true);
2871
2964
  const singleAttribute = attributes[0];
2872
- const hasEmptyValue = singleAttribute &&
2965
+ singleAttribute &&
2873
2966
  (singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
2874
2967
  singleAttribute.value?.children.length === 0;
2875
2968
  const shouldKeepInline = attributes.length <= 3 &&
2876
- !hasEmptyValue &&
2877
2969
  inline.length + indent.length <= this.maxLineLength;
2878
2970
  if (shouldKeepInline) {
2879
2971
  this.push(indent + inline);
@@ -3046,15 +3138,50 @@ class Printer extends Visitor {
3046
3138
  }
3047
3139
  }
3048
3140
  visitERBIfNode(node) {
3049
- this.printERBNode(node);
3050
- this.withIndent(() => {
3051
- node.statements.forEach(child => this.visit(child));
3052
- });
3053
- if (node.subsequent) {
3054
- this.visit(node.subsequent);
3141
+ if (this.inlineMode) {
3142
+ const open = node.tag_opening?.value ?? "";
3143
+ const content = node.content?.value ?? "";
3144
+ const close = node.tag_closing?.value ?? "";
3145
+ this.lines.push(open + content + close);
3146
+ if (node.statements.length > 0) {
3147
+ this.lines.push(" ");
3148
+ }
3149
+ node.statements.forEach((child, index) => {
3150
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
3151
+ this.lines.push(this.renderAttribute(child));
3152
+ }
3153
+ else {
3154
+ this.visit(child);
3155
+ }
3156
+ if (index < node.statements.length - 1) {
3157
+ this.lines.push(" ");
3158
+ }
3159
+ });
3160
+ if (node.statements.length > 0) {
3161
+ this.lines.push(" ");
3162
+ }
3163
+ if (node.subsequent) {
3164
+ this.visit(node.subsequent);
3165
+ }
3166
+ if (node.end_node) {
3167
+ const endNode = node.end_node;
3168
+ const endOpen = endNode.tag_opening?.value ?? "";
3169
+ const endContent = endNode.content?.value ?? "";
3170
+ const endClose = endNode.tag_closing?.value ?? "";
3171
+ this.lines.push(endOpen + endContent + endClose);
3172
+ }
3055
3173
  }
3056
- if (node.end_node) {
3057
- this.printERBNode(node.end_node);
3174
+ else {
3175
+ this.printERBNode(node);
3176
+ this.withIndent(() => {
3177
+ node.statements.forEach(child => this.visit(child));
3178
+ });
3179
+ if (node.subsequent) {
3180
+ this.visit(node.subsequent);
3181
+ }
3182
+ if (node.end_node) {
3183
+ this.printERBNode(node.end_node);
3184
+ }
3058
3185
  }
3059
3186
  }
3060
3187
  visitERBElseNode(node) {
@@ -3134,8 +3261,54 @@ class Printer extends Visitor {
3134
3261
  this.visit(node.end_node);
3135
3262
  }
3136
3263
  // --- Utility methods ---
3137
- renderInlineOpen(name, attributes, selfClose) {
3264
+ renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
3138
3265
  const parts = attributes.map(attribute => this.renderAttribute(attribute));
3266
+ if (inlineNodes.length > 0) {
3267
+ let result = `<${name}`;
3268
+ if (allChildren.length > 0) {
3269
+ const currentIndentLevel = this.indentLevel;
3270
+ this.indentLevel = 0;
3271
+ const tempLines = this.lines;
3272
+ this.lines = [];
3273
+ allChildren.forEach(child => {
3274
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
3275
+ this.lines.push(" " + this.renderAttribute(child));
3276
+ }
3277
+ else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
3278
+ const wasInlineMode = this.inlineMode;
3279
+ this.inlineMode = true;
3280
+ this.lines.push(" ");
3281
+ this.visit(child);
3282
+ this.inlineMode = wasInlineMode;
3283
+ }
3284
+ });
3285
+ const inlineContent = this.lines.join("");
3286
+ this.lines = tempLines;
3287
+ this.indentLevel = currentIndentLevel;
3288
+ result += inlineContent;
3289
+ }
3290
+ else {
3291
+ if (parts.length > 0) {
3292
+ result += ` ${parts.join(" ")}`;
3293
+ }
3294
+ const currentIndentLevel = this.indentLevel;
3295
+ this.indentLevel = 0;
3296
+ const tempLines = this.lines;
3297
+ this.lines = [];
3298
+ inlineNodes.forEach(node => {
3299
+ const wasInlineMode = this.inlineMode;
3300
+ this.inlineMode = true;
3301
+ this.visit(node);
3302
+ this.inlineMode = wasInlineMode;
3303
+ });
3304
+ const inlineContent = this.lines.join("");
3305
+ this.lines = tempLines;
3306
+ this.indentLevel = currentIndentLevel;
3307
+ result += inlineContent;
3308
+ }
3309
+ result += selfClose ? " />" : ">";
3310
+ return result;
3311
+ }
3139
3312
  return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " /" : ""}>`;
3140
3313
  }
3141
3314
  renderAttribute(attribute) {
@@ -3147,8 +3320,7 @@ class Printer extends Visitor {
3147
3320
  const open_quote = (attrValue.open_quote?.value ?? "");
3148
3321
  const close_quote = (attrValue.close_quote?.value ?? "");
3149
3322
  const attribute_value = attrValue.children.map((attr) => {
3150
- if (attr instanceof HTMLTextNode || attr.type === 'AST_HTML_TEXT_NODE' ||
3151
- attr instanceof LiteralNode || attr.type === 'AST_LITERAL_NODE') {
3323
+ if (attr instanceof HTMLTextNode || attr.type === 'AST_HTML_TEXT_NODE' || attr instanceof LiteralNode || attr.type === 'AST_LITERAL_NODE') {
3152
3324
  return attr.content;
3153
3325
  }
3154
3326
  else if (attr instanceof ERBContentNode || attr.type === 'AST_ERB_CONTENT_NODE') {
@@ -3161,6 +3333,124 @@ class Printer extends Visitor {
3161
3333
  }
3162
3334
  return name + equals + value;
3163
3335
  }
3336
+ /**
3337
+ * Try to render children inline if they are simple enough.
3338
+ * Returns the inline string if possible, null otherwise.
3339
+ */
3340
+ tryRenderInline(children, tagName, depth = 0) {
3341
+ if (children.length > 10) {
3342
+ return null;
3343
+ }
3344
+ const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3345
+ if (maxNestingDepth > 1) {
3346
+ this.isInComplexNesting = true;
3347
+ return null;
3348
+ }
3349
+ for (const child of children) {
3350
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3351
+ const textContent = child.content;
3352
+ if (textContent.includes('\n')) {
3353
+ return null;
3354
+ }
3355
+ }
3356
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3357
+ const element = child;
3358
+ const openTag = element.open_tag;
3359
+ const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3360
+ if (attributes.length > 0) {
3361
+ return null;
3362
+ }
3363
+ }
3364
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
3365
+ else {
3366
+ return null;
3367
+ }
3368
+ }
3369
+ const oldLines = this.lines;
3370
+ const oldInlineMode = this.inlineMode;
3371
+ try {
3372
+ this.lines = [];
3373
+ this.inlineMode = true;
3374
+ let content = '';
3375
+ for (const child of children) {
3376
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3377
+ content += child.content;
3378
+ }
3379
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3380
+ const element = child;
3381
+ const openTag = element.open_tag;
3382
+ const childTagName = openTag?.tag_name?.value || '';
3383
+ const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3384
+ const attributesString = attributes.length > 0
3385
+ ? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
3386
+ : '';
3387
+ const elementContent = this.renderElementInline(element);
3388
+ content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
3389
+ }
3390
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3391
+ const erbNode = child;
3392
+ const open = erbNode.tag_opening?.value ?? "";
3393
+ const erbContent = erbNode.content?.value ?? "";
3394
+ const close = erbNode.tag_closing?.value ?? "";
3395
+ content += `${open} ${erbContent.trim()} ${close}`;
3396
+ }
3397
+ }
3398
+ content = content.replace(/\s+/g, ' ').trim();
3399
+ return `<${tagName}>${content}</${tagName}>`;
3400
+ }
3401
+ finally {
3402
+ this.lines = oldLines;
3403
+ this.inlineMode = oldInlineMode;
3404
+ }
3405
+ }
3406
+ /**
3407
+ * Calculate the maximum nesting depth in a subtree of nodes.
3408
+ */
3409
+ getMaxNestingDepth(children, currentDepth) {
3410
+ let maxDepth = currentDepth;
3411
+ for (const child of children) {
3412
+ if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3413
+ const element = child;
3414
+ const elementChildren = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
3415
+ !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
3416
+ const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1);
3417
+ maxDepth = Math.max(maxDepth, childDepth);
3418
+ }
3419
+ }
3420
+ return maxDepth;
3421
+ }
3422
+ /**
3423
+ * Render an HTML element's content inline (without the wrapping tags).
3424
+ */
3425
+ renderElementInline(element) {
3426
+ const children = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
3427
+ !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
3428
+ let content = '';
3429
+ for (const child of children) {
3430
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3431
+ content += child.content;
3432
+ }
3433
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3434
+ const childElement = child;
3435
+ const openTag = childElement.open_tag;
3436
+ const childTagName = openTag?.tag_name?.value || '';
3437
+ const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3438
+ const attributesString = attributes.length > 0
3439
+ ? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
3440
+ : '';
3441
+ const childContent = this.renderElementInline(childElement);
3442
+ content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
3443
+ }
3444
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3445
+ const erbNode = child;
3446
+ const open = erbNode.tag_opening?.value ?? "";
3447
+ const erbContent = erbNode.content?.value ?? "";
3448
+ const close = erbNode.tag_closing?.value ?? "";
3449
+ content += `${open} ${erbContent.trim()} ${close}`;
3450
+ }
3451
+ }
3452
+ return content.replace(/\s+/g, ' ').trim();
3453
+ }
3164
3454
  }
3165
3455
 
3166
3456
  /**