@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/herb-format.js +773 -105
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +758 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +758 -93
- package/dist/index.esm.js.map +1 -1
- package/dist/types/printer.d.ts +71 -0
- package/package.json +3 -2
- package/src/cli.ts +11 -6
- package/src/printer.ts +986 -103
package/dist/index.esm.js
CHANGED
|
@@ -129,7 +129,7 @@ class Token {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
132
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-
|
|
132
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-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-
|
|
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-
|
|
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)
|
|
@@ -2690,6 +2690,13 @@ class Printer extends Visitor {
|
|
|
2690
2690
|
lines = [];
|
|
2691
2691
|
indentLevel = 0;
|
|
2692
2692
|
inlineMode = false;
|
|
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
|
+
]);
|
|
2693
2700
|
constructor(source, options) {
|
|
2694
2701
|
super();
|
|
2695
2702
|
this.source = source;
|
|
@@ -2703,13 +2710,14 @@ class Printer extends Visitor {
|
|
|
2703
2710
|
const node = object;
|
|
2704
2711
|
this.lines = [];
|
|
2705
2712
|
this.indentLevel = indentLevel;
|
|
2713
|
+
this.isInComplexNesting = false; // Reset for each top-level element
|
|
2706
2714
|
if (typeof node.accept === 'function') {
|
|
2707
2715
|
node.accept(this);
|
|
2708
2716
|
}
|
|
2709
2717
|
else {
|
|
2710
2718
|
this.visit(node);
|
|
2711
2719
|
}
|
|
2712
|
-
return this.lines.
|
|
2720
|
+
return this.lines.join("\n");
|
|
2713
2721
|
}
|
|
2714
2722
|
push(line) {
|
|
2715
2723
|
this.lines.push(line);
|
|
@@ -2724,38 +2732,167 @@ class Printer extends Visitor {
|
|
|
2724
2732
|
return " ".repeat(this.indentLevel * this.indentWidth);
|
|
2725
2733
|
}
|
|
2726
2734
|
/**
|
|
2727
|
-
*
|
|
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.
|
|
2728
2737
|
*/
|
|
2729
|
-
|
|
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) {
|
|
2730
2808
|
const indent = this.indent();
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
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}>`);
|
|
2739
2829
|
}
|
|
2740
2830
|
else {
|
|
2741
|
-
|
|
2742
|
-
inner = txt.trim() ? ` ${txt.trim()} ` : "";
|
|
2831
|
+
this.push(indent + ">");
|
|
2743
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);
|
|
2744
2843
|
this.push(indent + open + inner + close);
|
|
2745
2844
|
}
|
|
2746
2845
|
// --- Visitor methods ---
|
|
2747
2846
|
visitDocumentNode(node) {
|
|
2748
|
-
|
|
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
|
+
}
|
|
2749
2874
|
}
|
|
2750
2875
|
visitHTMLElementNode(node) {
|
|
2751
2876
|
const open = node.open_tag;
|
|
2752
2877
|
const tagName = open.tag_name?.value ?? "";
|
|
2753
2878
|
const indent = this.indent();
|
|
2754
|
-
const attributes = open.children
|
|
2755
|
-
const inlineNodes = open.children
|
|
2756
|
-
|
|
2757
|
-
const children = node.body.filter(child =>
|
|
2758
|
-
|
|
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);
|
|
2759
2896
|
const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
|
|
2760
2897
|
const isSelfClosing = open.tag_closing?.value === "/>";
|
|
2761
2898
|
if (!hasClosing) {
|
|
@@ -2775,9 +2912,68 @@ class Printer extends Visitor {
|
|
|
2775
2912
|
}
|
|
2776
2913
|
return;
|
|
2777
2914
|
}
|
|
2915
|
+
if (children.length >= 1) {
|
|
2916
|
+
if (this.isInComplexNesting) {
|
|
2917
|
+
if (children.length === 1) {
|
|
2918
|
+
const child = children[0];
|
|
2919
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
2920
|
+
const textContent = child.content.trim();
|
|
2921
|
+
const singleLine = `<${tagName}>${textContent}</${tagName}>`;
|
|
2922
|
+
if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
|
|
2923
|
+
this.push(indent + singleLine);
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
else {
|
|
2930
|
+
const inlineResult = this.tryRenderInline(children, tagName, 0, false, hasTextFlow);
|
|
2931
|
+
if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
|
|
2932
|
+
this.push(indent + inlineResult);
|
|
2933
|
+
return;
|
|
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
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2778
2969
|
this.push(indent + `<${tagName}>`);
|
|
2779
2970
|
this.withIndent(() => {
|
|
2780
|
-
|
|
2971
|
+
if (hasTextFlow) {
|
|
2972
|
+
this.visitTextFlowChildren(children);
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
children.forEach(child => this.visit(child));
|
|
2976
|
+
}
|
|
2781
2977
|
});
|
|
2782
2978
|
if (!node.is_void && !isSelfClosing) {
|
|
2783
2979
|
this.push(indent + `</${tagName}>`);
|
|
@@ -2804,15 +3000,24 @@ class Printer extends Visitor {
|
|
|
2804
3000
|
}
|
|
2805
3001
|
return;
|
|
2806
3002
|
}
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
const
|
|
2810
|
-
(
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
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);
|
|
2816
3021
|
if (shouldKeepInline) {
|
|
2817
3022
|
if (children.length === 0) {
|
|
2818
3023
|
if (isSelfClosing) {
|
|
@@ -2822,7 +3027,53 @@ class Printer extends Visitor {
|
|
|
2822
3027
|
this.push(indent + inline);
|
|
2823
3028
|
}
|
|
2824
3029
|
else {
|
|
2825
|
-
|
|
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}>`);
|
|
2826
3077
|
}
|
|
2827
3078
|
return;
|
|
2828
3079
|
}
|
|
@@ -2833,15 +3084,20 @@ class Printer extends Visitor {
|
|
|
2833
3084
|
this.push(indent + inline);
|
|
2834
3085
|
}
|
|
2835
3086
|
this.withIndent(() => {
|
|
2836
|
-
|
|
3087
|
+
if (hasTextFlow) {
|
|
3088
|
+
this.visitTextFlowChildren(children);
|
|
3089
|
+
}
|
|
3090
|
+
else {
|
|
3091
|
+
children.forEach(child => this.visit(child));
|
|
3092
|
+
}
|
|
2837
3093
|
});
|
|
2838
3094
|
if (!node.is_void && !isSelfClosing) {
|
|
2839
3095
|
this.push(indent + `</${tagName}>`);
|
|
2840
3096
|
}
|
|
2841
3097
|
return;
|
|
2842
3098
|
}
|
|
2843
|
-
if (inlineNodes.length > 0) {
|
|
2844
|
-
this.
|
|
3099
|
+
if (inlineNodes.length > 0 && hasERBControlFlow) {
|
|
3100
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
|
|
2845
3101
|
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
2846
3102
|
this.withIndent(() => {
|
|
2847
3103
|
children.forEach(child => this.visit(child));
|
|
@@ -2849,26 +3105,50 @@ class Printer extends Visitor {
|
|
|
2849
3105
|
this.push(indent + `</${tagName}>`);
|
|
2850
3106
|
}
|
|
2851
3107
|
}
|
|
2852
|
-
else {
|
|
2853
|
-
this.push(indent +
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
3108
|
+
else if (inlineNodes.length > 0) {
|
|
3109
|
+
this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
|
|
3110
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
3111
|
+
this.withIndent(() => {
|
|
3112
|
+
children.forEach(child => this.visit(child));
|
|
2857
3113
|
});
|
|
2858
|
-
|
|
2859
|
-
if (isSelfClosing) {
|
|
2860
|
-
this.push(indent + "/>");
|
|
3114
|
+
this.push(indent + `</${tagName}>`);
|
|
2861
3115
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
3116
|
+
}
|
|
3117
|
+
else {
|
|
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
|
+
}
|
|
2864
3127
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
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;
|
|
2867
3142
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
3143
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
|
|
3144
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
2870
3145
|
this.withIndent(() => {
|
|
2871
|
-
|
|
3146
|
+
if (hasTextFlow) {
|
|
3147
|
+
this.visitTextFlowChildren(children);
|
|
3148
|
+
}
|
|
3149
|
+
else {
|
|
3150
|
+
children.forEach(child => this.visit(child));
|
|
3151
|
+
}
|
|
2872
3152
|
});
|
|
2873
3153
|
this.push(indent + `</${tagName}>`);
|
|
2874
3154
|
}
|
|
@@ -2877,48 +3157,35 @@ class Printer extends Visitor {
|
|
|
2877
3157
|
visitHTMLOpenTagNode(node) {
|
|
2878
3158
|
const tagName = node.tag_name?.value ?? "";
|
|
2879
3159
|
const indent = this.indent();
|
|
2880
|
-
const attributes = node.children
|
|
3160
|
+
const attributes = this.extractAttributes(node.children);
|
|
3161
|
+
const inlineNodes = this.extractInlineNodes(node.children);
|
|
2881
3162
|
const hasClosing = node.tag_closing?.value === ">";
|
|
2882
3163
|
if (!hasClosing) {
|
|
2883
3164
|
this.push(indent + `<${tagName}`);
|
|
2884
3165
|
return;
|
|
2885
3166
|
}
|
|
2886
|
-
const inline = this.renderInlineOpen(tagName, attributes, node.is_void);
|
|
2887
|
-
|
|
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) {
|
|
2888
3171
|
this.push(indent + inline);
|
|
2889
3172
|
return;
|
|
2890
3173
|
}
|
|
2891
|
-
this.
|
|
2892
|
-
this.withIndent(() => {
|
|
2893
|
-
attributes.forEach(attribute => {
|
|
2894
|
-
this.push(this.indent() + this.renderAttribute(attribute));
|
|
2895
|
-
});
|
|
2896
|
-
});
|
|
2897
|
-
this.push(indent + (node.is_void ? "/>" : ">"));
|
|
3174
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.children, false, node.is_void, false);
|
|
2898
3175
|
}
|
|
2899
3176
|
visitHTMLSelfCloseTagNode(node) {
|
|
2900
3177
|
const tagName = node.tag_name?.value ?? "";
|
|
2901
3178
|
const indent = this.indent();
|
|
2902
|
-
const attributes = node.attributes
|
|
2903
|
-
const
|
|
2904
|
-
const
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
singleAttribute.value?.children.length === 0;
|
|
2908
|
-
const shouldKeepInline = attributes.length <= 3 &&
|
|
2909
|
-
!hasEmptyValue &&
|
|
2910
|
-
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);
|
|
2911
3184
|
if (shouldKeepInline) {
|
|
2912
3185
|
this.push(indent + inline);
|
|
2913
3186
|
return;
|
|
2914
3187
|
}
|
|
2915
|
-
this.
|
|
2916
|
-
this.withIndent(() => {
|
|
2917
|
-
attributes.forEach(attribute => {
|
|
2918
|
-
this.push(this.indent() + this.renderAttribute(attribute));
|
|
2919
|
-
});
|
|
2920
|
-
});
|
|
2921
|
-
this.push(indent + "/>");
|
|
3188
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false);
|
|
2922
3189
|
}
|
|
2923
3190
|
visitHTMLCloseTagNode(node) {
|
|
2924
3191
|
const indent = this.indent();
|
|
@@ -2928,6 +3195,13 @@ class Printer extends Visitor {
|
|
|
2928
3195
|
this.push(indent + open + name + close);
|
|
2929
3196
|
}
|
|
2930
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
|
+
}
|
|
2931
3205
|
const indent = this.indent();
|
|
2932
3206
|
let text = node.content.trim();
|
|
2933
3207
|
if (!text)
|
|
@@ -3061,9 +3335,17 @@ class Printer extends Visitor {
|
|
|
3061
3335
|
}
|
|
3062
3336
|
visitERBInNode(node) {
|
|
3063
3337
|
this.printERBNode(node);
|
|
3338
|
+
this.withIndent(() => {
|
|
3339
|
+
node.statements.forEach(stmt => this.visit(stmt));
|
|
3340
|
+
});
|
|
3064
3341
|
}
|
|
3065
3342
|
visitERBCaseMatchNode(node) {
|
|
3066
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);
|
|
3067
3349
|
}
|
|
3068
3350
|
visitERBBlockNode(node) {
|
|
3069
3351
|
const indent = this.indent();
|
|
@@ -3083,21 +3365,30 @@ class Printer extends Visitor {
|
|
|
3083
3365
|
const open = node.tag_opening?.value ?? "";
|
|
3084
3366
|
const content = node.content?.value ?? "";
|
|
3085
3367
|
const close = node.tag_closing?.value ?? "";
|
|
3086
|
-
this.
|
|
3087
|
-
|
|
3368
|
+
const inner = this.formatERBContent(content);
|
|
3369
|
+
this.lines.push(open + inner + close);
|
|
3370
|
+
node.statements.forEach((child, _index) => {
|
|
3371
|
+
this.lines.push(" ");
|
|
3088
3372
|
if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
3089
|
-
this.lines.push(
|
|
3373
|
+
this.lines.push(this.renderAttribute(child));
|
|
3090
3374
|
}
|
|
3091
3375
|
else {
|
|
3092
3376
|
this.visit(child);
|
|
3093
3377
|
}
|
|
3094
3378
|
});
|
|
3379
|
+
if (node.statements.length > 0 && node.end_node) {
|
|
3380
|
+
this.lines.push(" ");
|
|
3381
|
+
}
|
|
3382
|
+
if (node.subsequent) {
|
|
3383
|
+
this.visit(node.subsequent);
|
|
3384
|
+
}
|
|
3095
3385
|
if (node.end_node) {
|
|
3096
3386
|
const endNode = node.end_node;
|
|
3097
3387
|
const endOpen = endNode.tag_opening?.value ?? "";
|
|
3098
3388
|
const endContent = endNode.content?.value ?? "";
|
|
3099
3389
|
const endClose = endNode.tag_closing?.value ?? "";
|
|
3100
|
-
this.
|
|
3390
|
+
const endInner = this.formatERBContent(endContent);
|
|
3391
|
+
this.lines.push(endOpen + endInner + endClose);
|
|
3101
3392
|
}
|
|
3102
3393
|
}
|
|
3103
3394
|
else {
|
|
@@ -3126,7 +3417,6 @@ class Printer extends Visitor {
|
|
|
3126
3417
|
});
|
|
3127
3418
|
}
|
|
3128
3419
|
visitERBCaseNode(node) {
|
|
3129
|
-
this.indentLevel;
|
|
3130
3420
|
const indent = this.indent();
|
|
3131
3421
|
const open = node.tag_opening?.value ?? "";
|
|
3132
3422
|
const content = node.content?.value ?? "";
|
|
@@ -3190,6 +3480,147 @@ class Printer extends Visitor {
|
|
|
3190
3480
|
this.visit(node.end_node);
|
|
3191
3481
|
}
|
|
3192
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
|
+
}
|
|
3193
3624
|
renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
|
|
3194
3625
|
const parts = attributes.map(attribute => this.renderAttribute(attribute));
|
|
3195
3626
|
if (inlineNodes.length > 0) {
|
|
@@ -3226,7 +3657,9 @@ class Printer extends Visitor {
|
|
|
3226
3657
|
this.lines = [];
|
|
3227
3658
|
inlineNodes.forEach(node => {
|
|
3228
3659
|
const wasInlineMode = this.inlineMode;
|
|
3229
|
-
this.
|
|
3660
|
+
if (!this.isERBControlFlow(node)) {
|
|
3661
|
+
this.inlineMode = true;
|
|
3662
|
+
}
|
|
3230
3663
|
this.visit(node);
|
|
3231
3664
|
this.inlineMode = wasInlineMode;
|
|
3232
3665
|
});
|
|
@@ -3245,23 +3678,255 @@ class Printer extends Visitor {
|
|
|
3245
3678
|
const equals = attribute.equals?.value ?? "";
|
|
3246
3679
|
let value = "";
|
|
3247
3680
|
if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || attribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
|
|
3248
|
-
const
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
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;
|
|
3254
3690
|
}
|
|
3255
|
-
else if (
|
|
3256
|
-
const
|
|
3257
|
-
return
|
|
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;
|
|
3258
3694
|
}
|
|
3259
3695
|
return "";
|
|
3260
3696
|
}).join("");
|
|
3261
|
-
|
|
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;
|
|
3262
3706
|
}
|
|
3263
3707
|
return name + equals + value;
|
|
3264
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
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Try to render children inline if they are simple enough.
|
|
3788
|
+
* Returns the inline string if possible, null otherwise.
|
|
3789
|
+
*/
|
|
3790
|
+
tryRenderInline(children, tagName, depth = 0, forceInline = false, hasTextFlow = false) {
|
|
3791
|
+
if (!forceInline && children.length > 10) {
|
|
3792
|
+
return null;
|
|
3793
|
+
}
|
|
3794
|
+
const maxNestingDepth = this.getMaxNestingDepth(children, 0);
|
|
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) {
|
|
3803
|
+
this.isInComplexNesting = true;
|
|
3804
|
+
return null;
|
|
3805
|
+
}
|
|
3806
|
+
for (const child of children) {
|
|
3807
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
3808
|
+
const textContent = child.content;
|
|
3809
|
+
if (textContent.includes('\n')) {
|
|
3810
|
+
return null;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3814
|
+
const element = child;
|
|
3815
|
+
const openTag = element.open_tag;
|
|
3816
|
+
const elementTagName = openTag?.tag_name?.value || '';
|
|
3817
|
+
const isInlineElement = this.isInlineElement(elementTagName);
|
|
3818
|
+
if (!isInlineElement) {
|
|
3819
|
+
return null;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
|
|
3823
|
+
else {
|
|
3824
|
+
return null;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
const oldLines = this.lines;
|
|
3828
|
+
const oldInlineMode = this.inlineMode;
|
|
3829
|
+
try {
|
|
3830
|
+
this.lines = [];
|
|
3831
|
+
this.inlineMode = true;
|
|
3832
|
+
let content = '';
|
|
3833
|
+
for (const child of children) {
|
|
3834
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
3835
|
+
content += child.content;
|
|
3836
|
+
}
|
|
3837
|
+
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3838
|
+
const element = child;
|
|
3839
|
+
const openTag = element.open_tag;
|
|
3840
|
+
const childTagName = openTag?.tag_name?.value || '';
|
|
3841
|
+
const attributes = this.extractAttributes(openTag.children);
|
|
3842
|
+
const attributesString = this.renderAttributesString(attributes);
|
|
3843
|
+
const elementContent = this.renderElementInline(element);
|
|
3844
|
+
content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
|
|
3845
|
+
}
|
|
3846
|
+
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
|
|
3847
|
+
const erbNode = child;
|
|
3848
|
+
const open = erbNode.tag_opening?.value ?? "";
|
|
3849
|
+
const erbContent = erbNode.content?.value ?? "";
|
|
3850
|
+
const close = erbNode.tag_closing?.value ?? "";
|
|
3851
|
+
content += `${open}${this.formatERBContent(erbContent)}${close}`;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
content = content.replace(/\s+/g, ' ').trim();
|
|
3855
|
+
return `<${tagName}>${content}</${tagName}>`;
|
|
3856
|
+
}
|
|
3857
|
+
finally {
|
|
3858
|
+
this.lines = oldLines;
|
|
3859
|
+
this.inlineMode = oldInlineMode;
|
|
3860
|
+
}
|
|
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
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
3886
|
+
*/
|
|
3887
|
+
getMaxNestingDepth(children, currentDepth) {
|
|
3888
|
+
let maxDepth = currentDepth;
|
|
3889
|
+
for (const child of children) {
|
|
3890
|
+
if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3891
|
+
const element = child;
|
|
3892
|
+
const elementChildren = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
|
|
3893
|
+
!((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
|
|
3894
|
+
const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1);
|
|
3895
|
+
maxDepth = Math.max(maxDepth, childDepth);
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
return maxDepth;
|
|
3899
|
+
}
|
|
3900
|
+
/**
|
|
3901
|
+
* Render an HTML element's content inline (without the wrapping tags).
|
|
3902
|
+
*/
|
|
3903
|
+
renderElementInline(element) {
|
|
3904
|
+
const children = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
|
|
3905
|
+
!((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
|
|
3906
|
+
let content = '';
|
|
3907
|
+
for (const child of children) {
|
|
3908
|
+
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
|
|
3909
|
+
content += child.content;
|
|
3910
|
+
}
|
|
3911
|
+
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3912
|
+
const childElement = child;
|
|
3913
|
+
const openTag = childElement.open_tag;
|
|
3914
|
+
const childTagName = openTag?.tag_name?.value || '';
|
|
3915
|
+
const attributes = this.extractAttributes(openTag.children);
|
|
3916
|
+
const attributesString = this.renderAttributesString(attributes);
|
|
3917
|
+
const childContent = this.renderElementInline(childElement);
|
|
3918
|
+
content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
|
|
3919
|
+
}
|
|
3920
|
+
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
|
|
3921
|
+
const erbNode = child;
|
|
3922
|
+
const open = erbNode.tag_opening?.value ?? "";
|
|
3923
|
+
const erbContent = erbNode.content?.value ?? "";
|
|
3924
|
+
const close = erbNode.tag_closing?.value ?? "";
|
|
3925
|
+
content += `${open}${this.formatERBContent(erbContent)}${close}`;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
return content.replace(/\s+/g, ' ').trim();
|
|
3929
|
+
}
|
|
3265
3930
|
}
|
|
3266
3931
|
|
|
3267
3932
|
/**
|