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