@herb-tools/formatter 0.5.0 → 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/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-0.5.0/templates/javascript/packages/core/src/errors.ts.erb
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-0.5.0/templates/javascript/packages/core/src/nodes.ts.erb
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
- name;
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
- name: data.name ? Token.from(data.name) : null,
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.name = props.name;
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
- name: this.name ? this.name.toJSON() : null,
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 += `└── name: ${this.name ? this.name.treeInspect() : ""}\n`;
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
- // output += "\n";
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
- // NOTE: This file is generated by the templates/template.rb script and should not
2579
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.5.0/templates/javascript/packages/core/src/visitor.ts.erb
2580
- class Visitor {
2581
- visit(node) {
2582
- if (!node)
2583
- return;
2584
- node.accept(this);
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
- visitHTMLOpenTagNode(node) {
2599
- this.visitChildNodes(node);
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
- visitHTMLCloseTagNode(node) {
2602
- this.visitChildNodes(node);
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
- visitHTMLSelfCloseTagNode(node) {
2605
- this.visitChildNodes(node);
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
- visitHTMLElementNode(node) {
2608
- this.visitChildNodes(node);
2637
+ constructor(message, location) {
2638
+ this.message = message;
2639
+ this.location = location;
2609
2640
  }
2610
- visitHTMLAttributeValueNode(node) {
2611
- this.visitChildNodes(node);
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
- visitHTMLAttributeNameNode(node) {
2614
- this.visitChildNodes(node);
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
- visitHTMLAttributeNode(node) {
2617
- this.visitChildNodes(node);
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
- visitHTMLTextNode(node) {
2620
- this.visitChildNodes(node);
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
- visitHTMLCommentNode(node) {
2623
- this.visitChildNodes(node);
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
- visitHTMLDoctypeNode(node) {
2626
- this.visitChildNodes(node);
2691
+ recursiveErrors() {
2692
+ return [...this.errors, ...this.value.recursiveErrors()];
2627
2693
  }
2628
- visitWhitespaceNode(node) {
2629
- this.visitChildNodes(node);
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
- visitERBContentNode(node) {
2632
- this.visitChildNodes(node);
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
- visitERBEndNode(node) {
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) {
@@ -2671,14 +3249,441 @@ class Visitor {
2671
3249
  this.visitChildNodes(node);
2672
3250
  }
2673
3251
  visitERBUnlessNode(node) {
2674
- this.visitChildNodes(node);
3252
+ this.visitChildNodes(node);
3253
+ }
3254
+ visitERBYieldNode(node) {
3255
+ this.visitChildNodes(node);
3256
+ }
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
+ }
2675
3633
  }
2676
3634
  visitERBYieldNode(node) {
2677
- this.visitChildNodes(node);
3635
+ this.printERBNode(node);
2678
3636
  }
2679
3637
  visitERBInNode(node) {
2680
- this.visitChildNodes(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
 
2684
3689
  // TODO: we can probably expand this list with more tags/attributes
@@ -2690,43 +3695,132 @@ const FORMATTABLE_ATTRIBUTES = {
2690
3695
  * Printer traverses the Herb AST using the Visitor pattern
2691
3696
  * and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
2692
3697
  */
2693
- class Printer extends Visitor {
3698
+ class FormatPrinter extends Printer {
3699
+ /**
3700
+ * @deprecated integrate indentWidth into this.options and update FormatOptions to extend from @herb-tools/printer options
3701
+ */
2694
3702
  indentWidth;
3703
+ /**
3704
+ * @deprecated integrate maxLineLength into this.options and update FormatOptions to extend from @herb-tools/printer options
3705
+ */
2695
3706
  maxLineLength;
2696
- source;
3707
+ /**
3708
+ * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
3709
+ */
2697
3710
  lines = [];
2698
3711
  indentLevel = 0;
2699
3712
  inlineMode = false;
2700
- isInComplexNesting = false;
2701
- currentTagName = "";
3713
+ currentAttributeName = null;
3714
+ elementStack = [];
3715
+ elementFormattingAnalysis = new Map();
3716
+ source;
3717
+ // TODO: extract
2702
3718
  static INLINE_ELEMENTS = new Set([
2703
3719
  'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'cite', 'code',
2704
3720
  'dfn', 'em', 'i', 'img', 'kbd', 'label', 'map', 'object', 'q',
2705
3721
  'samp', 'small', 'span', 'strong', 'sub', 'sup',
2706
3722
  'tt', 'var', 'del', 'ins', 'mark', 's', 'u', 'time', 'wbr'
2707
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;
2708
3735
  constructor(source, options) {
2709
3736
  super();
2710
3737
  this.source = source;
2711
3738
  this.indentWidth = options.indentWidth;
2712
3739
  this.maxLineLength = options.maxLineLength;
2713
3740
  }
2714
- print(object, indentLevel = 0) {
2715
- if (object instanceof Token || object.type?.startsWith('TOKEN_')) {
2716
- return object.value;
2717
- }
2718
- const node = object;
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)
2719
3746
  this.lines = [];
2720
- this.indentLevel = indentLevel;
2721
- this.isInComplexNesting = false; // Reset for each top-level element
2722
- if (typeof node.accept === 'function') {
2723
- node.accept(this);
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;
2724
3769
  }
2725
3770
  else {
2726
- this.visit(node);
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;
2727
3819
  }
2728
- return this.lines.join("\n");
2729
3820
  }
3821
+ /**
3822
+ * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
3823
+ */
2730
3824
  push(line) {
2731
3825
  this.lines.push(line);
2732
3826
  }
@@ -2736,7 +3830,7 @@ class Printer extends Visitor {
2736
3830
  this.indentLevel--;
2737
3831
  return result;
2738
3832
  }
2739
- indent() {
3833
+ get indent() {
2740
3834
  return " ".repeat(this.indentLevel * this.indentWidth);
2741
3835
  }
2742
3836
  /**
@@ -2746,45 +3840,128 @@ class Printer extends Visitor {
2746
3840
  formatERBContent(content) {
2747
3841
  return content.trim() ? ` ${content.trim()} ` : "";
2748
3842
  }
2749
- /**
2750
- * Check if a node is an ERB control flow node (if, unless, block, case, while, for)
2751
- */
2752
- isERBControlFlow(node) {
2753
- return node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE' ||
2754
- node instanceof ERBUnlessNode || node.type === 'AST_ERB_UNLESS_NODE' ||
2755
- node instanceof ERBBlockNode || node.type === 'AST_ERB_BLOCK_NODE' ||
2756
- node instanceof ERBCaseNode || node.type === 'AST_ERB_CASE_NODE' ||
2757
- node instanceof ERBCaseMatchNode || node.type === 'AST_ERB_CASE_MATCH_NODE' ||
2758
- node instanceof ERBWhileNode || node.type === 'AST_ERB_WHILE_NODE' ||
2759
- node instanceof ERBForNode || node.type === 'AST_ERB_FOR_NODE';
2760
- }
2761
3843
  /**
2762
3844
  * Count total attributes including those inside ERB conditionals
2763
3845
  */
2764
3846
  getTotalAttributeCount(attributes, inlineNodes = []) {
2765
3847
  let totalAttributeCount = attributes.length;
2766
3848
  inlineNodes.forEach(node => {
2767
- if (this.isERBControlFlow(node)) {
2768
- const erbNode = node;
2769
- if (erbNode.statements) {
2770
- totalAttributeCount += erbNode.statements.length;
2771
- }
3849
+ if (isERBControlFlowNode(node)) {
3850
+ const capturedNodes = this.captureNodes(() => this.visit(node));
3851
+ const attributeNodes = filterNodes(capturedNodes, HTMLAttributeNode);
3852
+ totalAttributeCount += attributeNodes.length;
2772
3853
  }
2773
3854
  });
2774
3855
  return totalAttributeCount;
2775
3856
  }
2776
3857
  /**
2777
- * Extract HTML attributes from a list of nodes
3858
+ * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
2778
3859
  */
2779
- extractAttributes(nodes) {
2780
- return nodes.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3860
+ extractInlineNodes(nodes) {
3861
+ return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode));
2781
3862
  }
2782
3863
  /**
2783
- * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
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
2784
3877
  */
2785
- extractInlineNodes(nodes) {
2786
- return nodes.filter(child => !(child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') &&
2787
- !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE'));
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;
3927
+ }
3928
+ /**
3929
+ * Token list attributes that contain space-separated values and benefit from
3930
+ * spacing around ERB content for readability
3931
+ */
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;
2788
3965
  }
2789
3966
  /**
2790
3967
  * Render attributes as a space-separated string
@@ -2792,39 +3969,79 @@ class Printer extends Visitor {
2792
3969
  renderAttributesString(attributes) {
2793
3970
  if (attributes.length === 0)
2794
3971
  return "";
2795
- return ` ${attributes.map(attr => this.renderAttribute(attr)).join(" ")}`;
3972
+ return ` ${attributes.map(attribute => this.renderAttribute(attribute)).join(" ")}`;
2796
3973
  }
2797
3974
  /**
2798
3975
  * Determine if a tag should be rendered inline based on attribute count and other factors
2799
3976
  */
2800
- shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false, _nestingDepth = 0, _inlineNodesLength = 0, hasMultilineAttributes = false) {
3977
+ shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false, hasMultilineAttributes = false, attributes = []) {
2801
3978
  if (hasComplexERB || hasMultilineAttributes)
2802
3979
  return false;
2803
3980
  if (totalAttributeCount === 0) {
2804
3981
  return inlineLength + indentLength <= maxLineLength;
2805
3982
  }
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
+ }
2806
3997
  if (totalAttributeCount > 3 || inlineLength + indentLength > maxLineLength) {
2807
3998
  return false;
2808
3999
  }
2809
4000
  return true;
2810
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
+ }
2811
4038
  hasMultilineAttributes(attributes) {
2812
4039
  return attributes.some(attribute => {
2813
- if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || attribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
2814
- const attributeValue = attribute.value;
2815
- const content = attributeValue.children.map((child) => {
2816
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE' || child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
2817
- return child.content;
2818
- }
2819
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
2820
- const erbAttribute = child;
2821
- return erbAttribute.tag_opening.value + erbAttribute.content.value + erbAttribute.tag_closing.value;
2822
- }
2823
- return "";
2824
- }).join("");
4040
+ if (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
4041
+ const content = getCombinedStringFromNodes(attribute.value.children);
2825
4042
  if (/\r?\n/.test(content)) {
2826
- const name = attribute.name.name.value ?? "";
2827
- if (name === 'class') {
4043
+ const name = attribute.name ? getCombinedAttributeName(attribute.name) : "";
4044
+ if (name === "class") {
2828
4045
  const normalizedContent = content.replace(/\s+/g, ' ').trim();
2829
4046
  return normalizedContent.length > 80;
2830
4047
  }
@@ -2849,6 +4066,9 @@ class Printer extends Visitor {
2849
4066
  const currentIndent = this.indentLevel * this.indentWidth;
2850
4067
  const attributeLine = `${name}${equals}${open_quote}${normalizedContent}${close_quote}`;
2851
4068
  if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
4069
+ if (/<%[^%]*%>/.test(normalizedContent)) {
4070
+ return open_quote + normalizedContent + close_quote;
4071
+ }
2852
4072
  const classes = normalizedContent.split(' ');
2853
4073
  const lines = this.breakTokensIntoLines(classes, currentIndent);
2854
4074
  if (lines.length > 1) {
@@ -2862,7 +4082,7 @@ class Printer extends Visitor {
2862
4082
  const tagSpecificFormattable = FORMATTABLE_ATTRIBUTES[tagName.toLowerCase()] || [];
2863
4083
  return globalFormattable.includes(attributeName) || tagSpecificFormattable.includes(attributeName);
2864
4084
  }
2865
- formatMultilineAttribute(content, name, equals, open_quote, close_quote) {
4085
+ formatMultilineAttribute(content, name, open_quote, close_quote) {
2866
4086
  if (name === 'srcset' || name === 'sizes') {
2867
4087
  const normalizedContent = content.replace(/\s+/g, ' ').trim();
2868
4088
  return open_quote + normalizedContent + close_quote;
@@ -2904,42 +4124,43 @@ class Printer extends Visitor {
2904
4124
  /**
2905
4125
  * Render multiline attributes for a tag
2906
4126
  */
2907
- renderMultilineAttributes(tagName, _attributes, _inlineNodes = [], allChildren = [], isSelfClosing = false, isVoid = false, hasBodyContent = false) {
2908
- const indent = this.indent();
2909
- this.push(indent + `<${tagName}`);
4127
+ renderMultilineAttributes(tagName, allChildren = [], isSelfClosing = false) {
4128
+ this.push(this.indent + `<${tagName}`);
2910
4129
  this.withIndent(() => {
2911
4130
  allChildren.forEach(child => {
2912
- if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
2913
- this.push(this.indent() + this.renderAttribute(child));
4131
+ if (isNode(child, HTMLAttributeNode)) {
4132
+ this.push(this.indent + this.renderAttribute(child));
2914
4133
  }
2915
- else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
4134
+ else if (!isNode(child, WhitespaceNode)) {
2916
4135
  this.visit(child);
2917
4136
  }
2918
4137
  });
2919
4138
  });
2920
4139
  if (isSelfClosing) {
2921
- this.push(indent + "/>");
2922
- }
2923
- else if (isVoid) {
2924
- this.push(indent + ">");
2925
- }
2926
- else if (!hasBodyContent) {
2927
- this.push(indent + `></${tagName}>`);
4140
+ this.push(this.indent + "/>");
2928
4141
  }
2929
4142
  else {
2930
- this.push(indent + ">");
4143
+ this.push(this.indent + ">");
2931
4144
  }
2932
4145
  }
2933
4146
  /**
2934
- * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
4147
+ * Reconstruct the text representation of an ERB node
4148
+ * @param withFormatting - if true, format the content; if false, preserve original
2935
4149
  */
2936
- printERBNode(node) {
2937
- const indent = this.inlineMode ? "" : this.indent();
4150
+ reconstructERBNode(node, withFormatting = true) {
2938
4151
  const open = node.tag_opening?.value ?? "";
2939
4152
  const close = node.tag_closing?.value ?? "";
2940
4153
  const content = node.content?.value ?? "";
2941
- const inner = this.formatERBContent(content);
2942
- this.push(indent + open + inner + close);
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);
2943
4164
  }
2944
4165
  // --- Visitor methods ---
2945
4166
  visitDocumentNode(node) {
@@ -2947,14 +4168,13 @@ class Printer extends Visitor {
2947
4168
  let hasHandledSpacing = false;
2948
4169
  for (let i = 0; i < node.children.length; i++) {
2949
4170
  const child = node.children[i];
2950
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2951
- const textNode = child;
2952
- const isWhitespaceOnly = textNode.content.trim() === "";
4171
+ if (isNode(child, HTMLTextNode)) {
4172
+ const isWhitespaceOnly = child.content.trim() === "";
2953
4173
  if (isWhitespaceOnly) {
2954
- const hasPrevNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1]);
4174
+ const hasPreviousNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1]);
2955
4175
  const hasNextNonWhitespace = i < node.children.length - 1 && this.isNonWhitespaceNode(node.children[i + 1]);
2956
- const hasMultipleNewlines = textNode.content.includes('\n\n');
2957
- if (hasPrevNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
4176
+ const hasMultipleNewlines = child.content.includes('\n\n');
4177
+ if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
2958
4178
  this.push("");
2959
4179
  hasHandledSpacing = true;
2960
4180
  }
@@ -2972,332 +4192,145 @@ class Printer extends Visitor {
2972
4192
  }
2973
4193
  }
2974
4194
  visitHTMLElementNode(node) {
2975
- const open = node.open_tag;
2976
- const tagName = open.tag_name?.value ?? "";
2977
- const indent = this.indent();
2978
- this.currentTagName = tagName;
2979
- const attributes = this.extractAttributes(open.children);
2980
- const inlineNodes = this.extractInlineNodes(open.children);
2981
- const hasTextFlow = this.isInTextFlowContext(null, node.body);
2982
- const children = node.body.filter(child => {
2983
- if (child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') {
2984
- return false;
2985
- }
2986
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2987
- const content = child.content;
2988
- if (hasTextFlow && content === " ") {
2989
- return true;
2990
- }
2991
- return content.trim() !== "";
2992
- }
2993
- return true;
2994
- });
2995
- const isInlineElement = this.isInlineElement(tagName);
2996
- const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
2997
- const isSelfClosing = open.tag_closing?.value === "/>";
2998
- if (!hasClosing) {
2999
- this.push(indent + `<${tagName}`);
3000
- 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);
3001
4200
  }
3002
- if (attributes.length === 0 && inlineNodes.length === 0) {
3003
- if (children.length === 0) {
3004
- if (isSelfClosing) {
3005
- this.push(indent + `<${tagName} />`);
3006
- }
3007
- else if (node.is_void) {
3008
- this.push(indent + `<${tagName}>`);
3009
- }
3010
- else {
3011
- this.push(indent + `<${tagName}></${tagName}>`);
3012
- }
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)
3013
4212
  return;
3014
- }
3015
- if (children.length >= 1) {
3016
- if (this.isInComplexNesting) {
3017
- if (children.length === 1) {
3018
- const child = children[0];
3019
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3020
- const textContent = child.content.trim();
3021
- const singleLine = `<${tagName}>${textContent}</${tagName}>`;
3022
- if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
3023
- this.push(indent + singleLine);
3024
- return;
3025
- }
3026
- }
3027
- }
3028
- }
3029
- else {
3030
- const inlineResult = this.tryRenderInline(children, tagName, 0, false, hasTextFlow);
3031
- if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
3032
- this.push(indent + inlineResult);
3033
- return;
3034
- }
3035
- if (hasTextFlow) {
3036
- const hasAnyNewlines = children.some(child => {
3037
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3038
- 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);
3039
4223
  }
3040
- return false;
3041
- });
3042
- if (!hasAnyNewlines) {
3043
- const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children);
3044
- if (fullInlineResult) {
3045
- const totalLength = indent.length + fullInlineResult.length;
3046
- const maxNesting = this.getMaxNestingDepth(children, 0);
3047
- const maxInlineLength = maxNesting <= 1 ? this.maxLineLength : 60;
3048
- if (totalLength <= maxInlineLength) {
3049
- this.push(indent + fullInlineResult);
3050
- return;
3051
- }
4224
+ else if (normalizedContent === ' ') {
4225
+ this.push(' ');
3052
4226
  }
3053
4227
  }
3054
- }
3055
- }
3056
- }
3057
- if (hasTextFlow) {
3058
- const fullInlineResult = this.tryRenderInlineFull(node, tagName, [], children);
3059
- if (fullInlineResult) {
3060
- const totalLength = indent.length + fullInlineResult.length;
3061
- const maxNesting = this.getMaxNestingDepth(children, 0);
3062
- const maxInlineLength = maxNesting <= 1 ? this.maxLineLength : 60;
3063
- if (totalLength <= maxInlineLength) {
3064
- this.push(indent + fullInlineResult);
3065
- return;
3066
- }
3067
- }
3068
- }
3069
- this.push(indent + `<${tagName}>`);
3070
- this.withIndent(() => {
3071
- if (hasTextFlow) {
3072
- this.visitTextFlowChildren(children);
3073
- }
3074
- else {
3075
- children.forEach(child => this.visit(child));
3076
- }
3077
- });
3078
- if (!node.is_void && !isSelfClosing) {
3079
- this.push(indent + `</${tagName}>`);
3080
- }
3081
- return;
3082
- }
3083
- if (attributes.length === 0 && inlineNodes.length > 0) {
3084
- const inline = this.renderInlineOpen(tagName, [], isSelfClosing, inlineNodes, open.children);
3085
- if (children.length === 0) {
3086
- if (isSelfClosing || node.is_void) {
3087
- this.push(indent + inline);
3088
- }
3089
- else {
3090
- this.push(indent + inline + `</${tagName}>`);
3091
- }
3092
- return;
3093
- }
3094
- this.push(indent + inline);
3095
- this.withIndent(() => {
3096
- children.forEach(child => this.visit(child));
3097
- });
3098
- if (!node.is_void && !isSelfClosing) {
3099
- this.push(indent + `</${tagName}>`);
3100
- }
3101
- return;
3102
- }
3103
- const hasERBControlFlow = inlineNodes.some(node => this.isERBControlFlow(node)) ||
3104
- open.children.some(node => this.isERBControlFlow(node));
3105
- const hasComplexERB = hasERBControlFlow && inlineNodes.some(node => {
3106
- if (node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE') {
3107
- const erbNode = node;
3108
- if (erbNode.statements.length > 0 && erbNode.location) {
3109
- const startLine = erbNode.location.start.line;
3110
- const endLine = erbNode.location.end.line;
3111
- return startLine !== endLine;
3112
- }
3113
- return false;
3114
- }
3115
- return false;
3116
- });
3117
- const inline = hasComplexERB ? "" : this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
3118
- const nestingDepth = this.getMaxNestingDepth(children, 0);
3119
- const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3120
- const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, hasComplexERB, nestingDepth, inlineNodes.length, this.hasMultilineAttributes(attributes));
3121
- if (shouldKeepInline) {
3122
- if (children.length === 0) {
3123
- if (isSelfClosing) {
3124
- this.push(indent + inline);
3125
- }
3126
- else if (node.is_void) {
3127
- this.push(indent + inline);
3128
- }
3129
- else {
3130
- let result = `<${tagName}`;
3131
- result += this.renderAttributesString(attributes);
3132
- if (inlineNodes.length > 0) {
3133
- const currentIndentLevel = this.indentLevel;
3134
- this.indentLevel = 0;
3135
- const tempLines = this.lines;
3136
- this.lines = [];
3137
- inlineNodes.forEach(node => {
3138
- const wasInlineMode = this.inlineMode;
3139
- if (!this.isERBControlFlow(node)) {
3140
- this.inlineMode = true;
4228
+ else {
4229
+ const normalizedContent = child.content.replace(/\s+/g, ' ').trim();
4230
+ if (normalizedContent) {
4231
+ this.push(normalizedContent);
3141
4232
  }
3142
- this.visit(node);
3143
- this.inlineMode = wasInlineMode;
3144
- });
3145
- const inlineContent = this.lines.join("");
3146
- this.lines = tempLines;
3147
- this.indentLevel = currentIndentLevel;
3148
- result += inlineContent;
4233
+ }
3149
4234
  }
3150
- result += `></${tagName}>`;
3151
- this.push(indent + result);
3152
- }
3153
- return;
3154
- }
3155
- if (isInlineElement && children.length > 0 && !hasERBControlFlow) {
3156
- const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children);
3157
- if (fullInlineResult) {
3158
- const totalLength = indent.length + fullInlineResult.length;
3159
- if (totalLength <= this.maxLineLength || totalLength <= 120) {
3160
- this.push(indent + fullInlineResult);
4235
+ else if (isNode(child, WhitespaceNode)) {
3161
4236
  return;
3162
4237
  }
3163
- }
3164
- }
3165
- if (!isInlineElement && children.length > 0 && !hasERBControlFlow) {
3166
- this.push(indent + inline);
3167
- this.withIndent(() => {
3168
- if (hasTextFlow) {
3169
- this.visitTextFlowChildren(children);
3170
- }
3171
4238
  else {
3172
- children.forEach(child => this.visit(child));
4239
+ this.visit(child);
3173
4240
  }
3174
4241
  });
3175
- if (!node.is_void && !isSelfClosing) {
3176
- this.push(indent + `</${tagName}>`);
3177
- }
3178
- return;
3179
- }
3180
- if (isSelfClosing) {
3181
- this.push(indent + inline.replace(' />', '>'));
3182
- }
3183
- else {
3184
- this.push(indent + inline);
3185
- }
3186
- this.withIndent(() => {
3187
- if (hasTextFlow) {
3188
- this.visitTextFlowChildren(children);
3189
- }
3190
- else {
3191
- children.forEach(child => this.visit(child));
3192
- }
3193
4242
  });
3194
- if (!node.is_void && !isSelfClosing) {
3195
- this.push(indent + `</${tagName}>`);
4243
+ const content = lines.join('');
4244
+ const inlineContent = hasTextFlow ? content.replace(/\s+/g, ' ').trim() : content.trim();
4245
+ if (inlineContent) {
4246
+ this.pushToLastLine(inlineContent);
3196
4247
  }
4248
+ this.inlineMode = oldInlineMode;
3197
4249
  return;
3198
4250
  }
3199
- if (inlineNodes.length > 0 && hasERBControlFlow) {
3200
- this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
3201
- if (!isSelfClosing && !node.is_void && children.length > 0) {
3202
- this.withIndent(() => {
3203
- children.forEach(child => this.visit(child));
3204
- });
3205
- this.push(indent + `</${tagName}>`);
4251
+ if (children.length === 0)
4252
+ return;
4253
+ this.withIndent(() => {
4254
+ if (hasTextFlow) {
4255
+ this.visitTextFlowChildren(children);
3206
4256
  }
3207
- }
3208
- else if (inlineNodes.length > 0) {
3209
- this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
3210
- if (!isSelfClosing && !node.is_void && children.length > 0) {
3211
- this.withIndent(() => {
3212
- children.forEach(child => this.visit(child));
3213
- });
3214
- this.push(indent + `</${tagName}>`);
4257
+ else {
4258
+ this.visitElementChildren(body, element);
3215
4259
  }
3216
- }
3217
- else {
3218
- if (isInlineElement && children.length > 0) {
3219
- const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children);
3220
- if (fullInlineResult) {
3221
- const totalLength = indent.length + fullInlineResult.length;
3222
- if (totalLength <= this.maxLineLength || totalLength <= 120) {
3223
- this.push(indent + fullInlineResult);
3224
- return;
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;
3225
4279
  }
4280
+ continue;
3226
4281
  }
3227
4282
  }
3228
- if (isInlineElement && children.length === 0) {
3229
- const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
3230
- const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3231
- const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, false, 0, inlineNodes.length, this.hasMultilineAttributes(attributes));
3232
- if (shouldKeepInline) {
3233
- let result = `<${tagName}`;
3234
- result += this.renderAttributesString(attributes);
3235
- if (isSelfClosing) {
3236
- result += " />";
3237
- }
3238
- else if (node.is_void) {
3239
- result += ">";
3240
- }
3241
- else {
3242
- result += `></${tagName}>`;
3243
- }
3244
- this.push(indent + result);
3245
- return;
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("");
3246
4289
  }
3247
4290
  }
3248
- this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0);
3249
- if (!isSelfClosing && !node.is_void && children.length > 0) {
3250
- this.withIndent(() => {
3251
- if (hasTextFlow) {
3252
- this.visitTextFlowChildren(children);
3253
- }
3254
- else {
3255
- children.forEach(child => this.visit(child));
3256
- }
3257
- });
3258
- this.push(indent + `</${tagName}>`);
4291
+ this.visit(child);
4292
+ if (this.isNonWhitespaceNode(child)) {
4293
+ lastWasMeaningful = true;
4294
+ hasHandledSpacing = false;
3259
4295
  }
3260
4296
  }
3261
4297
  }
3262
4298
  visitHTMLOpenTagNode(node) {
3263
- const tagName = node.tag_name?.value ?? "";
3264
- const indent = this.indent();
3265
- const attributes = this.extractAttributes(node.children);
4299
+ const attributes = filterNodes(node.children, HTMLAttributeNode);
3266
4300
  const inlineNodes = this.extractInlineNodes(node.children);
3267
- const hasClosing = node.tag_closing?.value === ">";
3268
- if (!hasClosing) {
3269
- this.push(indent + `<${tagName}`);
3270
- return;
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
+ }
3271
4313
  }
3272
- const inline = this.renderInlineOpen(tagName, attributes, node.is_void, inlineNodes, node.children);
4314
+ const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children);
3273
4315
  const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3274
- const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, false, 0, inlineNodes.length, this.hasMultilineAttributes(attributes));
4316
+ const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, this.indent.length, this.maxLineLength, false, this.hasMultilineAttributes(attributes), attributes);
3275
4317
  if (shouldKeepInline) {
3276
- this.push(indent + inline);
3277
- return;
4318
+ this.push(this.inlineMode ? inline : this.indent + inline);
3278
4319
  }
3279
- this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.children, false, node.is_void, false);
3280
- }
3281
- visitHTMLSelfCloseTagNode(node) {
3282
- const tagName = node.tag_name?.value ?? "";
3283
- const indent = this.indent();
3284
- const attributes = this.extractAttributes(node.attributes);
3285
- const inlineNodes = this.extractInlineNodes(node.attributes);
3286
- const inline = this.renderInlineOpen(tagName, attributes, true, inlineNodes, node.attributes);
3287
- const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
3288
- const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, indent.length, this.maxLineLength, false, 0, inlineNodes.length, this.hasMultilineAttributes(attributes));
3289
- if (shouldKeepInline) {
3290
- this.push(indent + inline);
3291
- return;
4320
+ else {
4321
+ this.renderMultilineAttributes(getTagName(node), node.children, isSelfClosing);
3292
4322
  }
3293
- this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false);
3294
4323
  }
3295
4324
  visitHTMLCloseTagNode(node) {
3296
- const indent = this.indent();
3297
- const open = node.tag_opening?.value ?? "";
3298
- const name = node.tag_name?.value ?? "";
3299
- const close = node.tag_closing?.value ?? "";
3300
- this.push(indent + open + name + close);
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
+ }
3301
4334
  }
3302
4335
  visitHTMLTextNode(node) {
3303
4336
  if (this.inlineMode) {
@@ -3307,17 +4340,16 @@ class Printer extends Visitor {
3307
4340
  }
3308
4341
  return;
3309
4342
  }
3310
- const indent = this.indent();
3311
4343
  let text = node.content.trim();
3312
4344
  if (!text)
3313
4345
  return;
3314
- const wrapWidth = this.maxLineLength - indent.length;
4346
+ const wrapWidth = this.maxLineLength - this.indent.length;
3315
4347
  const words = text.split(/\s+/);
3316
4348
  const lines = [];
3317
4349
  let line = "";
3318
4350
  for (const word of words) {
3319
4351
  if ((line + (line ? " " : "") + word).length > wrapWidth && line) {
3320
- lines.push(indent + line);
4352
+ lines.push(this.indent + line);
3321
4353
  line = word;
3322
4354
  }
3323
4355
  else {
@@ -3325,63 +4357,74 @@ class Printer extends Visitor {
3325
4357
  }
3326
4358
  }
3327
4359
  if (line)
3328
- lines.push(indent + line);
4360
+ lines.push(this.indent + line);
3329
4361
  lines.forEach(line => this.push(line));
3330
4362
  }
3331
4363
  visitHTMLAttributeNode(node) {
3332
- const indent = this.indent();
3333
- this.push(indent + this.renderAttribute(node));
4364
+ this.push(this.indent + this.renderAttribute(node));
3334
4365
  }
3335
4366
  visitHTMLAttributeNameNode(node) {
3336
- const indent = this.indent();
3337
- const name = node.name?.value ?? "";
3338
- this.push(indent + name);
4367
+ this.push(this.indent + getCombinedAttributeName(node));
3339
4368
  }
3340
4369
  visitHTMLAttributeValueNode(node) {
3341
- const indent = this.indent();
3342
- const open_quote = node.open_quote?.value ?? "";
3343
- const close_quote = node.close_quote?.value ?? "";
3344
- const attribute_value = node.children.map(child => {
3345
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE' ||
3346
- child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
3347
- return child.content;
3348
- }
3349
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3350
- const erbChild = child;
3351
- return (erbChild.tag_opening.value + erbChild.content.value + erbChild.tag_closing.value);
3352
- }
3353
- return "";
3354
- }).join("");
3355
- this.push(indent + open_quote + attribute_value + close_quote);
4370
+ this.push(this.indent + IdentityPrinter.print(node));
3356
4371
  }
4372
+ // TODO: rework
3357
4373
  visitHTMLCommentNode(node) {
3358
- const indent = this.indent();
3359
4374
  const open = node.comment_start?.value ?? "";
3360
4375
  const close = node.comment_end?.value ?? "";
3361
4376
  let inner;
3362
4377
  if (node.children && node.children.length > 0) {
3363
4378
  inner = node.children.map(child => {
3364
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
4379
+ if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
3365
4380
  return child.content;
3366
4381
  }
3367
- else if (child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
3368
- return child.content;
4382
+ else if (isERBNode(child) || isNode(child, ERBContentNode)) {
4383
+ return this.reconstructERBNode(child, false);
3369
4384
  }
3370
4385
  else {
3371
- const prevLines = this.lines.length;
3372
- this.visit(child);
3373
- return this.lines.slice(prevLines).join("");
4386
+ return "";
3374
4387
  }
3375
4388
  }).join("");
3376
- inner = ` ${inner.trim()} `;
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
+ }
3377
4421
  }
3378
4422
  else {
3379
4423
  inner = "";
3380
4424
  }
3381
- this.push(indent + open + inner + close);
4425
+ this.push(this.indent + open + inner + close);
3382
4426
  }
3383
4427
  visitERBCommentNode(node) {
3384
- const indent = this.indent();
3385
4428
  const open = node.tag_opening?.value ?? "";
3386
4429
  const close = node.tag_closing?.value ?? "";
3387
4430
  let inner;
@@ -3389,7 +4432,7 @@ class Printer extends Visitor {
3389
4432
  const rawInner = node.content.value;
3390
4433
  const lines = rawInner.split("\n");
3391
4434
  if (lines.length > 2) {
3392
- const childIndent = indent + " ".repeat(this.indentWidth);
4435
+ const childIndent = this.indent + " ".repeat(this.indentWidth);
3393
4436
  const innerLines = lines.slice(1, -1).map(line => childIndent + line.trim());
3394
4437
  inner = "\n" + innerLines.join("\n") + "\n";
3395
4438
  }
@@ -3397,43 +4440,19 @@ class Printer extends Visitor {
3397
4440
  inner = ` ${rawInner.trim()} `;
3398
4441
  }
3399
4442
  }
3400
- else if (node.children) {
3401
- inner = node.children.map((child) => {
3402
- const prevLines = this.lines.length;
3403
- this.visit(child);
3404
- return this.lines.slice(prevLines).join("");
3405
- }).join("");
3406
- }
3407
4443
  else {
3408
4444
  inner = "";
3409
4445
  }
3410
- this.push(indent + open + inner + close);
4446
+ this.push(this.indent + open + inner + close);
3411
4447
  }
3412
4448
  visitHTMLDoctypeNode(node) {
3413
- const indent = this.indent();
3414
- const open = node.tag_opening?.value ?? "";
3415
- let innerDoctype = node.children.map(child => {
3416
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3417
- return child.content;
3418
- }
3419
- else if (child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
3420
- return child.content;
3421
- }
3422
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3423
- const erbNode = child;
3424
- const erbOpen = erbNode.tag_opening?.value ?? "";
3425
- const erbContent = erbNode.content?.value ?? "";
3426
- const erbClose = erbNode.tag_closing?.value ?? "";
3427
- return erbOpen + (erbContent ? ` ${erbContent.trim()} ` : "") + erbClose;
3428
- }
3429
- else {
3430
- const prevLines = this.lines.length;
3431
- this.visit(child);
3432
- return this.lines.slice(prevLines).join("");
3433
- }
3434
- }).join("");
3435
- const close = node.tag_closing?.value ?? "";
3436
- 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));
3437
4456
  }
3438
4457
  visitERBContentNode(node) {
3439
4458
  // TODO: this feels hacky
@@ -3452,109 +4471,81 @@ class Printer extends Visitor {
3452
4471
  }
3453
4472
  visitERBInNode(node) {
3454
4473
  this.printERBNode(node);
3455
- this.withIndent(() => {
3456
- node.statements.forEach(stmt => this.visit(stmt));
3457
- });
4474
+ this.withIndent(() => this.visitAll(node.statements));
3458
4475
  }
3459
4476
  visitERBCaseMatchNode(node) {
3460
4477
  this.printERBNode(node);
3461
- node.conditions.forEach(condition => this.visit(condition));
4478
+ this.visitAll(node.conditions);
3462
4479
  if (node.else_clause)
3463
4480
  this.visit(node.else_clause);
3464
4481
  if (node.end_node)
3465
4482
  this.visit(node.end_node);
3466
4483
  }
3467
4484
  visitERBBlockNode(node) {
3468
- const indent = this.indent();
3469
- const open = node.tag_opening?.value ?? "";
3470
- const content = node.content?.value ?? "";
3471
- const close = node.tag_closing?.value ?? "";
3472
- this.push(indent + open + content + close);
3473
- this.withIndent(() => {
3474
- node.body.forEach(child => this.visit(child));
3475
- });
3476
- if (node.end_node) {
4485
+ this.printERBNode(node);
4486
+ this.withIndent(() => this.visitElementChildren(node.body, null));
4487
+ if (node.end_node)
3477
4488
  this.visit(node.end_node);
3478
- }
3479
4489
  }
3480
4490
  visitERBIfNode(node) {
3481
4491
  if (this.inlineMode) {
3482
- const open = node.tag_opening?.value ?? "";
3483
- const content = node.content?.value ?? "";
3484
- const close = node.tag_closing?.value ?? "";
3485
- const inner = this.formatERBContent(content);
3486
- this.lines.push(open + inner + close);
3487
- node.statements.forEach((child, _index) => {
3488
- this.lines.push(" ");
3489
- 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(" ");
3490
4496
  this.lines.push(this.renderAttribute(child));
3491
4497
  }
3492
4498
  else {
4499
+ const shouldAddSpaces = this.isInTokenListAttribute();
4500
+ if (shouldAddSpaces) {
4501
+ this.lines.push(" ");
4502
+ }
3493
4503
  this.visit(child);
4504
+ if (shouldAddSpaces) {
4505
+ this.lines.push(" ");
4506
+ }
3494
4507
  }
3495
4508
  });
3496
- if (node.statements.length > 0 && node.end_node) {
4509
+ const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode));
4510
+ const isTokenList = this.isInTokenListAttribute();
4511
+ if ((hasHTMLAttributes || isTokenList) && node.end_node) {
3497
4512
  this.lines.push(" ");
3498
4513
  }
3499
- if (node.subsequent) {
3500
- this.visit(node.subsequent);
3501
- }
3502
- if (node.end_node) {
3503
- const endNode = node.end_node;
3504
- const endOpen = endNode.tag_opening?.value ?? "";
3505
- const endContent = endNode.content?.value ?? "";
3506
- const endClose = endNode.tag_closing?.value ?? "";
3507
- const endInner = this.formatERBContent(endContent);
3508
- this.lines.push(endOpen + endInner + endClose);
3509
- }
4514
+ if (node.subsequent)
4515
+ this.visit(node.end_node);
4516
+ if (node.end_node)
4517
+ this.visit(node.end_node);
3510
4518
  }
3511
4519
  else {
3512
4520
  this.printERBNode(node);
3513
4521
  this.withIndent(() => {
3514
4522
  node.statements.forEach(child => this.visit(child));
3515
4523
  });
3516
- if (node.subsequent) {
4524
+ if (node.subsequent)
3517
4525
  this.visit(node.subsequent);
3518
- }
3519
- if (node.end_node) {
3520
- this.printERBNode(node.end_node);
3521
- }
4526
+ if (node.end_node)
4527
+ this.visit(node.end_node);
3522
4528
  }
3523
4529
  }
3524
4530
  visitERBElseNode(node) {
3525
4531
  this.printERBNode(node);
3526
- this.withIndent(() => {
3527
- node.statements.forEach(child => this.visit(child));
3528
- });
4532
+ this.withIndent(() => node.statements.forEach(statement => this.visit(statement)));
3529
4533
  }
3530
4534
  visitERBWhenNode(node) {
3531
4535
  this.printERBNode(node);
3532
- this.withIndent(() => {
3533
- node.statements.forEach(stmt => this.visit(stmt));
3534
- });
4536
+ this.withIndent(() => this.visitAll(node.statements));
3535
4537
  }
3536
4538
  visitERBCaseNode(node) {
3537
- const indent = this.indent();
3538
- const open = node.tag_opening?.value ?? "";
3539
- const content = node.content?.value ?? "";
3540
- const close = node.tag_closing?.value ?? "";
3541
- this.push(indent + open + content + close);
3542
- node.conditions.forEach(condition => this.visit(condition));
4539
+ this.printERBNode(node);
4540
+ this.visitAll(node.conditions);
3543
4541
  if (node.else_clause)
3544
4542
  this.visit(node.else_clause);
3545
- if (node.end_node) {
4543
+ if (node.end_node)
3546
4544
  this.visit(node.end_node);
3547
- }
3548
4545
  }
3549
4546
  visitERBBeginNode(node) {
3550
- const indent = this.indent();
3551
- const open = node.tag_opening?.value ?? "";
3552
- const content = node.content?.value ?? "";
3553
- const close = node.tag_closing?.value ?? "";
3554
- this.push(indent + open + content + close);
3555
- this.withIndent(() => {
3556
- node.statements.forEach(statement => this.visit(statement));
3557
- });
4547
+ this.printERBNode(node);
4548
+ this.withIndent(() => this.visitAll(node.statements));
3558
4549
  if (node.rescue_clause)
3559
4550
  this.visit(node.rescue_clause);
3560
4551
  if (node.else_clause)
@@ -3565,61 +4556,147 @@ class Printer extends Visitor {
3565
4556
  this.visit(node.end_node);
3566
4557
  }
3567
4558
  visitERBWhileNode(node) {
3568
- this.visitERBGeneric(node);
4559
+ this.printERBNode(node);
4560
+ this.withIndent(() => this.visitAll(node.statements));
4561
+ if (node.end_node)
4562
+ this.visit(node.end_node);
3569
4563
  }
3570
4564
  visitERBUntilNode(node) {
3571
- this.visitERBGeneric(node);
4565
+ this.printERBNode(node);
4566
+ this.withIndent(() => this.visitAll(node.statements));
4567
+ if (node.end_node)
4568
+ this.visit(node.end_node);
3572
4569
  }
3573
4570
  visitERBForNode(node) {
3574
- this.visitERBGeneric(node);
4571
+ this.printERBNode(node);
4572
+ this.withIndent(() => this.visitAll(node.statements));
4573
+ if (node.end_node)
4574
+ this.visit(node.end_node);
3575
4575
  }
3576
4576
  visitERBRescueNode(node) {
3577
- this.visitERBGeneric(node);
4577
+ this.printERBNode(node);
4578
+ this.withIndent(() => this.visitAll(node.statements));
3578
4579
  }
3579
4580
  visitERBEnsureNode(node) {
3580
- this.visitERBGeneric(node);
4581
+ this.printERBNode(node);
4582
+ this.withIndent(() => this.visitAll(node.statements));
3581
4583
  }
3582
4584
  visitERBUnlessNode(node) {
3583
- this.visitERBGeneric(node);
3584
- }
3585
- // TODO: don't use any
3586
- visitERBGeneric(node) {
3587
- const indent = this.indent();
3588
- const open = node.tag_opening?.value ?? "";
3589
- const content = node.content?.value ?? "";
3590
- const close = node.tag_closing?.value ?? "";
3591
- this.push(indent + open + content + close);
3592
- this.withIndent(() => {
3593
- const statements = node.statements ?? node.body ?? node.children ?? [];
3594
- statements.forEach(statement => this.visit(statement));
3595
- });
4585
+ this.printERBNode(node);
4586
+ this.withIndent(() => this.visitAll(node.statements));
4587
+ if (node.else_clause)
4588
+ this.visit(node.else_clause);
3596
4589
  if (node.end_node)
3597
4590
  this.visit(node.end_node);
3598
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
+ }
3599
4679
  // --- Utility methods ---
3600
4680
  isNonWhitespaceNode(node) {
3601
- if (node instanceof HTMLTextNode || node.type === 'AST_HTML_TEXT_NODE') {
3602
- return node.content.trim() !== "";
3603
- }
3604
- if (node instanceof WhitespaceNode || node.type === 'AST_WHITESPACE_NODE') {
4681
+ if (isNode(node, WhitespaceNode))
3605
4682
  return false;
3606
- }
4683
+ if (isNode(node, HTMLTextNode))
4684
+ return node.content.trim() !== "";
3607
4685
  return true;
3608
4686
  }
3609
4687
  /**
3610
4688
  * Check if an element should be treated as inline based on its tag name
3611
4689
  */
3612
4690
  isInlineElement(tagName) {
3613
- return Printer.INLINE_ELEMENTS.has(tagName.toLowerCase());
4691
+ return FormatPrinter.INLINE_ELEMENTS.has(tagName.toLowerCase());
3614
4692
  }
3615
4693
  /**
3616
4694
  * Check if we're in a text flow context (parent contains mixed text and inline elements)
3617
4695
  */
3618
4696
  visitTextFlowChildren(children) {
3619
- const indent = this.indent();
3620
4697
  let currentLineContent = "";
3621
4698
  for (const child of children) {
3622
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
4699
+ if (isNode(child, HTMLTextNode)) {
3623
4700
  const content = child.content;
3624
4701
  let processedContent = content.replace(/\s+/g, ' ').trim();
3625
4702
  if (processedContent) {
@@ -3632,29 +4709,26 @@ class Printer extends Visitor {
3632
4709
  if (hasTrailingSpace && !currentLineContent.endsWith(' ')) {
3633
4710
  currentLineContent += ' ';
3634
4711
  }
3635
- if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
3636
- this.visitTextFlowChildrenMultiline(children);
4712
+ if ((this.indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
4713
+ children.forEach(child => this.visit(child));
3637
4714
  return;
3638
4715
  }
3639
4716
  }
3640
4717
  }
3641
- else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3642
- const element = child;
3643
- const openTag = element.open_tag;
3644
- const childTagName = openTag?.tag_name?.value || '';
4718
+ else if (isNode(child, HTMLElementNode)) {
4719
+ const childTagName = getTagName(child);
3645
4720
  if (this.isInlineElement(childTagName)) {
3646
- const childInline = this.tryRenderInlineFull(element, childTagName, this.extractAttributes(openTag.children), element.body.filter(c => !(c instanceof WhitespaceNode || c.type === 'AST_WHITESPACE_NODE') &&
3647
- !((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));
3648
4722
  if (childInline) {
3649
4723
  currentLineContent += childInline;
3650
- if ((indent.length + currentLineContent.length) > this.maxLineLength) {
3651
- this.visitTextFlowChildrenMultiline(children);
4724
+ if ((this.indent.length + currentLineContent.length) > this.maxLineLength) {
4725
+ children.forEach(child => this.visit(child));
3652
4726
  return;
3653
4727
  }
3654
4728
  }
3655
4729
  else {
3656
4730
  if (currentLineContent.trim()) {
3657
- this.push(indent + currentLineContent.trim());
4731
+ this.push(this.indent + currentLineContent.trim());
3658
4732
  currentLineContent = "";
3659
4733
  }
3660
4734
  this.visit(child);
@@ -3662,25 +4736,26 @@ class Printer extends Visitor {
3662
4736
  }
3663
4737
  else {
3664
4738
  if (currentLineContent.trim()) {
3665
- this.push(indent + currentLineContent.trim());
4739
+ this.push(this.indent + currentLineContent.trim());
3666
4740
  currentLineContent = "";
3667
4741
  }
3668
4742
  this.visit(child);
3669
4743
  }
3670
4744
  }
3671
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
4745
+ else if (isNode(child, ERBContentNode)) {
3672
4746
  const oldLines = this.lines;
3673
4747
  const oldInlineMode = this.inlineMode;
4748
+ // TODO: use this.capture
3674
4749
  try {
3675
4750
  this.lines = [];
3676
4751
  this.inlineMode = true;
3677
4752
  this.visit(child);
3678
4753
  const erbContent = this.lines.join("");
3679
4754
  currentLineContent += erbContent;
3680
- if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
4755
+ if ((this.indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
3681
4756
  this.lines = oldLines;
3682
4757
  this.inlineMode = oldInlineMode;
3683
- this.visitTextFlowChildrenMultiline(children);
4758
+ children.forEach(child => this.visit(child));
3684
4759
  return;
3685
4760
  }
3686
4761
  }
@@ -3691,53 +4766,38 @@ class Printer extends Visitor {
3691
4766
  }
3692
4767
  else {
3693
4768
  if (currentLineContent.trim()) {
3694
- this.push(indent + currentLineContent.trim());
4769
+ this.push(this.indent + currentLineContent.trim());
3695
4770
  currentLineContent = "";
3696
4771
  }
3697
4772
  this.visit(child);
3698
4773
  }
3699
4774
  }
3700
4775
  if (currentLineContent.trim()) {
3701
- const finalLine = indent + currentLineContent.trim();
4776
+ const finalLine = this.indent + currentLineContent.trim();
3702
4777
  if (finalLine.length > Math.max(this.maxLineLength, 120)) {
3703
- this.visitTextFlowChildrenMultiline(children);
4778
+ this.visitAll(children);
3704
4779
  return;
3705
4780
  }
3706
4781
  this.push(finalLine);
3707
4782
  }
3708
4783
  }
3709
- visitTextFlowChildrenMultiline(children) {
3710
- children.forEach(child => this.visit(child));
3711
- }
3712
- isInTextFlowContext(parent, children) {
3713
- const hasTextContent = children.some(child => (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') &&
3714
- child.content.trim() !== "");
3715
- 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)
3716
4788
  return false;
3717
- }
3718
- const nonTextChildren = children.filter(child => !(child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE'));
3719
- if (nonTextChildren.length === 0) {
4789
+ if (nonTextChildren.length === 0)
3720
4790
  return false;
3721
- }
3722
4791
  const allInline = nonTextChildren.every(child => {
3723
- if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
4792
+ if (isNode(child, ERBContentNode))
3724
4793
  return true;
3725
- }
3726
- if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3727
- const element = child;
3728
- const openTag = element.open_tag;
3729
- const tagName = openTag?.tag_name?.value || '';
3730
- return this.isInlineElement(tagName);
4794
+ if (isNode(child, HTMLElementNode)) {
4795
+ return this.isInlineElement(getTagName(child));
3731
4796
  }
3732
4797
  return false;
3733
4798
  });
3734
- if (!allInline) {
3735
- return false;
3736
- }
3737
- const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3738
- if (maxNestingDepth > 2) {
4799
+ if (!allInline)
3739
4800
  return false;
3740
- }
3741
4801
  return true;
3742
4802
  }
3743
4803
  renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
@@ -3745,73 +4805,68 @@ class Printer extends Visitor {
3745
4805
  if (inlineNodes.length > 0) {
3746
4806
  let result = `<${name}`;
3747
4807
  if (allChildren.length > 0) {
3748
- const currentIndentLevel = this.indentLevel;
3749
- this.indentLevel = 0;
3750
- const tempLines = this.lines;
3751
- this.lines = [];
3752
- allChildren.forEach(child => {
3753
- if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
3754
- this.lines.push(" " + this.renderAttribute(child));
3755
- }
3756
- else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
3757
- const wasInlineMode = this.inlineMode;
3758
- this.inlineMode = true;
3759
- this.lines.push(" ");
3760
- this.visit(child);
3761
- this.inlineMode = wasInlineMode;
3762
- }
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
+ });
3763
4821
  });
3764
- const inlineContent = this.lines.join("");
3765
- this.lines = tempLines;
3766
- this.indentLevel = currentIndentLevel;
3767
- result += inlineContent;
4822
+ result += lines.join("");
3768
4823
  }
3769
4824
  else {
3770
4825
  if (parts.length > 0) {
3771
4826
  result += ` ${parts.join(" ")}`;
3772
4827
  }
3773
- const currentIndentLevel = this.indentLevel;
3774
- this.indentLevel = 0;
3775
- const tempLines = this.lines;
3776
- this.lines = [];
3777
- inlineNodes.forEach(node => {
3778
- const wasInlineMode = this.inlineMode;
3779
- if (!this.isERBControlFlow(node)) {
3780
- this.inlineMode = true;
3781
- }
3782
- this.visit(node);
3783
- 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
+ });
3784
4837
  });
3785
- const inlineContent = this.lines.join("");
3786
- this.lines = tempLines;
3787
- this.indentLevel = currentIndentLevel;
3788
- result += inlineContent;
4838
+ result += lines.join("");
3789
4839
  }
3790
4840
  result += selfClose ? " />" : ">";
3791
4841
  return result;
3792
4842
  }
3793
- return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " /" : ""}>`;
4843
+ return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " />" : ">"}`;
3794
4844
  }
3795
4845
  renderAttribute(attribute) {
3796
- const name = attribute.name.name.value ?? "";
4846
+ const name = attribute.name ? getCombinedAttributeName(attribute.name) : "";
3797
4847
  const equals = attribute.equals?.value ?? "";
4848
+ this.currentAttributeName = name;
3798
4849
  let value = "";
3799
- if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || attribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
4850
+ if (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
3800
4851
  const attributeValue = attribute.value;
3801
4852
  let open_quote = attributeValue.open_quote?.value ?? "";
3802
4853
  let close_quote = attributeValue.close_quote?.value ?? "";
3803
4854
  let htmlTextContent = "";
3804
4855
  const content = attributeValue.children.map((child) => {
3805
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE' || child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
3806
- const textContent = child.content;
3807
- htmlTextContent += textContent;
3808
- return textContent;
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);
3809
4862
  }
3810
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3811
- const erbAttribute = child;
3812
- return erbAttribute.tag_opening.value + erbAttribute.content.value + erbAttribute.tag_closing.value;
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;
3813
4869
  }
3814
- return "";
3815
4870
  }).join("");
3816
4871
  if (open_quote === "" && close_quote === "") {
3817
4872
  open_quote = '"';
@@ -3826,13 +4881,14 @@ class Printer extends Visitor {
3826
4881
  value = this.formatClassAttribute(content, name, equals, open_quote, close_quote);
3827
4882
  }
3828
4883
  else {
3829
- value = this.formatMultilineAttribute(content, name, equals, open_quote, close_quote);
4884
+ value = this.formatMultilineAttribute(content, name, open_quote, close_quote);
3830
4885
  }
3831
4886
  }
3832
4887
  else {
3833
4888
  value = open_quote + content + close_quote;
3834
4889
  }
3835
4890
  }
4891
+ this.currentAttributeName = null;
3836
4892
  return name + equals + value;
3837
4893
  }
3838
4894
  /**
@@ -3843,9 +4899,8 @@ class Printer extends Visitor {
3843
4899
  result += this.renderAttributesString(attributes);
3844
4900
  result += ">";
3845
4901
  const childrenContent = this.tryRenderChildrenInline(children);
3846
- if (!childrenContent) {
4902
+ if (!childrenContent)
3847
4903
  return null;
3848
- }
3849
4904
  result += childrenContent;
3850
4905
  result += `</${tagName}>`;
3851
4906
  return result;
@@ -3856,11 +4911,10 @@ class Printer extends Visitor {
3856
4911
  tryRenderChildrenInline(children) {
3857
4912
  let result = "";
3858
4913
  for (const child of children) {
3859
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3860
- const content = child.content;
3861
- const normalizedContent = content.replace(/\s+/g, ' ');
3862
- const hasLeadingSpace = /^\s/.test(content);
3863
- 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);
3864
4918
  const trimmedContent = normalizedContent.trim();
3865
4919
  if (trimmedContent) {
3866
4920
  let finalContent = trimmedContent;
@@ -3878,36 +4932,19 @@ class Printer extends Visitor {
3878
4932
  }
3879
4933
  }
3880
4934
  }
3881
- else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3882
- const element = child;
3883
- const openTag = element.open_tag;
3884
- const childTagName = openTag?.tag_name?.value || '';
3885
- if (!this.isInlineElement(childTagName)) {
4935
+ else if (isNode(child, HTMLElementNode)) {
4936
+ const tagName = getTagName(child);
4937
+ if (!this.isInlineElement(tagName)) {
3886
4938
  return null;
3887
4939
  }
3888
- const childInline = this.tryRenderInlineFull(element, childTagName, this.extractAttributes(openTag.children), element.body.filter(c => !(c instanceof WhitespaceNode || c.type === 'AST_WHITESPACE_NODE') &&
3889
- !((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));
3890
4941
  if (!childInline) {
3891
4942
  return null;
3892
4943
  }
3893
4944
  result += childInline;
3894
4945
  }
3895
4946
  else {
3896
- const oldLines = this.lines;
3897
- const oldInlineMode = this.inlineMode;
3898
- const oldIndentLevel = this.indentLevel;
3899
- try {
3900
- this.lines = [];
3901
- this.inlineMode = true;
3902
- this.indentLevel = 0;
3903
- this.visit(child);
3904
- result += this.lines.join("");
3905
- }
3906
- finally {
3907
- this.lines = oldLines;
3908
- this.inlineMode = oldInlineMode;
3909
- this.indentLevel = oldIndentLevel;
3910
- }
4947
+ result += this.capture(() => this.visit(child)).join("");
3911
4948
  }
3912
4949
  }
3913
4950
  return result.trim();
@@ -3916,142 +4953,145 @@ class Printer extends Visitor {
3916
4953
  * Try to render children inline if they are simple enough.
3917
4954
  * Returns the inline string if possible, null otherwise.
3918
4955
  */
3919
- tryRenderInline(children, tagName, depth = 0, forceInline = false, hasTextFlow = false) {
3920
- if (!forceInline && children.length > 10) {
3921
- return null;
3922
- }
3923
- const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3924
- let maxAllowedDepth = forceInline ? 5 : (tagName && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div'].includes(tagName) ? 1 : 2);
3925
- if (hasTextFlow && maxNestingDepth >= 2) {
3926
- const roughContentLength = this.estimateContentLength(children);
3927
- if (roughContentLength > 47) {
3928
- maxAllowedDepth = 1;
3929
- }
3930
- }
3931
- if (!forceInline && maxNestingDepth > maxAllowedDepth) {
3932
- this.isInComplexNesting = true;
3933
- return null;
3934
- }
4956
+ tryRenderInline(children, tagName) {
3935
4957
  for (const child of children) {
3936
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3937
- const textContent = child.content;
3938
- if (textContent.includes('\n')) {
4958
+ if (isNode(child, HTMLTextNode)) {
4959
+ if (child.content.includes('\n')) {
3939
4960
  return null;
3940
4961
  }
3941
4962
  }
3942
- else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3943
- const element = child;
3944
- const openTag = element.open_tag;
3945
- const elementTagName = openTag?.tag_name?.value || '';
3946
- const isInlineElement = this.isInlineElement(elementTagName);
4963
+ else if (isNode(child, HTMLElementNode)) {
4964
+ const isInlineElement = this.isInlineElement(getTagName(child));
3947
4965
  if (!isInlineElement) {
3948
4966
  return null;
3949
4967
  }
3950
4968
  }
3951
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
4969
+ else if (isNode(child, ERBContentNode)) ;
3952
4970
  else {
3953
4971
  return null;
3954
4972
  }
3955
4973
  }
3956
- const oldLines = this.lines;
3957
- const oldInlineMode = this.inlineMode;
3958
- try {
3959
- this.lines = [];
3960
- this.inlineMode = true;
3961
- let content = '';
3962
- for (const child of children) {
3963
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3964
- content += child.content;
3965
- }
3966
- else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3967
- const element = child;
3968
- const openTag = element.open_tag;
3969
- const childTagName = openTag?.tag_name?.value || '';
3970
- const attributes = this.extractAttributes(openTag.children);
3971
- const attributesString = this.renderAttributesString(attributes);
3972
- const elementContent = this.renderElementInline(element);
3973
- content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
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;
3974
4992
  }
3975
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3976
- const erbNode = child;
3977
- const open = erbNode.tag_opening?.value ?? "";
3978
- const erbContent = erbNode.content?.value ?? "";
3979
- const close = erbNode.tag_closing?.value ?? "";
3980
- content += `${open}${this.formatERBContent(erbContent)}${close}`;
4993
+ }
4994
+ else if (isNode(child, HTMLElementNode)) {
4995
+ if (this.isInlineElement(getTagName(child))) {
4996
+ hasInlineElements = true;
3981
4997
  }
3982
4998
  }
3983
- content = content.replace(/\s+/g, ' ').trim();
3984
- return `<${tagName}>${content}</${tagName}>`;
3985
- }
3986
- finally {
3987
- this.lines = oldLines;
3988
- this.inlineMode = oldInlineMode;
3989
4999
  }
5000
+ return (hasText && hasInlineElements) || (hasERBOutput(children) && hasText);
3990
5001
  }
3991
5002
  /**
3992
- * Estimate the total content length of children nodes for decision making.
5003
+ * Check if children contain any text content with newlines
3993
5004
  */
3994
- estimateContentLength(children) {
3995
- let length = 0;
5005
+ hasMultilineTextContent(children) {
3996
5006
  for (const child of children) {
3997
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3998
- length += child.content.length;
3999
- }
4000
- else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
4001
- const element = child;
4002
- const openTag = element.open_tag;
4003
- const tagName = openTag?.tag_name?.value || '';
4004
- length += tagName.length + 5; // Rough estimate for tag overhead
4005
- length += this.estimateContentLength(element.body);
5007
+ if (isNode(child, HTMLTextNode)) {
5008
+ return child.content.includes('\n');
4006
5009
  }
4007
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
4008
- length += child.content?.value.length || 0;
5010
+ if (isNode(child, HTMLElementNode)) {
5011
+ const nestedChildren = this.filterEmptyNodes(child.body);
5012
+ if (this.hasMultilineTextContent(nestedChildren)) {
5013
+ return true;
5014
+ }
4009
5015
  }
4010
5016
  }
4011
- return length;
5017
+ return false;
4012
5018
  }
4013
5019
  /**
4014
- * Calculate the maximum nesting depth in a subtree of nodes.
5020
+ * Check if all nested elements in the children are inline elements
4015
5021
  */
4016
- getMaxNestingDepth(children, currentDepth) {
4017
- let maxDepth = currentDepth;
5022
+ areAllNestedElementsInline(children) {
4018
5023
  for (const child of children) {
4019
- if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
4020
- const element = child;
4021
- const elementChildren = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
4022
- !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
4023
- const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1);
4024
- maxDepth = Math.max(maxDepth, childDepth);
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;
4025
5035
  }
4026
5036
  }
4027
- return maxDepth;
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
+ });
4028
5069
  }
4029
5070
  /**
4030
- * Render an HTML element's content inline (without the wrapping tags).
5071
+ * Filter out empty text nodes and whitespace nodes
4031
5072
  */
5073
+ filterEmptyNodes(nodes) {
5074
+ return nodes.filter(child => !isNode(child, WhitespaceNode) && !(isNode(child, HTMLTextNode) && child.content.trim() === ""));
5075
+ }
4032
5076
  renderElementInline(element) {
4033
- const children = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
4034
- !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
5077
+ const children = this.filterEmptyNodes(element.body);
5078
+ return this.renderChildrenInline(children);
5079
+ }
5080
+ renderChildrenInline(children) {
4035
5081
  let content = '';
4036
5082
  for (const child of children) {
4037
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
5083
+ if (isNode(child, HTMLTextNode)) {
4038
5084
  content += child.content;
4039
5085
  }
4040
- else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
4041
- const childElement = child;
4042
- const openTag = childElement.open_tag;
4043
- const childTagName = openTag?.tag_name?.value || '';
4044
- 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);
4045
5089
  const attributesString = this.renderAttributesString(attributes);
4046
- const childContent = this.renderElementInline(childElement);
4047
- content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
5090
+ const childContent = this.renderElementInline(child);
5091
+ content += `<${tagName}${attributesString}>${childContent}</${tagName}>`;
4048
5092
  }
4049
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
4050
- const erbNode = child;
4051
- const open = erbNode.tag_opening?.value ?? "";
4052
- const erbContent = erbNode.content?.value ?? "";
4053
- const close = erbNode.tag_closing?.value ?? "";
4054
- content += `${open}${this.formatERBContent(erbContent)}${close}`;
5093
+ else if (isNode(child, ERBContentNode)) {
5094
+ content += this.reconstructERBNode(child, true);
4055
5095
  }
4056
5096
  }
4057
5097
  return content.replace(/\s+/g, ' ').trim();
@@ -4096,7 +5136,7 @@ class Formatter {
4096
5136
  if (result.failed)
4097
5137
  return source;
4098
5138
  const resolvedOptions = resolveFormatOptions({ ...this.options, ...options });
4099
- return new Printer(source, resolvedOptions).print(result.value);
5139
+ return new FormatPrinter(source, resolvedOptions).print(result.value);
4100
5140
  }
4101
5141
  parse(source) {
4102
5142
  this.herb.ensureBackend();
@@ -4104,6 +5144,7 @@ class Formatter {
4104
5144
  }
4105
5145
  }
4106
5146
 
5147
+ exports.FormatPrinter = FormatPrinter;
4107
5148
  exports.Formatter = Formatter;
4108
5149
  exports.defaultFormatOptions = defaultFormatOptions;
4109
5150
  exports.resolveFormatOptions = resolveFormatOptions;