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