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