@herb-tools/formatter 0.4.1 → 0.4.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/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-4/templates/javascript/packages/core/src/errors.ts.erb
134
+ // be modified manually. See /Users/marcoroth/Development/herb-release-7/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-4/templates/javascript/packages/core/src/nodes.ts.erb
584
+ // be modified manually. See /Users/marcoroth/Development/herb-release-7/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-4/templates/javascript/packages/core/src/visitor.ts.erb
2579
+ // be modified manually. See /Users/marcoroth/Development/herb-release-7/templates/javascript/packages/core/src/visitor.ts.erb
2580
2580
  class Visitor {
2581
2581
  visit(node) {
2582
2582
  if (!node)
@@ -2692,6 +2692,13 @@ class Printer extends Visitor {
2692
2692
  lines = [];
2693
2693
  indentLevel = 0;
2694
2694
  inlineMode = false;
2695
+ isInComplexNesting = false;
2696
+ static INLINE_ELEMENTS = new Set([
2697
+ 'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'cite', 'code',
2698
+ 'dfn', 'em', 'i', 'img', 'kbd', 'label', 'map', 'object', 'q',
2699
+ 'samp', 'small', 'span', 'strong', 'sub', 'sup',
2700
+ 'tt', 'var', 'del', 'ins', 'mark', 's', 'u', 'time', 'wbr'
2701
+ ]);
2695
2702
  constructor(source, options) {
2696
2703
  super();
2697
2704
  this.source = source;
@@ -2705,13 +2712,14 @@ class Printer extends Visitor {
2705
2712
  const node = object;
2706
2713
  this.lines = [];
2707
2714
  this.indentLevel = indentLevel;
2715
+ this.isInComplexNesting = false; // Reset for each top-level element
2708
2716
  if (typeof node.accept === 'function') {
2709
2717
  node.accept(this);
2710
2718
  }
2711
2719
  else {
2712
2720
  this.visit(node);
2713
2721
  }
2714
- return this.lines.filter(Boolean).join("\n");
2722
+ return this.lines.join("\n");
2715
2723
  }
2716
2724
  push(line) {
2717
2725
  this.lines.push(line);
@@ -2726,38 +2734,167 @@ class Printer extends Visitor {
2726
2734
  return " ".repeat(this.indentLevel * this.indentWidth);
2727
2735
  }
2728
2736
  /**
2729
- * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
2737
+ * Format ERB content with proper spacing around the inner content.
2738
+ * Returns empty string if content is empty, otherwise wraps content with single spaces.
2730
2739
  */
2731
- printERBNode(node) {
2740
+ formatERBContent(content) {
2741
+ return content.trim() ? ` ${content.trim()} ` : "";
2742
+ }
2743
+ /**
2744
+ * Check if a node is an ERB control flow node (if, unless, block, case, while, for)
2745
+ */
2746
+ isERBControlFlow(node) {
2747
+ return node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE' ||
2748
+ node instanceof ERBUnlessNode || node.type === 'AST_ERB_UNLESS_NODE' ||
2749
+ node instanceof ERBBlockNode || node.type === 'AST_ERB_BLOCK_NODE' ||
2750
+ node instanceof ERBCaseNode || node.type === 'AST_ERB_CASE_NODE' ||
2751
+ node instanceof ERBCaseMatchNode || node.type === 'AST_ERB_CASE_MATCH_NODE' ||
2752
+ node instanceof ERBWhileNode || node.type === 'AST_ERB_WHILE_NODE' ||
2753
+ node instanceof ERBForNode || node.type === 'AST_ERB_FOR_NODE';
2754
+ }
2755
+ /**
2756
+ * Count total attributes including those inside ERB conditionals
2757
+ */
2758
+ getTotalAttributeCount(attributes, inlineNodes = []) {
2759
+ let totalAttributeCount = attributes.length;
2760
+ inlineNodes.forEach(node => {
2761
+ if (this.isERBControlFlow(node)) {
2762
+ const erbNode = node;
2763
+ if (erbNode.statements) {
2764
+ totalAttributeCount += erbNode.statements.length;
2765
+ }
2766
+ }
2767
+ });
2768
+ return totalAttributeCount;
2769
+ }
2770
+ /**
2771
+ * Extract HTML attributes from a list of nodes
2772
+ */
2773
+ extractAttributes(nodes) {
2774
+ return nodes.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
2775
+ }
2776
+ /**
2777
+ * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
2778
+ */
2779
+ extractInlineNodes(nodes) {
2780
+ return nodes.filter(child => !(child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') &&
2781
+ !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE'));
2782
+ }
2783
+ /**
2784
+ * Render attributes as a space-separated string
2785
+ */
2786
+ renderAttributesString(attributes) {
2787
+ if (attributes.length === 0)
2788
+ return "";
2789
+ return ` ${attributes.map(attr => this.renderAttribute(attr)).join(" ")}`;
2790
+ }
2791
+ /**
2792
+ * Determine if a tag should be rendered inline based on attribute count and other factors
2793
+ */
2794
+ shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false, nestingDepth = 0, inlineNodesLength = 0) {
2795
+ if (hasComplexERB)
2796
+ return false;
2797
+ // Special case: no attributes at all, always inline if it fits
2798
+ if (totalAttributeCount === 0) {
2799
+ return inlineLength + indentLength <= maxLineLength;
2800
+ }
2801
+ const basicInlineCondition = totalAttributeCount <= 3 &&
2802
+ inlineLength + indentLength <= maxLineLength;
2803
+ const erbInlineCondition = inlineNodesLength > 0 && totalAttributeCount <= 3;
2804
+ return basicInlineCondition || erbInlineCondition;
2805
+ }
2806
+ /**
2807
+ * Render multiline attributes for a tag
2808
+ */
2809
+ renderMultilineAttributes(tagName, attributes, inlineNodes = [], allChildren = [], isSelfClosing = false, isVoid = false, hasBodyContent = false) {
2732
2810
  const indent = this.indent();
2733
- const open = node.tag_opening?.value ?? "";
2734
- const close = node.tag_closing?.value ?? "";
2735
- let inner;
2736
- if (node.tag_opening && node.tag_closing) {
2737
- const [, openingEnd] = node.tag_opening.range.toArray();
2738
- const [closingStart] = node.tag_closing.range.toArray();
2739
- const rawInner = this.source.slice(openingEnd, closingStart);
2740
- inner = ` ${rawInner.trim()} `;
2811
+ this.push(indent + `<${tagName}`);
2812
+ this.withIndent(() => {
2813
+ // Render children in order, handling both attributes and ERB nodes
2814
+ allChildren.forEach(child => {
2815
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
2816
+ this.push(this.indent() + this.renderAttribute(child));
2817
+ }
2818
+ else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
2819
+ this.visit(child);
2820
+ }
2821
+ });
2822
+ });
2823
+ if (isSelfClosing) {
2824
+ this.push(indent + "/>");
2825
+ }
2826
+ else if (isVoid) {
2827
+ this.push(indent + ">");
2828
+ }
2829
+ else if (!hasBodyContent) {
2830
+ this.push(indent + `></${tagName}>`);
2741
2831
  }
2742
2832
  else {
2743
- const txt = node.content?.value ?? "";
2744
- inner = txt.trim() ? ` ${txt.trim()} ` : "";
2833
+ this.push(indent + ">");
2745
2834
  }
2835
+ }
2836
+ /**
2837
+ * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
2838
+ */
2839
+ printERBNode(node) {
2840
+ const indent = this.inlineMode ? "" : this.indent();
2841
+ const open = node.tag_opening?.value ?? "";
2842
+ const close = node.tag_closing?.value ?? "";
2843
+ const content = node.content?.value ?? "";
2844
+ const inner = this.formatERBContent(content);
2746
2845
  this.push(indent + open + inner + close);
2747
2846
  }
2748
2847
  // --- Visitor methods ---
2749
2848
  visitDocumentNode(node) {
2750
- node.children.forEach(child => this.visit(child));
2849
+ let lastWasMeaningful = false;
2850
+ let hasHandledSpacing = false;
2851
+ for (let i = 0; i < node.children.length; i++) {
2852
+ const child = node.children[i];
2853
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2854
+ const textNode = child;
2855
+ const isWhitespaceOnly = textNode.content.trim() === "";
2856
+ if (isWhitespaceOnly) {
2857
+ const hasPrevNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1]);
2858
+ const hasNextNonWhitespace = i < node.children.length - 1 && this.isNonWhitespaceNode(node.children[i + 1]);
2859
+ const hasMultipleNewlines = textNode.content.includes('\n\n');
2860
+ if (hasPrevNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
2861
+ this.push("");
2862
+ hasHandledSpacing = true;
2863
+ }
2864
+ continue;
2865
+ }
2866
+ }
2867
+ if (this.isNonWhitespaceNode(child) && lastWasMeaningful && !hasHandledSpacing) {
2868
+ this.push("");
2869
+ }
2870
+ this.visit(child);
2871
+ if (this.isNonWhitespaceNode(child)) {
2872
+ lastWasMeaningful = true;
2873
+ hasHandledSpacing = false;
2874
+ }
2875
+ }
2751
2876
  }
2752
2877
  visitHTMLElementNode(node) {
2753
2878
  const open = node.open_tag;
2754
2879
  const tagName = open.tag_name?.value ?? "";
2755
2880
  const indent = this.indent();
2756
- const attributes = open.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
2757
- const inlineNodes = open.children.filter(child => !(child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') &&
2758
- !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE'));
2759
- const children = node.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
2760
- !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
2881
+ const attributes = this.extractAttributes(open.children);
2882
+ const inlineNodes = this.extractInlineNodes(open.children);
2883
+ const hasTextFlow = this.isInTextFlowContext(null, node.body);
2884
+ const children = node.body.filter(child => {
2885
+ if (child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') {
2886
+ return false;
2887
+ }
2888
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2889
+ const content = child.content;
2890
+ if (hasTextFlow && content === " ") {
2891
+ return true;
2892
+ }
2893
+ return content.trim() !== "";
2894
+ }
2895
+ return true;
2896
+ });
2897
+ const isInlineElement = this.isInlineElement(tagName);
2761
2898
  const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
2762
2899
  const isSelfClosing = open.tag_closing?.value === "/>";
2763
2900
  if (!hasClosing) {
@@ -2777,9 +2914,68 @@ class Printer extends Visitor {
2777
2914
  }
2778
2915
  return;
2779
2916
  }
2917
+ if (children.length >= 1) {
2918
+ if (this.isInComplexNesting) {
2919
+ if (children.length === 1) {
2920
+ const child = children[0];
2921
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2922
+ const textContent = child.content.trim();
2923
+ const singleLine = `<${tagName}>${textContent}</${tagName}>`;
2924
+ if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
2925
+ this.push(indent + singleLine);
2926
+ return;
2927
+ }
2928
+ }
2929
+ }
2930
+ }
2931
+ else {
2932
+ const inlineResult = this.tryRenderInline(children, tagName, 0, false, hasTextFlow);
2933
+ if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
2934
+ this.push(indent + inlineResult);
2935
+ return;
2936
+ }
2937
+ if (hasTextFlow) {
2938
+ const hasAnyNewlines = children.some(child => {
2939
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2940
+ return child.content.includes('\n');
2941
+ }
2942
+ return false;
2943
+ });
2944
+ if (!hasAnyNewlines) {
2945
+ const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children);
2946
+ if (fullInlineResult) {
2947
+ const totalLength = indent.length + fullInlineResult.length;
2948
+ const maxNesting = this.getMaxNestingDepth(children, 0);
2949
+ const maxInlineLength = maxNesting <= 1 ? this.maxLineLength : 60;
2950
+ if (totalLength <= maxInlineLength) {
2951
+ this.push(indent + fullInlineResult);
2952
+ return;
2953
+ }
2954
+ }
2955
+ }
2956
+ }
2957
+ }
2958
+ }
2959
+ if (hasTextFlow) {
2960
+ const fullInlineResult = this.tryRenderInlineFull(node, tagName, [], children);
2961
+ if (fullInlineResult) {
2962
+ const totalLength = indent.length + fullInlineResult.length;
2963
+ const maxNesting = this.getMaxNestingDepth(children, 0);
2964
+ const maxInlineLength = maxNesting <= 1 ? this.maxLineLength : 60;
2965
+ if (totalLength <= maxInlineLength) {
2966
+ this.push(indent + fullInlineResult);
2967
+ return;
2968
+ }
2969
+ }
2970
+ }
2780
2971
  this.push(indent + `<${tagName}>`);
2781
2972
  this.withIndent(() => {
2782
- children.forEach(child => this.visit(child));
2973
+ if (hasTextFlow) {
2974
+ this.visitTextFlowChildren(children);
2975
+ }
2976
+ else {
2977
+ children.forEach(child => this.visit(child));
2978
+ }
2783
2979
  });
2784
2980
  if (!node.is_void && !isSelfClosing) {
2785
2981
  this.push(indent + `</${tagName}>`);
@@ -2806,15 +3002,24 @@ class Printer extends Visitor {
2806
3002
  }
2807
3003
  return;
2808
3004
  }
2809
- const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
2810
- const singleAttribute = attributes[0];
2811
- const hasEmptyValue = singleAttribute &&
2812
- (singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
2813
- singleAttribute.value?.children.length === 0;
2814
- const shouldKeepInline = (attributes.length <= 3 &&
2815
- !hasEmptyValue &&
2816
- inline.length + indent.length <= this.maxLineLength) ||
2817
- inlineNodes.length > 0;
3005
+ const hasERBControlFlow = inlineNodes.some(node => this.isERBControlFlow(node)) ||
3006
+ open.children.some(node => this.isERBControlFlow(node));
3007
+ const hasComplexERB = hasERBControlFlow && inlineNodes.some(node => {
3008
+ if (node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE') {
3009
+ const erbNode = node;
3010
+ if (erbNode.statements.length > 0 && erbNode.location) {
3011
+ const startLine = erbNode.location.start.line;
3012
+ const endLine = erbNode.location.end.line;
3013
+ return startLine !== endLine;
3014
+ }
3015
+ return false;
3016
+ }
3017
+ return false;
3018
+ });
3019
+ const inline = hasComplexERB ? "" : this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
3020
+ const nestingDepth = this.getMaxNestingDepth(children, 0);
3021
+ const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3022
+ const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, hasComplexERB, nestingDepth, inlineNodes.length);
2818
3023
  if (shouldKeepInline) {
2819
3024
  if (children.length === 0) {
2820
3025
  if (isSelfClosing) {
@@ -2824,7 +3029,53 @@ class Printer extends Visitor {
2824
3029
  this.push(indent + inline);
2825
3030
  }
2826
3031
  else {
2827
- this.push(indent + inline.replace('>', `></${tagName}>`));
3032
+ let result = `<${tagName}`;
3033
+ result += this.renderAttributesString(attributes);
3034
+ if (inlineNodes.length > 0) {
3035
+ const currentIndentLevel = this.indentLevel;
3036
+ this.indentLevel = 0;
3037
+ const tempLines = this.lines;
3038
+ this.lines = [];
3039
+ inlineNodes.forEach(node => {
3040
+ const wasInlineMode = this.inlineMode;
3041
+ if (!this.isERBControlFlow(node)) {
3042
+ this.inlineMode = true;
3043
+ }
3044
+ this.visit(node);
3045
+ this.inlineMode = wasInlineMode;
3046
+ });
3047
+ const inlineContent = this.lines.join("");
3048
+ this.lines = tempLines;
3049
+ this.indentLevel = currentIndentLevel;
3050
+ result += inlineContent;
3051
+ }
3052
+ result += `></${tagName}>`;
3053
+ this.push(indent + result);
3054
+ }
3055
+ return;
3056
+ }
3057
+ if (isInlineElement && children.length > 0 && !hasERBControlFlow) {
3058
+ const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children);
3059
+ if (fullInlineResult) {
3060
+ const totalLength = indent.length + fullInlineResult.length;
3061
+ if (totalLength <= this.maxLineLength || totalLength <= 120) {
3062
+ this.push(indent + fullInlineResult);
3063
+ return;
3064
+ }
3065
+ }
3066
+ }
3067
+ if (!isInlineElement && children.length > 0 && !hasERBControlFlow) {
3068
+ this.push(indent + inline);
3069
+ this.withIndent(() => {
3070
+ if (hasTextFlow) {
3071
+ this.visitTextFlowChildren(children);
3072
+ }
3073
+ else {
3074
+ children.forEach(child => this.visit(child));
3075
+ }
3076
+ });
3077
+ if (!node.is_void && !isSelfClosing) {
3078
+ this.push(indent + `</${tagName}>`);
2828
3079
  }
2829
3080
  return;
2830
3081
  }
@@ -2835,15 +3086,20 @@ class Printer extends Visitor {
2835
3086
  this.push(indent + inline);
2836
3087
  }
2837
3088
  this.withIndent(() => {
2838
- children.forEach(child => this.visit(child));
3089
+ if (hasTextFlow) {
3090
+ this.visitTextFlowChildren(children);
3091
+ }
3092
+ else {
3093
+ children.forEach(child => this.visit(child));
3094
+ }
2839
3095
  });
2840
3096
  if (!node.is_void && !isSelfClosing) {
2841
3097
  this.push(indent + `</${tagName}>`);
2842
3098
  }
2843
3099
  return;
2844
3100
  }
2845
- if (inlineNodes.length > 0) {
2846
- this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
3101
+ if (inlineNodes.length > 0 && hasERBControlFlow) {
3102
+ this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
2847
3103
  if (!isSelfClosing && !node.is_void && children.length > 0) {
2848
3104
  this.withIndent(() => {
2849
3105
  children.forEach(child => this.visit(child));
@@ -2851,26 +3107,50 @@ class Printer extends Visitor {
2851
3107
  this.push(indent + `</${tagName}>`);
2852
3108
  }
2853
3109
  }
2854
- else {
2855
- this.push(indent + `<${tagName}`);
2856
- this.withIndent(() => {
2857
- attributes.forEach(attribute => {
2858
- this.push(this.indent() + this.renderAttribute(attribute));
3110
+ else if (inlineNodes.length > 0) {
3111
+ this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
3112
+ if (!isSelfClosing && !node.is_void && children.length > 0) {
3113
+ this.withIndent(() => {
3114
+ children.forEach(child => this.visit(child));
2859
3115
  });
2860
- });
2861
- if (isSelfClosing) {
2862
- this.push(indent + "/>");
3116
+ this.push(indent + `</${tagName}>`);
2863
3117
  }
2864
- else if (node.is_void) {
2865
- this.push(indent + ">");
3118
+ }
3119
+ else {
3120
+ if (isInlineElement && children.length > 0) {
3121
+ const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children);
3122
+ if (fullInlineResult) {
3123
+ const totalLength = indent.length + fullInlineResult.length;
3124
+ if (totalLength <= this.maxLineLength || totalLength <= 120) {
3125
+ this.push(indent + fullInlineResult);
3126
+ return;
3127
+ }
3128
+ }
2866
3129
  }
2867
- else if (children.length === 0) {
2868
- this.push(indent + ">" + `</${tagName}>`);
3130
+ if (isInlineElement && children.length === 0) {
3131
+ let result = `<${tagName}`;
3132
+ result += this.renderAttributesString(attributes);
3133
+ if (isSelfClosing) {
3134
+ result += " />";
3135
+ }
3136
+ else if (node.is_void) {
3137
+ result += ">";
3138
+ }
3139
+ else {
3140
+ result += `></${tagName}>`;
3141
+ }
3142
+ this.push(indent + result);
3143
+ return;
2869
3144
  }
2870
- else {
2871
- this.push(indent + ">");
3145
+ this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
3146
+ if (!isSelfClosing && !node.is_void && children.length > 0) {
2872
3147
  this.withIndent(() => {
2873
- children.forEach(child => this.visit(child));
3148
+ if (hasTextFlow) {
3149
+ this.visitTextFlowChildren(children);
3150
+ }
3151
+ else {
3152
+ children.forEach(child => this.visit(child));
3153
+ }
2874
3154
  });
2875
3155
  this.push(indent + `</${tagName}>`);
2876
3156
  }
@@ -2879,48 +3159,35 @@ class Printer extends Visitor {
2879
3159
  visitHTMLOpenTagNode(node) {
2880
3160
  const tagName = node.tag_name?.value ?? "";
2881
3161
  const indent = this.indent();
2882
- const attributes = node.children.filter((attribute) => attribute instanceof HTMLAttributeNode || attribute.type === 'AST_HTML_ATTRIBUTE_NODE');
3162
+ const attributes = this.extractAttributes(node.children);
3163
+ const inlineNodes = this.extractInlineNodes(node.children);
2883
3164
  const hasClosing = node.tag_closing?.value === ">";
2884
3165
  if (!hasClosing) {
2885
3166
  this.push(indent + `<${tagName}`);
2886
3167
  return;
2887
3168
  }
2888
- const inline = this.renderInlineOpen(tagName, attributes, node.is_void);
2889
- if (attributes.length === 0 || inline.length + indent.length <= this.maxLineLength) {
3169
+ const inline = this.renderInlineOpen(tagName, attributes, node.is_void, inlineNodes, node.children);
3170
+ const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3171
+ const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, false, 0, inlineNodes.length);
3172
+ if (shouldKeepInline) {
2890
3173
  this.push(indent + inline);
2891
3174
  return;
2892
3175
  }
2893
- this.push(indent + `<${tagName}`);
2894
- this.withIndent(() => {
2895
- attributes.forEach(attribute => {
2896
- this.push(this.indent() + this.renderAttribute(attribute));
2897
- });
2898
- });
2899
- this.push(indent + (node.is_void ? "/>" : ">"));
3176
+ this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.children, false, node.is_void, false);
2900
3177
  }
2901
3178
  visitHTMLSelfCloseTagNode(node) {
2902
3179
  const tagName = node.tag_name?.value ?? "";
2903
3180
  const indent = this.indent();
2904
- const attributes = node.attributes.filter((attribute) => attribute instanceof HTMLAttributeNode || attribute.type === 'AST_HTML_ATTRIBUTE_NODE');
2905
- const inline = this.renderInlineOpen(tagName, attributes, true);
2906
- const singleAttribute = attributes[0];
2907
- const hasEmptyValue = singleAttribute &&
2908
- (singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
2909
- singleAttribute.value?.children.length === 0;
2910
- const shouldKeepInline = attributes.length <= 3 &&
2911
- !hasEmptyValue &&
2912
- inline.length + indent.length <= this.maxLineLength;
3181
+ const attributes = this.extractAttributes(node.attributes);
3182
+ const inlineNodes = this.extractInlineNodes(node.attributes);
3183
+ const inline = this.renderInlineOpen(tagName, attributes, true, inlineNodes, node.attributes);
3184
+ const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3185
+ const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, false, 0, inlineNodes.length);
2913
3186
  if (shouldKeepInline) {
2914
3187
  this.push(indent + inline);
2915
3188
  return;
2916
3189
  }
2917
- this.push(indent + `<${tagName}`);
2918
- this.withIndent(() => {
2919
- attributes.forEach(attribute => {
2920
- this.push(this.indent() + this.renderAttribute(attribute));
2921
- });
2922
- });
2923
- this.push(indent + "/>");
3190
+ this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false);
2924
3191
  }
2925
3192
  visitHTMLCloseTagNode(node) {
2926
3193
  const indent = this.indent();
@@ -2930,6 +3197,13 @@ class Printer extends Visitor {
2930
3197
  this.push(indent + open + name + close);
2931
3198
  }
2932
3199
  visitHTMLTextNode(node) {
3200
+ if (this.inlineMode) {
3201
+ const normalizedContent = node.content.replace(/\s+/g, ' ').trim();
3202
+ if (normalizedContent) {
3203
+ this.push(normalizedContent);
3204
+ }
3205
+ return;
3206
+ }
2933
3207
  const indent = this.indent();
2934
3208
  let text = node.content.trim();
2935
3209
  if (!text)
@@ -3063,9 +3337,17 @@ class Printer extends Visitor {
3063
3337
  }
3064
3338
  visitERBInNode(node) {
3065
3339
  this.printERBNode(node);
3340
+ this.withIndent(() => {
3341
+ node.statements.forEach(stmt => this.visit(stmt));
3342
+ });
3066
3343
  }
3067
3344
  visitERBCaseMatchNode(node) {
3068
3345
  this.printERBNode(node);
3346
+ node.conditions.forEach(condition => this.visit(condition));
3347
+ if (node.else_clause)
3348
+ this.visit(node.else_clause);
3349
+ if (node.end_node)
3350
+ this.visit(node.end_node);
3069
3351
  }
3070
3352
  visitERBBlockNode(node) {
3071
3353
  const indent = this.indent();
@@ -3085,21 +3367,30 @@ class Printer extends Visitor {
3085
3367
  const open = node.tag_opening?.value ?? "";
3086
3368
  const content = node.content?.value ?? "";
3087
3369
  const close = node.tag_closing?.value ?? "";
3088
- this.lines.push(open + content + close);
3089
- node.statements.forEach(child => {
3370
+ const inner = this.formatERBContent(content);
3371
+ this.lines.push(open + inner + close);
3372
+ node.statements.forEach((child, _index) => {
3373
+ this.lines.push(" ");
3090
3374
  if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
3091
- this.lines.push(" " + this.renderAttribute(child) + " ");
3375
+ this.lines.push(this.renderAttribute(child));
3092
3376
  }
3093
3377
  else {
3094
3378
  this.visit(child);
3095
3379
  }
3096
3380
  });
3381
+ if (node.statements.length > 0 && node.end_node) {
3382
+ this.lines.push(" ");
3383
+ }
3384
+ if (node.subsequent) {
3385
+ this.visit(node.subsequent);
3386
+ }
3097
3387
  if (node.end_node) {
3098
3388
  const endNode = node.end_node;
3099
3389
  const endOpen = endNode.tag_opening?.value ?? "";
3100
3390
  const endContent = endNode.content?.value ?? "";
3101
3391
  const endClose = endNode.tag_closing?.value ?? "";
3102
- this.lines.push(endOpen + endContent + endClose);
3392
+ const endInner = this.formatERBContent(endContent);
3393
+ this.lines.push(endOpen + endInner + endClose);
3103
3394
  }
3104
3395
  }
3105
3396
  else {
@@ -3128,7 +3419,6 @@ class Printer extends Visitor {
3128
3419
  });
3129
3420
  }
3130
3421
  visitERBCaseNode(node) {
3131
- this.indentLevel;
3132
3422
  const indent = this.indent();
3133
3423
  const open = node.tag_opening?.value ?? "";
3134
3424
  const content = node.content?.value ?? "";
@@ -3192,6 +3482,147 @@ class Printer extends Visitor {
3192
3482
  this.visit(node.end_node);
3193
3483
  }
3194
3484
  // --- Utility methods ---
3485
+ isNonWhitespaceNode(node) {
3486
+ if (node instanceof HTMLTextNode || node.type === 'AST_HTML_TEXT_NODE') {
3487
+ return node.content.trim() !== "";
3488
+ }
3489
+ if (node instanceof WhitespaceNode || node.type === 'AST_WHITESPACE_NODE') {
3490
+ return false;
3491
+ }
3492
+ return true;
3493
+ }
3494
+ /**
3495
+ * Check if an element should be treated as inline based on its tag name
3496
+ */
3497
+ isInlineElement(tagName) {
3498
+ return Printer.INLINE_ELEMENTS.has(tagName.toLowerCase());
3499
+ }
3500
+ /**
3501
+ * Check if we're in a text flow context (parent contains mixed text and inline elements)
3502
+ */
3503
+ visitTextFlowChildren(children) {
3504
+ const indent = this.indent();
3505
+ let currentLineContent = "";
3506
+ for (const child of children) {
3507
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3508
+ const content = child.content;
3509
+ let processedContent = content.replace(/\s+/g, ' ').trim();
3510
+ if (processedContent) {
3511
+ const hasLeadingSpace = /^\s/.test(content);
3512
+ if (currentLineContent && hasLeadingSpace && !currentLineContent.endsWith(' ')) {
3513
+ currentLineContent += ' ';
3514
+ }
3515
+ currentLineContent += processedContent;
3516
+ const hasTrailingSpace = /\s$/.test(content);
3517
+ if (hasTrailingSpace && !currentLineContent.endsWith(' ')) {
3518
+ currentLineContent += ' ';
3519
+ }
3520
+ if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
3521
+ this.visitTextFlowChildrenMultiline(children);
3522
+ return;
3523
+ }
3524
+ }
3525
+ }
3526
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3527
+ const element = child;
3528
+ const openTag = element.open_tag;
3529
+ const childTagName = openTag?.tag_name?.value || '';
3530
+ if (this.isInlineElement(childTagName)) {
3531
+ const childInline = this.tryRenderInlineFull(element, childTagName, this.extractAttributes(openTag.children), element.body.filter(c => !(c instanceof WhitespaceNode || c.type === 'AST_WHITESPACE_NODE') &&
3532
+ !((c instanceof HTMLTextNode || c.type === 'AST_HTML_TEXT_NODE') && c?.content.trim() === "")));
3533
+ if (childInline) {
3534
+ currentLineContent += childInline;
3535
+ if ((indent.length + currentLineContent.length) > this.maxLineLength) {
3536
+ this.visitTextFlowChildrenMultiline(children);
3537
+ return;
3538
+ }
3539
+ }
3540
+ else {
3541
+ if (currentLineContent.trim()) {
3542
+ this.push(indent + currentLineContent.trim());
3543
+ currentLineContent = "";
3544
+ }
3545
+ this.visit(child);
3546
+ }
3547
+ }
3548
+ else {
3549
+ if (currentLineContent.trim()) {
3550
+ this.push(indent + currentLineContent.trim());
3551
+ currentLineContent = "";
3552
+ }
3553
+ this.visit(child);
3554
+ }
3555
+ }
3556
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3557
+ const oldLines = this.lines;
3558
+ const oldInlineMode = this.inlineMode;
3559
+ try {
3560
+ this.lines = [];
3561
+ this.inlineMode = true;
3562
+ this.visit(child);
3563
+ const erbContent = this.lines.join("");
3564
+ currentLineContent += erbContent;
3565
+ if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
3566
+ this.visitTextFlowChildrenMultiline(children);
3567
+ return;
3568
+ }
3569
+ }
3570
+ finally {
3571
+ this.lines = oldLines;
3572
+ this.inlineMode = oldInlineMode;
3573
+ }
3574
+ }
3575
+ else {
3576
+ if (currentLineContent.trim()) {
3577
+ this.push(indent + currentLineContent.trim());
3578
+ currentLineContent = "";
3579
+ }
3580
+ this.visit(child);
3581
+ }
3582
+ }
3583
+ if (currentLineContent.trim()) {
3584
+ const finalLine = indent + currentLineContent.trim();
3585
+ if (finalLine.length > Math.max(this.maxLineLength, 120)) {
3586
+ this.visitTextFlowChildrenMultiline(children);
3587
+ return;
3588
+ }
3589
+ this.push(finalLine);
3590
+ }
3591
+ }
3592
+ visitTextFlowChildrenMultiline(children) {
3593
+ children.forEach(child => this.visit(child));
3594
+ }
3595
+ isInTextFlowContext(parent, children) {
3596
+ const hasTextContent = children.some(child => (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') &&
3597
+ child.content.trim() !== "");
3598
+ if (!hasTextContent) {
3599
+ return false;
3600
+ }
3601
+ const nonTextChildren = children.filter(child => !(child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE'));
3602
+ if (nonTextChildren.length === 0) {
3603
+ return false;
3604
+ }
3605
+ const allInline = nonTextChildren.every(child => {
3606
+ if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3607
+ return true;
3608
+ }
3609
+ if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3610
+ const element = child;
3611
+ const openTag = element.open_tag;
3612
+ const tagName = openTag?.tag_name?.value || '';
3613
+ return this.isInlineElement(tagName);
3614
+ }
3615
+ return false;
3616
+ });
3617
+ if (!allInline) {
3618
+ return false;
3619
+ }
3620
+ const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3621
+ if (maxNestingDepth > 2) {
3622
+ return false;
3623
+ }
3624
+ return true;
3625
+ }
3195
3626
  renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
3196
3627
  const parts = attributes.map(attribute => this.renderAttribute(attribute));
3197
3628
  if (inlineNodes.length > 0) {
@@ -3228,7 +3659,9 @@ class Printer extends Visitor {
3228
3659
  this.lines = [];
3229
3660
  inlineNodes.forEach(node => {
3230
3661
  const wasInlineMode = this.inlineMode;
3231
- this.inlineMode = true;
3662
+ if (!this.isERBControlFlow(node)) {
3663
+ this.inlineMode = true;
3664
+ }
3232
3665
  this.visit(node);
3233
3666
  this.inlineMode = wasInlineMode;
3234
3667
  });
@@ -3247,23 +3680,255 @@ class Printer extends Visitor {
3247
3680
  const equals = attribute.equals?.value ?? "";
3248
3681
  let value = "";
3249
3682
  if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || attribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
3250
- const attrValue = attribute.value;
3251
- const open_quote = (attrValue.open_quote?.value ?? "");
3252
- const close_quote = (attrValue.close_quote?.value ?? "");
3253
- const attribute_value = attrValue.children.map((attr) => {
3254
- if (attr instanceof HTMLTextNode || attr.type === 'AST_HTML_TEXT_NODE' || attr instanceof LiteralNode || attr.type === 'AST_LITERAL_NODE') {
3255
- return attr.content;
3683
+ const attributeValue = attribute.value;
3684
+ let open_quote = attributeValue.open_quote?.value ?? "";
3685
+ let close_quote = attributeValue.close_quote?.value ?? "";
3686
+ let htmlTextContent = "";
3687
+ const content = attributeValue.children.map((child) => {
3688
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE' || child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
3689
+ const textContent = child.content;
3690
+ htmlTextContent += textContent;
3691
+ return textContent;
3256
3692
  }
3257
- else if (attr instanceof ERBContentNode || attr.type === 'AST_ERB_CONTENT_NODE') {
3258
- const erbAttr = attr;
3259
- return (erbAttr.tag_opening.value + erbAttr.content.value + erbAttr.tag_closing.value);
3693
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3694
+ const erbAttribute = child;
3695
+ return erbAttribute.tag_opening.value + erbAttribute.content.value + erbAttribute.tag_closing.value;
3260
3696
  }
3261
3697
  return "";
3262
3698
  }).join("");
3263
- value = open_quote + attribute_value + close_quote;
3699
+ if (open_quote === "" && close_quote === "") {
3700
+ open_quote = '"';
3701
+ close_quote = '"';
3702
+ }
3703
+ else if (open_quote === "'" && close_quote === "'" && !htmlTextContent.includes('"')) {
3704
+ open_quote = '"';
3705
+ close_quote = '"';
3706
+ }
3707
+ value = open_quote + content + close_quote;
3264
3708
  }
3265
3709
  return name + equals + value;
3266
3710
  }
3711
+ /**
3712
+ * Try to render a complete element inline including opening tag, children, and closing tag
3713
+ */
3714
+ tryRenderInlineFull(node, tagName, attributes, children) {
3715
+ let result = `<${tagName}`;
3716
+ result += this.renderAttributesString(attributes);
3717
+ result += ">";
3718
+ const childrenContent = this.tryRenderChildrenInline(children);
3719
+ if (!childrenContent) {
3720
+ return null;
3721
+ }
3722
+ result += childrenContent;
3723
+ result += `</${tagName}>`;
3724
+ return result;
3725
+ }
3726
+ /**
3727
+ * Try to render just the children inline (without tags)
3728
+ */
3729
+ tryRenderChildrenInline(children) {
3730
+ let result = "";
3731
+ for (const child of children) {
3732
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3733
+ const content = child.content;
3734
+ const normalizedContent = content.replace(/\s+/g, ' ');
3735
+ const hasLeadingSpace = /^\s/.test(content);
3736
+ const hasTrailingSpace = /\s$/.test(content);
3737
+ const trimmedContent = normalizedContent.trim();
3738
+ if (trimmedContent) {
3739
+ let finalContent = trimmedContent;
3740
+ if (hasLeadingSpace && result && !result.endsWith(' ')) {
3741
+ finalContent = ' ' + finalContent;
3742
+ }
3743
+ if (hasTrailingSpace) {
3744
+ finalContent = finalContent + ' ';
3745
+ }
3746
+ result += finalContent;
3747
+ }
3748
+ else if (hasLeadingSpace || hasTrailingSpace) {
3749
+ if (result && !result.endsWith(' ')) {
3750
+ result += ' ';
3751
+ }
3752
+ }
3753
+ }
3754
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3755
+ const element = child;
3756
+ const openTag = element.open_tag;
3757
+ const childTagName = openTag?.tag_name?.value || '';
3758
+ if (!this.isInlineElement(childTagName)) {
3759
+ return null;
3760
+ }
3761
+ const childInline = this.tryRenderInlineFull(element, childTagName, this.extractAttributes(openTag.children), element.body.filter(c => !(c instanceof WhitespaceNode || c.type === 'AST_WHITESPACE_NODE') &&
3762
+ !((c instanceof HTMLTextNode || c.type === 'AST_HTML_TEXT_NODE') && c?.content.trim() === "")));
3763
+ if (!childInline) {
3764
+ return null;
3765
+ }
3766
+ result += childInline;
3767
+ }
3768
+ else {
3769
+ const oldLines = this.lines;
3770
+ const oldInlineMode = this.inlineMode;
3771
+ const oldIndentLevel = this.indentLevel;
3772
+ try {
3773
+ this.lines = [];
3774
+ this.inlineMode = true;
3775
+ this.indentLevel = 0;
3776
+ this.visit(child);
3777
+ result += this.lines.join("");
3778
+ }
3779
+ finally {
3780
+ this.lines = oldLines;
3781
+ this.inlineMode = oldInlineMode;
3782
+ this.indentLevel = oldIndentLevel;
3783
+ }
3784
+ }
3785
+ }
3786
+ return result.trim();
3787
+ }
3788
+ /**
3789
+ * Try to render children inline if they are simple enough.
3790
+ * Returns the inline string if possible, null otherwise.
3791
+ */
3792
+ tryRenderInline(children, tagName, depth = 0, forceInline = false, hasTextFlow = false) {
3793
+ if (!forceInline && children.length > 10) {
3794
+ return null;
3795
+ }
3796
+ const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3797
+ let maxAllowedDepth = forceInline ? 5 : (tagName && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div'].includes(tagName) ? 1 : 2);
3798
+ if (hasTextFlow && maxNestingDepth >= 2) {
3799
+ const roughContentLength = this.estimateContentLength(children);
3800
+ if (roughContentLength > 47) {
3801
+ maxAllowedDepth = 1;
3802
+ }
3803
+ }
3804
+ if (!forceInline && maxNestingDepth > maxAllowedDepth) {
3805
+ this.isInComplexNesting = true;
3806
+ return null;
3807
+ }
3808
+ for (const child of children) {
3809
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3810
+ const textContent = child.content;
3811
+ if (textContent.includes('\n')) {
3812
+ return null;
3813
+ }
3814
+ }
3815
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3816
+ const element = child;
3817
+ const openTag = element.open_tag;
3818
+ const elementTagName = openTag?.tag_name?.value || '';
3819
+ const isInlineElement = this.isInlineElement(elementTagName);
3820
+ if (!isInlineElement) {
3821
+ return null;
3822
+ }
3823
+ }
3824
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
3825
+ else {
3826
+ return null;
3827
+ }
3828
+ }
3829
+ const oldLines = this.lines;
3830
+ const oldInlineMode = this.inlineMode;
3831
+ try {
3832
+ this.lines = [];
3833
+ this.inlineMode = true;
3834
+ let content = '';
3835
+ for (const child of children) {
3836
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3837
+ content += child.content;
3838
+ }
3839
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3840
+ const element = child;
3841
+ const openTag = element.open_tag;
3842
+ const childTagName = openTag?.tag_name?.value || '';
3843
+ const attributes = this.extractAttributes(openTag.children);
3844
+ const attributesString = this.renderAttributesString(attributes);
3845
+ const elementContent = this.renderElementInline(element);
3846
+ content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
3847
+ }
3848
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3849
+ const erbNode = child;
3850
+ const open = erbNode.tag_opening?.value ?? "";
3851
+ const erbContent = erbNode.content?.value ?? "";
3852
+ const close = erbNode.tag_closing?.value ?? "";
3853
+ content += `${open}${this.formatERBContent(erbContent)}${close}`;
3854
+ }
3855
+ }
3856
+ content = content.replace(/\s+/g, ' ').trim();
3857
+ return `<${tagName}>${content}</${tagName}>`;
3858
+ }
3859
+ finally {
3860
+ this.lines = oldLines;
3861
+ this.inlineMode = oldInlineMode;
3862
+ }
3863
+ }
3864
+ /**
3865
+ * Estimate the total content length of children nodes for decision making.
3866
+ */
3867
+ estimateContentLength(children) {
3868
+ let length = 0;
3869
+ for (const child of children) {
3870
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3871
+ length += child.content.length;
3872
+ }
3873
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3874
+ const element = child;
3875
+ const openTag = element.open_tag;
3876
+ const tagName = openTag?.tag_name?.value || '';
3877
+ length += tagName.length + 5; // Rough estimate for tag overhead
3878
+ length += this.estimateContentLength(element.body);
3879
+ }
3880
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3881
+ length += child.content?.value.length || 0;
3882
+ }
3883
+ }
3884
+ return length;
3885
+ }
3886
+ /**
3887
+ * Calculate the maximum nesting depth in a subtree of nodes.
3888
+ */
3889
+ getMaxNestingDepth(children, currentDepth) {
3890
+ let maxDepth = currentDepth;
3891
+ for (const child of children) {
3892
+ if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3893
+ const element = child;
3894
+ const elementChildren = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
3895
+ !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
3896
+ const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1);
3897
+ maxDepth = Math.max(maxDepth, childDepth);
3898
+ }
3899
+ }
3900
+ return maxDepth;
3901
+ }
3902
+ /**
3903
+ * Render an HTML element's content inline (without the wrapping tags).
3904
+ */
3905
+ renderElementInline(element) {
3906
+ const children = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
3907
+ !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
3908
+ let content = '';
3909
+ for (const child of children) {
3910
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3911
+ content += child.content;
3912
+ }
3913
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3914
+ const childElement = child;
3915
+ const openTag = childElement.open_tag;
3916
+ const childTagName = openTag?.tag_name?.value || '';
3917
+ const attributes = this.extractAttributes(openTag.children);
3918
+ const attributesString = this.renderAttributesString(attributes);
3919
+ const childContent = this.renderElementInline(childElement);
3920
+ content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
3921
+ }
3922
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3923
+ const erbNode = child;
3924
+ const open = erbNode.tag_opening?.value ?? "";
3925
+ const erbContent = erbNode.content?.value ?? "";
3926
+ const close = erbNode.tag_closing?.value ?? "";
3927
+ content += `${open}${this.formatERBContent(erbContent)}${close}`;
3928
+ }
3929
+ }
3930
+ return content.replace(/\s+/g, ' ').trim();
3931
+ }
3267
3932
  }
3268
3933
 
3269
3934
  /**