@herb-tools/formatter 0.4.3 → 0.6.0
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/README.md +113 -15
- package/dist/herb-format.js +3379 -2212
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +2069 -901
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +2069 -902
- package/dist/index.esm.js.map +1 -1
- package/dist/types/format-printer.d.ts +243 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +5 -2
- package/src/cli.ts +57 -29
- package/src/format-printer.ts +1822 -0
- package/src/formatter.ts +2 -2
- package/src/index.ts +2 -3
- package/dist/types/printer.d.ts +0 -127
- package/src/printer.ts +0 -1644
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-0.6.0/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-0.6.0/templates/javascript/packages/core/src/nodes.ts.erb
|
|
585
585
|
class Node {
|
|
586
586
|
type;
|
|
587
587
|
location;
|
|
@@ -686,7 +686,6 @@ class DocumentNode extends Node {
|
|
|
686
686
|
output += `@ DocumentNode ${this.location.treeInspectWithLabel()}\n`;
|
|
687
687
|
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
688
688
|
output += `└── children: ${this.inspectArray(this.children, " ")}`;
|
|
689
|
-
// output += "\n";
|
|
690
689
|
return output;
|
|
691
690
|
}
|
|
692
691
|
}
|
|
@@ -730,7 +729,6 @@ class LiteralNode extends Node {
|
|
|
730
729
|
output += `@ LiteralNode ${this.location.treeInspectWithLabel()}\n`;
|
|
731
730
|
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
732
731
|
output += `└── content: ${this.content ? JSON.stringify(this.content) : "∅"}\n`;
|
|
733
|
-
// output += "\n";
|
|
734
732
|
return output;
|
|
735
733
|
}
|
|
736
734
|
}
|
|
@@ -797,13 +795,13 @@ class HTMLOpenTagNode extends Node {
|
|
|
797
795
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
798
796
|
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
799
797
|
output += `└── is_void: ${typeof this.is_void === 'boolean' ? String(this.is_void) : "∅"}\n`;
|
|
800
|
-
// output += "\n";
|
|
801
798
|
return output;
|
|
802
799
|
}
|
|
803
800
|
}
|
|
804
801
|
class HTMLCloseTagNode extends Node {
|
|
805
802
|
tag_opening;
|
|
806
803
|
tag_name;
|
|
804
|
+
children;
|
|
807
805
|
tag_closing;
|
|
808
806
|
static from(data) {
|
|
809
807
|
return new HTMLCloseTagNode({
|
|
@@ -812,6 +810,7 @@ class HTMLCloseTagNode extends Node {
|
|
|
812
810
|
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
813
811
|
tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
|
|
814
812
|
tag_name: data.tag_name ? Token.from(data.tag_name) : null,
|
|
813
|
+
children: (data.children || []).map(node => fromSerializedNode(node)),
|
|
815
814
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
816
815
|
});
|
|
817
816
|
}
|
|
@@ -819,13 +818,16 @@ class HTMLCloseTagNode extends Node {
|
|
|
819
818
|
super(props.type, props.location, props.errors);
|
|
820
819
|
this.tag_opening = props.tag_opening;
|
|
821
820
|
this.tag_name = props.tag_name;
|
|
821
|
+
this.children = props.children;
|
|
822
822
|
this.tag_closing = props.tag_closing;
|
|
823
823
|
}
|
|
824
824
|
accept(visitor) {
|
|
825
825
|
visitor.visitHTMLCloseTagNode(this);
|
|
826
826
|
}
|
|
827
827
|
childNodes() {
|
|
828
|
-
return [
|
|
828
|
+
return [
|
|
829
|
+
...this.children,
|
|
830
|
+
];
|
|
829
831
|
}
|
|
830
832
|
compactChildNodes() {
|
|
831
833
|
return this.childNodes().filter(node => node !== null && node !== undefined);
|
|
@@ -833,6 +835,7 @@ class HTMLCloseTagNode extends Node {
|
|
|
833
835
|
recursiveErrors() {
|
|
834
836
|
return [
|
|
835
837
|
...this.errors,
|
|
838
|
+
...this.children.map(node => node.recursiveErrors()),
|
|
836
839
|
].flat();
|
|
837
840
|
}
|
|
838
841
|
toJSON() {
|
|
@@ -841,6 +844,7 @@ class HTMLCloseTagNode extends Node {
|
|
|
841
844
|
type: "AST_HTML_CLOSE_TAG_NODE",
|
|
842
845
|
tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
|
|
843
846
|
tag_name: this.tag_name ? this.tag_name.toJSON() : null,
|
|
847
|
+
children: this.children.map(node => node.toJSON()),
|
|
844
848
|
tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
|
|
845
849
|
};
|
|
846
850
|
}
|
|
@@ -850,75 +854,8 @@ class HTMLCloseTagNode extends Node {
|
|
|
850
854
|
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
851
855
|
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
852
856
|
output += `├── tag_name: ${this.tag_name ? this.tag_name.treeInspect() : "∅"}\n`;
|
|
857
|
+
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
853
858
|
output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
854
|
-
// output += "\n";
|
|
855
|
-
return output;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
class HTMLSelfCloseTagNode extends Node {
|
|
859
|
-
tag_opening;
|
|
860
|
-
tag_name;
|
|
861
|
-
attributes;
|
|
862
|
-
tag_closing;
|
|
863
|
-
is_void;
|
|
864
|
-
static from(data) {
|
|
865
|
-
return new HTMLSelfCloseTagNode({
|
|
866
|
-
type: data.type,
|
|
867
|
-
location: Location.from(data.location),
|
|
868
|
-
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
869
|
-
tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
|
|
870
|
-
tag_name: data.tag_name ? Token.from(data.tag_name) : null,
|
|
871
|
-
attributes: (data.attributes || []).map(node => fromSerializedNode(node)),
|
|
872
|
-
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
873
|
-
is_void: data.is_void,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
constructor(props) {
|
|
877
|
-
super(props.type, props.location, props.errors);
|
|
878
|
-
this.tag_opening = props.tag_opening;
|
|
879
|
-
this.tag_name = props.tag_name;
|
|
880
|
-
this.attributes = props.attributes;
|
|
881
|
-
this.tag_closing = props.tag_closing;
|
|
882
|
-
this.is_void = props.is_void;
|
|
883
|
-
}
|
|
884
|
-
accept(visitor) {
|
|
885
|
-
visitor.visitHTMLSelfCloseTagNode(this);
|
|
886
|
-
}
|
|
887
|
-
childNodes() {
|
|
888
|
-
return [
|
|
889
|
-
...this.attributes,
|
|
890
|
-
];
|
|
891
|
-
}
|
|
892
|
-
compactChildNodes() {
|
|
893
|
-
return this.childNodes().filter(node => node !== null && node !== undefined);
|
|
894
|
-
}
|
|
895
|
-
recursiveErrors() {
|
|
896
|
-
return [
|
|
897
|
-
...this.errors,
|
|
898
|
-
...this.attributes.map(node => node.recursiveErrors()),
|
|
899
|
-
].flat();
|
|
900
|
-
}
|
|
901
|
-
toJSON() {
|
|
902
|
-
return {
|
|
903
|
-
...super.toJSON(),
|
|
904
|
-
type: "AST_HTML_SELF_CLOSE_TAG_NODE",
|
|
905
|
-
tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
|
|
906
|
-
tag_name: this.tag_name ? this.tag_name.toJSON() : null,
|
|
907
|
-
attributes: this.attributes.map(node => node.toJSON()),
|
|
908
|
-
tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
|
|
909
|
-
is_void: this.is_void,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
treeInspect() {
|
|
913
|
-
let output = "";
|
|
914
|
-
output += `@ HTMLSelfCloseTagNode ${this.location.treeInspectWithLabel()}\n`;
|
|
915
|
-
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
916
|
-
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
917
|
-
output += `├── tag_name: ${this.tag_name ? this.tag_name.treeInspect() : "∅"}\n`;
|
|
918
|
-
output += `├── attributes: ${this.inspectArray(this.attributes, "│ ")}`;
|
|
919
|
-
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
920
|
-
output += `└── is_void: ${typeof this.is_void === 'boolean' ? String(this.is_void) : "∅"}\n`;
|
|
921
|
-
// output += "\n";
|
|
922
859
|
return output;
|
|
923
860
|
}
|
|
924
861
|
}
|
|
@@ -933,10 +870,10 @@ class HTMLElementNode extends Node {
|
|
|
933
870
|
type: data.type,
|
|
934
871
|
location: Location.from(data.location),
|
|
935
872
|
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
936
|
-
open_tag: data.open_tag ? fromSerializedNode(data.open_tag) : null,
|
|
873
|
+
open_tag: data.open_tag ? fromSerializedNode((data.open_tag)) : null,
|
|
937
874
|
tag_name: data.tag_name ? Token.from(data.tag_name) : null,
|
|
938
875
|
body: (data.body || []).map(node => fromSerializedNode(node)),
|
|
939
|
-
close_tag: data.close_tag ? fromSerializedNode(data.close_tag) : null,
|
|
876
|
+
close_tag: data.close_tag ? fromSerializedNode((data.close_tag)) : null,
|
|
940
877
|
is_void: data.is_void,
|
|
941
878
|
});
|
|
942
879
|
}
|
|
@@ -989,7 +926,6 @@ class HTMLElementNode extends Node {
|
|
|
989
926
|
output += `├── body: ${this.inspectArray(this.body, "│ ")}`;
|
|
990
927
|
output += `├── close_tag: ${this.inspectNode(this.close_tag, "│ ")}`;
|
|
991
928
|
output += `└── is_void: ${typeof this.is_void === 'boolean' ? String(this.is_void) : "∅"}\n`;
|
|
992
|
-
// output += "\n";
|
|
993
929
|
return output;
|
|
994
930
|
}
|
|
995
931
|
}
|
|
@@ -1051,29 +987,30 @@ class HTMLAttributeValueNode extends Node {
|
|
|
1051
987
|
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
1052
988
|
output += `├── close_quote: ${this.close_quote ? this.close_quote.treeInspect() : "∅"}\n`;
|
|
1053
989
|
output += `└── quoted: ${typeof this.quoted === 'boolean' ? String(this.quoted) : "∅"}\n`;
|
|
1054
|
-
// output += "\n";
|
|
1055
990
|
return output;
|
|
1056
991
|
}
|
|
1057
992
|
}
|
|
1058
993
|
class HTMLAttributeNameNode extends Node {
|
|
1059
|
-
|
|
994
|
+
children;
|
|
1060
995
|
static from(data) {
|
|
1061
996
|
return new HTMLAttributeNameNode({
|
|
1062
997
|
type: data.type,
|
|
1063
998
|
location: Location.from(data.location),
|
|
1064
999
|
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
1065
|
-
|
|
1000
|
+
children: (data.children || []).map(node => fromSerializedNode(node)),
|
|
1066
1001
|
});
|
|
1067
1002
|
}
|
|
1068
1003
|
constructor(props) {
|
|
1069
1004
|
super(props.type, props.location, props.errors);
|
|
1070
|
-
this.
|
|
1005
|
+
this.children = props.children;
|
|
1071
1006
|
}
|
|
1072
1007
|
accept(visitor) {
|
|
1073
1008
|
visitor.visitHTMLAttributeNameNode(this);
|
|
1074
1009
|
}
|
|
1075
1010
|
childNodes() {
|
|
1076
|
-
return [
|
|
1011
|
+
return [
|
|
1012
|
+
...this.children,
|
|
1013
|
+
];
|
|
1077
1014
|
}
|
|
1078
1015
|
compactChildNodes() {
|
|
1079
1016
|
return this.childNodes().filter(node => node !== null && node !== undefined);
|
|
@@ -1081,21 +1018,21 @@ class HTMLAttributeNameNode extends Node {
|
|
|
1081
1018
|
recursiveErrors() {
|
|
1082
1019
|
return [
|
|
1083
1020
|
...this.errors,
|
|
1021
|
+
...this.children.map(node => node.recursiveErrors()),
|
|
1084
1022
|
].flat();
|
|
1085
1023
|
}
|
|
1086
1024
|
toJSON() {
|
|
1087
1025
|
return {
|
|
1088
1026
|
...super.toJSON(),
|
|
1089
1027
|
type: "AST_HTML_ATTRIBUTE_NAME_NODE",
|
|
1090
|
-
|
|
1028
|
+
children: this.children.map(node => node.toJSON()),
|
|
1091
1029
|
};
|
|
1092
1030
|
}
|
|
1093
1031
|
treeInspect() {
|
|
1094
1032
|
let output = "";
|
|
1095
1033
|
output += `@ HTMLAttributeNameNode ${this.location.treeInspectWithLabel()}\n`;
|
|
1096
1034
|
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
1097
|
-
output += `└──
|
|
1098
|
-
// output += "\n";
|
|
1035
|
+
output += `└── children: ${this.inspectArray(this.children, " ")}`;
|
|
1099
1036
|
return output;
|
|
1100
1037
|
}
|
|
1101
1038
|
}
|
|
@@ -1108,9 +1045,9 @@ class HTMLAttributeNode extends Node {
|
|
|
1108
1045
|
type: data.type,
|
|
1109
1046
|
location: Location.from(data.location),
|
|
1110
1047
|
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
1111
|
-
name: data.name ? fromSerializedNode(data.name) : null,
|
|
1048
|
+
name: data.name ? fromSerializedNode((data.name)) : null,
|
|
1112
1049
|
equals: data.equals ? Token.from(data.equals) : null,
|
|
1113
|
-
value: data.value ? fromSerializedNode(data.value) : null,
|
|
1050
|
+
value: data.value ? fromSerializedNode((data.value)) : null,
|
|
1114
1051
|
});
|
|
1115
1052
|
}
|
|
1116
1053
|
constructor(props) {
|
|
@@ -1154,7 +1091,6 @@ class HTMLAttributeNode extends Node {
|
|
|
1154
1091
|
output += `├── name: ${this.inspectNode(this.name, "│ ")}`;
|
|
1155
1092
|
output += `├── equals: ${this.equals ? this.equals.treeInspect() : "∅"}\n`;
|
|
1156
1093
|
output += `└── value: ${this.inspectNode(this.value, " ")}`;
|
|
1157
|
-
// output += "\n";
|
|
1158
1094
|
return output;
|
|
1159
1095
|
}
|
|
1160
1096
|
}
|
|
@@ -1198,7 +1134,6 @@ class HTMLTextNode extends Node {
|
|
|
1198
1134
|
output += `@ HTMLTextNode ${this.location.treeInspectWithLabel()}\n`;
|
|
1199
1135
|
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
1200
1136
|
output += `└── content: ${this.content ? JSON.stringify(this.content) : "∅"}\n`;
|
|
1201
|
-
// output += "\n";
|
|
1202
1137
|
return output;
|
|
1203
1138
|
}
|
|
1204
1139
|
}
|
|
@@ -1255,7 +1190,6 @@ class HTMLCommentNode extends Node {
|
|
|
1255
1190
|
output += `├── comment_start: ${this.comment_start ? this.comment_start.treeInspect() : "∅"}\n`;
|
|
1256
1191
|
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
1257
1192
|
output += `└── comment_end: ${this.comment_end ? this.comment_end.treeInspect() : "∅"}\n`;
|
|
1258
|
-
// output += "\n";
|
|
1259
1193
|
return output;
|
|
1260
1194
|
}
|
|
1261
1195
|
}
|
|
@@ -1312,7 +1246,118 @@ class HTMLDoctypeNode extends Node {
|
|
|
1312
1246
|
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
1313
1247
|
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
1314
1248
|
output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1315
|
-
|
|
1249
|
+
return output;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
class XMLDeclarationNode extends Node {
|
|
1253
|
+
tag_opening;
|
|
1254
|
+
children;
|
|
1255
|
+
tag_closing;
|
|
1256
|
+
static from(data) {
|
|
1257
|
+
return new XMLDeclarationNode({
|
|
1258
|
+
type: data.type,
|
|
1259
|
+
location: Location.from(data.location),
|
|
1260
|
+
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
1261
|
+
tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
|
|
1262
|
+
children: (data.children || []).map(node => fromSerializedNode(node)),
|
|
1263
|
+
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
constructor(props) {
|
|
1267
|
+
super(props.type, props.location, props.errors);
|
|
1268
|
+
this.tag_opening = props.tag_opening;
|
|
1269
|
+
this.children = props.children;
|
|
1270
|
+
this.tag_closing = props.tag_closing;
|
|
1271
|
+
}
|
|
1272
|
+
accept(visitor) {
|
|
1273
|
+
visitor.visitXMLDeclarationNode(this);
|
|
1274
|
+
}
|
|
1275
|
+
childNodes() {
|
|
1276
|
+
return [
|
|
1277
|
+
...this.children,
|
|
1278
|
+
];
|
|
1279
|
+
}
|
|
1280
|
+
compactChildNodes() {
|
|
1281
|
+
return this.childNodes().filter(node => node !== null && node !== undefined);
|
|
1282
|
+
}
|
|
1283
|
+
recursiveErrors() {
|
|
1284
|
+
return [
|
|
1285
|
+
...this.errors,
|
|
1286
|
+
...this.children.map(node => node.recursiveErrors()),
|
|
1287
|
+
].flat();
|
|
1288
|
+
}
|
|
1289
|
+
toJSON() {
|
|
1290
|
+
return {
|
|
1291
|
+
...super.toJSON(),
|
|
1292
|
+
type: "AST_XML_DECLARATION_NODE",
|
|
1293
|
+
tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
|
|
1294
|
+
children: this.children.map(node => node.toJSON()),
|
|
1295
|
+
tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
treeInspect() {
|
|
1299
|
+
let output = "";
|
|
1300
|
+
output += `@ XMLDeclarationNode ${this.location.treeInspectWithLabel()}\n`;
|
|
1301
|
+
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
1302
|
+
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
1303
|
+
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
1304
|
+
output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1305
|
+
return output;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
class CDATANode extends Node {
|
|
1309
|
+
tag_opening;
|
|
1310
|
+
children;
|
|
1311
|
+
tag_closing;
|
|
1312
|
+
static from(data) {
|
|
1313
|
+
return new CDATANode({
|
|
1314
|
+
type: data.type,
|
|
1315
|
+
location: Location.from(data.location),
|
|
1316
|
+
errors: (data.errors || []).map(error => HerbError.from(error)),
|
|
1317
|
+
tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
|
|
1318
|
+
children: (data.children || []).map(node => fromSerializedNode(node)),
|
|
1319
|
+
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
constructor(props) {
|
|
1323
|
+
super(props.type, props.location, props.errors);
|
|
1324
|
+
this.tag_opening = props.tag_opening;
|
|
1325
|
+
this.children = props.children;
|
|
1326
|
+
this.tag_closing = props.tag_closing;
|
|
1327
|
+
}
|
|
1328
|
+
accept(visitor) {
|
|
1329
|
+
visitor.visitCDATANode(this);
|
|
1330
|
+
}
|
|
1331
|
+
childNodes() {
|
|
1332
|
+
return [
|
|
1333
|
+
...this.children,
|
|
1334
|
+
];
|
|
1335
|
+
}
|
|
1336
|
+
compactChildNodes() {
|
|
1337
|
+
return this.childNodes().filter(node => node !== null && node !== undefined);
|
|
1338
|
+
}
|
|
1339
|
+
recursiveErrors() {
|
|
1340
|
+
return [
|
|
1341
|
+
...this.errors,
|
|
1342
|
+
...this.children.map(node => node.recursiveErrors()),
|
|
1343
|
+
].flat();
|
|
1344
|
+
}
|
|
1345
|
+
toJSON() {
|
|
1346
|
+
return {
|
|
1347
|
+
...super.toJSON(),
|
|
1348
|
+
type: "AST_CDATA_NODE",
|
|
1349
|
+
tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
|
|
1350
|
+
children: this.children.map(node => node.toJSON()),
|
|
1351
|
+
tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
treeInspect() {
|
|
1355
|
+
let output = "";
|
|
1356
|
+
output += `@ CDATANode ${this.location.treeInspectWithLabel()}\n`;
|
|
1357
|
+
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
1358
|
+
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
1359
|
+
output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
|
|
1360
|
+
output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1316
1361
|
return output;
|
|
1317
1362
|
}
|
|
1318
1363
|
}
|
|
@@ -1356,7 +1401,6 @@ class WhitespaceNode extends Node {
|
|
|
1356
1401
|
output += `@ WhitespaceNode ${this.location.treeInspectWithLabel()}\n`;
|
|
1357
1402
|
output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
|
|
1358
1403
|
output += `└── value: ${this.value ? this.value.treeInspect() : "∅"}\n`;
|
|
1359
|
-
// output += "\n";
|
|
1360
1404
|
return output;
|
|
1361
1405
|
}
|
|
1362
1406
|
}
|
|
@@ -1425,7 +1469,6 @@ class ERBContentNode extends Node {
|
|
|
1425
1469
|
// no-op for analyzed_ruby
|
|
1426
1470
|
output += `├── parsed: ${typeof this.parsed === 'boolean' ? String(this.parsed) : "∅"}\n`;
|
|
1427
1471
|
output += `└── valid: ${typeof this.valid === 'boolean' ? String(this.valid) : "∅"}\n`;
|
|
1428
|
-
// output += "\n";
|
|
1429
1472
|
return output;
|
|
1430
1473
|
}
|
|
1431
1474
|
}
|
|
@@ -1479,7 +1522,6 @@ class ERBEndNode extends Node {
|
|
|
1479
1522
|
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
1480
1523
|
output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
|
|
1481
1524
|
output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1482
|
-
// output += "\n";
|
|
1483
1525
|
return output;
|
|
1484
1526
|
}
|
|
1485
1527
|
}
|
|
@@ -1541,7 +1583,6 @@ class ERBElseNode extends Node {
|
|
|
1541
1583
|
output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
|
|
1542
1584
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1543
1585
|
output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
|
|
1544
|
-
// output += "\n";
|
|
1545
1586
|
return output;
|
|
1546
1587
|
}
|
|
1547
1588
|
}
|
|
@@ -1561,8 +1602,8 @@ class ERBIfNode extends Node {
|
|
|
1561
1602
|
content: data.content ? Token.from(data.content) : null,
|
|
1562
1603
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1563
1604
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
1564
|
-
subsequent: data.subsequent ? fromSerializedNode(data.subsequent) : null,
|
|
1565
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
1605
|
+
subsequent: data.subsequent ? fromSerializedNode((data.subsequent)) : null,
|
|
1606
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
1566
1607
|
});
|
|
1567
1608
|
}
|
|
1568
1609
|
constructor(props) {
|
|
@@ -1617,7 +1658,6 @@ class ERBIfNode extends Node {
|
|
|
1617
1658
|
output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
|
|
1618
1659
|
output += `├── subsequent: ${this.inspectNode(this.subsequent, "│ ")}`;
|
|
1619
1660
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
1620
|
-
// output += "\n";
|
|
1621
1661
|
return output;
|
|
1622
1662
|
}
|
|
1623
1663
|
}
|
|
@@ -1636,7 +1676,7 @@ class ERBBlockNode extends Node {
|
|
|
1636
1676
|
content: data.content ? Token.from(data.content) : null,
|
|
1637
1677
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1638
1678
|
body: (data.body || []).map(node => fromSerializedNode(node)),
|
|
1639
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
1679
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
1640
1680
|
});
|
|
1641
1681
|
}
|
|
1642
1682
|
constructor(props) {
|
|
@@ -1686,7 +1726,6 @@ class ERBBlockNode extends Node {
|
|
|
1686
1726
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1687
1727
|
output += `├── body: ${this.inspectArray(this.body, "│ ")}`;
|
|
1688
1728
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
1689
|
-
// output += "\n";
|
|
1690
1729
|
return output;
|
|
1691
1730
|
}
|
|
1692
1731
|
}
|
|
@@ -1748,7 +1787,6 @@ class ERBWhenNode extends Node {
|
|
|
1748
1787
|
output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
|
|
1749
1788
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1750
1789
|
output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
|
|
1751
|
-
// output += "\n";
|
|
1752
1790
|
return output;
|
|
1753
1791
|
}
|
|
1754
1792
|
}
|
|
@@ -1770,8 +1808,8 @@ class ERBCaseNode extends Node {
|
|
|
1770
1808
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1771
1809
|
children: (data.children || []).map(node => fromSerializedNode(node)),
|
|
1772
1810
|
conditions: (data.conditions || []).map(node => fromSerializedNode(node)),
|
|
1773
|
-
else_clause: data.else_clause ? fromSerializedNode(data.else_clause) : null,
|
|
1774
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
1811
|
+
else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
|
|
1812
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
1775
1813
|
});
|
|
1776
1814
|
}
|
|
1777
1815
|
constructor(props) {
|
|
@@ -1831,7 +1869,6 @@ class ERBCaseNode extends Node {
|
|
|
1831
1869
|
output += `├── conditions: ${this.inspectArray(this.conditions, "│ ")}`;
|
|
1832
1870
|
output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
|
|
1833
1871
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
1834
|
-
// output += "\n";
|
|
1835
1872
|
return output;
|
|
1836
1873
|
}
|
|
1837
1874
|
}
|
|
@@ -1853,8 +1890,8 @@ class ERBCaseMatchNode extends Node {
|
|
|
1853
1890
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1854
1891
|
children: (data.children || []).map(node => fromSerializedNode(node)),
|
|
1855
1892
|
conditions: (data.conditions || []).map(node => fromSerializedNode(node)),
|
|
1856
|
-
else_clause: data.else_clause ? fromSerializedNode(data.else_clause) : null,
|
|
1857
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
1893
|
+
else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
|
|
1894
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
1858
1895
|
});
|
|
1859
1896
|
}
|
|
1860
1897
|
constructor(props) {
|
|
@@ -1914,7 +1951,6 @@ class ERBCaseMatchNode extends Node {
|
|
|
1914
1951
|
output += `├── conditions: ${this.inspectArray(this.conditions, "│ ")}`;
|
|
1915
1952
|
output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
|
|
1916
1953
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
1917
|
-
// output += "\n";
|
|
1918
1954
|
return output;
|
|
1919
1955
|
}
|
|
1920
1956
|
}
|
|
@@ -1933,7 +1969,7 @@ class ERBWhileNode extends Node {
|
|
|
1933
1969
|
content: data.content ? Token.from(data.content) : null,
|
|
1934
1970
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
1935
1971
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
1936
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
1972
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
1937
1973
|
});
|
|
1938
1974
|
}
|
|
1939
1975
|
constructor(props) {
|
|
@@ -1983,7 +2019,6 @@ class ERBWhileNode extends Node {
|
|
|
1983
2019
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
1984
2020
|
output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
|
|
1985
2021
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
1986
|
-
// output += "\n";
|
|
1987
2022
|
return output;
|
|
1988
2023
|
}
|
|
1989
2024
|
}
|
|
@@ -2002,7 +2037,7 @@ class ERBUntilNode extends Node {
|
|
|
2002
2037
|
content: data.content ? Token.from(data.content) : null,
|
|
2003
2038
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
2004
2039
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
2005
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
2040
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
2006
2041
|
});
|
|
2007
2042
|
}
|
|
2008
2043
|
constructor(props) {
|
|
@@ -2052,7 +2087,6 @@ class ERBUntilNode extends Node {
|
|
|
2052
2087
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
2053
2088
|
output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
|
|
2054
2089
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
2055
|
-
// output += "\n";
|
|
2056
2090
|
return output;
|
|
2057
2091
|
}
|
|
2058
2092
|
}
|
|
@@ -2071,7 +2105,7 @@ class ERBForNode extends Node {
|
|
|
2071
2105
|
content: data.content ? Token.from(data.content) : null,
|
|
2072
2106
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
2073
2107
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
2074
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
2108
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
2075
2109
|
});
|
|
2076
2110
|
}
|
|
2077
2111
|
constructor(props) {
|
|
@@ -2121,7 +2155,6 @@ class ERBForNode extends Node {
|
|
|
2121
2155
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
2122
2156
|
output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
|
|
2123
2157
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
2124
|
-
// output += "\n";
|
|
2125
2158
|
return output;
|
|
2126
2159
|
}
|
|
2127
2160
|
}
|
|
@@ -2140,7 +2173,7 @@ class ERBRescueNode extends Node {
|
|
|
2140
2173
|
content: data.content ? Token.from(data.content) : null,
|
|
2141
2174
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
2142
2175
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
2143
|
-
subsequent: data.subsequent ? fromSerializedNode(data.subsequent) : null,
|
|
2176
|
+
subsequent: data.subsequent ? fromSerializedNode((data.subsequent)) : null,
|
|
2144
2177
|
});
|
|
2145
2178
|
}
|
|
2146
2179
|
constructor(props) {
|
|
@@ -2190,7 +2223,6 @@ class ERBRescueNode extends Node {
|
|
|
2190
2223
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
2191
2224
|
output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
|
|
2192
2225
|
output += `└── subsequent: ${this.inspectNode(this.subsequent, " ")}`;
|
|
2193
|
-
// output += "\n";
|
|
2194
2226
|
return output;
|
|
2195
2227
|
}
|
|
2196
2228
|
}
|
|
@@ -2252,7 +2284,6 @@ class ERBEnsureNode extends Node {
|
|
|
2252
2284
|
output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
|
|
2253
2285
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
2254
2286
|
output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
|
|
2255
|
-
// output += "\n";
|
|
2256
2287
|
return output;
|
|
2257
2288
|
}
|
|
2258
2289
|
}
|
|
@@ -2274,10 +2305,10 @@ class ERBBeginNode extends Node {
|
|
|
2274
2305
|
content: data.content ? Token.from(data.content) : null,
|
|
2275
2306
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
2276
2307
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
2277
|
-
rescue_clause: data.rescue_clause ? fromSerializedNode(data.rescue_clause) : null,
|
|
2278
|
-
else_clause: data.else_clause ? fromSerializedNode(data.else_clause) : null,
|
|
2279
|
-
ensure_clause: data.ensure_clause ? fromSerializedNode(data.ensure_clause) : null,
|
|
2280
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
2308
|
+
rescue_clause: data.rescue_clause ? fromSerializedNode((data.rescue_clause)) : null,
|
|
2309
|
+
else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
|
|
2310
|
+
ensure_clause: data.ensure_clause ? fromSerializedNode((data.ensure_clause)) : null,
|
|
2311
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
2281
2312
|
});
|
|
2282
2313
|
}
|
|
2283
2314
|
constructor(props) {
|
|
@@ -2342,7 +2373,6 @@ class ERBBeginNode extends Node {
|
|
|
2342
2373
|
output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
|
|
2343
2374
|
output += `├── ensure_clause: ${this.inspectNode(this.ensure_clause, "│ ")}`;
|
|
2344
2375
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
2345
|
-
// output += "\n";
|
|
2346
2376
|
return output;
|
|
2347
2377
|
}
|
|
2348
2378
|
}
|
|
@@ -2362,8 +2392,8 @@ class ERBUnlessNode extends Node {
|
|
|
2362
2392
|
content: data.content ? Token.from(data.content) : null,
|
|
2363
2393
|
tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
|
|
2364
2394
|
statements: (data.statements || []).map(node => fromSerializedNode(node)),
|
|
2365
|
-
else_clause: data.else_clause ? fromSerializedNode(data.else_clause) : null,
|
|
2366
|
-
end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
|
|
2395
|
+
else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
|
|
2396
|
+
end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
|
|
2367
2397
|
});
|
|
2368
2398
|
}
|
|
2369
2399
|
constructor(props) {
|
|
@@ -2418,7 +2448,6 @@ class ERBUnlessNode extends Node {
|
|
|
2418
2448
|
output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
|
|
2419
2449
|
output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
|
|
2420
2450
|
output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
|
|
2421
|
-
// output += "\n";
|
|
2422
2451
|
return output;
|
|
2423
2452
|
}
|
|
2424
2453
|
}
|
|
@@ -2472,7 +2501,6 @@ class ERBYieldNode extends Node {
|
|
|
2472
2501
|
output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
|
|
2473
2502
|
output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
|
|
2474
2503
|
output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
2475
|
-
// output += "\n";
|
|
2476
2504
|
return output;
|
|
2477
2505
|
}
|
|
2478
2506
|
}
|
|
@@ -2534,7 +2562,6 @@ class ERBInNode extends Node {
|
|
|
2534
2562
|
output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
|
|
2535
2563
|
output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
|
|
2536
2564
|
output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
|
|
2537
|
-
// output += "\n";
|
|
2538
2565
|
return output;
|
|
2539
2566
|
}
|
|
2540
2567
|
}
|
|
@@ -2544,7 +2571,6 @@ function fromSerializedNode(node) {
|
|
|
2544
2571
|
case "AST_LITERAL_NODE": return LiteralNode.from(node);
|
|
2545
2572
|
case "AST_HTML_OPEN_TAG_NODE": return HTMLOpenTagNode.from(node);
|
|
2546
2573
|
case "AST_HTML_CLOSE_TAG_NODE": return HTMLCloseTagNode.from(node);
|
|
2547
|
-
case "AST_HTML_SELF_CLOSE_TAG_NODE": return HTMLSelfCloseTagNode.from(node);
|
|
2548
2574
|
case "AST_HTML_ELEMENT_NODE": return HTMLElementNode.from(node);
|
|
2549
2575
|
case "AST_HTML_ATTRIBUTE_VALUE_NODE": return HTMLAttributeValueNode.from(node);
|
|
2550
2576
|
case "AST_HTML_ATTRIBUTE_NAME_NODE": return HTMLAttributeNameNode.from(node);
|
|
@@ -2552,6 +2578,8 @@ function fromSerializedNode(node) {
|
|
|
2552
2578
|
case "AST_HTML_TEXT_NODE": return HTMLTextNode.from(node);
|
|
2553
2579
|
case "AST_HTML_COMMENT_NODE": return HTMLCommentNode.from(node);
|
|
2554
2580
|
case "AST_HTML_DOCTYPE_NODE": return HTMLDoctypeNode.from(node);
|
|
2581
|
+
case "AST_XML_DECLARATION_NODE": return XMLDeclarationNode.from(node);
|
|
2582
|
+
case "AST_CDATA_NODE": return CDATANode.from(node);
|
|
2555
2583
|
case "AST_WHITESPACE_NODE": return WhitespaceNode.from(node);
|
|
2556
2584
|
case "AST_ERB_CONTENT_NODE": return ERBContentNode.from(node);
|
|
2557
2585
|
case "AST_ERB_END_NODE": return ERBEndNode.from(node);
|
|
@@ -2575,63 +2603,613 @@ function fromSerializedNode(node) {
|
|
|
2575
2603
|
}
|
|
2576
2604
|
}
|
|
2577
2605
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
visitAll(nodes) {
|
|
2587
|
-
nodes.forEach(node => node?.accept(this));
|
|
2588
|
-
}
|
|
2589
|
-
visitChildNodes(node) {
|
|
2590
|
-
node.compactChildNodes().forEach(node => node.accept(this));
|
|
2591
|
-
}
|
|
2592
|
-
visitDocumentNode(node) {
|
|
2593
|
-
this.visitChildNodes(node);
|
|
2594
|
-
}
|
|
2595
|
-
visitLiteralNode(node) {
|
|
2596
|
-
this.visitChildNodes(node);
|
|
2606
|
+
class Result {
|
|
2607
|
+
source;
|
|
2608
|
+
warnings;
|
|
2609
|
+
errors;
|
|
2610
|
+
constructor(source, warnings = [], errors = []) {
|
|
2611
|
+
this.source = source;
|
|
2612
|
+
this.warnings = warnings || [];
|
|
2613
|
+
this.errors = errors || [];
|
|
2597
2614
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2615
|
+
/**
|
|
2616
|
+
* Determines if the parsing was successful.
|
|
2617
|
+
* @returns `true` if there are no errors, otherwise `false`.
|
|
2618
|
+
*/
|
|
2619
|
+
get successful() {
|
|
2620
|
+
return this.errors.length === 0;
|
|
2600
2621
|
}
|
|
2601
|
-
|
|
2602
|
-
|
|
2622
|
+
/**
|
|
2623
|
+
* Determines if the parsing failed.
|
|
2624
|
+
* @returns `true` if there are errors, otherwise `false`.
|
|
2625
|
+
*/
|
|
2626
|
+
get failed() {
|
|
2627
|
+
return this.errors.length > 0;
|
|
2603
2628
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
class HerbWarning {
|
|
2632
|
+
message;
|
|
2633
|
+
location;
|
|
2634
|
+
static from(warning) {
|
|
2635
|
+
return new HerbWarning(warning.message, Location.from(warning.location));
|
|
2606
2636
|
}
|
|
2607
|
-
|
|
2608
|
-
this.
|
|
2637
|
+
constructor(message, location) {
|
|
2638
|
+
this.message = message;
|
|
2639
|
+
this.location = location;
|
|
2609
2640
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/**
|
|
2644
|
+
* Represents the result of a parsing operation, extending the base `Result` class.
|
|
2645
|
+
* It contains the parsed document node, source code, warnings, and errors.
|
|
2646
|
+
*/
|
|
2647
|
+
class ParseResult extends Result {
|
|
2648
|
+
/** The document node generated from the source code. */
|
|
2649
|
+
value;
|
|
2650
|
+
/**
|
|
2651
|
+
* Creates a `ParseResult` instance from a serialized result.
|
|
2652
|
+
* @param result - The serialized parse result containing the value and source.
|
|
2653
|
+
* @returns A new `ParseResult` instance.
|
|
2654
|
+
*/
|
|
2655
|
+
static from(result) {
|
|
2656
|
+
return new ParseResult(DocumentNode.from(result.value), result.source, result.warnings.map((warning) => HerbWarning.from(warning)), result.errors.map((error) => HerbError.from(error)));
|
|
2612
2657
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2658
|
+
/**
|
|
2659
|
+
* Constructs a new `ParseResult`.
|
|
2660
|
+
* @param value - The document node.
|
|
2661
|
+
* @param source - The source code that was parsed.
|
|
2662
|
+
* @param warnings - An array of warnings encountered during parsing.
|
|
2663
|
+
* @param errors - An array of errors encountered during parsing.
|
|
2664
|
+
*/
|
|
2665
|
+
constructor(value, source, warnings = [], errors = []) {
|
|
2666
|
+
super(source, warnings, errors);
|
|
2667
|
+
this.value = value;
|
|
2615
2668
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2669
|
+
/**
|
|
2670
|
+
* Determines if the parsing failed.
|
|
2671
|
+
* @returns `true` if there are errors, otherwise `false`.
|
|
2672
|
+
*/
|
|
2673
|
+
get failed() {
|
|
2674
|
+
// Consider errors on this result and recursively in the document tree
|
|
2675
|
+
return this.recursiveErrors().length > 0;
|
|
2618
2676
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2677
|
+
/**
|
|
2678
|
+
* Determines if the parsing was successful.
|
|
2679
|
+
* @returns `true` if there are no errors, otherwise `false`.
|
|
2680
|
+
*/
|
|
2681
|
+
get successful() {
|
|
2682
|
+
return !this.failed;
|
|
2621
2683
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2684
|
+
/**
|
|
2685
|
+
* Returns a pretty-printed JSON string of the errors.
|
|
2686
|
+
* @returns A string representation of the errors.
|
|
2687
|
+
*/
|
|
2688
|
+
prettyErrors() {
|
|
2689
|
+
return JSON.stringify([...this.errors, ...this.value.errors], null, 2);
|
|
2624
2690
|
}
|
|
2625
|
-
|
|
2626
|
-
this.
|
|
2691
|
+
recursiveErrors() {
|
|
2692
|
+
return [...this.errors, ...this.value.recursiveErrors()];
|
|
2627
2693
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
2694
|
+
/**
|
|
2695
|
+
* Returns a pretty-printed string of the parse result.
|
|
2696
|
+
* @returns A string representation of the parse result.
|
|
2697
|
+
*/
|
|
2698
|
+
inspect() {
|
|
2699
|
+
return this.value.inspect();
|
|
2630
2700
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2701
|
+
/**
|
|
2702
|
+
* Accepts a visitor to traverse the document node.
|
|
2703
|
+
* @param visitor - The visitor instance.
|
|
2704
|
+
*/
|
|
2705
|
+
visit(visitor) {
|
|
2706
|
+
visitor.visit(this.value);
|
|
2633
2707
|
}
|
|
2634
|
-
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
2711
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.6.0/templates/javascript/packages/core/src/node-type-guards.ts.erb
|
|
2712
|
+
/**
|
|
2713
|
+
* Type guard functions for AST nodes.
|
|
2714
|
+
* These functions provide type checking by combining both instanceof
|
|
2715
|
+
* checks and type string comparisons for maximum reliability across different
|
|
2716
|
+
* runtime scenarios (e.g., serialized/deserialized nodes).
|
|
2717
|
+
*/
|
|
2718
|
+
/**
|
|
2719
|
+
* Checks if a node is a DocumentNode
|
|
2720
|
+
*/
|
|
2721
|
+
function isDocumentNode(node) {
|
|
2722
|
+
return node instanceof DocumentNode || node.type === "AST_DOCUMENT_NODE";
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Checks if a node is a LiteralNode
|
|
2726
|
+
*/
|
|
2727
|
+
function isLiteralNode(node) {
|
|
2728
|
+
return node instanceof LiteralNode || node.type === "AST_LITERAL_NODE";
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Checks if a node is a HTMLOpenTagNode
|
|
2732
|
+
*/
|
|
2733
|
+
function isHTMLOpenTagNode(node) {
|
|
2734
|
+
return node instanceof HTMLOpenTagNode || node.type === "AST_HTML_OPEN_TAG_NODE";
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Checks if a node is a HTMLCloseTagNode
|
|
2738
|
+
*/
|
|
2739
|
+
function isHTMLCloseTagNode(node) {
|
|
2740
|
+
return node instanceof HTMLCloseTagNode || node.type === "AST_HTML_CLOSE_TAG_NODE";
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Checks if a node is a HTMLElementNode
|
|
2744
|
+
*/
|
|
2745
|
+
function isHTMLElementNode(node) {
|
|
2746
|
+
return node instanceof HTMLElementNode || node.type === "AST_HTML_ELEMENT_NODE";
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Checks if a node is a HTMLAttributeValueNode
|
|
2750
|
+
*/
|
|
2751
|
+
function isHTMLAttributeValueNode(node) {
|
|
2752
|
+
return node instanceof HTMLAttributeValueNode || node.type === "AST_HTML_ATTRIBUTE_VALUE_NODE";
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Checks if a node is a HTMLAttributeNameNode
|
|
2756
|
+
*/
|
|
2757
|
+
function isHTMLAttributeNameNode(node) {
|
|
2758
|
+
return node instanceof HTMLAttributeNameNode || node.type === "AST_HTML_ATTRIBUTE_NAME_NODE";
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* Checks if a node is a HTMLAttributeNode
|
|
2762
|
+
*/
|
|
2763
|
+
function isHTMLAttributeNode(node) {
|
|
2764
|
+
return node instanceof HTMLAttributeNode || node.type === "AST_HTML_ATTRIBUTE_NODE";
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Checks if a node is a HTMLTextNode
|
|
2768
|
+
*/
|
|
2769
|
+
function isHTMLTextNode(node) {
|
|
2770
|
+
return node instanceof HTMLTextNode || node.type === "AST_HTML_TEXT_NODE";
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Checks if a node is a HTMLCommentNode
|
|
2774
|
+
*/
|
|
2775
|
+
function isHTMLCommentNode(node) {
|
|
2776
|
+
return node instanceof HTMLCommentNode || node.type === "AST_HTML_COMMENT_NODE";
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Checks if a node is a HTMLDoctypeNode
|
|
2780
|
+
*/
|
|
2781
|
+
function isHTMLDoctypeNode(node) {
|
|
2782
|
+
return node instanceof HTMLDoctypeNode || node.type === "AST_HTML_DOCTYPE_NODE";
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Checks if a node is a XMLDeclarationNode
|
|
2786
|
+
*/
|
|
2787
|
+
function isXMLDeclarationNode(node) {
|
|
2788
|
+
return node instanceof XMLDeclarationNode || node.type === "AST_XML_DECLARATION_NODE";
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Checks if a node is a CDATANode
|
|
2792
|
+
*/
|
|
2793
|
+
function isCDATANode(node) {
|
|
2794
|
+
return node instanceof CDATANode || node.type === "AST_CDATA_NODE";
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Checks if a node is a WhitespaceNode
|
|
2798
|
+
*/
|
|
2799
|
+
function isWhitespaceNode(node) {
|
|
2800
|
+
return node instanceof WhitespaceNode || node.type === "AST_WHITESPACE_NODE";
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Checks if a node is a ERBContentNode
|
|
2804
|
+
*/
|
|
2805
|
+
function isERBContentNode(node) {
|
|
2806
|
+
return node instanceof ERBContentNode || node.type === "AST_ERB_CONTENT_NODE";
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Checks if a node is a ERBEndNode
|
|
2810
|
+
*/
|
|
2811
|
+
function isERBEndNode(node) {
|
|
2812
|
+
return node instanceof ERBEndNode || node.type === "AST_ERB_END_NODE";
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Checks if a node is a ERBElseNode
|
|
2816
|
+
*/
|
|
2817
|
+
function isERBElseNode(node) {
|
|
2818
|
+
return node instanceof ERBElseNode || node.type === "AST_ERB_ELSE_NODE";
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* Checks if a node is a ERBIfNode
|
|
2822
|
+
*/
|
|
2823
|
+
function isERBIfNode(node) {
|
|
2824
|
+
return node instanceof ERBIfNode || node.type === "AST_ERB_IF_NODE";
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Checks if a node is a ERBBlockNode
|
|
2828
|
+
*/
|
|
2829
|
+
function isERBBlockNode(node) {
|
|
2830
|
+
return node instanceof ERBBlockNode || node.type === "AST_ERB_BLOCK_NODE";
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Checks if a node is a ERBWhenNode
|
|
2834
|
+
*/
|
|
2835
|
+
function isERBWhenNode(node) {
|
|
2836
|
+
return node instanceof ERBWhenNode || node.type === "AST_ERB_WHEN_NODE";
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Checks if a node is a ERBCaseNode
|
|
2840
|
+
*/
|
|
2841
|
+
function isERBCaseNode(node) {
|
|
2842
|
+
return node instanceof ERBCaseNode || node.type === "AST_ERB_CASE_NODE";
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Checks if a node is a ERBCaseMatchNode
|
|
2846
|
+
*/
|
|
2847
|
+
function isERBCaseMatchNode(node) {
|
|
2848
|
+
return node instanceof ERBCaseMatchNode || node.type === "AST_ERB_CASE_MATCH_NODE";
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Checks if a node is a ERBWhileNode
|
|
2852
|
+
*/
|
|
2853
|
+
function isERBWhileNode(node) {
|
|
2854
|
+
return node instanceof ERBWhileNode || node.type === "AST_ERB_WHILE_NODE";
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Checks if a node is a ERBUntilNode
|
|
2858
|
+
*/
|
|
2859
|
+
function isERBUntilNode(node) {
|
|
2860
|
+
return node instanceof ERBUntilNode || node.type === "AST_ERB_UNTIL_NODE";
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Checks if a node is a ERBForNode
|
|
2864
|
+
*/
|
|
2865
|
+
function isERBForNode(node) {
|
|
2866
|
+
return node instanceof ERBForNode || node.type === "AST_ERB_FOR_NODE";
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Checks if a node is a ERBRescueNode
|
|
2870
|
+
*/
|
|
2871
|
+
function isERBRescueNode(node) {
|
|
2872
|
+
return node instanceof ERBRescueNode || node.type === "AST_ERB_RESCUE_NODE";
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* Checks if a node is a ERBEnsureNode
|
|
2876
|
+
*/
|
|
2877
|
+
function isERBEnsureNode(node) {
|
|
2878
|
+
return node instanceof ERBEnsureNode || node.type === "AST_ERB_ENSURE_NODE";
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Checks if a node is a ERBBeginNode
|
|
2882
|
+
*/
|
|
2883
|
+
function isERBBeginNode(node) {
|
|
2884
|
+
return node instanceof ERBBeginNode || node.type === "AST_ERB_BEGIN_NODE";
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Checks if a node is a ERBUnlessNode
|
|
2888
|
+
*/
|
|
2889
|
+
function isERBUnlessNode(node) {
|
|
2890
|
+
return node instanceof ERBUnlessNode || node.type === "AST_ERB_UNLESS_NODE";
|
|
2891
|
+
}
|
|
2892
|
+
/**
|
|
2893
|
+
* Checks if a node is a ERBYieldNode
|
|
2894
|
+
*/
|
|
2895
|
+
function isERBYieldNode(node) {
|
|
2896
|
+
return node instanceof ERBYieldNode || node.type === "AST_ERB_YIELD_NODE";
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Checks if a node is a ERBInNode
|
|
2900
|
+
*/
|
|
2901
|
+
function isERBInNode(node) {
|
|
2902
|
+
return node instanceof ERBInNode || node.type === "AST_ERB_IN_NODE";
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Checks if a node is any ERB node type
|
|
2906
|
+
*/
|
|
2907
|
+
function isERBNode(node) {
|
|
2908
|
+
return isERBContentNode(node) ||
|
|
2909
|
+
isERBEndNode(node) ||
|
|
2910
|
+
isERBElseNode(node) ||
|
|
2911
|
+
isERBIfNode(node) ||
|
|
2912
|
+
isERBBlockNode(node) ||
|
|
2913
|
+
isERBWhenNode(node) ||
|
|
2914
|
+
isERBCaseNode(node) ||
|
|
2915
|
+
isERBCaseMatchNode(node) ||
|
|
2916
|
+
isERBWhileNode(node) ||
|
|
2917
|
+
isERBUntilNode(node) ||
|
|
2918
|
+
isERBForNode(node) ||
|
|
2919
|
+
isERBRescueNode(node) ||
|
|
2920
|
+
isERBEnsureNode(node) ||
|
|
2921
|
+
isERBBeginNode(node) ||
|
|
2922
|
+
isERBUnlessNode(node) ||
|
|
2923
|
+
isERBYieldNode(node) ||
|
|
2924
|
+
isERBInNode(node);
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Map of node classes to their corresponding type guard functions
|
|
2928
|
+
*
|
|
2929
|
+
* @example
|
|
2930
|
+
* const guard = NODE_TYPE_GUARDS[HTMLTextNode]
|
|
2931
|
+
*
|
|
2932
|
+
* if (guard(node)) {
|
|
2933
|
+
* // node is HTMLTextNode
|
|
2934
|
+
* }
|
|
2935
|
+
*/
|
|
2936
|
+
const NODE_TYPE_GUARDS = new Map([
|
|
2937
|
+
[DocumentNode, isDocumentNode],
|
|
2938
|
+
[LiteralNode, isLiteralNode],
|
|
2939
|
+
[HTMLOpenTagNode, isHTMLOpenTagNode],
|
|
2940
|
+
[HTMLCloseTagNode, isHTMLCloseTagNode],
|
|
2941
|
+
[HTMLElementNode, isHTMLElementNode],
|
|
2942
|
+
[HTMLAttributeValueNode, isHTMLAttributeValueNode],
|
|
2943
|
+
[HTMLAttributeNameNode, isHTMLAttributeNameNode],
|
|
2944
|
+
[HTMLAttributeNode, isHTMLAttributeNode],
|
|
2945
|
+
[HTMLTextNode, isHTMLTextNode],
|
|
2946
|
+
[HTMLCommentNode, isHTMLCommentNode],
|
|
2947
|
+
[HTMLDoctypeNode, isHTMLDoctypeNode],
|
|
2948
|
+
[XMLDeclarationNode, isXMLDeclarationNode],
|
|
2949
|
+
[CDATANode, isCDATANode],
|
|
2950
|
+
[WhitespaceNode, isWhitespaceNode],
|
|
2951
|
+
[ERBContentNode, isERBContentNode],
|
|
2952
|
+
[ERBEndNode, isERBEndNode],
|
|
2953
|
+
[ERBElseNode, isERBElseNode],
|
|
2954
|
+
[ERBIfNode, isERBIfNode],
|
|
2955
|
+
[ERBBlockNode, isERBBlockNode],
|
|
2956
|
+
[ERBWhenNode, isERBWhenNode],
|
|
2957
|
+
[ERBCaseNode, isERBCaseNode],
|
|
2958
|
+
[ERBCaseMatchNode, isERBCaseMatchNode],
|
|
2959
|
+
[ERBWhileNode, isERBWhileNode],
|
|
2960
|
+
[ERBUntilNode, isERBUntilNode],
|
|
2961
|
+
[ERBForNode, isERBForNode],
|
|
2962
|
+
[ERBRescueNode, isERBRescueNode],
|
|
2963
|
+
[ERBEnsureNode, isERBEnsureNode],
|
|
2964
|
+
[ERBBeginNode, isERBBeginNode],
|
|
2965
|
+
[ERBUnlessNode, isERBUnlessNode],
|
|
2966
|
+
[ERBYieldNode, isERBYieldNode],
|
|
2967
|
+
[ERBInNode, isERBInNode],
|
|
2968
|
+
]);
|
|
2969
|
+
/**
|
|
2970
|
+
* Map of AST node type strings to their corresponding type guard functions
|
|
2971
|
+
*
|
|
2972
|
+
* @example
|
|
2973
|
+
* const guard = AST_TYPE_GUARDS["AST_HTML_TEXT_NODE"]
|
|
2974
|
+
*
|
|
2975
|
+
* if (guard(node)) {
|
|
2976
|
+
* // node is HTMLTextNode
|
|
2977
|
+
* }
|
|
2978
|
+
*/
|
|
2979
|
+
const AST_TYPE_GUARDS = new Map([
|
|
2980
|
+
["AST_DOCUMENT_NODE", isDocumentNode],
|
|
2981
|
+
["AST_LITERAL_NODE", isLiteralNode],
|
|
2982
|
+
["AST_HTML_OPEN_TAG_NODE", isHTMLOpenTagNode],
|
|
2983
|
+
["AST_HTML_CLOSE_TAG_NODE", isHTMLCloseTagNode],
|
|
2984
|
+
["AST_HTML_ELEMENT_NODE", isHTMLElementNode],
|
|
2985
|
+
["AST_HTML_ATTRIBUTE_VALUE_NODE", isHTMLAttributeValueNode],
|
|
2986
|
+
["AST_HTML_ATTRIBUTE_NAME_NODE", isHTMLAttributeNameNode],
|
|
2987
|
+
["AST_HTML_ATTRIBUTE_NODE", isHTMLAttributeNode],
|
|
2988
|
+
["AST_HTML_TEXT_NODE", isHTMLTextNode],
|
|
2989
|
+
["AST_HTML_COMMENT_NODE", isHTMLCommentNode],
|
|
2990
|
+
["AST_HTML_DOCTYPE_NODE", isHTMLDoctypeNode],
|
|
2991
|
+
["AST_XML_DECLARATION_NODE", isXMLDeclarationNode],
|
|
2992
|
+
["AST_CDATA_NODE", isCDATANode],
|
|
2993
|
+
["AST_WHITESPACE_NODE", isWhitespaceNode],
|
|
2994
|
+
["AST_ERB_CONTENT_NODE", isERBContentNode],
|
|
2995
|
+
["AST_ERB_END_NODE", isERBEndNode],
|
|
2996
|
+
["AST_ERB_ELSE_NODE", isERBElseNode],
|
|
2997
|
+
["AST_ERB_IF_NODE", isERBIfNode],
|
|
2998
|
+
["AST_ERB_BLOCK_NODE", isERBBlockNode],
|
|
2999
|
+
["AST_ERB_WHEN_NODE", isERBWhenNode],
|
|
3000
|
+
["AST_ERB_CASE_NODE", isERBCaseNode],
|
|
3001
|
+
["AST_ERB_CASE_MATCH_NODE", isERBCaseMatchNode],
|
|
3002
|
+
["AST_ERB_WHILE_NODE", isERBWhileNode],
|
|
3003
|
+
["AST_ERB_UNTIL_NODE", isERBUntilNode],
|
|
3004
|
+
["AST_ERB_FOR_NODE", isERBForNode],
|
|
3005
|
+
["AST_ERB_RESCUE_NODE", isERBRescueNode],
|
|
3006
|
+
["AST_ERB_ENSURE_NODE", isERBEnsureNode],
|
|
3007
|
+
["AST_ERB_BEGIN_NODE", isERBBeginNode],
|
|
3008
|
+
["AST_ERB_UNLESS_NODE", isERBUnlessNode],
|
|
3009
|
+
["AST_ERB_YIELD_NODE", isERBYieldNode],
|
|
3010
|
+
["AST_ERB_IN_NODE", isERBInNode],
|
|
3011
|
+
]);
|
|
3012
|
+
/**
|
|
3013
|
+
* Checks if a node matches any of the provided type identifiers with proper type narrowing
|
|
3014
|
+
* Supports AST type strings, node classes, or type guard functions
|
|
3015
|
+
*
|
|
3016
|
+
* @example
|
|
3017
|
+
* if (isAnyOf(node, "AST_HTML_TEXT_NODE", "AST_LITERAL_NODE")) {
|
|
3018
|
+
* // node is narrowed to HTMLTextNode | LiteralNode
|
|
3019
|
+
* }
|
|
3020
|
+
*
|
|
3021
|
+
* @example
|
|
3022
|
+
* if (isAnyOf(node, HTMLTextNode, LiteralNode)) {
|
|
3023
|
+
* // node is narrowed to HTMLTextNode | LiteralNode
|
|
3024
|
+
* }
|
|
3025
|
+
*/
|
|
3026
|
+
function isAnyOf(node, ...types) {
|
|
3027
|
+
return types.some(type => {
|
|
3028
|
+
if (typeof type === 'string') {
|
|
3029
|
+
return isNode(node, type);
|
|
3030
|
+
}
|
|
3031
|
+
else if (typeof type === 'function' && type.prototype && type.prototype.constructor === type && NODE_TYPE_GUARDS.has(type)) {
|
|
3032
|
+
return isNode(node, type);
|
|
3033
|
+
}
|
|
3034
|
+
else if (typeof type === 'function') {
|
|
3035
|
+
return type(node);
|
|
3036
|
+
}
|
|
3037
|
+
else {
|
|
3038
|
+
return false;
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Checks if a node does NOT match any of the provided type identifiers
|
|
3044
|
+
* Supports AST type strings, node classes, or type guard functions
|
|
3045
|
+
* This is the logical inverse of isAnyOf
|
|
3046
|
+
*
|
|
3047
|
+
* @example
|
|
3048
|
+
* if (isNoneOf(node, "AST_HTML_TEXT_NODE", "AST_LITERAL_NODE")) {
|
|
3049
|
+
* // node is neither HTMLTextNode nor LiteralNode
|
|
3050
|
+
* }
|
|
3051
|
+
*
|
|
3052
|
+
* @example
|
|
3053
|
+
* if (isNoneOf(node, HTMLTextNode, LiteralNode)) {
|
|
3054
|
+
* // node is neither HTMLTextNode nor LiteralNode
|
|
3055
|
+
* }
|
|
3056
|
+
*
|
|
3057
|
+
* @example
|
|
3058
|
+
* if (isNoneOf(node, isHTMLTextNode, isLiteralNode)) {
|
|
3059
|
+
* // node is neither HTMLTextNode nor LiteralNode
|
|
3060
|
+
* }
|
|
3061
|
+
*/
|
|
3062
|
+
function isNoneOf(node, ...types) {
|
|
3063
|
+
return !isAnyOf(node, ...types);
|
|
3064
|
+
}
|
|
3065
|
+
function filterNodes(nodes, ...types) {
|
|
3066
|
+
if (!nodes)
|
|
3067
|
+
return [];
|
|
3068
|
+
return nodes.filter(node => isAnyOf(node, ...types));
|
|
3069
|
+
}
|
|
3070
|
+
function isNode(node, type) {
|
|
3071
|
+
if (typeof type === 'string') {
|
|
3072
|
+
const guard = AST_TYPE_GUARDS.get(type);
|
|
3073
|
+
return guard ? guard(node) : false;
|
|
3074
|
+
}
|
|
3075
|
+
else if (typeof type === 'function') {
|
|
3076
|
+
const guard = NODE_TYPE_GUARDS.get(type);
|
|
3077
|
+
return guard ? guard(node) : false;
|
|
3078
|
+
}
|
|
3079
|
+
else {
|
|
3080
|
+
return false;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
function isToken(object) {
|
|
3084
|
+
return (object instanceof Token) || (object?.constructor?.name === "Token" && "value" in object) || object.type?.startsWith('TOKEN_');
|
|
3085
|
+
}
|
|
3086
|
+
function isParseResult(object) {
|
|
3087
|
+
return (object instanceof ParseResult) || (object?.constructor?.name === "ParseResult" && "value" in object);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
/**
|
|
3091
|
+
* Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
|
|
3092
|
+
*/
|
|
3093
|
+
function isERBOutputNode(node) {
|
|
3094
|
+
return isNode(node, ERBContentNode) && ["<%=", "<%=="].includes(node.tag_opening?.value);
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Checks if a node is a non-output ERB node (control flow: <% %>)
|
|
3098
|
+
*/
|
|
3099
|
+
function isERBControlFlowNode(node) {
|
|
3100
|
+
return isAnyOf(node, ERBIfNode, ERBUnlessNode, ERBBlockNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBForNode, ERBBeginNode);
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Checks if an array of nodes contains any ERB output nodes (dynamic content)
|
|
3104
|
+
*/
|
|
3105
|
+
function hasERBOutput(nodes) {
|
|
3106
|
+
return nodes.some(isERBOutputNode);
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* Extracts a combined string from nodes, including ERB content
|
|
3110
|
+
* For ERB nodes, includes the full tag syntax (e.g., "<%= foo %>")
|
|
3111
|
+
* This is useful for debugging or displaying the full attribute name
|
|
3112
|
+
*/
|
|
3113
|
+
function getCombinedStringFromNodes(nodes) {
|
|
3114
|
+
return nodes.map(node => {
|
|
3115
|
+
if (isLiteralNode(node)) {
|
|
3116
|
+
return node.content;
|
|
3117
|
+
}
|
|
3118
|
+
else if (isERBContentNode(node)) {
|
|
3119
|
+
const opening = node.tag_opening?.value || "";
|
|
3120
|
+
const content = node.content?.value || "";
|
|
3121
|
+
const closing = node.tag_closing?.value || "";
|
|
3122
|
+
return `${opening}${content}${closing}`;
|
|
3123
|
+
}
|
|
3124
|
+
else {
|
|
3125
|
+
// For other node types, return a placeholder or empty string
|
|
3126
|
+
return `[${node.type}]`;
|
|
3127
|
+
}
|
|
3128
|
+
}).join("");
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Gets the combined string representation of an HTML attribute name node
|
|
3132
|
+
* This includes both static and dynamic content, useful for debugging
|
|
3133
|
+
*/
|
|
3134
|
+
function getCombinedAttributeName(attributeNameNode) {
|
|
3135
|
+
if (!attributeNameNode.children) {
|
|
3136
|
+
return "";
|
|
3137
|
+
}
|
|
3138
|
+
return getCombinedStringFromNodes(attributeNameNode.children);
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Gets the tag name of an HTML element node
|
|
3142
|
+
*/
|
|
3143
|
+
function getTagName(node) {
|
|
3144
|
+
return node.tag_name?.value ?? "";
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Check if a node is a comment (HTML comment or ERB comment)
|
|
3148
|
+
*/
|
|
3149
|
+
function isCommentNode(node) {
|
|
3150
|
+
return isNode(node, HTMLCommentNode) || (isERBNode(node) && !isERBControlFlowNode(node));
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
3154
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.6.0/templates/javascript/packages/core/src/visitor.ts.erb
|
|
3155
|
+
class Visitor {
|
|
3156
|
+
visit(node) {
|
|
3157
|
+
if (!node)
|
|
3158
|
+
return;
|
|
3159
|
+
node.accept(this);
|
|
3160
|
+
}
|
|
3161
|
+
visitAll(nodes) {
|
|
3162
|
+
nodes.forEach(node => node?.accept(this));
|
|
3163
|
+
}
|
|
3164
|
+
visitChildNodes(node) {
|
|
3165
|
+
node.compactChildNodes().forEach(node => node.accept(this));
|
|
3166
|
+
}
|
|
3167
|
+
visitDocumentNode(node) {
|
|
3168
|
+
this.visitChildNodes(node);
|
|
3169
|
+
}
|
|
3170
|
+
visitLiteralNode(node) {
|
|
3171
|
+
this.visitChildNodes(node);
|
|
3172
|
+
}
|
|
3173
|
+
visitHTMLOpenTagNode(node) {
|
|
3174
|
+
this.visitChildNodes(node);
|
|
3175
|
+
}
|
|
3176
|
+
visitHTMLCloseTagNode(node) {
|
|
3177
|
+
this.visitChildNodes(node);
|
|
3178
|
+
}
|
|
3179
|
+
visitHTMLElementNode(node) {
|
|
3180
|
+
this.visitChildNodes(node);
|
|
3181
|
+
}
|
|
3182
|
+
visitHTMLAttributeValueNode(node) {
|
|
3183
|
+
this.visitChildNodes(node);
|
|
3184
|
+
}
|
|
3185
|
+
visitHTMLAttributeNameNode(node) {
|
|
3186
|
+
this.visitChildNodes(node);
|
|
3187
|
+
}
|
|
3188
|
+
visitHTMLAttributeNode(node) {
|
|
3189
|
+
this.visitChildNodes(node);
|
|
3190
|
+
}
|
|
3191
|
+
visitHTMLTextNode(node) {
|
|
3192
|
+
this.visitChildNodes(node);
|
|
3193
|
+
}
|
|
3194
|
+
visitHTMLCommentNode(node) {
|
|
3195
|
+
this.visitChildNodes(node);
|
|
3196
|
+
}
|
|
3197
|
+
visitHTMLDoctypeNode(node) {
|
|
3198
|
+
this.visitChildNodes(node);
|
|
3199
|
+
}
|
|
3200
|
+
visitXMLDeclarationNode(node) {
|
|
3201
|
+
this.visitChildNodes(node);
|
|
3202
|
+
}
|
|
3203
|
+
visitCDATANode(node) {
|
|
3204
|
+
this.visitChildNodes(node);
|
|
3205
|
+
}
|
|
3206
|
+
visitWhitespaceNode(node) {
|
|
3207
|
+
this.visitChildNodes(node);
|
|
3208
|
+
}
|
|
3209
|
+
visitERBContentNode(node) {
|
|
3210
|
+
this.visitChildNodes(node);
|
|
3211
|
+
}
|
|
3212
|
+
visitERBEndNode(node) {
|
|
2635
3213
|
this.visitChildNodes(node);
|
|
2636
3214
|
}
|
|
2637
3215
|
visitERBElseNode(node) {
|
|
@@ -2676,51 +3254,573 @@ class Visitor {
|
|
|
2676
3254
|
visitERBYieldNode(node) {
|
|
2677
3255
|
this.visitChildNodes(node);
|
|
2678
3256
|
}
|
|
2679
|
-
visitERBInNode(node) {
|
|
2680
|
-
this.visitChildNodes(node);
|
|
3257
|
+
visitERBInNode(node) {
|
|
3258
|
+
this.visitChildNodes(node);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
class PrintContext {
|
|
3263
|
+
output = "";
|
|
3264
|
+
indentLevel = 0;
|
|
3265
|
+
currentColumn = 0;
|
|
3266
|
+
preserveStack = [];
|
|
3267
|
+
/**
|
|
3268
|
+
* Write text to the output
|
|
3269
|
+
*/
|
|
3270
|
+
write(text) {
|
|
3271
|
+
this.output += text;
|
|
3272
|
+
this.currentColumn += text.length;
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Write text and update column tracking for newlines
|
|
3276
|
+
*/
|
|
3277
|
+
writeWithColumnTracking(text) {
|
|
3278
|
+
this.output += text;
|
|
3279
|
+
const lines = text.split('\n');
|
|
3280
|
+
if (lines.length > 1) {
|
|
3281
|
+
this.currentColumn = lines[lines.length - 1].length;
|
|
3282
|
+
}
|
|
3283
|
+
else {
|
|
3284
|
+
this.currentColumn += text.length;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Increase indentation level
|
|
3289
|
+
*/
|
|
3290
|
+
indent() {
|
|
3291
|
+
this.indentLevel++;
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Decrease indentation level
|
|
3295
|
+
*/
|
|
3296
|
+
dedent() {
|
|
3297
|
+
if (this.indentLevel > 0) {
|
|
3298
|
+
this.indentLevel--;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Enter a tag that may preserve whitespace
|
|
3303
|
+
*/
|
|
3304
|
+
enterTag(tagName) {
|
|
3305
|
+
this.preserveStack.push(tagName.toLowerCase());
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Exit the current tag
|
|
3309
|
+
*/
|
|
3310
|
+
exitTag() {
|
|
3311
|
+
this.preserveStack.pop();
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Check if we're at the start of a line
|
|
3315
|
+
*/
|
|
3316
|
+
isAtStartOfLine() {
|
|
3317
|
+
return this.currentColumn === 0;
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Get current indentation level
|
|
3321
|
+
*/
|
|
3322
|
+
getCurrentIndentLevel() {
|
|
3323
|
+
return this.indentLevel;
|
|
3324
|
+
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Get current column position
|
|
3327
|
+
*/
|
|
3328
|
+
getCurrentColumn() {
|
|
3329
|
+
return this.currentColumn;
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* Get the current tag stack (for debugging)
|
|
3333
|
+
*/
|
|
3334
|
+
getTagStack() {
|
|
3335
|
+
return [...this.preserveStack];
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Get the complete output string
|
|
3339
|
+
*/
|
|
3340
|
+
getOutput() {
|
|
3341
|
+
return this.output;
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Reset the context for reuse
|
|
3345
|
+
*/
|
|
3346
|
+
reset() {
|
|
3347
|
+
this.output = "";
|
|
3348
|
+
this.indentLevel = 0;
|
|
3349
|
+
this.currentColumn = 0;
|
|
3350
|
+
this.preserveStack = [];
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
/**
|
|
3355
|
+
* Default print options used when none are provided
|
|
3356
|
+
*/
|
|
3357
|
+
const DEFAULT_PRINT_OPTIONS = {
|
|
3358
|
+
ignoreErrors: false
|
|
3359
|
+
};
|
|
3360
|
+
class Printer extends Visitor {
|
|
3361
|
+
context = new PrintContext();
|
|
3362
|
+
/**
|
|
3363
|
+
* Static method to print a node without creating an instance
|
|
3364
|
+
*
|
|
3365
|
+
* @param input - The AST Node, Token, or ParseResult to print
|
|
3366
|
+
* @param options - Print options to control behavior
|
|
3367
|
+
* @returns The printed string representation of the input
|
|
3368
|
+
* @throws {Error} When node has parse errors and ignoreErrors is false
|
|
3369
|
+
*/
|
|
3370
|
+
static print(input, options = DEFAULT_PRINT_OPTIONS) {
|
|
3371
|
+
const printer = new this();
|
|
3372
|
+
return printer.print(input, options);
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Print a node, token, or parse result to a string
|
|
3376
|
+
*
|
|
3377
|
+
* @param input - The AST Node, Token, or ParseResult to print
|
|
3378
|
+
* @param options - Print options to control behavior
|
|
3379
|
+
* @returns The printed string representation of the input
|
|
3380
|
+
* @throws {Error} When node has parse errors and ignoreErrors is false
|
|
3381
|
+
*/
|
|
3382
|
+
print(input, options = DEFAULT_PRINT_OPTIONS) {
|
|
3383
|
+
if (isToken(input)) {
|
|
3384
|
+
return input.value;
|
|
3385
|
+
}
|
|
3386
|
+
const node = isParseResult(input) ? input.value : input;
|
|
3387
|
+
if (options.ignoreErrors === false && node.recursiveErrors().length > 0) {
|
|
3388
|
+
throw new Error(`Cannot print the node (${node.type}) since it or any of its children has parse errors. Either pass in a valid Node or call \`print()\` using \`print(node, { ignoreErrors: true })\``);
|
|
3389
|
+
}
|
|
3390
|
+
this.context.reset();
|
|
3391
|
+
this.visit(node);
|
|
3392
|
+
return this.context.getOutput();
|
|
3393
|
+
}
|
|
3394
|
+
visitDocumentNode(node) {
|
|
3395
|
+
this.visitChildNodes(node);
|
|
3396
|
+
}
|
|
3397
|
+
visitLiteralNode(node) {
|
|
3398
|
+
this.context.write(node.content);
|
|
3399
|
+
}
|
|
3400
|
+
visitHTMLTextNode(node) {
|
|
3401
|
+
this.write(node.content);
|
|
3402
|
+
}
|
|
3403
|
+
visitWhitespaceNode(node) {
|
|
3404
|
+
if (node.value) {
|
|
3405
|
+
this.write(node.value.value);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
visitHTMLOpenTagNode(node) {
|
|
3409
|
+
if (node.tag_opening) {
|
|
3410
|
+
this.context.write(node.tag_opening.value);
|
|
3411
|
+
}
|
|
3412
|
+
if (node.tag_name) {
|
|
3413
|
+
this.context.write(node.tag_name.value);
|
|
3414
|
+
}
|
|
3415
|
+
this.visitChildNodes(node);
|
|
3416
|
+
if (node.tag_closing) {
|
|
3417
|
+
this.context.write(node.tag_closing.value);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
visitHTMLCloseTagNode(node) {
|
|
3421
|
+
if (node.tag_opening) {
|
|
3422
|
+
this.context.write(node.tag_opening.value);
|
|
3423
|
+
}
|
|
3424
|
+
if (node.tag_name) {
|
|
3425
|
+
this.context.write(node.tag_name.value);
|
|
3426
|
+
}
|
|
3427
|
+
if (node.tag_closing) {
|
|
3428
|
+
this.context.write(node.tag_closing.value);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
visitHTMLElementNode(node) {
|
|
3432
|
+
const tagName = node.tag_name?.value;
|
|
3433
|
+
if (tagName) {
|
|
3434
|
+
this.context.enterTag(tagName);
|
|
3435
|
+
}
|
|
3436
|
+
if (node.open_tag) {
|
|
3437
|
+
this.visit(node.open_tag);
|
|
3438
|
+
}
|
|
3439
|
+
if (node.body) {
|
|
3440
|
+
node.body.forEach(child => this.visit(child));
|
|
3441
|
+
}
|
|
3442
|
+
if (node.close_tag) {
|
|
3443
|
+
this.visit(node.close_tag);
|
|
3444
|
+
}
|
|
3445
|
+
if (tagName) {
|
|
3446
|
+
this.context.exitTag();
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
visitHTMLAttributeNode(node) {
|
|
3450
|
+
if (node.name) {
|
|
3451
|
+
this.visit(node.name);
|
|
3452
|
+
}
|
|
3453
|
+
if (node.equals) {
|
|
3454
|
+
this.context.write(node.equals.value);
|
|
3455
|
+
}
|
|
3456
|
+
if (node.equals && node.value) {
|
|
3457
|
+
this.visit(node.value);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
visitHTMLAttributeNameNode(node) {
|
|
3461
|
+
this.visitChildNodes(node);
|
|
3462
|
+
}
|
|
3463
|
+
visitHTMLAttributeValueNode(node) {
|
|
3464
|
+
if (node.quoted && node.open_quote) {
|
|
3465
|
+
this.context.write(node.open_quote.value);
|
|
3466
|
+
}
|
|
3467
|
+
this.visitChildNodes(node);
|
|
3468
|
+
if (node.quoted && node.close_quote) {
|
|
3469
|
+
this.context.write(node.close_quote.value);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
visitHTMLCommentNode(node) {
|
|
3473
|
+
if (node.comment_start) {
|
|
3474
|
+
this.context.write(node.comment_start.value);
|
|
3475
|
+
}
|
|
3476
|
+
this.visitChildNodes(node);
|
|
3477
|
+
if (node.comment_end) {
|
|
3478
|
+
this.context.write(node.comment_end.value);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
visitHTMLDoctypeNode(node) {
|
|
3482
|
+
if (node.tag_opening) {
|
|
3483
|
+
this.context.write(node.tag_opening.value);
|
|
3484
|
+
}
|
|
3485
|
+
this.visitChildNodes(node);
|
|
3486
|
+
if (node.tag_closing) {
|
|
3487
|
+
this.context.write(node.tag_closing.value);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
visitXMLDeclarationNode(node) {
|
|
3491
|
+
if (node.tag_opening) {
|
|
3492
|
+
this.context.write(node.tag_opening.value);
|
|
3493
|
+
}
|
|
3494
|
+
this.visitChildNodes(node);
|
|
3495
|
+
if (node.tag_closing) {
|
|
3496
|
+
this.context.write(node.tag_closing.value);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
visitCDATANode(node) {
|
|
3500
|
+
if (node.tag_opening) {
|
|
3501
|
+
this.context.write(node.tag_opening.value);
|
|
3502
|
+
}
|
|
3503
|
+
this.visitChildNodes(node);
|
|
3504
|
+
if (node.tag_closing) {
|
|
3505
|
+
this.context.write(node.tag_closing.value);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
visitERBContentNode(node) {
|
|
3509
|
+
this.printERBNode(node);
|
|
3510
|
+
}
|
|
3511
|
+
visitERBIfNode(node) {
|
|
3512
|
+
this.printERBNode(node);
|
|
3513
|
+
if (node.statements) {
|
|
3514
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3515
|
+
}
|
|
3516
|
+
if (node.subsequent) {
|
|
3517
|
+
this.visit(node.subsequent);
|
|
3518
|
+
}
|
|
3519
|
+
if (node.end_node) {
|
|
3520
|
+
this.visit(node.end_node);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
visitERBElseNode(node) {
|
|
3524
|
+
this.printERBNode(node);
|
|
3525
|
+
if (node.statements) {
|
|
3526
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
visitERBEndNode(node) {
|
|
3530
|
+
this.printERBNode(node);
|
|
3531
|
+
}
|
|
3532
|
+
visitERBBlockNode(node) {
|
|
3533
|
+
this.printERBNode(node);
|
|
3534
|
+
if (node.body) {
|
|
3535
|
+
node.body.forEach(child => this.visit(child));
|
|
3536
|
+
}
|
|
3537
|
+
if (node.end_node) {
|
|
3538
|
+
this.visit(node.end_node);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
visitERBCaseNode(node) {
|
|
3542
|
+
this.printERBNode(node);
|
|
3543
|
+
if (node.children) {
|
|
3544
|
+
node.children.forEach(child => this.visit(child));
|
|
3545
|
+
}
|
|
3546
|
+
if (node.conditions) {
|
|
3547
|
+
node.conditions.forEach(condition => this.visit(condition));
|
|
3548
|
+
}
|
|
3549
|
+
if (node.else_clause) {
|
|
3550
|
+
this.visit(node.else_clause);
|
|
3551
|
+
}
|
|
3552
|
+
if (node.end_node) {
|
|
3553
|
+
this.visit(node.end_node);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
visitERBWhenNode(node) {
|
|
3557
|
+
this.printERBNode(node);
|
|
3558
|
+
if (node.statements) {
|
|
3559
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
visitERBWhileNode(node) {
|
|
3563
|
+
this.printERBNode(node);
|
|
3564
|
+
if (node.statements) {
|
|
3565
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3566
|
+
}
|
|
3567
|
+
if (node.end_node) {
|
|
3568
|
+
this.visit(node.end_node);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
visitERBUntilNode(node) {
|
|
3572
|
+
this.printERBNode(node);
|
|
3573
|
+
if (node.statements) {
|
|
3574
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3575
|
+
}
|
|
3576
|
+
if (node.end_node) {
|
|
3577
|
+
this.visit(node.end_node);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
visitERBForNode(node) {
|
|
3581
|
+
this.printERBNode(node);
|
|
3582
|
+
if (node.statements) {
|
|
3583
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3584
|
+
}
|
|
3585
|
+
if (node.end_node) {
|
|
3586
|
+
this.visit(node.end_node);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
visitERBBeginNode(node) {
|
|
3590
|
+
this.printERBNode(node);
|
|
3591
|
+
if (node.statements) {
|
|
3592
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3593
|
+
}
|
|
3594
|
+
if (node.rescue_clause) {
|
|
3595
|
+
this.visit(node.rescue_clause);
|
|
3596
|
+
}
|
|
3597
|
+
if (node.else_clause) {
|
|
3598
|
+
this.visit(node.else_clause);
|
|
3599
|
+
}
|
|
3600
|
+
if (node.ensure_clause) {
|
|
3601
|
+
this.visit(node.ensure_clause);
|
|
3602
|
+
}
|
|
3603
|
+
if (node.end_node) {
|
|
3604
|
+
this.visit(node.end_node);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
visitERBRescueNode(node) {
|
|
3608
|
+
this.printERBNode(node);
|
|
3609
|
+
if (node.statements) {
|
|
3610
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3611
|
+
}
|
|
3612
|
+
if (node.subsequent) {
|
|
3613
|
+
this.visit(node.subsequent);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
visitERBEnsureNode(node) {
|
|
3617
|
+
this.printERBNode(node);
|
|
3618
|
+
if (node.statements) {
|
|
3619
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
visitERBUnlessNode(node) {
|
|
3623
|
+
this.printERBNode(node);
|
|
3624
|
+
if (node.statements) {
|
|
3625
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3626
|
+
}
|
|
3627
|
+
if (node.else_clause) {
|
|
3628
|
+
this.visit(node.else_clause);
|
|
3629
|
+
}
|
|
3630
|
+
if (node.end_node) {
|
|
3631
|
+
this.visit(node.end_node);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
visitERBYieldNode(node) {
|
|
3635
|
+
this.printERBNode(node);
|
|
3636
|
+
}
|
|
3637
|
+
visitERBInNode(node) {
|
|
3638
|
+
this.printERBNode(node);
|
|
3639
|
+
if (node.statements) {
|
|
3640
|
+
node.statements.forEach(statement => this.visit(statement));
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
visitERBCaseMatchNode(node) {
|
|
3644
|
+
this.printERBNode(node);
|
|
3645
|
+
if (node.children) {
|
|
3646
|
+
node.children.forEach(child => this.visit(child));
|
|
3647
|
+
}
|
|
3648
|
+
if (node.conditions) {
|
|
3649
|
+
node.conditions.forEach(condition => this.visit(condition));
|
|
3650
|
+
}
|
|
3651
|
+
if (node.else_clause) {
|
|
3652
|
+
this.visit(node.else_clause);
|
|
3653
|
+
}
|
|
3654
|
+
if (node.end_node) {
|
|
3655
|
+
this.visit(node.end_node);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Print ERB node tags and content
|
|
3660
|
+
*/
|
|
3661
|
+
printERBNode(node) {
|
|
3662
|
+
if (node.tag_opening) {
|
|
3663
|
+
this.context.write(node.tag_opening.value);
|
|
3664
|
+
}
|
|
3665
|
+
if (node.content) {
|
|
3666
|
+
this.context.write(node.content.value);
|
|
3667
|
+
}
|
|
3668
|
+
if (node.tag_closing) {
|
|
3669
|
+
this.context.write(node.tag_closing.value);
|
|
3670
|
+
}
|
|
2681
3671
|
}
|
|
3672
|
+
write(content) {
|
|
3673
|
+
this.context.write(content);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
/**
|
|
3678
|
+
* IdentityPrinter - Provides lossless reconstruction of the original source
|
|
3679
|
+
*
|
|
3680
|
+
* This printer aims to reconstruct the original input as faithfully as possible,
|
|
3681
|
+
* preserving all whitespace, formatting, and structure. It's useful for:
|
|
3682
|
+
* - Testing parser accuracy (input should equal output)
|
|
3683
|
+
* - Baseline printing before applying transformations
|
|
3684
|
+
* - Verifying AST round-trip fidelity
|
|
3685
|
+
*/
|
|
3686
|
+
class IdentityPrinter extends Printer {
|
|
2682
3687
|
}
|
|
2683
3688
|
|
|
3689
|
+
// TODO: we can probably expand this list with more tags/attributes
|
|
3690
|
+
const FORMATTABLE_ATTRIBUTES = {
|
|
3691
|
+
'*': ['class'],
|
|
3692
|
+
'img': ['srcset', 'sizes']
|
|
3693
|
+
};
|
|
2684
3694
|
/**
|
|
2685
3695
|
* Printer traverses the Herb AST using the Visitor pattern
|
|
2686
3696
|
* and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
|
|
2687
3697
|
*/
|
|
2688
|
-
class
|
|
3698
|
+
class FormatPrinter extends Printer {
|
|
3699
|
+
/**
|
|
3700
|
+
* @deprecated integrate indentWidth into this.options and update FormatOptions to extend from @herb-tools/printer options
|
|
3701
|
+
*/
|
|
2689
3702
|
indentWidth;
|
|
3703
|
+
/**
|
|
3704
|
+
* @deprecated integrate maxLineLength into this.options and update FormatOptions to extend from @herb-tools/printer options
|
|
3705
|
+
*/
|
|
2690
3706
|
maxLineLength;
|
|
2691
|
-
|
|
3707
|
+
/**
|
|
3708
|
+
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
3709
|
+
*/
|
|
2692
3710
|
lines = [];
|
|
2693
3711
|
indentLevel = 0;
|
|
2694
3712
|
inlineMode = false;
|
|
2695
|
-
|
|
3713
|
+
currentAttributeName = null;
|
|
3714
|
+
elementStack = [];
|
|
3715
|
+
elementFormattingAnalysis = new Map();
|
|
3716
|
+
source;
|
|
3717
|
+
// TODO: extract
|
|
2696
3718
|
static INLINE_ELEMENTS = new Set([
|
|
2697
3719
|
'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'cite', 'code',
|
|
2698
3720
|
'dfn', 'em', 'i', 'img', 'kbd', 'label', 'map', 'object', 'q',
|
|
2699
3721
|
'samp', 'small', 'span', 'strong', 'sub', 'sup',
|
|
2700
3722
|
'tt', 'var', 'del', 'ins', 'mark', 's', 'u', 'time', 'wbr'
|
|
2701
3723
|
]);
|
|
3724
|
+
static SPACEABLE_CONTAINERS = new Set([
|
|
3725
|
+
'div', 'section', 'article', 'main', 'header', 'footer', 'aside',
|
|
3726
|
+
'figure', 'details', 'summary', 'dialog', 'fieldset'
|
|
3727
|
+
]);
|
|
3728
|
+
static TIGHT_GROUP_PARENTS = new Set([
|
|
3729
|
+
'ul', 'ol', 'nav', 'select', 'datalist', 'optgroup', 'tr', 'thead', 'tbody', 'tfoot'
|
|
3730
|
+
]);
|
|
3731
|
+
static TIGHT_GROUP_CHILDREN = new Set([
|
|
3732
|
+
'li', 'option', 'td', 'th', 'dt', 'dd'
|
|
3733
|
+
]);
|
|
3734
|
+
static SPACING_THRESHOLD = 3;
|
|
2702
3735
|
constructor(source, options) {
|
|
2703
3736
|
super();
|
|
2704
3737
|
this.source = source;
|
|
2705
3738
|
this.indentWidth = options.indentWidth;
|
|
2706
3739
|
this.maxLineLength = options.maxLineLength;
|
|
2707
3740
|
}
|
|
2708
|
-
print(
|
|
2709
|
-
if (
|
|
2710
|
-
return
|
|
2711
|
-
|
|
2712
|
-
|
|
3741
|
+
print(input) {
|
|
3742
|
+
if (isToken(input))
|
|
3743
|
+
return input.value;
|
|
3744
|
+
const node = isParseResult(input) ? input.value : input;
|
|
3745
|
+
// TODO: refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
2713
3746
|
this.lines = [];
|
|
2714
|
-
this.indentLevel =
|
|
2715
|
-
this.
|
|
2716
|
-
|
|
2717
|
-
|
|
3747
|
+
this.indentLevel = 0;
|
|
3748
|
+
this.visit(node);
|
|
3749
|
+
return this.lines.join("\n");
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Get the current element (top of stack)
|
|
3753
|
+
*/
|
|
3754
|
+
get currentElement() {
|
|
3755
|
+
return this.elementStack.length > 0 ? this.elementStack[this.elementStack.length - 1] : null;
|
|
3756
|
+
}
|
|
3757
|
+
/**
|
|
3758
|
+
* Get the current tag name from the current element context
|
|
3759
|
+
*/
|
|
3760
|
+
get currentTagName() {
|
|
3761
|
+
return this.currentElement?.open_tag?.tag_name?.value ?? "";
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Append text to the last line instead of creating a new line
|
|
3765
|
+
*/
|
|
3766
|
+
pushToLastLine(text) {
|
|
3767
|
+
if (this.lines.length > 0) {
|
|
3768
|
+
this.lines[this.lines.length - 1] += text;
|
|
2718
3769
|
}
|
|
2719
3770
|
else {
|
|
2720
|
-
this.
|
|
3771
|
+
this.lines.push(text);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
/**
|
|
3775
|
+
* Capture output from a callback into a separate lines array
|
|
3776
|
+
* Useful for testing what output would be generated without affecting the main output
|
|
3777
|
+
*/
|
|
3778
|
+
capture(callback) {
|
|
3779
|
+
const previousLines = this.lines;
|
|
3780
|
+
const previousInlineMode = this.inlineMode;
|
|
3781
|
+
this.lines = [];
|
|
3782
|
+
try {
|
|
3783
|
+
callback();
|
|
3784
|
+
return this.lines;
|
|
3785
|
+
}
|
|
3786
|
+
finally {
|
|
3787
|
+
this.lines = previousLines;
|
|
3788
|
+
this.inlineMode = previousInlineMode;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* Capture all nodes that would be visited during a callback
|
|
3793
|
+
* Returns a flat list of all nodes without generating any output
|
|
3794
|
+
*/
|
|
3795
|
+
captureNodes(callback) {
|
|
3796
|
+
const capturedNodes = [];
|
|
3797
|
+
const previousLines = this.lines;
|
|
3798
|
+
const previousInlineMode = this.inlineMode;
|
|
3799
|
+
const originalPush = this.push.bind(this);
|
|
3800
|
+
const originalPushToLastLine = this.pushToLastLine.bind(this);
|
|
3801
|
+
const originalVisit = this.visit.bind(this);
|
|
3802
|
+
this.lines = [];
|
|
3803
|
+
this.push = () => { };
|
|
3804
|
+
this.pushToLastLine = () => { };
|
|
3805
|
+
this.visit = (node) => {
|
|
3806
|
+
capturedNodes.push(node);
|
|
3807
|
+
originalVisit(node);
|
|
3808
|
+
};
|
|
3809
|
+
try {
|
|
3810
|
+
callback();
|
|
3811
|
+
return capturedNodes;
|
|
3812
|
+
}
|
|
3813
|
+
finally {
|
|
3814
|
+
this.lines = previousLines;
|
|
3815
|
+
this.inlineMode = previousInlineMode;
|
|
3816
|
+
this.push = originalPush;
|
|
3817
|
+
this.pushToLastLine = originalPushToLastLine;
|
|
3818
|
+
this.visit = originalVisit;
|
|
2721
3819
|
}
|
|
2722
|
-
return this.lines.join("\n");
|
|
2723
3820
|
}
|
|
3821
|
+
/**
|
|
3822
|
+
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
3823
|
+
*/
|
|
2724
3824
|
push(line) {
|
|
2725
3825
|
this.lines.push(line);
|
|
2726
3826
|
}
|
|
@@ -2730,7 +3830,7 @@ class Printer extends Visitor {
|
|
|
2730
3830
|
this.indentLevel--;
|
|
2731
3831
|
return result;
|
|
2732
3832
|
}
|
|
2733
|
-
indent() {
|
|
3833
|
+
get indent() {
|
|
2734
3834
|
return " ".repeat(this.indentLevel * this.indentWidth);
|
|
2735
3835
|
}
|
|
2736
3836
|
/**
|
|
@@ -2740,45 +3840,128 @@ class Printer extends Visitor {
|
|
|
2740
3840
|
formatERBContent(content) {
|
|
2741
3841
|
return content.trim() ? ` ${content.trim()} ` : "";
|
|
2742
3842
|
}
|
|
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
3843
|
/**
|
|
2756
3844
|
* Count total attributes including those inside ERB conditionals
|
|
2757
3845
|
*/
|
|
2758
3846
|
getTotalAttributeCount(attributes, inlineNodes = []) {
|
|
2759
3847
|
let totalAttributeCount = attributes.length;
|
|
2760
3848
|
inlineNodes.forEach(node => {
|
|
2761
|
-
if (
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
}
|
|
3849
|
+
if (isERBControlFlowNode(node)) {
|
|
3850
|
+
const capturedNodes = this.captureNodes(() => this.visit(node));
|
|
3851
|
+
const attributeNodes = filterNodes(capturedNodes, HTMLAttributeNode);
|
|
3852
|
+
totalAttributeCount += attributeNodes.length;
|
|
2766
3853
|
}
|
|
2767
3854
|
});
|
|
2768
3855
|
return totalAttributeCount;
|
|
2769
3856
|
}
|
|
2770
3857
|
/**
|
|
2771
|
-
* Extract
|
|
3858
|
+
* Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
|
|
3859
|
+
*/
|
|
3860
|
+
extractInlineNodes(nodes) {
|
|
3861
|
+
return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode));
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Determine if spacing should be added between sibling elements
|
|
3865
|
+
*
|
|
3866
|
+
* This implements the "rule of three" intelligent spacing system:
|
|
3867
|
+
* - Adds spacing between 3 or more meaningful siblings
|
|
3868
|
+
* - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
|
|
3869
|
+
* - Groups comments with following elements
|
|
3870
|
+
* - Preserves user-added spacing
|
|
3871
|
+
*
|
|
3872
|
+
* @param parentElement - The parent element containing the siblings
|
|
3873
|
+
* @param siblings - Array of all sibling nodes
|
|
3874
|
+
* @param currentIndex - Index of the current node being evaluated
|
|
3875
|
+
* @param hasExistingSpacing - Whether user-added spacing already exists
|
|
3876
|
+
* @returns true if spacing should be added before the current element
|
|
2772
3877
|
*/
|
|
2773
|
-
|
|
2774
|
-
|
|
3878
|
+
shouldAddSpacingBetweenSiblings(parentElement, siblings, currentIndex, hasExistingSpacing) {
|
|
3879
|
+
if (hasExistingSpacing) {
|
|
3880
|
+
return true;
|
|
3881
|
+
}
|
|
3882
|
+
const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "");
|
|
3883
|
+
if (hasMixedContent) {
|
|
3884
|
+
return false;
|
|
3885
|
+
}
|
|
3886
|
+
const meaningfulSiblings = siblings.filter(child => this.isNonWhitespaceNode(child));
|
|
3887
|
+
if (meaningfulSiblings.length < FormatPrinter.SPACING_THRESHOLD) {
|
|
3888
|
+
return false;
|
|
3889
|
+
}
|
|
3890
|
+
const parentTagName = parentElement ? getTagName(parentElement) : null;
|
|
3891
|
+
if (parentTagName && FormatPrinter.TIGHT_GROUP_PARENTS.has(parentTagName)) {
|
|
3892
|
+
return false;
|
|
3893
|
+
}
|
|
3894
|
+
const isSpaceableContainer = !parentTagName || (parentTagName && FormatPrinter.SPACEABLE_CONTAINERS.has(parentTagName));
|
|
3895
|
+
if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
|
|
3896
|
+
return false;
|
|
3897
|
+
}
|
|
3898
|
+
const currentNode = siblings[currentIndex];
|
|
3899
|
+
const previousMeaningfulIndex = this.findPreviousMeaningfulSibling(siblings, currentIndex);
|
|
3900
|
+
const isCurrentComment = isCommentNode(currentNode);
|
|
3901
|
+
if (previousMeaningfulIndex !== -1) {
|
|
3902
|
+
const previousNode = siblings[previousMeaningfulIndex];
|
|
3903
|
+
const isPreviousComment = isCommentNode(previousNode);
|
|
3904
|
+
if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
|
|
3905
|
+
return false;
|
|
3906
|
+
}
|
|
3907
|
+
if (isPreviousComment && isCurrentComment) {
|
|
3908
|
+
return false;
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
if (isNode(currentNode, HTMLElementNode)) {
|
|
3912
|
+
const currentTagName = getTagName(currentNode);
|
|
3913
|
+
if (FormatPrinter.INLINE_ELEMENTS.has(currentTagName)) {
|
|
3914
|
+
return false;
|
|
3915
|
+
}
|
|
3916
|
+
if (FormatPrinter.TIGHT_GROUP_CHILDREN.has(currentTagName)) {
|
|
3917
|
+
return false;
|
|
3918
|
+
}
|
|
3919
|
+
if (currentTagName === 'a' && parentTagName === 'nav') {
|
|
3920
|
+
return false;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
const isBlockElement = this.isBlockLevelNode(currentNode);
|
|
3924
|
+
const isERBBlock = isERBNode(currentNode) && isERBControlFlowNode(currentNode);
|
|
3925
|
+
const isComment = isCommentNode(currentNode);
|
|
3926
|
+
return isBlockElement || isERBBlock || isComment;
|
|
2775
3927
|
}
|
|
2776
3928
|
/**
|
|
2777
|
-
*
|
|
3929
|
+
* Token list attributes that contain space-separated values and benefit from
|
|
3930
|
+
* spacing around ERB content for readability
|
|
2778
3931
|
*/
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
3932
|
+
static TOKEN_LIST_ATTRIBUTES = new Set([
|
|
3933
|
+
'class', 'data-controller', 'data-action'
|
|
3934
|
+
]);
|
|
3935
|
+
/**
|
|
3936
|
+
* Check if we're currently processing a token list attribute that needs spacing
|
|
3937
|
+
*/
|
|
3938
|
+
isInTokenListAttribute() {
|
|
3939
|
+
return this.currentAttributeName !== null &&
|
|
3940
|
+
FormatPrinter.TOKEN_LIST_ATTRIBUTES.has(this.currentAttributeName);
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Find the previous meaningful (non-whitespace) sibling
|
|
3944
|
+
*/
|
|
3945
|
+
findPreviousMeaningfulSibling(siblings, currentIndex) {
|
|
3946
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
3947
|
+
if (this.isNonWhitespaceNode(siblings[i])) {
|
|
3948
|
+
return i;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return -1;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Check if a node represents a block-level element
|
|
3955
|
+
*/
|
|
3956
|
+
isBlockLevelNode(node) {
|
|
3957
|
+
if (!isNode(node, HTMLElementNode)) {
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
const tagName = getTagName(node);
|
|
3961
|
+
if (FormatPrinter.INLINE_ELEMENTS.has(tagName)) {
|
|
3962
|
+
return false;
|
|
3963
|
+
}
|
|
3964
|
+
return true;
|
|
2782
3965
|
}
|
|
2783
3966
|
/**
|
|
2784
3967
|
* Render attributes as a space-separated string
|
|
@@ -2786,63 +3969,198 @@ class Printer extends Visitor {
|
|
|
2786
3969
|
renderAttributesString(attributes) {
|
|
2787
3970
|
if (attributes.length === 0)
|
|
2788
3971
|
return "";
|
|
2789
|
-
return ` ${attributes.map(
|
|
3972
|
+
return ` ${attributes.map(attribute => this.renderAttribute(attribute)).join(" ")}`;
|
|
2790
3973
|
}
|
|
2791
3974
|
/**
|
|
2792
3975
|
* Determine if a tag should be rendered inline based on attribute count and other factors
|
|
2793
3976
|
*/
|
|
2794
|
-
shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false,
|
|
2795
|
-
if (hasComplexERB)
|
|
3977
|
+
shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false, hasMultilineAttributes = false, attributes = []) {
|
|
3978
|
+
if (hasComplexERB || hasMultilineAttributes)
|
|
2796
3979
|
return false;
|
|
2797
|
-
// Special case: no attributes at all, always inline if it fits
|
|
2798
3980
|
if (totalAttributeCount === 0) {
|
|
2799
3981
|
return inlineLength + indentLength <= maxLineLength;
|
|
2800
3982
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3983
|
+
if (totalAttributeCount === 1 && attributes.length === 1) {
|
|
3984
|
+
const attribute = attributes[0];
|
|
3985
|
+
const attributeName = this.getAttributeName(attribute);
|
|
3986
|
+
if (attributeName === 'class') {
|
|
3987
|
+
const attributeValue = this.getAttributeValue(attribute);
|
|
3988
|
+
const wouldBeMultiline = this.wouldClassAttributeBeMultiline(attributeValue, indentLength);
|
|
3989
|
+
if (!wouldBeMultiline) {
|
|
3990
|
+
return true;
|
|
3991
|
+
}
|
|
3992
|
+
else {
|
|
3993
|
+
return false;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
if (totalAttributeCount > 3 || inlineLength + indentLength > maxLineLength) {
|
|
3998
|
+
return false;
|
|
3999
|
+
}
|
|
4000
|
+
return true;
|
|
4001
|
+
}
|
|
4002
|
+
getAttributeName(attribute) {
|
|
4003
|
+
return attribute.name ? getCombinedAttributeName(attribute.name) : "";
|
|
4004
|
+
}
|
|
4005
|
+
wouldClassAttributeBeMultiline(content, indentLength) {
|
|
4006
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim();
|
|
4007
|
+
const hasActualNewlines = /\r?\n/.test(content);
|
|
4008
|
+
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
4009
|
+
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line);
|
|
4010
|
+
if (lines.length > 1) {
|
|
4011
|
+
return true;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
const attributeLine = `class="${normalizedContent}"`;
|
|
4015
|
+
const currentIndent = indentLength;
|
|
4016
|
+
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
4017
|
+
if (/<%[^%]*%>/.test(normalizedContent)) {
|
|
4018
|
+
return false;
|
|
4019
|
+
}
|
|
4020
|
+
const classes = normalizedContent.split(' ');
|
|
4021
|
+
const lines = this.breakTokensIntoLines(classes, currentIndent);
|
|
4022
|
+
return lines.length > 1;
|
|
4023
|
+
}
|
|
4024
|
+
return false;
|
|
4025
|
+
}
|
|
4026
|
+
getAttributeValue(attribute) {
|
|
4027
|
+
if (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
4028
|
+
const content = attribute.value.children.map(child => {
|
|
4029
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4030
|
+
return child.content;
|
|
4031
|
+
}
|
|
4032
|
+
return IdentityPrinter.print(child);
|
|
4033
|
+
}).join('');
|
|
4034
|
+
return content;
|
|
4035
|
+
}
|
|
4036
|
+
return '';
|
|
4037
|
+
}
|
|
4038
|
+
hasMultilineAttributes(attributes) {
|
|
4039
|
+
return attributes.some(attribute => {
|
|
4040
|
+
if (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
4041
|
+
const content = getCombinedStringFromNodes(attribute.value.children);
|
|
4042
|
+
if (/\r?\n/.test(content)) {
|
|
4043
|
+
const name = attribute.name ? getCombinedAttributeName(attribute.name) : "";
|
|
4044
|
+
if (name === "class") {
|
|
4045
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim();
|
|
4046
|
+
return normalizedContent.length > 80;
|
|
4047
|
+
}
|
|
4048
|
+
const lines = content.split(/\r?\n/);
|
|
4049
|
+
if (lines.length > 1) {
|
|
4050
|
+
return lines.slice(1).some(line => /^\s+/.test(line));
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return false;
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
formatClassAttribute(content, name, equals, open_quote, close_quote) {
|
|
4058
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim();
|
|
4059
|
+
const hasActualNewlines = /\r?\n/.test(content);
|
|
4060
|
+
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
4061
|
+
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line);
|
|
4062
|
+
if (lines.length > 1) {
|
|
4063
|
+
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
const currentIndent = this.indentLevel * this.indentWidth;
|
|
4067
|
+
const attributeLine = `${name}${equals}${open_quote}${normalizedContent}${close_quote}`;
|
|
4068
|
+
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
4069
|
+
if (/<%[^%]*%>/.test(normalizedContent)) {
|
|
4070
|
+
return open_quote + normalizedContent + close_quote;
|
|
4071
|
+
}
|
|
4072
|
+
const classes = normalizedContent.split(' ');
|
|
4073
|
+
const lines = this.breakTokensIntoLines(classes, currentIndent);
|
|
4074
|
+
if (lines.length > 1) {
|
|
4075
|
+
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
return open_quote + normalizedContent + close_quote;
|
|
4079
|
+
}
|
|
4080
|
+
isFormattableAttribute(attributeName, tagName) {
|
|
4081
|
+
const globalFormattable = FORMATTABLE_ATTRIBUTES['*'] || [];
|
|
4082
|
+
const tagSpecificFormattable = FORMATTABLE_ATTRIBUTES[tagName.toLowerCase()] || [];
|
|
4083
|
+
return globalFormattable.includes(attributeName) || tagSpecificFormattable.includes(attributeName);
|
|
4084
|
+
}
|
|
4085
|
+
formatMultilineAttribute(content, name, open_quote, close_quote) {
|
|
4086
|
+
if (name === 'srcset' || name === 'sizes') {
|
|
4087
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim();
|
|
4088
|
+
return open_quote + normalizedContent + close_quote;
|
|
4089
|
+
}
|
|
4090
|
+
const lines = content.split('\n');
|
|
4091
|
+
if (lines.length <= 1) {
|
|
4092
|
+
return open_quote + content + close_quote;
|
|
4093
|
+
}
|
|
4094
|
+
const formattedContent = this.formatMultilineAttributeValue(lines);
|
|
4095
|
+
return open_quote + formattedContent + close_quote;
|
|
4096
|
+
}
|
|
4097
|
+
formatMultilineAttributeValue(lines) {
|
|
4098
|
+
const indent = " ".repeat((this.indentLevel + 1) * this.indentWidth);
|
|
4099
|
+
const closeIndent = " ".repeat(this.indentLevel * this.indentWidth);
|
|
4100
|
+
return "\n" + lines.map(line => indent + line).join("\n") + "\n" + closeIndent;
|
|
4101
|
+
}
|
|
4102
|
+
breakTokensIntoLines(tokens, currentIndent, separator = ' ') {
|
|
4103
|
+
const lines = [];
|
|
4104
|
+
let currentLine = '';
|
|
4105
|
+
for (const token of tokens) {
|
|
4106
|
+
const testLine = currentLine ? currentLine + separator + token : token;
|
|
4107
|
+
if (testLine.length > (this.maxLineLength - currentIndent - 6)) {
|
|
4108
|
+
if (currentLine) {
|
|
4109
|
+
lines.push(currentLine);
|
|
4110
|
+
currentLine = token;
|
|
4111
|
+
}
|
|
4112
|
+
else {
|
|
4113
|
+
lines.push(token);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
else {
|
|
4117
|
+
currentLine = testLine;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
if (currentLine)
|
|
4121
|
+
lines.push(currentLine);
|
|
4122
|
+
return lines;
|
|
2805
4123
|
}
|
|
2806
4124
|
/**
|
|
2807
4125
|
* Render multiline attributes for a tag
|
|
2808
4126
|
*/
|
|
2809
|
-
renderMultilineAttributes(tagName,
|
|
2810
|
-
|
|
2811
|
-
this.push(indent + `<${tagName}`);
|
|
4127
|
+
renderMultilineAttributes(tagName, allChildren = [], isSelfClosing = false) {
|
|
4128
|
+
this.push(this.indent + `<${tagName}`);
|
|
2812
4129
|
this.withIndent(() => {
|
|
2813
|
-
// Render children in order, handling both attributes and ERB nodes
|
|
2814
4130
|
allChildren.forEach(child => {
|
|
2815
|
-
if (child
|
|
2816
|
-
this.push(this.indent
|
|
4131
|
+
if (isNode(child, HTMLAttributeNode)) {
|
|
4132
|
+
this.push(this.indent + this.renderAttribute(child));
|
|
2817
4133
|
}
|
|
2818
|
-
else if (!(child
|
|
4134
|
+
else if (!isNode(child, WhitespaceNode)) {
|
|
2819
4135
|
this.visit(child);
|
|
2820
4136
|
}
|
|
2821
4137
|
});
|
|
2822
4138
|
});
|
|
2823
4139
|
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}>`);
|
|
4140
|
+
this.push(this.indent + "/>");
|
|
2831
4141
|
}
|
|
2832
4142
|
else {
|
|
2833
|
-
this.push(indent + ">");
|
|
4143
|
+
this.push(this.indent + ">");
|
|
2834
4144
|
}
|
|
2835
4145
|
}
|
|
2836
4146
|
/**
|
|
2837
|
-
*
|
|
4147
|
+
* Reconstruct the text representation of an ERB node
|
|
4148
|
+
* @param withFormatting - if true, format the content; if false, preserve original
|
|
2838
4149
|
*/
|
|
2839
|
-
|
|
2840
|
-
const indent = this.inlineMode ? "" : this.indent();
|
|
4150
|
+
reconstructERBNode(node, withFormatting = true) {
|
|
2841
4151
|
const open = node.tag_opening?.value ?? "";
|
|
2842
4152
|
const close = node.tag_closing?.value ?? "";
|
|
2843
4153
|
const content = node.content?.value ?? "";
|
|
2844
|
-
const inner = this.formatERBContent(content);
|
|
2845
|
-
|
|
4154
|
+
const inner = withFormatting ? this.formatERBContent(content) : content;
|
|
4155
|
+
return open + inner + close;
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
|
|
4159
|
+
*/
|
|
4160
|
+
printERBNode(node) {
|
|
4161
|
+
const indent = this.inlineMode ? "" : this.indent;
|
|
4162
|
+
const erbText = this.reconstructERBNode(node, true);
|
|
4163
|
+
this.push(indent + erbText);
|
|
2846
4164
|
}
|
|
2847
4165
|
// --- Visitor methods ---
|
|
2848
4166
|
visitDocumentNode(node) {
|
|
@@ -2850,14 +4168,13 @@ class Printer extends Visitor {
|
|
|
2850
4168
|
let hasHandledSpacing = false;
|
|
2851
4169
|
for (let i = 0; i < node.children.length; i++) {
|
|
2852
4170
|
const child = node.children[i];
|
|
2853
|
-
if (child
|
|
2854
|
-
const
|
|
2855
|
-
const isWhitespaceOnly = textNode.content.trim() === "";
|
|
4171
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4172
|
+
const isWhitespaceOnly = child.content.trim() === "";
|
|
2856
4173
|
if (isWhitespaceOnly) {
|
|
2857
|
-
const
|
|
4174
|
+
const hasPreviousNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1]);
|
|
2858
4175
|
const hasNextNonWhitespace = i < node.children.length - 1 && this.isNonWhitespaceNode(node.children[i + 1]);
|
|
2859
|
-
const hasMultipleNewlines =
|
|
2860
|
-
if (
|
|
4176
|
+
const hasMultipleNewlines = child.content.includes('\n\n');
|
|
4177
|
+
if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
|
|
2861
4178
|
this.push("");
|
|
2862
4179
|
hasHandledSpacing = true;
|
|
2863
4180
|
}
|
|
@@ -2875,326 +4192,145 @@ class Printer extends Visitor {
|
|
|
2875
4192
|
}
|
|
2876
4193
|
}
|
|
2877
4194
|
visitHTMLElementNode(node) {
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
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);
|
|
2898
|
-
const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
|
|
2899
|
-
const isSelfClosing = open.tag_closing?.value === "/>";
|
|
2900
|
-
if (!hasClosing) {
|
|
2901
|
-
this.push(indent + `<${tagName}`);
|
|
2902
|
-
return;
|
|
4195
|
+
this.elementStack.push(node);
|
|
4196
|
+
this.elementFormattingAnalysis.set(node, this.analyzeElementFormatting(node));
|
|
4197
|
+
this.visit(node.open_tag);
|
|
4198
|
+
if (node.body.length > 0) {
|
|
4199
|
+
this.visitHTMLElementBody(node.body, node);
|
|
2903
4200
|
}
|
|
2904
|
-
if (
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
4201
|
+
if (node.close_tag) {
|
|
4202
|
+
this.visit(node.close_tag);
|
|
4203
|
+
}
|
|
4204
|
+
this.elementStack.pop();
|
|
4205
|
+
}
|
|
4206
|
+
visitHTMLElementBody(body, element) {
|
|
4207
|
+
const analysis = this.elementFormattingAnalysis.get(element);
|
|
4208
|
+
const hasTextFlow = this.isInTextFlowContext(null, body);
|
|
4209
|
+
const children = this.filterSignificantChildren(body, hasTextFlow);
|
|
4210
|
+
if (analysis?.elementContentInline) {
|
|
4211
|
+
if (children.length === 0)
|
|
2915
4212
|
return;
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
const
|
|
2924
|
-
if (
|
|
2925
|
-
this.push(
|
|
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');
|
|
4213
|
+
const oldInlineMode = this.inlineMode;
|
|
4214
|
+
const nodesToRender = hasTextFlow ? body : children;
|
|
4215
|
+
this.inlineMode = true;
|
|
4216
|
+
const lines = this.capture(() => {
|
|
4217
|
+
nodesToRender.forEach(child => {
|
|
4218
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4219
|
+
if (hasTextFlow) {
|
|
4220
|
+
const normalizedContent = child.content.replace(/\s+/g, ' ');
|
|
4221
|
+
if (normalizedContent && normalizedContent !== ' ') {
|
|
4222
|
+
this.push(normalizedContent);
|
|
2941
4223
|
}
|
|
2942
|
-
|
|
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
|
-
}
|
|
4224
|
+
else if (normalizedContent === ' ') {
|
|
4225
|
+
this.push(' ');
|
|
2954
4226
|
}
|
|
2955
4227
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
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
|
-
}
|
|
2971
|
-
this.push(indent + `<${tagName}>`);
|
|
2972
|
-
this.withIndent(() => {
|
|
2973
|
-
if (hasTextFlow) {
|
|
2974
|
-
this.visitTextFlowChildren(children);
|
|
2975
|
-
}
|
|
2976
|
-
else {
|
|
2977
|
-
children.forEach(child => this.visit(child));
|
|
2978
|
-
}
|
|
2979
|
-
});
|
|
2980
|
-
if (!node.is_void && !isSelfClosing) {
|
|
2981
|
-
this.push(indent + `</${tagName}>`);
|
|
2982
|
-
}
|
|
2983
|
-
return;
|
|
2984
|
-
}
|
|
2985
|
-
if (attributes.length === 0 && inlineNodes.length > 0) {
|
|
2986
|
-
const inline = this.renderInlineOpen(tagName, [], isSelfClosing, inlineNodes, open.children);
|
|
2987
|
-
if (children.length === 0) {
|
|
2988
|
-
if (isSelfClosing || node.is_void) {
|
|
2989
|
-
this.push(indent + inline);
|
|
2990
|
-
}
|
|
2991
|
-
else {
|
|
2992
|
-
this.push(indent + inline + `</${tagName}>`);
|
|
2993
|
-
}
|
|
2994
|
-
return;
|
|
2995
|
-
}
|
|
2996
|
-
this.push(indent + inline);
|
|
2997
|
-
this.withIndent(() => {
|
|
2998
|
-
children.forEach(child => this.visit(child));
|
|
2999
|
-
});
|
|
3000
|
-
if (!node.is_void && !isSelfClosing) {
|
|
3001
|
-
this.push(indent + `</${tagName}>`);
|
|
3002
|
-
}
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
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);
|
|
3023
|
-
if (shouldKeepInline) {
|
|
3024
|
-
if (children.length === 0) {
|
|
3025
|
-
if (isSelfClosing) {
|
|
3026
|
-
this.push(indent + inline);
|
|
3027
|
-
}
|
|
3028
|
-
else if (node.is_void) {
|
|
3029
|
-
this.push(indent + inline);
|
|
3030
|
-
}
|
|
3031
|
-
else {
|
|
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;
|
|
4228
|
+
else {
|
|
4229
|
+
const normalizedContent = child.content.replace(/\s+/g, ' ').trim();
|
|
4230
|
+
if (normalizedContent) {
|
|
4231
|
+
this.push(normalizedContent);
|
|
3043
4232
|
}
|
|
3044
|
-
|
|
3045
|
-
this.inlineMode = wasInlineMode;
|
|
3046
|
-
});
|
|
3047
|
-
const inlineContent = this.lines.join("");
|
|
3048
|
-
this.lines = tempLines;
|
|
3049
|
-
this.indentLevel = currentIndentLevel;
|
|
3050
|
-
result += inlineContent;
|
|
4233
|
+
}
|
|
3051
4234
|
}
|
|
3052
|
-
|
|
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);
|
|
4235
|
+
else if (isNode(child, WhitespaceNode)) {
|
|
3063
4236
|
return;
|
|
3064
4237
|
}
|
|
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
4238
|
else {
|
|
3074
|
-
|
|
4239
|
+
this.visit(child);
|
|
3075
4240
|
}
|
|
3076
4241
|
});
|
|
3077
|
-
if (!node.is_void && !isSelfClosing) {
|
|
3078
|
-
this.push(indent + `</${tagName}>`);
|
|
3079
|
-
}
|
|
3080
|
-
return;
|
|
3081
|
-
}
|
|
3082
|
-
if (isSelfClosing) {
|
|
3083
|
-
this.push(indent + inline.replace(' />', '>'));
|
|
3084
|
-
}
|
|
3085
|
-
else {
|
|
3086
|
-
this.push(indent + inline);
|
|
3087
|
-
}
|
|
3088
|
-
this.withIndent(() => {
|
|
3089
|
-
if (hasTextFlow) {
|
|
3090
|
-
this.visitTextFlowChildren(children);
|
|
3091
|
-
}
|
|
3092
|
-
else {
|
|
3093
|
-
children.forEach(child => this.visit(child));
|
|
3094
|
-
}
|
|
3095
4242
|
});
|
|
3096
|
-
|
|
3097
|
-
|
|
4243
|
+
const content = lines.join('');
|
|
4244
|
+
const inlineContent = hasTextFlow ? content.replace(/\s+/g, ' ').trim() : content.trim();
|
|
4245
|
+
if (inlineContent) {
|
|
4246
|
+
this.pushToLastLine(inlineContent);
|
|
3098
4247
|
}
|
|
4248
|
+
this.inlineMode = oldInlineMode;
|
|
3099
4249
|
return;
|
|
3100
4250
|
}
|
|
3101
|
-
if (
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
});
|
|
3107
|
-
this.push(indent + `</${tagName}>`);
|
|
4251
|
+
if (children.length === 0)
|
|
4252
|
+
return;
|
|
4253
|
+
this.withIndent(() => {
|
|
4254
|
+
if (hasTextFlow) {
|
|
4255
|
+
this.visitTextFlowChildren(children);
|
|
3108
4256
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
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));
|
|
3115
|
-
});
|
|
3116
|
-
this.push(indent + `</${tagName}>`);
|
|
4257
|
+
else {
|
|
4258
|
+
this.visitElementChildren(body, element);
|
|
3117
4259
|
}
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Visit element children with intelligent spacing logic
|
|
4264
|
+
*/
|
|
4265
|
+
visitElementChildren(body, parentElement) {
|
|
4266
|
+
let lastWasMeaningful = false;
|
|
4267
|
+
let hasHandledSpacing = false;
|
|
4268
|
+
for (let i = 0; i < body.length; i++) {
|
|
4269
|
+
const child = body[i];
|
|
4270
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4271
|
+
const isWhitespaceOnly = child.content.trim() === "";
|
|
4272
|
+
if (isWhitespaceOnly) {
|
|
4273
|
+
const hasPreviousNonWhitespace = i > 0 && this.isNonWhitespaceNode(body[i - 1]);
|
|
4274
|
+
const hasNextNonWhitespace = i < body.length - 1 && this.isNonWhitespaceNode(body[i + 1]);
|
|
4275
|
+
const hasMultipleNewlines = child.content.includes('\n\n');
|
|
4276
|
+
if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
|
|
4277
|
+
this.push("");
|
|
4278
|
+
hasHandledSpacing = true;
|
|
3127
4279
|
}
|
|
4280
|
+
continue;
|
|
3128
4281
|
}
|
|
3129
4282
|
}
|
|
3130
|
-
if (
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
else if (node.is_void) {
|
|
3137
|
-
result += ">";
|
|
3138
|
-
}
|
|
3139
|
-
else {
|
|
3140
|
-
result += `></${tagName}>`;
|
|
4283
|
+
if (this.isNonWhitespaceNode(child) && lastWasMeaningful && !hasHandledSpacing) {
|
|
4284
|
+
const element = body[i - 1];
|
|
4285
|
+
const hasExistingSpacing = i > 0 && isNode(element, HTMLTextNode) && element.content.trim() === "" && (element.content.includes('\n\n') || element.content.split('\n').length > 2);
|
|
4286
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, i, hasExistingSpacing);
|
|
4287
|
+
if (shouldAddSpacing) {
|
|
4288
|
+
this.push("");
|
|
3141
4289
|
}
|
|
3142
|
-
this.push(indent + result);
|
|
3143
|
-
return;
|
|
3144
4290
|
}
|
|
3145
|
-
this.
|
|
3146
|
-
if (
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
this.visitTextFlowChildren(children);
|
|
3150
|
-
}
|
|
3151
|
-
else {
|
|
3152
|
-
children.forEach(child => this.visit(child));
|
|
3153
|
-
}
|
|
3154
|
-
});
|
|
3155
|
-
this.push(indent + `</${tagName}>`);
|
|
4291
|
+
this.visit(child);
|
|
4292
|
+
if (this.isNonWhitespaceNode(child)) {
|
|
4293
|
+
lastWasMeaningful = true;
|
|
4294
|
+
hasHandledSpacing = false;
|
|
3156
4295
|
}
|
|
3157
4296
|
}
|
|
3158
4297
|
}
|
|
3159
4298
|
visitHTMLOpenTagNode(node) {
|
|
3160
|
-
const
|
|
3161
|
-
const indent = this.indent();
|
|
3162
|
-
const attributes = this.extractAttributes(node.children);
|
|
4299
|
+
const attributes = filterNodes(node.children, HTMLAttributeNode);
|
|
3163
4300
|
const inlineNodes = this.extractInlineNodes(node.children);
|
|
3164
|
-
const
|
|
3165
|
-
if (
|
|
3166
|
-
this.
|
|
3167
|
-
|
|
4301
|
+
const isSelfClosing = node.tag_closing?.value === "/>";
|
|
4302
|
+
if (this.currentElement && this.elementFormattingAnalysis.has(this.currentElement)) {
|
|
4303
|
+
const analysis = this.elementFormattingAnalysis.get(this.currentElement);
|
|
4304
|
+
if (analysis.openTagInline) {
|
|
4305
|
+
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children);
|
|
4306
|
+
this.push(this.inlineMode ? inline : this.indent + inline);
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
else {
|
|
4310
|
+
this.renderMultilineAttributes(getTagName(node), node.children, isSelfClosing);
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
3168
4313
|
}
|
|
3169
|
-
const inline = this.renderInlineOpen(
|
|
4314
|
+
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children);
|
|
3170
4315
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
|
|
3171
|
-
const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, false,
|
|
4316
|
+
const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, this.indent.length, this.maxLineLength, false, this.hasMultilineAttributes(attributes), attributes);
|
|
3172
4317
|
if (shouldKeepInline) {
|
|
3173
|
-
this.push(indent + inline);
|
|
3174
|
-
return;
|
|
4318
|
+
this.push(this.inlineMode ? inline : this.indent + inline);
|
|
3175
4319
|
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
visitHTMLSelfCloseTagNode(node) {
|
|
3179
|
-
const tagName = node.tag_name?.value ?? "";
|
|
3180
|
-
const indent = this.indent();
|
|
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);
|
|
3186
|
-
if (shouldKeepInline) {
|
|
3187
|
-
this.push(indent + inline);
|
|
3188
|
-
return;
|
|
4320
|
+
else {
|
|
4321
|
+
this.renderMultilineAttributes(getTagName(node), node.children, isSelfClosing);
|
|
3189
4322
|
}
|
|
3190
|
-
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false);
|
|
3191
4323
|
}
|
|
3192
4324
|
visitHTMLCloseTagNode(node) {
|
|
3193
|
-
const
|
|
3194
|
-
const
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3197
|
-
|
|
4325
|
+
const closingTag = IdentityPrinter.print(node);
|
|
4326
|
+
const analysis = this.currentElement && this.elementFormattingAnalysis.get(this.currentElement);
|
|
4327
|
+
const closeTagInline = analysis?.closeTagInline;
|
|
4328
|
+
if (this.currentElement && closeTagInline) {
|
|
4329
|
+
this.pushToLastLine(closingTag);
|
|
4330
|
+
}
|
|
4331
|
+
else {
|
|
4332
|
+
this.push(this.indent + closingTag);
|
|
4333
|
+
}
|
|
3198
4334
|
}
|
|
3199
4335
|
visitHTMLTextNode(node) {
|
|
3200
4336
|
if (this.inlineMode) {
|
|
@@ -3204,17 +4340,16 @@ class Printer extends Visitor {
|
|
|
3204
4340
|
}
|
|
3205
4341
|
return;
|
|
3206
4342
|
}
|
|
3207
|
-
const indent = this.indent();
|
|
3208
4343
|
let text = node.content.trim();
|
|
3209
4344
|
if (!text)
|
|
3210
4345
|
return;
|
|
3211
|
-
const wrapWidth = this.maxLineLength - indent.length;
|
|
4346
|
+
const wrapWidth = this.maxLineLength - this.indent.length;
|
|
3212
4347
|
const words = text.split(/\s+/);
|
|
3213
4348
|
const lines = [];
|
|
3214
4349
|
let line = "";
|
|
3215
4350
|
for (const word of words) {
|
|
3216
4351
|
if ((line + (line ? " " : "") + word).length > wrapWidth && line) {
|
|
3217
|
-
lines.push(indent + line);
|
|
4352
|
+
lines.push(this.indent + line);
|
|
3218
4353
|
line = word;
|
|
3219
4354
|
}
|
|
3220
4355
|
else {
|
|
@@ -3222,68 +4357,82 @@ class Printer extends Visitor {
|
|
|
3222
4357
|
}
|
|
3223
4358
|
}
|
|
3224
4359
|
if (line)
|
|
3225
|
-
lines.push(indent + line);
|
|
4360
|
+
lines.push(this.indent + line);
|
|
3226
4361
|
lines.forEach(line => this.push(line));
|
|
3227
4362
|
}
|
|
3228
4363
|
visitHTMLAttributeNode(node) {
|
|
3229
|
-
|
|
3230
|
-
this.push(indent + this.renderAttribute(node));
|
|
4364
|
+
this.push(this.indent + this.renderAttribute(node));
|
|
3231
4365
|
}
|
|
3232
4366
|
visitHTMLAttributeNameNode(node) {
|
|
3233
|
-
|
|
3234
|
-
const name = node.name?.value ?? "";
|
|
3235
|
-
this.push(indent + name);
|
|
4367
|
+
this.push(this.indent + getCombinedAttributeName(node));
|
|
3236
4368
|
}
|
|
3237
4369
|
visitHTMLAttributeValueNode(node) {
|
|
3238
|
-
|
|
3239
|
-
const open_quote = node.open_quote?.value ?? "";
|
|
3240
|
-
const close_quote = node.close_quote?.value ?? "";
|
|
3241
|
-
const attribute_value = node.children.map(child => {
|
|
3242
|
-
if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE' ||
|
|
3243
|
-
child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
|
|
3244
|
-
return child.content;
|
|
3245
|
-
}
|
|
3246
|
-
else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
|
|
3247
|
-
const erbChild = child;
|
|
3248
|
-
return (erbChild.tag_opening.value + erbChild.content.value + erbChild.tag_closing.value);
|
|
3249
|
-
}
|
|
3250
|
-
return "";
|
|
3251
|
-
}).join("");
|
|
3252
|
-
this.push(indent + open_quote + attribute_value + close_quote);
|
|
4370
|
+
this.push(this.indent + IdentityPrinter.print(node));
|
|
3253
4371
|
}
|
|
4372
|
+
// TODO: rework
|
|
3254
4373
|
visitHTMLCommentNode(node) {
|
|
3255
|
-
const indent = this.indent();
|
|
3256
4374
|
const open = node.comment_start?.value ?? "";
|
|
3257
4375
|
const close = node.comment_end?.value ?? "";
|
|
3258
4376
|
let inner;
|
|
3259
|
-
if (node.
|
|
3260
|
-
// TODO: use .value
|
|
3261
|
-
const [_, startIndex] = node.comment_start.range.toArray();
|
|
3262
|
-
const [endIndex] = node.comment_end.range.toArray();
|
|
3263
|
-
const rawInner = this.source.slice(startIndex, endIndex);
|
|
3264
|
-
inner = ` ${rawInner.trim()} `;
|
|
3265
|
-
}
|
|
3266
|
-
else {
|
|
4377
|
+
if (node.children && node.children.length > 0) {
|
|
3267
4378
|
inner = node.children.map(child => {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
4379
|
+
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
4380
|
+
return child.content;
|
|
4381
|
+
}
|
|
4382
|
+
else if (isERBNode(child) || isNode(child, ERBContentNode)) {
|
|
4383
|
+
return this.reconstructERBNode(child, false);
|
|
4384
|
+
}
|
|
4385
|
+
else {
|
|
4386
|
+
return "";
|
|
4387
|
+
}
|
|
3271
4388
|
}).join("");
|
|
4389
|
+
const hasNewlines = inner.includes('\n');
|
|
4390
|
+
if (hasNewlines) {
|
|
4391
|
+
const lines = inner.split('\n');
|
|
4392
|
+
const childIndent = " ".repeat(this.indentWidth);
|
|
4393
|
+
const firstLineHasContent = lines[0].trim() !== '';
|
|
4394
|
+
if (firstLineHasContent && lines.length > 1) {
|
|
4395
|
+
const contentLines = lines.map(line => line.trim()).filter(line => line !== '');
|
|
4396
|
+
inner = '\n' + contentLines.map(line => childIndent + line).join('\n') + '\n';
|
|
4397
|
+
}
|
|
4398
|
+
else {
|
|
4399
|
+
const contentLines = lines.filter((line, index) => {
|
|
4400
|
+
return line.trim() !== '' && !(index === 0 || index === lines.length - 1);
|
|
4401
|
+
});
|
|
4402
|
+
const minIndent = contentLines.length > 0 ? Math.min(...contentLines.map(line => line.length - line.trimStart().length)) : 0;
|
|
4403
|
+
const processedLines = lines.map((line, index) => {
|
|
4404
|
+
const trimmedLine = line.trim();
|
|
4405
|
+
if ((index === 0 || index === lines.length - 1) && trimmedLine === '') {
|
|
4406
|
+
return line;
|
|
4407
|
+
}
|
|
4408
|
+
if (trimmedLine !== '') {
|
|
4409
|
+
const currentIndent = line.length - line.trimStart().length;
|
|
4410
|
+
const relativeIndent = Math.max(0, currentIndent - minIndent);
|
|
4411
|
+
return childIndent + " ".repeat(relativeIndent) + trimmedLine;
|
|
4412
|
+
}
|
|
4413
|
+
return line;
|
|
4414
|
+
});
|
|
4415
|
+
inner = processedLines.join('\n');
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
else {
|
|
4419
|
+
inner = ` ${inner.trim()} `;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
else {
|
|
4423
|
+
inner = "";
|
|
3272
4424
|
}
|
|
3273
|
-
this.push(indent + open + inner + close);
|
|
4425
|
+
this.push(this.indent + open + inner + close);
|
|
3274
4426
|
}
|
|
3275
4427
|
visitERBCommentNode(node) {
|
|
3276
|
-
const indent = this.indent();
|
|
3277
4428
|
const open = node.tag_opening?.value ?? "";
|
|
3278
4429
|
const close = node.tag_closing?.value ?? "";
|
|
3279
4430
|
let inner;
|
|
3280
|
-
if (node.
|
|
3281
|
-
const
|
|
3282
|
-
const [closingStart] = node.tag_closing.range.toArray();
|
|
3283
|
-
const rawInner = this.source.slice(openingEnd, closingStart);
|
|
4431
|
+
if (node.content && node.content.value) {
|
|
4432
|
+
const rawInner = node.content.value;
|
|
3284
4433
|
const lines = rawInner.split("\n");
|
|
3285
4434
|
if (lines.length > 2) {
|
|
3286
|
-
const childIndent = indent + " ".repeat(this.indentWidth);
|
|
4435
|
+
const childIndent = this.indent + " ".repeat(this.indentWidth);
|
|
3287
4436
|
const innerLines = lines.slice(1, -1).map(line => childIndent + line.trim());
|
|
3288
4437
|
inner = "\n" + innerLines.join("\n") + "\n";
|
|
3289
4438
|
}
|
|
@@ -3292,33 +4441,18 @@ class Printer extends Visitor {
|
|
|
3292
4441
|
}
|
|
3293
4442
|
}
|
|
3294
4443
|
else {
|
|
3295
|
-
inner =
|
|
3296
|
-
.map((child) => {
|
|
3297
|
-
const prevLines = this.lines.length;
|
|
3298
|
-
this.visit(child);
|
|
3299
|
-
return this.lines.slice(prevLines).join("");
|
|
3300
|
-
})
|
|
3301
|
-
.join("");
|
|
4444
|
+
inner = "";
|
|
3302
4445
|
}
|
|
3303
|
-
this.push(indent + open + inner + close);
|
|
4446
|
+
this.push(this.indent + open + inner + close);
|
|
3304
4447
|
}
|
|
3305
4448
|
visitHTMLDoctypeNode(node) {
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
innerDoctype = this.source.slice(openingEnd, closingStart);
|
|
3314
|
-
}
|
|
3315
|
-
else {
|
|
3316
|
-
innerDoctype = node.children
|
|
3317
|
-
.map(child => child instanceof HTMLTextNode ? child.content : (() => { const prevLines = this.lines.length; this.visit(child); return this.lines.slice(prevLines).join(""); })())
|
|
3318
|
-
.join("");
|
|
3319
|
-
}
|
|
3320
|
-
const close = node.tag_closing?.value ?? "";
|
|
3321
|
-
this.push(indent + open + innerDoctype + close);
|
|
4449
|
+
this.push(this.indent + IdentityPrinter.print(node));
|
|
4450
|
+
}
|
|
4451
|
+
visitXMLDeclarationNode(node) {
|
|
4452
|
+
this.push(this.indent + IdentityPrinter.print(node));
|
|
4453
|
+
}
|
|
4454
|
+
visitCDATANode(node) {
|
|
4455
|
+
this.push(this.indent + IdentityPrinter.print(node));
|
|
3322
4456
|
}
|
|
3323
4457
|
visitERBContentNode(node) {
|
|
3324
4458
|
// TODO: this feels hacky
|
|
@@ -3337,109 +4471,81 @@ class Printer extends Visitor {
|
|
|
3337
4471
|
}
|
|
3338
4472
|
visitERBInNode(node) {
|
|
3339
4473
|
this.printERBNode(node);
|
|
3340
|
-
this.withIndent(() =>
|
|
3341
|
-
node.statements.forEach(stmt => this.visit(stmt));
|
|
3342
|
-
});
|
|
4474
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
3343
4475
|
}
|
|
3344
4476
|
visitERBCaseMatchNode(node) {
|
|
3345
4477
|
this.printERBNode(node);
|
|
3346
|
-
node.conditions
|
|
4478
|
+
this.visitAll(node.conditions);
|
|
3347
4479
|
if (node.else_clause)
|
|
3348
4480
|
this.visit(node.else_clause);
|
|
3349
4481
|
if (node.end_node)
|
|
3350
4482
|
this.visit(node.end_node);
|
|
3351
4483
|
}
|
|
3352
4484
|
visitERBBlockNode(node) {
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
const close = node.tag_closing?.value ?? "";
|
|
3357
|
-
this.push(indent + open + content + close);
|
|
3358
|
-
this.withIndent(() => {
|
|
3359
|
-
node.body.forEach(child => this.visit(child));
|
|
3360
|
-
});
|
|
3361
|
-
if (node.end_node) {
|
|
4485
|
+
this.printERBNode(node);
|
|
4486
|
+
this.withIndent(() => this.visitElementChildren(node.body, null));
|
|
4487
|
+
if (node.end_node)
|
|
3362
4488
|
this.visit(node.end_node);
|
|
3363
|
-
}
|
|
3364
4489
|
}
|
|
3365
4490
|
visitERBIfNode(node) {
|
|
3366
4491
|
if (this.inlineMode) {
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
this.lines.push(open + inner + close);
|
|
3372
|
-
node.statements.forEach((child, _index) => {
|
|
3373
|
-
this.lines.push(" ");
|
|
3374
|
-
if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
4492
|
+
this.printERBNode(node);
|
|
4493
|
+
node.statements.forEach(child => {
|
|
4494
|
+
if (isNode(child, HTMLAttributeNode)) {
|
|
4495
|
+
this.lines.push(" ");
|
|
3375
4496
|
this.lines.push(this.renderAttribute(child));
|
|
3376
4497
|
}
|
|
3377
4498
|
else {
|
|
4499
|
+
const shouldAddSpaces = this.isInTokenListAttribute();
|
|
4500
|
+
if (shouldAddSpaces) {
|
|
4501
|
+
this.lines.push(" ");
|
|
4502
|
+
}
|
|
3378
4503
|
this.visit(child);
|
|
4504
|
+
if (shouldAddSpaces) {
|
|
4505
|
+
this.lines.push(" ");
|
|
4506
|
+
}
|
|
3379
4507
|
}
|
|
3380
4508
|
});
|
|
3381
|
-
|
|
4509
|
+
const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode));
|
|
4510
|
+
const isTokenList = this.isInTokenListAttribute();
|
|
4511
|
+
if ((hasHTMLAttributes || isTokenList) && node.end_node) {
|
|
3382
4512
|
this.lines.push(" ");
|
|
3383
4513
|
}
|
|
3384
|
-
if (node.subsequent)
|
|
3385
|
-
this.visit(node.
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
const endNode = node.end_node;
|
|
3389
|
-
const endOpen = endNode.tag_opening?.value ?? "";
|
|
3390
|
-
const endContent = endNode.content?.value ?? "";
|
|
3391
|
-
const endClose = endNode.tag_closing?.value ?? "";
|
|
3392
|
-
const endInner = this.formatERBContent(endContent);
|
|
3393
|
-
this.lines.push(endOpen + endInner + endClose);
|
|
3394
|
-
}
|
|
4514
|
+
if (node.subsequent)
|
|
4515
|
+
this.visit(node.end_node);
|
|
4516
|
+
if (node.end_node)
|
|
4517
|
+
this.visit(node.end_node);
|
|
3395
4518
|
}
|
|
3396
4519
|
else {
|
|
3397
4520
|
this.printERBNode(node);
|
|
3398
4521
|
this.withIndent(() => {
|
|
3399
4522
|
node.statements.forEach(child => this.visit(child));
|
|
3400
4523
|
});
|
|
3401
|
-
if (node.subsequent)
|
|
4524
|
+
if (node.subsequent)
|
|
3402
4525
|
this.visit(node.subsequent);
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
this.printERBNode(node.end_node);
|
|
3406
|
-
}
|
|
4526
|
+
if (node.end_node)
|
|
4527
|
+
this.visit(node.end_node);
|
|
3407
4528
|
}
|
|
3408
4529
|
}
|
|
3409
4530
|
visitERBElseNode(node) {
|
|
3410
4531
|
this.printERBNode(node);
|
|
3411
|
-
this.withIndent(() =>
|
|
3412
|
-
node.statements.forEach(child => this.visit(child));
|
|
3413
|
-
});
|
|
4532
|
+
this.withIndent(() => node.statements.forEach(statement => this.visit(statement)));
|
|
3414
4533
|
}
|
|
3415
4534
|
visitERBWhenNode(node) {
|
|
3416
4535
|
this.printERBNode(node);
|
|
3417
|
-
this.withIndent(() =>
|
|
3418
|
-
node.statements.forEach(stmt => this.visit(stmt));
|
|
3419
|
-
});
|
|
4536
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
3420
4537
|
}
|
|
3421
4538
|
visitERBCaseNode(node) {
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
const content = node.content?.value ?? "";
|
|
3425
|
-
const close = node.tag_closing?.value ?? "";
|
|
3426
|
-
this.push(indent + open + content + close);
|
|
3427
|
-
node.conditions.forEach(condition => this.visit(condition));
|
|
4539
|
+
this.printERBNode(node);
|
|
4540
|
+
this.visitAll(node.conditions);
|
|
3428
4541
|
if (node.else_clause)
|
|
3429
4542
|
this.visit(node.else_clause);
|
|
3430
|
-
if (node.end_node)
|
|
4543
|
+
if (node.end_node)
|
|
3431
4544
|
this.visit(node.end_node);
|
|
3432
|
-
}
|
|
3433
4545
|
}
|
|
3434
4546
|
visitERBBeginNode(node) {
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
const content = node.content?.value ?? "";
|
|
3438
|
-
const close = node.tag_closing?.value ?? "";
|
|
3439
|
-
this.push(indent + open + content + close);
|
|
3440
|
-
this.withIndent(() => {
|
|
3441
|
-
node.statements.forEach(statement => this.visit(statement));
|
|
3442
|
-
});
|
|
4547
|
+
this.printERBNode(node);
|
|
4548
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
3443
4549
|
if (node.rescue_clause)
|
|
3444
4550
|
this.visit(node.rescue_clause);
|
|
3445
4551
|
if (node.else_clause)
|
|
@@ -3450,61 +4556,147 @@ class Printer extends Visitor {
|
|
|
3450
4556
|
this.visit(node.end_node);
|
|
3451
4557
|
}
|
|
3452
4558
|
visitERBWhileNode(node) {
|
|
3453
|
-
this.
|
|
4559
|
+
this.printERBNode(node);
|
|
4560
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
4561
|
+
if (node.end_node)
|
|
4562
|
+
this.visit(node.end_node);
|
|
3454
4563
|
}
|
|
3455
4564
|
visitERBUntilNode(node) {
|
|
3456
|
-
this.
|
|
4565
|
+
this.printERBNode(node);
|
|
4566
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
4567
|
+
if (node.end_node)
|
|
4568
|
+
this.visit(node.end_node);
|
|
3457
4569
|
}
|
|
3458
4570
|
visitERBForNode(node) {
|
|
3459
|
-
this.
|
|
4571
|
+
this.printERBNode(node);
|
|
4572
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
4573
|
+
if (node.end_node)
|
|
4574
|
+
this.visit(node.end_node);
|
|
3460
4575
|
}
|
|
3461
4576
|
visitERBRescueNode(node) {
|
|
3462
|
-
this.
|
|
4577
|
+
this.printERBNode(node);
|
|
4578
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
3463
4579
|
}
|
|
3464
4580
|
visitERBEnsureNode(node) {
|
|
3465
|
-
this.
|
|
4581
|
+
this.printERBNode(node);
|
|
4582
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
3466
4583
|
}
|
|
3467
4584
|
visitERBUnlessNode(node) {
|
|
3468
|
-
this.
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
const indent = this.indent();
|
|
3473
|
-
const open = node.tag_opening?.value ?? "";
|
|
3474
|
-
const content = node.content?.value ?? "";
|
|
3475
|
-
const close = node.tag_closing?.value ?? "";
|
|
3476
|
-
this.push(indent + open + content + close);
|
|
3477
|
-
this.withIndent(() => {
|
|
3478
|
-
const statements = node.statements ?? node.body ?? node.children ?? [];
|
|
3479
|
-
statements.forEach(statement => this.visit(statement));
|
|
3480
|
-
});
|
|
4585
|
+
this.printERBNode(node);
|
|
4586
|
+
this.withIndent(() => this.visitAll(node.statements));
|
|
4587
|
+
if (node.else_clause)
|
|
4588
|
+
this.visit(node.else_clause);
|
|
3481
4589
|
if (node.end_node)
|
|
3482
4590
|
this.visit(node.end_node);
|
|
3483
4591
|
}
|
|
4592
|
+
// --- Element Formatting Analysis Helpers ---
|
|
4593
|
+
/**
|
|
4594
|
+
* Analyzes an HTMLElementNode and returns formatting decisions for all parts
|
|
4595
|
+
*/
|
|
4596
|
+
analyzeElementFormatting(node) {
|
|
4597
|
+
const openTagInline = this.shouldRenderOpenTagInline(node);
|
|
4598
|
+
const elementContentInline = this.shouldRenderElementContentInline(node);
|
|
4599
|
+
const closeTagInline = this.shouldRenderCloseTagInline(node, elementContentInline);
|
|
4600
|
+
return {
|
|
4601
|
+
openTagInline,
|
|
4602
|
+
elementContentInline,
|
|
4603
|
+
closeTagInline
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
/**
|
|
4607
|
+
* Determines if the open tag should be rendered inline
|
|
4608
|
+
*/
|
|
4609
|
+
shouldRenderOpenTagInline(node) {
|
|
4610
|
+
const children = node.open_tag?.children || [];
|
|
4611
|
+
const attributes = filterNodes(children, HTMLAttributeNode);
|
|
4612
|
+
const inlineNodes = this.extractInlineNodes(children);
|
|
4613
|
+
const hasERBControlFlow = inlineNodes.some(node => isERBControlFlowNode(node)) || children.some(node => isERBControlFlowNode(node));
|
|
4614
|
+
const hasComplexERB = hasERBControlFlow && this.hasComplexERBControlFlow(inlineNodes);
|
|
4615
|
+
if (hasComplexERB)
|
|
4616
|
+
return false;
|
|
4617
|
+
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
|
|
4618
|
+
const hasMultilineAttrs = this.hasMultilineAttributes(attributes);
|
|
4619
|
+
if (hasMultilineAttrs)
|
|
4620
|
+
return false;
|
|
4621
|
+
const inline = this.renderInlineOpen(getTagName(node), attributes, node.open_tag?.tag_closing?.value === "/>", inlineNodes, children);
|
|
4622
|
+
return this.shouldRenderInline(totalAttributeCount, inline.length, this.indent.length, this.maxLineLength, hasComplexERB, hasMultilineAttrs, attributes);
|
|
4623
|
+
}
|
|
4624
|
+
/**
|
|
4625
|
+
* Determines if the element content should be rendered inline
|
|
4626
|
+
*/
|
|
4627
|
+
shouldRenderElementContentInline(node) {
|
|
4628
|
+
const tagName = getTagName(node);
|
|
4629
|
+
const children = this.filterSignificantChildren(node.body, this.isInTextFlowContext(null, node.body));
|
|
4630
|
+
const isInlineElement = this.isInlineElement(tagName);
|
|
4631
|
+
const openTagInline = this.shouldRenderOpenTagInline(node);
|
|
4632
|
+
if (!openTagInline)
|
|
4633
|
+
return false;
|
|
4634
|
+
if (children.length === 0)
|
|
4635
|
+
return true;
|
|
4636
|
+
if (isInlineElement) {
|
|
4637
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), children);
|
|
4638
|
+
if (fullInlineResult) {
|
|
4639
|
+
const totalLength = this.indent.length + fullInlineResult.length;
|
|
4640
|
+
return totalLength <= this.maxLineLength || totalLength <= 120;
|
|
4641
|
+
}
|
|
4642
|
+
return false;
|
|
4643
|
+
}
|
|
4644
|
+
const allNestedAreInline = this.areAllNestedElementsInline(children);
|
|
4645
|
+
const hasMultilineText = this.hasMultilineTextContent(children);
|
|
4646
|
+
const hasMixedContent = this.hasMixedTextAndInlineContent(children);
|
|
4647
|
+
if (allNestedAreInline && (!hasMultilineText || hasMixedContent)) {
|
|
4648
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), children);
|
|
4649
|
+
if (fullInlineResult) {
|
|
4650
|
+
const totalLength = this.indent.length + fullInlineResult.length;
|
|
4651
|
+
if (totalLength <= this.maxLineLength) {
|
|
4652
|
+
return true;
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
const inlineResult = this.tryRenderInline(children, tagName);
|
|
4657
|
+
if (inlineResult) {
|
|
4658
|
+
const openTagResult = this.renderInlineOpen(tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), false, [], node.open_tag?.children || []);
|
|
4659
|
+
const childrenContent = this.renderChildrenInline(children);
|
|
4660
|
+
const fullLine = openTagResult + childrenContent + `</${tagName}>`;
|
|
4661
|
+
if ((this.indent.length + fullLine.length) <= this.maxLineLength) {
|
|
4662
|
+
return true;
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
return false;
|
|
4666
|
+
}
|
|
4667
|
+
/**
|
|
4668
|
+
* Determines if the close tag should be rendered inline (usually follows content decision)
|
|
4669
|
+
*/
|
|
4670
|
+
shouldRenderCloseTagInline(node, elementContentInline) {
|
|
4671
|
+
const isSelfClosing = node.open_tag?.tag_closing?.value === "/>";
|
|
4672
|
+
if (isSelfClosing || node.is_void)
|
|
4673
|
+
return true;
|
|
4674
|
+
const children = this.filterSignificantChildren(node.body, this.isInTextFlowContext(null, node.body));
|
|
4675
|
+
if (children.length === 0)
|
|
4676
|
+
return true;
|
|
4677
|
+
return elementContentInline;
|
|
4678
|
+
}
|
|
3484
4679
|
// --- Utility methods ---
|
|
3485
4680
|
isNonWhitespaceNode(node) {
|
|
3486
|
-
if (node
|
|
3487
|
-
return node.content.trim() !== "";
|
|
3488
|
-
}
|
|
3489
|
-
if (node instanceof WhitespaceNode || node.type === 'AST_WHITESPACE_NODE') {
|
|
4681
|
+
if (isNode(node, WhitespaceNode))
|
|
3490
4682
|
return false;
|
|
3491
|
-
|
|
4683
|
+
if (isNode(node, HTMLTextNode))
|
|
4684
|
+
return node.content.trim() !== "";
|
|
3492
4685
|
return true;
|
|
3493
4686
|
}
|
|
3494
4687
|
/**
|
|
3495
4688
|
* Check if an element should be treated as inline based on its tag name
|
|
3496
4689
|
*/
|
|
3497
4690
|
isInlineElement(tagName) {
|
|
3498
|
-
return
|
|
4691
|
+
return FormatPrinter.INLINE_ELEMENTS.has(tagName.toLowerCase());
|
|
3499
4692
|
}
|
|
3500
4693
|
/**
|
|
3501
4694
|
* Check if we're in a text flow context (parent contains mixed text and inline elements)
|
|
3502
4695
|
*/
|
|
3503
4696
|
visitTextFlowChildren(children) {
|
|
3504
|
-
const indent = this.indent();
|
|
3505
4697
|
let currentLineContent = "";
|
|
3506
4698
|
for (const child of children) {
|
|
3507
|
-
if (child
|
|
4699
|
+
if (isNode(child, HTMLTextNode)) {
|
|
3508
4700
|
const content = child.content;
|
|
3509
4701
|
let processedContent = content.replace(/\s+/g, ' ').trim();
|
|
3510
4702
|
if (processedContent) {
|
|
@@ -3517,29 +4709,26 @@ class Printer extends Visitor {
|
|
|
3517
4709
|
if (hasTrailingSpace && !currentLineContent.endsWith(' ')) {
|
|
3518
4710
|
currentLineContent += ' ';
|
|
3519
4711
|
}
|
|
3520
|
-
if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
|
|
3521
|
-
this.
|
|
4712
|
+
if ((this.indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
|
|
4713
|
+
children.forEach(child => this.visit(child));
|
|
3522
4714
|
return;
|
|
3523
4715
|
}
|
|
3524
4716
|
}
|
|
3525
4717
|
}
|
|
3526
|
-
else if (child
|
|
3527
|
-
const
|
|
3528
|
-
const openTag = element.open_tag;
|
|
3529
|
-
const childTagName = openTag?.tag_name?.value || '';
|
|
4718
|
+
else if (isNode(child, HTMLElementNode)) {
|
|
4719
|
+
const childTagName = getTagName(child);
|
|
3530
4720
|
if (this.isInlineElement(childTagName)) {
|
|
3531
|
-
const childInline = this.tryRenderInlineFull(
|
|
3532
|
-
!((c instanceof HTMLTextNode || c.type === 'AST_HTML_TEXT_NODE') && c?.content.trim() === "")));
|
|
4721
|
+
const childInline = this.tryRenderInlineFull(child, childTagName, filterNodes(child.open_tag?.children, HTMLAttributeNode), this.filterEmptyNodes(child.body));
|
|
3533
4722
|
if (childInline) {
|
|
3534
4723
|
currentLineContent += childInline;
|
|
3535
|
-
if ((indent.length + currentLineContent.length) > this.maxLineLength) {
|
|
3536
|
-
this.
|
|
4724
|
+
if ((this.indent.length + currentLineContent.length) > this.maxLineLength) {
|
|
4725
|
+
children.forEach(child => this.visit(child));
|
|
3537
4726
|
return;
|
|
3538
4727
|
}
|
|
3539
4728
|
}
|
|
3540
4729
|
else {
|
|
3541
4730
|
if (currentLineContent.trim()) {
|
|
3542
|
-
this.push(indent + currentLineContent.trim());
|
|
4731
|
+
this.push(this.indent + currentLineContent.trim());
|
|
3543
4732
|
currentLineContent = "";
|
|
3544
4733
|
}
|
|
3545
4734
|
this.visit(child);
|
|
@@ -3547,23 +4736,26 @@ class Printer extends Visitor {
|
|
|
3547
4736
|
}
|
|
3548
4737
|
else {
|
|
3549
4738
|
if (currentLineContent.trim()) {
|
|
3550
|
-
this.push(indent + currentLineContent.trim());
|
|
4739
|
+
this.push(this.indent + currentLineContent.trim());
|
|
3551
4740
|
currentLineContent = "";
|
|
3552
4741
|
}
|
|
3553
4742
|
this.visit(child);
|
|
3554
4743
|
}
|
|
3555
4744
|
}
|
|
3556
|
-
else if (child
|
|
4745
|
+
else if (isNode(child, ERBContentNode)) {
|
|
3557
4746
|
const oldLines = this.lines;
|
|
3558
4747
|
const oldInlineMode = this.inlineMode;
|
|
4748
|
+
// TODO: use this.capture
|
|
3559
4749
|
try {
|
|
3560
4750
|
this.lines = [];
|
|
3561
4751
|
this.inlineMode = true;
|
|
3562
4752
|
this.visit(child);
|
|
3563
4753
|
const erbContent = this.lines.join("");
|
|
3564
4754
|
currentLineContent += erbContent;
|
|
3565
|
-
if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
|
|
3566
|
-
this.
|
|
4755
|
+
if ((this.indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
|
|
4756
|
+
this.lines = oldLines;
|
|
4757
|
+
this.inlineMode = oldInlineMode;
|
|
4758
|
+
children.forEach(child => this.visit(child));
|
|
3567
4759
|
return;
|
|
3568
4760
|
}
|
|
3569
4761
|
}
|
|
@@ -3574,53 +4766,38 @@ class Printer extends Visitor {
|
|
|
3574
4766
|
}
|
|
3575
4767
|
else {
|
|
3576
4768
|
if (currentLineContent.trim()) {
|
|
3577
|
-
this.push(indent + currentLineContent.trim());
|
|
4769
|
+
this.push(this.indent + currentLineContent.trim());
|
|
3578
4770
|
currentLineContent = "";
|
|
3579
4771
|
}
|
|
3580
4772
|
this.visit(child);
|
|
3581
4773
|
}
|
|
3582
4774
|
}
|
|
3583
4775
|
if (currentLineContent.trim()) {
|
|
3584
|
-
const finalLine = indent + currentLineContent.trim();
|
|
4776
|
+
const finalLine = this.indent + currentLineContent.trim();
|
|
3585
4777
|
if (finalLine.length > Math.max(this.maxLineLength, 120)) {
|
|
3586
|
-
this.
|
|
4778
|
+
this.visitAll(children);
|
|
3587
4779
|
return;
|
|
3588
4780
|
}
|
|
3589
4781
|
this.push(finalLine);
|
|
3590
4782
|
}
|
|
3591
4783
|
}
|
|
3592
|
-
|
|
3593
|
-
children.
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const hasTextContent = children.some(child => (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') &&
|
|
3597
|
-
child.content.trim() !== "");
|
|
3598
|
-
if (!hasTextContent) {
|
|
4784
|
+
isInTextFlowContext(_parent, children) {
|
|
4785
|
+
const hasTextContent = children.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "");
|
|
4786
|
+
const nonTextChildren = children.filter(child => !isNode(child, HTMLTextNode));
|
|
4787
|
+
if (!hasTextContent)
|
|
3599
4788
|
return false;
|
|
3600
|
-
|
|
3601
|
-
const nonTextChildren = children.filter(child => !(child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE'));
|
|
3602
|
-
if (nonTextChildren.length === 0) {
|
|
4789
|
+
if (nonTextChildren.length === 0)
|
|
3603
4790
|
return false;
|
|
3604
|
-
}
|
|
3605
4791
|
const allInline = nonTextChildren.every(child => {
|
|
3606
|
-
if (child
|
|
4792
|
+
if (isNode(child, ERBContentNode))
|
|
3607
4793
|
return true;
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
const element = child;
|
|
3611
|
-
const openTag = element.open_tag;
|
|
3612
|
-
const tagName = openTag?.tag_name?.value || '';
|
|
3613
|
-
return this.isInlineElement(tagName);
|
|
4794
|
+
if (isNode(child, HTMLElementNode)) {
|
|
4795
|
+
return this.isInlineElement(getTagName(child));
|
|
3614
4796
|
}
|
|
3615
4797
|
return false;
|
|
3616
4798
|
});
|
|
3617
|
-
if (!allInline)
|
|
4799
|
+
if (!allInline)
|
|
3618
4800
|
return false;
|
|
3619
|
-
}
|
|
3620
|
-
const maxNestingDepth = this.getMaxNestingDepth(children, 0);
|
|
3621
|
-
if (maxNestingDepth > 2) {
|
|
3622
|
-
return false;
|
|
3623
|
-
}
|
|
3624
4801
|
return true;
|
|
3625
4802
|
}
|
|
3626
4803
|
renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
|
|
@@ -3628,73 +4805,68 @@ class Printer extends Visitor {
|
|
|
3628
4805
|
if (inlineNodes.length > 0) {
|
|
3629
4806
|
let result = `<${name}`;
|
|
3630
4807
|
if (allChildren.length > 0) {
|
|
3631
|
-
const
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
this.inlineMode = wasInlineMode;
|
|
3645
|
-
}
|
|
4808
|
+
const lines = this.capture(() => {
|
|
4809
|
+
allChildren.forEach(child => {
|
|
4810
|
+
if (isNode(child, HTMLAttributeNode)) {
|
|
4811
|
+
this.lines.push(" " + this.renderAttribute(child));
|
|
4812
|
+
}
|
|
4813
|
+
else if (!(isNode(child, WhitespaceNode))) {
|
|
4814
|
+
const wasInlineMode = this.inlineMode;
|
|
4815
|
+
this.inlineMode = true;
|
|
4816
|
+
this.lines.push(" ");
|
|
4817
|
+
this.visit(child);
|
|
4818
|
+
this.inlineMode = wasInlineMode;
|
|
4819
|
+
}
|
|
4820
|
+
});
|
|
3646
4821
|
});
|
|
3647
|
-
|
|
3648
|
-
this.lines = tempLines;
|
|
3649
|
-
this.indentLevel = currentIndentLevel;
|
|
3650
|
-
result += inlineContent;
|
|
4822
|
+
result += lines.join("");
|
|
3651
4823
|
}
|
|
3652
4824
|
else {
|
|
3653
4825
|
if (parts.length > 0) {
|
|
3654
4826
|
result += ` ${parts.join(" ")}`;
|
|
3655
4827
|
}
|
|
3656
|
-
const
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
this.inlineMode =
|
|
3664
|
-
}
|
|
3665
|
-
this.visit(node);
|
|
3666
|
-
this.inlineMode = wasInlineMode;
|
|
4828
|
+
const lines = this.capture(() => {
|
|
4829
|
+
inlineNodes.forEach(node => {
|
|
4830
|
+
const wasInlineMode = this.inlineMode;
|
|
4831
|
+
if (!isERBControlFlowNode(node)) {
|
|
4832
|
+
this.inlineMode = true;
|
|
4833
|
+
}
|
|
4834
|
+
this.visit(node);
|
|
4835
|
+
this.inlineMode = wasInlineMode;
|
|
4836
|
+
});
|
|
3667
4837
|
});
|
|
3668
|
-
|
|
3669
|
-
this.lines = tempLines;
|
|
3670
|
-
this.indentLevel = currentIndentLevel;
|
|
3671
|
-
result += inlineContent;
|
|
4838
|
+
result += lines.join("");
|
|
3672
4839
|
}
|
|
3673
4840
|
result += selfClose ? " />" : ">";
|
|
3674
4841
|
return result;
|
|
3675
4842
|
}
|
|
3676
|
-
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? "
|
|
4843
|
+
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " />" : ">"}`;
|
|
3677
4844
|
}
|
|
3678
4845
|
renderAttribute(attribute) {
|
|
3679
|
-
const name = attribute.name.name
|
|
4846
|
+
const name = attribute.name ? getCombinedAttributeName(attribute.name) : "";
|
|
3680
4847
|
const equals = attribute.equals?.value ?? "";
|
|
4848
|
+
this.currentAttributeName = name;
|
|
3681
4849
|
let value = "";
|
|
3682
|
-
if (attribute.value && (attribute.value
|
|
4850
|
+
if (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
3683
4851
|
const attributeValue = attribute.value;
|
|
3684
4852
|
let open_quote = attributeValue.open_quote?.value ?? "";
|
|
3685
4853
|
let close_quote = attributeValue.close_quote?.value ?? "";
|
|
3686
4854
|
let htmlTextContent = "";
|
|
3687
4855
|
const content = attributeValue.children.map((child) => {
|
|
3688
|
-
if (child
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
4856
|
+
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
4857
|
+
htmlTextContent += child.content;
|
|
4858
|
+
return child.content;
|
|
4859
|
+
}
|
|
4860
|
+
else if (isNode(child, ERBContentNode)) {
|
|
4861
|
+
return IdentityPrinter.print(child);
|
|
3692
4862
|
}
|
|
3693
|
-
else
|
|
3694
|
-
const
|
|
3695
|
-
|
|
4863
|
+
else {
|
|
4864
|
+
const printed = IdentityPrinter.print(child);
|
|
4865
|
+
if (this.currentAttributeName && FormatPrinter.TOKEN_LIST_ATTRIBUTES.has(this.currentAttributeName)) {
|
|
4866
|
+
return printed.replace(/%>([^<\s])/g, '%> $1').replace(/([^>\s])<%/g, '$1 <%');
|
|
4867
|
+
}
|
|
4868
|
+
return printed;
|
|
3696
4869
|
}
|
|
3697
|
-
return "";
|
|
3698
4870
|
}).join("");
|
|
3699
4871
|
if (open_quote === "" && close_quote === "") {
|
|
3700
4872
|
open_quote = '"';
|
|
@@ -3704,21 +4876,31 @@ class Printer extends Visitor {
|
|
|
3704
4876
|
open_quote = '"';
|
|
3705
4877
|
close_quote = '"';
|
|
3706
4878
|
}
|
|
3707
|
-
|
|
4879
|
+
if (this.isFormattableAttribute(name, this.currentTagName)) {
|
|
4880
|
+
if (name === 'class') {
|
|
4881
|
+
value = this.formatClassAttribute(content, name, equals, open_quote, close_quote);
|
|
4882
|
+
}
|
|
4883
|
+
else {
|
|
4884
|
+
value = this.formatMultilineAttribute(content, name, open_quote, close_quote);
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
else {
|
|
4888
|
+
value = open_quote + content + close_quote;
|
|
4889
|
+
}
|
|
3708
4890
|
}
|
|
4891
|
+
this.currentAttributeName = null;
|
|
3709
4892
|
return name + equals + value;
|
|
3710
4893
|
}
|
|
3711
4894
|
/**
|
|
3712
4895
|
* Try to render a complete element inline including opening tag, children, and closing tag
|
|
3713
4896
|
*/
|
|
3714
|
-
tryRenderInlineFull(
|
|
4897
|
+
tryRenderInlineFull(_node, tagName, attributes, children) {
|
|
3715
4898
|
let result = `<${tagName}`;
|
|
3716
4899
|
result += this.renderAttributesString(attributes);
|
|
3717
4900
|
result += ">";
|
|
3718
4901
|
const childrenContent = this.tryRenderChildrenInline(children);
|
|
3719
|
-
if (!childrenContent)
|
|
4902
|
+
if (!childrenContent)
|
|
3720
4903
|
return null;
|
|
3721
|
-
}
|
|
3722
4904
|
result += childrenContent;
|
|
3723
4905
|
result += `</${tagName}>`;
|
|
3724
4906
|
return result;
|
|
@@ -3729,11 +4911,10 @@ class Printer extends Visitor {
|
|
|
3729
4911
|
tryRenderChildrenInline(children) {
|
|
3730
4912
|
let result = "";
|
|
3731
4913
|
for (const child of children) {
|
|
3732
|
-
if (child
|
|
3733
|
-
const
|
|
3734
|
-
const
|
|
3735
|
-
const
|
|
3736
|
-
const hasTrailingSpace = /\s$/.test(content);
|
|
4914
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4915
|
+
const normalizedContent = child.content.replace(/\s+/g, ' ');
|
|
4916
|
+
const hasLeadingSpace = /^\s/.test(child.content);
|
|
4917
|
+
const hasTrailingSpace = /\s$/.test(child.content);
|
|
3737
4918
|
const trimmedContent = normalizedContent.trim();
|
|
3738
4919
|
if (trimmedContent) {
|
|
3739
4920
|
let finalContent = trimmedContent;
|
|
@@ -3751,36 +4932,19 @@ class Printer extends Visitor {
|
|
|
3751
4932
|
}
|
|
3752
4933
|
}
|
|
3753
4934
|
}
|
|
3754
|
-
else if (child
|
|
3755
|
-
const
|
|
3756
|
-
|
|
3757
|
-
const childTagName = openTag?.tag_name?.value || '';
|
|
3758
|
-
if (!this.isInlineElement(childTagName)) {
|
|
4935
|
+
else if (isNode(child, HTMLElementNode)) {
|
|
4936
|
+
const tagName = getTagName(child);
|
|
4937
|
+
if (!this.isInlineElement(tagName)) {
|
|
3759
4938
|
return null;
|
|
3760
4939
|
}
|
|
3761
|
-
const childInline = this.tryRenderInlineFull(
|
|
3762
|
-
!((c instanceof HTMLTextNode || c.type === 'AST_HTML_TEXT_NODE') && c?.content.trim() === "")));
|
|
4940
|
+
const childInline = this.tryRenderInlineFull(child, tagName, filterNodes(child.open_tag?.children, HTMLAttributeNode), this.filterEmptyNodes(child.body));
|
|
3763
4941
|
if (!childInline) {
|
|
3764
4942
|
return null;
|
|
3765
4943
|
}
|
|
3766
4944
|
result += childInline;
|
|
3767
4945
|
}
|
|
3768
4946
|
else {
|
|
3769
|
-
|
|
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
|
-
}
|
|
4947
|
+
result += this.capture(() => this.visit(child)).join("");
|
|
3784
4948
|
}
|
|
3785
4949
|
}
|
|
3786
4950
|
return result.trim();
|
|
@@ -3789,142 +4953,145 @@ class Printer extends Visitor {
|
|
|
3789
4953
|
* Try to render children inline if they are simple enough.
|
|
3790
4954
|
* Returns the inline string if possible, null otherwise.
|
|
3791
4955
|
*/
|
|
3792
|
-
tryRenderInline(children, tagName
|
|
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
|
-
}
|
|
4956
|
+
tryRenderInline(children, tagName) {
|
|
3808
4957
|
for (const child of children) {
|
|
3809
|
-
if (child
|
|
3810
|
-
|
|
3811
|
-
if (textContent.includes('\n')) {
|
|
4958
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4959
|
+
if (child.content.includes('\n')) {
|
|
3812
4960
|
return null;
|
|
3813
4961
|
}
|
|
3814
4962
|
}
|
|
3815
|
-
else if (child
|
|
3816
|
-
const
|
|
3817
|
-
const openTag = element.open_tag;
|
|
3818
|
-
const elementTagName = openTag?.tag_name?.value || '';
|
|
3819
|
-
const isInlineElement = this.isInlineElement(elementTagName);
|
|
4963
|
+
else if (isNode(child, HTMLElementNode)) {
|
|
4964
|
+
const isInlineElement = this.isInlineElement(getTagName(child));
|
|
3820
4965
|
if (!isInlineElement) {
|
|
3821
4966
|
return null;
|
|
3822
4967
|
}
|
|
3823
4968
|
}
|
|
3824
|
-
else if (child
|
|
4969
|
+
else if (isNode(child, ERBContentNode)) ;
|
|
3825
4970
|
else {
|
|
3826
4971
|
return null;
|
|
3827
4972
|
}
|
|
3828
4973
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
4974
|
+
let content = "";
|
|
4975
|
+
this.capture(() => {
|
|
4976
|
+
content = this.renderChildrenInline(children);
|
|
4977
|
+
});
|
|
4978
|
+
return `<${tagName}>${content}</${tagName}>`;
|
|
4979
|
+
}
|
|
4980
|
+
/**
|
|
4981
|
+
* Check if children contain mixed text and inline elements (like "text<em>inline</em>text")
|
|
4982
|
+
* or mixed ERB output and text (like "<%= value %> text")
|
|
4983
|
+
* This indicates content that should be formatted inline even with structural newlines
|
|
4984
|
+
*/
|
|
4985
|
+
hasMixedTextAndInlineContent(children) {
|
|
4986
|
+
let hasText = false;
|
|
4987
|
+
let hasInlineElements = false;
|
|
4988
|
+
for (const child of children) {
|
|
4989
|
+
if (isNode(child, HTMLTextNode)) {
|
|
4990
|
+
if (child.content.trim() !== "") {
|
|
4991
|
+
hasText = true;
|
|
3847
4992
|
}
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
const close = erbNode.tag_closing?.value ?? "";
|
|
3853
|
-
content += `${open}${this.formatERBContent(erbContent)}${close}`;
|
|
4993
|
+
}
|
|
4994
|
+
else if (isNode(child, HTMLElementNode)) {
|
|
4995
|
+
if (this.isInlineElement(getTagName(child))) {
|
|
4996
|
+
hasInlineElements = true;
|
|
3854
4997
|
}
|
|
3855
4998
|
}
|
|
3856
|
-
content = content.replace(/\s+/g, ' ').trim();
|
|
3857
|
-
return `<${tagName}>${content}</${tagName}>`;
|
|
3858
|
-
}
|
|
3859
|
-
finally {
|
|
3860
|
-
this.lines = oldLines;
|
|
3861
|
-
this.inlineMode = oldInlineMode;
|
|
3862
4999
|
}
|
|
5000
|
+
return (hasText && hasInlineElements) || (hasERBOutput(children) && hasText);
|
|
3863
5001
|
}
|
|
3864
5002
|
/**
|
|
3865
|
-
*
|
|
5003
|
+
* Check if children contain any text content with newlines
|
|
3866
5004
|
*/
|
|
3867
|
-
|
|
3868
|
-
let length = 0;
|
|
5005
|
+
hasMultilineTextContent(children) {
|
|
3869
5006
|
for (const child of children) {
|
|
3870
|
-
if (child
|
|
3871
|
-
|
|
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);
|
|
5007
|
+
if (isNode(child, HTMLTextNode)) {
|
|
5008
|
+
return child.content.includes('\n');
|
|
3879
5009
|
}
|
|
3880
|
-
|
|
3881
|
-
|
|
5010
|
+
if (isNode(child, HTMLElementNode)) {
|
|
5011
|
+
const nestedChildren = this.filterEmptyNodes(child.body);
|
|
5012
|
+
if (this.hasMultilineTextContent(nestedChildren)) {
|
|
5013
|
+
return true;
|
|
5014
|
+
}
|
|
3882
5015
|
}
|
|
3883
5016
|
}
|
|
3884
|
-
return
|
|
5017
|
+
return false;
|
|
3885
5018
|
}
|
|
3886
5019
|
/**
|
|
3887
|
-
*
|
|
5020
|
+
* Check if all nested elements in the children are inline elements
|
|
3888
5021
|
*/
|
|
3889
|
-
|
|
3890
|
-
let maxDepth = currentDepth;
|
|
5022
|
+
areAllNestedElementsInline(children) {
|
|
3891
5023
|
for (const child of children) {
|
|
3892
|
-
if (child
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
const
|
|
3897
|
-
|
|
5024
|
+
if (isNode(child, HTMLElementNode)) {
|
|
5025
|
+
if (!this.isInlineElement(getTagName(child))) {
|
|
5026
|
+
return false;
|
|
5027
|
+
}
|
|
5028
|
+
const nestedChildren = this.filterEmptyNodes(child.body);
|
|
5029
|
+
if (!this.areAllNestedElementsInline(nestedChildren)) {
|
|
5030
|
+
return false;
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
else if (isAnyOf(child, HTMLDoctypeNode, HTMLCommentNode, isERBControlFlowNode)) {
|
|
5034
|
+
return false;
|
|
3898
5035
|
}
|
|
3899
5036
|
}
|
|
3900
|
-
return
|
|
5037
|
+
return true;
|
|
5038
|
+
}
|
|
5039
|
+
/**
|
|
5040
|
+
* Check if element has complex ERB control flow
|
|
5041
|
+
*/
|
|
5042
|
+
hasComplexERBControlFlow(inlineNodes) {
|
|
5043
|
+
return inlineNodes.some(node => {
|
|
5044
|
+
if (isNode(node, ERBIfNode)) {
|
|
5045
|
+
if (node.statements.length > 0 && node.location) {
|
|
5046
|
+
const startLine = node.location.start.line;
|
|
5047
|
+
const endLine = node.location.end.line;
|
|
5048
|
+
return startLine !== endLine;
|
|
5049
|
+
}
|
|
5050
|
+
return false;
|
|
5051
|
+
}
|
|
5052
|
+
return false;
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
/**
|
|
5056
|
+
* Filter children to remove insignificant whitespace
|
|
5057
|
+
*/
|
|
5058
|
+
filterSignificantChildren(body, hasTextFlow) {
|
|
5059
|
+
return body.filter(child => {
|
|
5060
|
+
if (isNode(child, WhitespaceNode))
|
|
5061
|
+
return false;
|
|
5062
|
+
if (isNode(child, HTMLTextNode)) {
|
|
5063
|
+
if (hasTextFlow && child.content === " ")
|
|
5064
|
+
return true;
|
|
5065
|
+
return child.content.trim() !== "";
|
|
5066
|
+
}
|
|
5067
|
+
return true;
|
|
5068
|
+
});
|
|
3901
5069
|
}
|
|
3902
5070
|
/**
|
|
3903
|
-
*
|
|
5071
|
+
* Filter out empty text nodes and whitespace nodes
|
|
3904
5072
|
*/
|
|
5073
|
+
filterEmptyNodes(nodes) {
|
|
5074
|
+
return nodes.filter(child => !isNode(child, WhitespaceNode) && !(isNode(child, HTMLTextNode) && child.content.trim() === ""));
|
|
5075
|
+
}
|
|
3905
5076
|
renderElementInline(element) {
|
|
3906
|
-
const children = element.body
|
|
3907
|
-
|
|
5077
|
+
const children = this.filterEmptyNodes(element.body);
|
|
5078
|
+
return this.renderChildrenInline(children);
|
|
5079
|
+
}
|
|
5080
|
+
renderChildrenInline(children) {
|
|
3908
5081
|
let content = '';
|
|
3909
5082
|
for (const child of children) {
|
|
3910
|
-
if (child
|
|
5083
|
+
if (isNode(child, HTMLTextNode)) {
|
|
3911
5084
|
content += child.content;
|
|
3912
5085
|
}
|
|
3913
|
-
else if (child
|
|
3914
|
-
const
|
|
3915
|
-
const
|
|
3916
|
-
const childTagName = openTag?.tag_name?.value || '';
|
|
3917
|
-
const attributes = this.extractAttributes(openTag.children);
|
|
5086
|
+
else if (isNode(child, HTMLElementNode)) {
|
|
5087
|
+
const tagName = getTagName(child);
|
|
5088
|
+
const attributes = filterNodes(child.open_tag?.children, HTMLAttributeNode);
|
|
3918
5089
|
const attributesString = this.renderAttributesString(attributes);
|
|
3919
|
-
const childContent = this.renderElementInline(
|
|
3920
|
-
content += `<${
|
|
5090
|
+
const childContent = this.renderElementInline(child);
|
|
5091
|
+
content += `<${tagName}${attributesString}>${childContent}</${tagName}>`;
|
|
3921
5092
|
}
|
|
3922
|
-
else if (child
|
|
3923
|
-
|
|
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}`;
|
|
5093
|
+
else if (isNode(child, ERBContentNode)) {
|
|
5094
|
+
content += this.reconstructERBNode(child, true);
|
|
3928
5095
|
}
|
|
3929
5096
|
}
|
|
3930
5097
|
return content.replace(/\s+/g, ' ').trim();
|
|
@@ -3969,7 +5136,7 @@ class Formatter {
|
|
|
3969
5136
|
if (result.failed)
|
|
3970
5137
|
return source;
|
|
3971
5138
|
const resolvedOptions = resolveFormatOptions({ ...this.options, ...options });
|
|
3972
|
-
return new
|
|
5139
|
+
return new FormatPrinter(source, resolvedOptions).print(result.value);
|
|
3973
5140
|
}
|
|
3974
5141
|
parse(source) {
|
|
3975
5142
|
this.herb.ensureBackend();
|
|
@@ -3977,6 +5144,7 @@ class Formatter {
|
|
|
3977
5144
|
}
|
|
3978
5145
|
}
|
|
3979
5146
|
|
|
5147
|
+
exports.FormatPrinter = FormatPrinter;
|
|
3980
5148
|
exports.Formatter = Formatter;
|
|
3981
5149
|
exports.defaultFormatOptions = defaultFormatOptions;
|
|
3982
5150
|
exports.resolveFormatOptions = resolveFormatOptions;
|