@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.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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2757
|
-
const inlineNodes = open.children
|
|
2758
|
-
|
|
2759
|
-
const children = node.body.filter(child =>
|
|
2760
|
-
|
|
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
|
-
|
|
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
|
|
2810
|
-
|
|
2811
|
-
const
|
|
2812
|
-
(
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 +
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
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
|
-
|
|
2865
|
-
|
|
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
|
-
|
|
2868
|
-
|
|
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
|
-
|
|
2871
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
2905
|
-
const
|
|
2906
|
-
const
|
|
2907
|
-
const
|
|
2908
|
-
|
|
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.
|
|
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.
|
|
3089
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
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 (
|
|
3258
|
-
const
|
|
3259
|
-
return
|
|
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
|
-
|
|
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
|
/**
|