@herb-tools/formatter 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/herb-format.js +623 -145
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +617 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +617 -139
- package/dist/index.esm.js.map +1 -1
- package/dist/types/printer.d.ts +57 -0
- package/package.json +3 -2
- package/src/printer.ts +803 -154
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)
|
|
@@ -2693,6 +2693,12 @@ class Printer extends Visitor {
|
|
|
2693
2693
|
indentLevel = 0;
|
|
2694
2694
|
inlineMode = false;
|
|
2695
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
|
+
]);
|
|
2696
2702
|
constructor(source, options) {
|
|
2697
2703
|
super();
|
|
2698
2704
|
this.source = source;
|
|
@@ -2713,7 +2719,7 @@ class Printer extends Visitor {
|
|
|
2713
2719
|
else {
|
|
2714
2720
|
this.visit(node);
|
|
2715
2721
|
}
|
|
2716
|
-
return this.lines.
|
|
2722
|
+
return this.lines.join("\n");
|
|
2717
2723
|
}
|
|
2718
2724
|
push(line) {
|
|
2719
2725
|
this.lines.push(line);
|
|
@@ -2728,38 +2734,167 @@ class Printer extends Visitor {
|
|
|
2728
2734
|
return " ".repeat(this.indentLevel * this.indentWidth);
|
|
2729
2735
|
}
|
|
2730
2736
|
/**
|
|
2731
|
-
*
|
|
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.
|
|
2732
2739
|
*/
|
|
2733
|
-
|
|
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) {
|
|
2734
2810
|
const indent = this.indent();
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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}>`);
|
|
2743
2831
|
}
|
|
2744
2832
|
else {
|
|
2745
|
-
|
|
2746
|
-
inner = txt.trim() ? ` ${txt.trim()} ` : "";
|
|
2833
|
+
this.push(indent + ">");
|
|
2747
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);
|
|
2748
2845
|
this.push(indent + open + inner + close);
|
|
2749
2846
|
}
|
|
2750
2847
|
// --- Visitor methods ---
|
|
2751
2848
|
visitDocumentNode(node) {
|
|
2752
|
-
|
|
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
|
+
}
|
|
2753
2876
|
}
|
|
2754
2877
|
visitHTMLElementNode(node) {
|
|
2755
2878
|
const open = node.open_tag;
|
|
2756
2879
|
const tagName = open.tag_name?.value ?? "";
|
|
2757
2880
|
const indent = this.indent();
|
|
2758
|
-
const attributes = open.children
|
|
2759
|
-
const inlineNodes = open.children
|
|
2760
|
-
|
|
2761
|
-
const children = node.body.filter(child =>
|
|
2762
|
-
|
|
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);
|
|
2763
2898
|
const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
|
|
2764
2899
|
const isSelfClosing = open.tag_closing?.value === "/>";
|
|
2765
2900
|
if (!hasClosing) {
|
|
@@ -2794,16 +2929,53 @@ class Printer extends Visitor {
|
|
|
2794
2929
|
}
|
|
2795
2930
|
}
|
|
2796
2931
|
else {
|
|
2797
|
-
const inlineResult = this.tryRenderInline(children, tagName);
|
|
2932
|
+
const inlineResult = this.tryRenderInline(children, tagName, 0, false, hasTextFlow);
|
|
2798
2933
|
if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
|
|
2799
2934
|
this.push(indent + inlineResult);
|
|
2800
2935
|
return;
|
|
2801
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
|
+
}
|
|
2802
2969
|
}
|
|
2803
2970
|
}
|
|
2804
2971
|
this.push(indent + `<${tagName}>`);
|
|
2805
2972
|
this.withIndent(() => {
|
|
2806
|
-
|
|
2973
|
+
if (hasTextFlow) {
|
|
2974
|
+
this.visitTextFlowChildren(children);
|
|
2975
|
+
}
|
|
2976
|
+
else {
|
|
2977
|
+
children.forEach(child => this.visit(child));
|
|
2978
|
+
}
|
|
2807
2979
|
});
|
|
2808
2980
|
if (!node.is_void && !isSelfClosing) {
|
|
2809
2981
|
this.push(indent + `</${tagName}>`);
|
|
@@ -2830,20 +3002,24 @@ class Printer extends Visitor {
|
|
|
2830
3002
|
}
|
|
2831
3003
|
return;
|
|
2832
3004
|
}
|
|
2833
|
-
const
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
(
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
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);
|
|
2847
3023
|
if (shouldKeepInline) {
|
|
2848
3024
|
if (children.length === 0) {
|
|
2849
3025
|
if (isSelfClosing) {
|
|
@@ -2853,7 +3029,53 @@ class Printer extends Visitor {
|
|
|
2853
3029
|
this.push(indent + inline);
|
|
2854
3030
|
}
|
|
2855
3031
|
else {
|
|
2856
|
-
|
|
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}>`);
|
|
2857
3079
|
}
|
|
2858
3080
|
return;
|
|
2859
3081
|
}
|
|
@@ -2864,7 +3086,12 @@ class Printer extends Visitor {
|
|
|
2864
3086
|
this.push(indent + inline);
|
|
2865
3087
|
}
|
|
2866
3088
|
this.withIndent(() => {
|
|
2867
|
-
|
|
3089
|
+
if (hasTextFlow) {
|
|
3090
|
+
this.visitTextFlowChildren(children);
|
|
3091
|
+
}
|
|
3092
|
+
else {
|
|
3093
|
+
children.forEach(child => this.visit(child));
|
|
3094
|
+
}
|
|
2868
3095
|
});
|
|
2869
3096
|
if (!node.is_void && !isSelfClosing) {
|
|
2870
3097
|
this.push(indent + `</${tagName}>`);
|
|
@@ -2872,28 +3099,8 @@ class Printer extends Visitor {
|
|
|
2872
3099
|
return;
|
|
2873
3100
|
}
|
|
2874
3101
|
if (inlineNodes.length > 0 && hasERBControlFlow) {
|
|
2875
|
-
this.
|
|
2876
|
-
|
|
2877
|
-
open.children.forEach(child => {
|
|
2878
|
-
if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
2879
|
-
this.push(this.indent() + this.renderAttribute(child));
|
|
2880
|
-
}
|
|
2881
|
-
else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
|
|
2882
|
-
this.visit(child);
|
|
2883
|
-
}
|
|
2884
|
-
});
|
|
2885
|
-
});
|
|
2886
|
-
if (isSelfClosing) {
|
|
2887
|
-
this.push(indent + "/>");
|
|
2888
|
-
}
|
|
2889
|
-
else if (node.is_void) {
|
|
2890
|
-
this.push(indent + ">");
|
|
2891
|
-
}
|
|
2892
|
-
else if (children.length === 0) {
|
|
2893
|
-
this.push(indent + ">" + `</${tagName}>`);
|
|
2894
|
-
}
|
|
2895
|
-
else {
|
|
2896
|
-
this.push(indent + ">");
|
|
3102
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
|
|
3103
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
2897
3104
|
this.withIndent(() => {
|
|
2898
3105
|
children.forEach(child => this.visit(child));
|
|
2899
3106
|
});
|
|
@@ -2910,25 +3117,40 @@ class Printer extends Visitor {
|
|
|
2910
3117
|
}
|
|
2911
3118
|
}
|
|
2912
3119
|
else {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
else if (node.is_void) {
|
|
2923
|
-
this.push(indent + ">");
|
|
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
|
+
}
|
|
2924
3129
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
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;
|
|
2927
3144
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
3145
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
|
|
3146
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
2930
3147
|
this.withIndent(() => {
|
|
2931
|
-
|
|
3148
|
+
if (hasTextFlow) {
|
|
3149
|
+
this.visitTextFlowChildren(children);
|
|
3150
|
+
}
|
|
3151
|
+
else {
|
|
3152
|
+
children.forEach(child => this.visit(child));
|
|
3153
|
+
}
|
|
2932
3154
|
});
|
|
2933
3155
|
this.push(indent + `</${tagName}>`);
|
|
2934
3156
|
}
|
|
@@ -2937,47 +3159,35 @@ class Printer extends Visitor {
|
|
|
2937
3159
|
visitHTMLOpenTagNode(node) {
|
|
2938
3160
|
const tagName = node.tag_name?.value ?? "";
|
|
2939
3161
|
const indent = this.indent();
|
|
2940
|
-
const attributes = node.children
|
|
3162
|
+
const attributes = this.extractAttributes(node.children);
|
|
3163
|
+
const inlineNodes = this.extractInlineNodes(node.children);
|
|
2941
3164
|
const hasClosing = node.tag_closing?.value === ">";
|
|
2942
3165
|
if (!hasClosing) {
|
|
2943
3166
|
this.push(indent + `<${tagName}`);
|
|
2944
3167
|
return;
|
|
2945
3168
|
}
|
|
2946
|
-
const inline = this.renderInlineOpen(tagName, attributes, node.is_void);
|
|
2947
|
-
|
|
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) {
|
|
2948
3173
|
this.push(indent + inline);
|
|
2949
3174
|
return;
|
|
2950
3175
|
}
|
|
2951
|
-
this.
|
|
2952
|
-
this.withIndent(() => {
|
|
2953
|
-
attributes.forEach(attribute => {
|
|
2954
|
-
this.push(this.indent() + this.renderAttribute(attribute));
|
|
2955
|
-
});
|
|
2956
|
-
});
|
|
2957
|
-
this.push(indent + (node.is_void ? "/>" : ">"));
|
|
3176
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.children, false, node.is_void, false);
|
|
2958
3177
|
}
|
|
2959
3178
|
visitHTMLSelfCloseTagNode(node) {
|
|
2960
3179
|
const tagName = node.tag_name?.value ?? "";
|
|
2961
3180
|
const indent = this.indent();
|
|
2962
|
-
const attributes = node.attributes
|
|
2963
|
-
const
|
|
2964
|
-
const
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
singleAttribute.value?.children.length === 0;
|
|
2968
|
-
const shouldKeepInline = attributes.length <= 3 &&
|
|
2969
|
-
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);
|
|
2970
3186
|
if (shouldKeepInline) {
|
|
2971
3187
|
this.push(indent + inline);
|
|
2972
3188
|
return;
|
|
2973
3189
|
}
|
|
2974
|
-
this.
|
|
2975
|
-
this.withIndent(() => {
|
|
2976
|
-
attributes.forEach(attribute => {
|
|
2977
|
-
this.push(this.indent() + this.renderAttribute(attribute));
|
|
2978
|
-
});
|
|
2979
|
-
});
|
|
2980
|
-
this.push(indent + "/>");
|
|
3190
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false);
|
|
2981
3191
|
}
|
|
2982
3192
|
visitHTMLCloseTagNode(node) {
|
|
2983
3193
|
const indent = this.indent();
|
|
@@ -2987,6 +3197,13 @@ class Printer extends Visitor {
|
|
|
2987
3197
|
this.push(indent + open + name + close);
|
|
2988
3198
|
}
|
|
2989
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
|
+
}
|
|
2990
3207
|
const indent = this.indent();
|
|
2991
3208
|
let text = node.content.trim();
|
|
2992
3209
|
if (!text)
|
|
@@ -3120,9 +3337,17 @@ class Printer extends Visitor {
|
|
|
3120
3337
|
}
|
|
3121
3338
|
visitERBInNode(node) {
|
|
3122
3339
|
this.printERBNode(node);
|
|
3340
|
+
this.withIndent(() => {
|
|
3341
|
+
node.statements.forEach(stmt => this.visit(stmt));
|
|
3342
|
+
});
|
|
3123
3343
|
}
|
|
3124
3344
|
visitERBCaseMatchNode(node) {
|
|
3125
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);
|
|
3126
3351
|
}
|
|
3127
3352
|
visitERBBlockNode(node) {
|
|
3128
3353
|
const indent = this.indent();
|
|
@@ -3142,22 +3367,18 @@ class Printer extends Visitor {
|
|
|
3142
3367
|
const open = node.tag_opening?.value ?? "";
|
|
3143
3368
|
const content = node.content?.value ?? "";
|
|
3144
3369
|
const close = node.tag_closing?.value ?? "";
|
|
3145
|
-
this.
|
|
3146
|
-
|
|
3370
|
+
const inner = this.formatERBContent(content);
|
|
3371
|
+
this.lines.push(open + inner + close);
|
|
3372
|
+
node.statements.forEach((child, _index) => {
|
|
3147
3373
|
this.lines.push(" ");
|
|
3148
|
-
}
|
|
3149
|
-
node.statements.forEach((child, index) => {
|
|
3150
3374
|
if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
3151
3375
|
this.lines.push(this.renderAttribute(child));
|
|
3152
3376
|
}
|
|
3153
3377
|
else {
|
|
3154
3378
|
this.visit(child);
|
|
3155
3379
|
}
|
|
3156
|
-
if (index < node.statements.length - 1) {
|
|
3157
|
-
this.lines.push(" ");
|
|
3158
|
-
}
|
|
3159
3380
|
});
|
|
3160
|
-
if (node.statements.length > 0) {
|
|
3381
|
+
if (node.statements.length > 0 && node.end_node) {
|
|
3161
3382
|
this.lines.push(" ");
|
|
3162
3383
|
}
|
|
3163
3384
|
if (node.subsequent) {
|
|
@@ -3168,7 +3389,8 @@ class Printer extends Visitor {
|
|
|
3168
3389
|
const endOpen = endNode.tag_opening?.value ?? "";
|
|
3169
3390
|
const endContent = endNode.content?.value ?? "";
|
|
3170
3391
|
const endClose = endNode.tag_closing?.value ?? "";
|
|
3171
|
-
this.
|
|
3392
|
+
const endInner = this.formatERBContent(endContent);
|
|
3393
|
+
this.lines.push(endOpen + endInner + endClose);
|
|
3172
3394
|
}
|
|
3173
3395
|
}
|
|
3174
3396
|
else {
|
|
@@ -3197,7 +3419,6 @@ class Printer extends Visitor {
|
|
|
3197
3419
|
});
|
|
3198
3420
|
}
|
|
3199
3421
|
visitERBCaseNode(node) {
|
|
3200
|
-
this.indentLevel;
|
|
3201
3422
|
const indent = this.indent();
|
|
3202
3423
|
const open = node.tag_opening?.value ?? "";
|
|
3203
3424
|
const content = node.content?.value ?? "";
|
|
@@ -3261,6 +3482,147 @@ class Printer extends Visitor {
|
|
|
3261
3482
|
this.visit(node.end_node);
|
|
3262
3483
|
}
|
|
3263
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
|
+
}
|
|
3264
3626
|
renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
|
|
3265
3627
|
const parts = attributes.map(attribute => this.renderAttribute(attribute));
|
|
3266
3628
|
if (inlineNodes.length > 0) {
|
|
@@ -3297,7 +3659,9 @@ class Printer extends Visitor {
|
|
|
3297
3659
|
this.lines = [];
|
|
3298
3660
|
inlineNodes.forEach(node => {
|
|
3299
3661
|
const wasInlineMode = this.inlineMode;
|
|
3300
|
-
this.
|
|
3662
|
+
if (!this.isERBControlFlow(node)) {
|
|
3663
|
+
this.inlineMode = true;
|
|
3664
|
+
}
|
|
3301
3665
|
this.visit(node);
|
|
3302
3666
|
this.inlineMode = wasInlineMode;
|
|
3303
3667
|
});
|
|
@@ -3316,33 +3680,128 @@ class Printer extends Visitor {
|
|
|
3316
3680
|
const equals = attribute.equals?.value ?? "";
|
|
3317
3681
|
let value = "";
|
|
3318
3682
|
if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || attribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
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;
|
|
3325
3692
|
}
|
|
3326
|
-
else if (
|
|
3327
|
-
const
|
|
3328
|
-
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;
|
|
3329
3696
|
}
|
|
3330
3697
|
return "";
|
|
3331
3698
|
}).join("");
|
|
3332
|
-
|
|
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;
|
|
3333
3708
|
}
|
|
3334
3709
|
return name + equals + value;
|
|
3335
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
|
+
}
|
|
3336
3788
|
/**
|
|
3337
3789
|
* Try to render children inline if they are simple enough.
|
|
3338
3790
|
* Returns the inline string if possible, null otherwise.
|
|
3339
3791
|
*/
|
|
3340
|
-
tryRenderInline(children, tagName, depth = 0) {
|
|
3341
|
-
if (children.length > 10) {
|
|
3792
|
+
tryRenderInline(children, tagName, depth = 0, forceInline = false, hasTextFlow = false) {
|
|
3793
|
+
if (!forceInline && children.length > 10) {
|
|
3342
3794
|
return null;
|
|
3343
3795
|
}
|
|
3344
3796
|
const maxNestingDepth = this.getMaxNestingDepth(children, 0);
|
|
3345
|
-
|
|
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) {
|
|
3346
3805
|
this.isInComplexNesting = true;
|
|
3347
3806
|
return null;
|
|
3348
3807
|
}
|
|
@@ -3356,8 +3815,9 @@ class Printer extends Visitor {
|
|
|
3356
3815
|
else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
|
|
3357
3816
|
const element = child;
|
|
3358
3817
|
const openTag = element.open_tag;
|
|
3359
|
-
const
|
|
3360
|
-
|
|
3818
|
+
const elementTagName = openTag?.tag_name?.value || '';
|
|
3819
|
+
const isInlineElement = this.isInlineElement(elementTagName);
|
|
3820
|
+
if (!isInlineElement) {
|
|
3361
3821
|
return null;
|
|
3362
3822
|
}
|
|
3363
3823
|
}
|
|
@@ -3380,10 +3840,8 @@ class Printer extends Visitor {
|
|
|
3380
3840
|
const element = child;
|
|
3381
3841
|
const openTag = element.open_tag;
|
|
3382
3842
|
const childTagName = openTag?.tag_name?.value || '';
|
|
3383
|
-
const attributes = openTag.children
|
|
3384
|
-
const attributesString = attributes
|
|
3385
|
-
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
3386
|
-
: '';
|
|
3843
|
+
const attributes = this.extractAttributes(openTag.children);
|
|
3844
|
+
const attributesString = this.renderAttributesString(attributes);
|
|
3387
3845
|
const elementContent = this.renderElementInline(element);
|
|
3388
3846
|
content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
|
|
3389
3847
|
}
|
|
@@ -3392,7 +3850,7 @@ class Printer extends Visitor {
|
|
|
3392
3850
|
const open = erbNode.tag_opening?.value ?? "";
|
|
3393
3851
|
const erbContent = erbNode.content?.value ?? "";
|
|
3394
3852
|
const close = erbNode.tag_closing?.value ?? "";
|
|
3395
|
-
content += `${open}
|
|
3853
|
+
content += `${open}${this.formatERBContent(erbContent)}${close}`;
|
|
3396
3854
|
}
|
|
3397
3855
|
}
|
|
3398
3856
|
content = content.replace(/\s+/g, ' ').trim();
|
|
@@ -3403,6 +3861,28 @@ class Printer extends Visitor {
|
|
|
3403
3861
|
this.inlineMode = oldInlineMode;
|
|
3404
3862
|
}
|
|
3405
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
|
+
}
|
|
3406
3886
|
/**
|
|
3407
3887
|
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
3408
3888
|
*/
|
|
@@ -3434,10 +3914,8 @@ class Printer extends Visitor {
|
|
|
3434
3914
|
const childElement = child;
|
|
3435
3915
|
const openTag = childElement.open_tag;
|
|
3436
3916
|
const childTagName = openTag?.tag_name?.value || '';
|
|
3437
|
-
const attributes = openTag.children
|
|
3438
|
-
const attributesString = attributes
|
|
3439
|
-
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
3440
|
-
: '';
|
|
3917
|
+
const attributes = this.extractAttributes(openTag.children);
|
|
3918
|
+
const attributesString = this.renderAttributesString(attributes);
|
|
3441
3919
|
const childContent = this.renderElementInline(childElement);
|
|
3442
3920
|
content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
|
|
3443
3921
|
}
|
|
@@ -3446,7 +3924,7 @@ class Printer extends Visitor {
|
|
|
3446
3924
|
const open = erbNode.tag_opening?.value ?? "";
|
|
3447
3925
|
const erbContent = erbNode.content?.value ?? "";
|
|
3448
3926
|
const close = erbNode.tag_closing?.value ?? "";
|
|
3449
|
-
content += `${open}
|
|
3927
|
+
content += `${open}${this.formatERBContent(erbContent)}${close}`;
|
|
3450
3928
|
}
|
|
3451
3929
|
}
|
|
3452
3930
|
return content.replace(/\s+/g, ' ').trim();
|