@herb-tools/formatter 0.5.0 → 0.6.1

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
@@ -1,3 +1,70 @@
1
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
2
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
3
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
+ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
5
+ function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
6
+ const dedent = createDedent({});
7
+ function createDedent(options) {
8
+ dedent.withOptions = newOptions => createDedent(_objectSpread(_objectSpread({}, options), newOptions));
9
+ return dedent;
10
+ function dedent(strings, ...values) {
11
+ const raw = typeof strings === "string" ? [strings] : strings.raw;
12
+ const {
13
+ escapeSpecialCharacters = Array.isArray(strings),
14
+ trimWhitespace = true
15
+ } = options;
16
+
17
+ // first, perform interpolation
18
+ let result = "";
19
+ for (let i = 0; i < raw.length; i++) {
20
+ let next = raw[i];
21
+ if (escapeSpecialCharacters) {
22
+ // handle escaped newlines, backticks, and interpolation characters
23
+ next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
24
+ }
25
+ result += next;
26
+ if (i < values.length) {
27
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
28
+ result += values[i];
29
+ }
30
+ }
31
+
32
+ // now strip indentation
33
+ const lines = result.split("\n");
34
+ let mindent = null;
35
+ for (const l of lines) {
36
+ const m = l.match(/^(\s+)\S+/);
37
+ if (m) {
38
+ const indent = m[1].length;
39
+ if (!mindent) {
40
+ // this is the first indented line
41
+ mindent = indent;
42
+ } else {
43
+ mindent = Math.min(mindent, indent);
44
+ }
45
+ }
46
+ }
47
+ if (mindent !== null) {
48
+ const m = mindent; // appease TypeScript
49
+ result = lines
50
+ // https://github.com/typescript-eslint/typescript-eslint/issues/7140
51
+ // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
52
+ .map(l => l[0] === " " || l[0] === "\t" ? l.slice(m) : l).join("\n");
53
+ }
54
+
55
+ // dedent eats leading and trailing whitespace too
56
+ if (trimWhitespace) {
57
+ result = result.trim();
58
+ }
59
+
60
+ // handle escaped newlines at the end to ensure they don't get stripped too
61
+ if (escapeSpecialCharacters) {
62
+ result = result.replace(/\\n/g, "\n");
63
+ }
64
+ return result;
65
+ }
66
+ }
67
+
1
68
  class Position {
2
69
  line;
3
70
  column;
@@ -129,7 +196,7 @@ class Token {
129
196
  }
130
197
 
131
198
  // 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
199
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.6.1/templates/javascript/packages/core/src/errors.ts.erb
133
200
  class HerbError {
134
201
  type;
135
202
  message;
@@ -579,7 +646,7 @@ function convertToUTF8(string) {
579
646
  }
580
647
 
581
648
  // 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
649
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.6.1/templates/javascript/packages/core/src/nodes.ts.erb
583
650
  class Node {
584
651
  type;
585
652
  location;
@@ -587,6 +654,9 @@ class Node {
587
654
  static from(node) {
588
655
  return fromSerializedNode(node);
589
656
  }
657
+ static get type() {
658
+ throw new Error("AST_NODE");
659
+ }
590
660
  constructor(type, location, errors) {
591
661
  this.type = type;
592
662
  this.location = location;
@@ -602,6 +672,12 @@ class Node {
602
672
  inspect() {
603
673
  return this.treeInspect(0);
604
674
  }
675
+ is(nodeClass) {
676
+ return this.type === nodeClass.type;
677
+ }
678
+ isOfType(type) {
679
+ return this.type === type;
680
+ }
605
681
  get isSingleLine() {
606
682
  return this.location.start.line === this.location.end.line;
607
683
  }
@@ -643,6 +719,9 @@ class Node {
643
719
  }
644
720
  class DocumentNode extends Node {
645
721
  children;
722
+ static get type() {
723
+ return "AST_DOCUMENT_NODE";
724
+ }
646
725
  static from(data) {
647
726
  return new DocumentNode({
648
727
  type: data.type,
@@ -684,12 +763,14 @@ class DocumentNode extends Node {
684
763
  output += `@ DocumentNode ${this.location.treeInspectWithLabel()}\n`;
685
764
  output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
686
765
  output += `└── children: ${this.inspectArray(this.children, " ")}`;
687
- // output += "\n";
688
766
  return output;
689
767
  }
690
768
  }
691
769
  class LiteralNode extends Node {
692
770
  content;
771
+ static get type() {
772
+ return "AST_LITERAL_NODE";
773
+ }
693
774
  static from(data) {
694
775
  return new LiteralNode({
695
776
  type: data.type,
@@ -728,7 +809,6 @@ class LiteralNode extends Node {
728
809
  output += `@ LiteralNode ${this.location.treeInspectWithLabel()}\n`;
729
810
  output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
730
811
  output += `└── content: ${this.content ? JSON.stringify(this.content) : "∅"}\n`;
731
- // output += "\n";
732
812
  return output;
733
813
  }
734
814
  }
@@ -738,6 +818,9 @@ class HTMLOpenTagNode extends Node {
738
818
  tag_closing;
739
819
  children;
740
820
  is_void;
821
+ static get type() {
822
+ return "AST_HTML_OPEN_TAG_NODE";
823
+ }
741
824
  static from(data) {
742
825
  return new HTMLOpenTagNode({
743
826
  type: data.type,
@@ -795,14 +878,17 @@ class HTMLOpenTagNode extends Node {
795
878
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
796
879
  output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
797
880
  output += `└── is_void: ${typeof this.is_void === 'boolean' ? String(this.is_void) : "∅"}\n`;
798
- // output += "\n";
799
881
  return output;
800
882
  }
801
883
  }
802
884
  class HTMLCloseTagNode extends Node {
803
885
  tag_opening;
804
886
  tag_name;
887
+ children;
805
888
  tag_closing;
889
+ static get type() {
890
+ return "AST_HTML_CLOSE_TAG_NODE";
891
+ }
806
892
  static from(data) {
807
893
  return new HTMLCloseTagNode({
808
894
  type: data.type,
@@ -810,6 +896,7 @@ class HTMLCloseTagNode extends Node {
810
896
  errors: (data.errors || []).map(error => HerbError.from(error)),
811
897
  tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
812
898
  tag_name: data.tag_name ? Token.from(data.tag_name) : null,
899
+ children: (data.children || []).map(node => fromSerializedNode(node)),
813
900
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
814
901
  });
815
902
  }
@@ -817,13 +904,16 @@ class HTMLCloseTagNode extends Node {
817
904
  super(props.type, props.location, props.errors);
818
905
  this.tag_opening = props.tag_opening;
819
906
  this.tag_name = props.tag_name;
907
+ this.children = props.children;
820
908
  this.tag_closing = props.tag_closing;
821
909
  }
822
910
  accept(visitor) {
823
911
  visitor.visitHTMLCloseTagNode(this);
824
912
  }
825
913
  childNodes() {
826
- return [];
914
+ return [
915
+ ...this.children,
916
+ ];
827
917
  }
828
918
  compactChildNodes() {
829
919
  return this.childNodes().filter(node => node !== null && node !== undefined);
@@ -831,6 +921,7 @@ class HTMLCloseTagNode extends Node {
831
921
  recursiveErrors() {
832
922
  return [
833
923
  ...this.errors,
924
+ ...this.children.map(node => node.recursiveErrors()),
834
925
  ].flat();
835
926
  }
836
927
  toJSON() {
@@ -839,6 +930,7 @@ class HTMLCloseTagNode extends Node {
839
930
  type: "AST_HTML_CLOSE_TAG_NODE",
840
931
  tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
841
932
  tag_name: this.tag_name ? this.tag_name.toJSON() : null,
933
+ children: this.children.map(node => node.toJSON()),
842
934
  tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
843
935
  };
844
936
  }
@@ -848,75 +940,8 @@ class HTMLCloseTagNode extends Node {
848
940
  output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
849
941
  output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
850
942
  output += `├── tag_name: ${this.tag_name ? this.tag_name.treeInspect() : "∅"}\n`;
943
+ output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
851
944
  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
945
  return output;
921
946
  }
922
947
  }
@@ -926,15 +951,18 @@ class HTMLElementNode extends Node {
926
951
  body;
927
952
  close_tag;
928
953
  is_void;
954
+ static get type() {
955
+ return "AST_HTML_ELEMENT_NODE";
956
+ }
929
957
  static from(data) {
930
958
  return new HTMLElementNode({
931
959
  type: data.type,
932
960
  location: Location.from(data.location),
933
961
  errors: (data.errors || []).map(error => HerbError.from(error)),
934
- open_tag: data.open_tag ? fromSerializedNode(data.open_tag) : null,
962
+ open_tag: data.open_tag ? fromSerializedNode((data.open_tag)) : null,
935
963
  tag_name: data.tag_name ? Token.from(data.tag_name) : null,
936
964
  body: (data.body || []).map(node => fromSerializedNode(node)),
937
- close_tag: data.close_tag ? fromSerializedNode(data.close_tag) : null,
965
+ close_tag: data.close_tag ? fromSerializedNode((data.close_tag)) : null,
938
966
  is_void: data.is_void,
939
967
  });
940
968
  }
@@ -987,7 +1015,6 @@ class HTMLElementNode extends Node {
987
1015
  output += `├── body: ${this.inspectArray(this.body, "│ ")}`;
988
1016
  output += `├── close_tag: ${this.inspectNode(this.close_tag, "│ ")}`;
989
1017
  output += `└── is_void: ${typeof this.is_void === 'boolean' ? String(this.is_void) : "∅"}\n`;
990
- // output += "\n";
991
1018
  return output;
992
1019
  }
993
1020
  }
@@ -996,6 +1023,9 @@ class HTMLAttributeValueNode extends Node {
996
1023
  children;
997
1024
  close_quote;
998
1025
  quoted;
1026
+ static get type() {
1027
+ return "AST_HTML_ATTRIBUTE_VALUE_NODE";
1028
+ }
999
1029
  static from(data) {
1000
1030
  return new HTMLAttributeValueNode({
1001
1031
  type: data.type,
@@ -1049,29 +1079,33 @@ class HTMLAttributeValueNode extends Node {
1049
1079
  output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
1050
1080
  output += `├── close_quote: ${this.close_quote ? this.close_quote.treeInspect() : "∅"}\n`;
1051
1081
  output += `└── quoted: ${typeof this.quoted === 'boolean' ? String(this.quoted) : "∅"}\n`;
1052
- // output += "\n";
1053
1082
  return output;
1054
1083
  }
1055
1084
  }
1056
1085
  class HTMLAttributeNameNode extends Node {
1057
- name;
1086
+ children;
1087
+ static get type() {
1088
+ return "AST_HTML_ATTRIBUTE_NAME_NODE";
1089
+ }
1058
1090
  static from(data) {
1059
1091
  return new HTMLAttributeNameNode({
1060
1092
  type: data.type,
1061
1093
  location: Location.from(data.location),
1062
1094
  errors: (data.errors || []).map(error => HerbError.from(error)),
1063
- name: data.name ? Token.from(data.name) : null,
1095
+ children: (data.children || []).map(node => fromSerializedNode(node)),
1064
1096
  });
1065
1097
  }
1066
1098
  constructor(props) {
1067
1099
  super(props.type, props.location, props.errors);
1068
- this.name = props.name;
1100
+ this.children = props.children;
1069
1101
  }
1070
1102
  accept(visitor) {
1071
1103
  visitor.visitHTMLAttributeNameNode(this);
1072
1104
  }
1073
1105
  childNodes() {
1074
- return [];
1106
+ return [
1107
+ ...this.children,
1108
+ ];
1075
1109
  }
1076
1110
  compactChildNodes() {
1077
1111
  return this.childNodes().filter(node => node !== null && node !== undefined);
@@ -1079,21 +1113,21 @@ class HTMLAttributeNameNode extends Node {
1079
1113
  recursiveErrors() {
1080
1114
  return [
1081
1115
  ...this.errors,
1116
+ ...this.children.map(node => node.recursiveErrors()),
1082
1117
  ].flat();
1083
1118
  }
1084
1119
  toJSON() {
1085
1120
  return {
1086
1121
  ...super.toJSON(),
1087
1122
  type: "AST_HTML_ATTRIBUTE_NAME_NODE",
1088
- name: this.name ? this.name.toJSON() : null,
1123
+ children: this.children.map(node => node.toJSON()),
1089
1124
  };
1090
1125
  }
1091
1126
  treeInspect() {
1092
1127
  let output = "";
1093
1128
  output += `@ HTMLAttributeNameNode ${this.location.treeInspectWithLabel()}\n`;
1094
1129
  output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
1095
- output += `└── name: ${this.name ? this.name.treeInspect() : ""}\n`;
1096
- // output += "\n";
1130
+ output += `└── children: ${this.inspectArray(this.children, " ")}`;
1097
1131
  return output;
1098
1132
  }
1099
1133
  }
@@ -1101,14 +1135,17 @@ class HTMLAttributeNode extends Node {
1101
1135
  name;
1102
1136
  equals;
1103
1137
  value;
1138
+ static get type() {
1139
+ return "AST_HTML_ATTRIBUTE_NODE";
1140
+ }
1104
1141
  static from(data) {
1105
1142
  return new HTMLAttributeNode({
1106
1143
  type: data.type,
1107
1144
  location: Location.from(data.location),
1108
1145
  errors: (data.errors || []).map(error => HerbError.from(error)),
1109
- name: data.name ? fromSerializedNode(data.name) : null,
1146
+ name: data.name ? fromSerializedNode((data.name)) : null,
1110
1147
  equals: data.equals ? Token.from(data.equals) : null,
1111
- value: data.value ? fromSerializedNode(data.value) : null,
1148
+ value: data.value ? fromSerializedNode((data.value)) : null,
1112
1149
  });
1113
1150
  }
1114
1151
  constructor(props) {
@@ -1152,12 +1189,14 @@ class HTMLAttributeNode extends Node {
1152
1189
  output += `├── name: ${this.inspectNode(this.name, "│ ")}`;
1153
1190
  output += `├── equals: ${this.equals ? this.equals.treeInspect() : "∅"}\n`;
1154
1191
  output += `└── value: ${this.inspectNode(this.value, " ")}`;
1155
- // output += "\n";
1156
1192
  return output;
1157
1193
  }
1158
1194
  }
1159
1195
  class HTMLTextNode extends Node {
1160
1196
  content;
1197
+ static get type() {
1198
+ return "AST_HTML_TEXT_NODE";
1199
+ }
1161
1200
  static from(data) {
1162
1201
  return new HTMLTextNode({
1163
1202
  type: data.type,
@@ -1196,7 +1235,6 @@ class HTMLTextNode extends Node {
1196
1235
  output += `@ HTMLTextNode ${this.location.treeInspectWithLabel()}\n`;
1197
1236
  output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
1198
1237
  output += `└── content: ${this.content ? JSON.stringify(this.content) : "∅"}\n`;
1199
- // output += "\n";
1200
1238
  return output;
1201
1239
  }
1202
1240
  }
@@ -1204,6 +1242,9 @@ class HTMLCommentNode extends Node {
1204
1242
  comment_start;
1205
1243
  children;
1206
1244
  comment_end;
1245
+ static get type() {
1246
+ return "AST_HTML_COMMENT_NODE";
1247
+ }
1207
1248
  static from(data) {
1208
1249
  return new HTMLCommentNode({
1209
1250
  type: data.type,
@@ -1253,7 +1294,6 @@ class HTMLCommentNode extends Node {
1253
1294
  output += `├── comment_start: ${this.comment_start ? this.comment_start.treeInspect() : "∅"}\n`;
1254
1295
  output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
1255
1296
  output += `└── comment_end: ${this.comment_end ? this.comment_end.treeInspect() : "∅"}\n`;
1256
- // output += "\n";
1257
1297
  return output;
1258
1298
  }
1259
1299
  }
@@ -1261,6 +1301,9 @@ class HTMLDoctypeNode extends Node {
1261
1301
  tag_opening;
1262
1302
  children;
1263
1303
  tag_closing;
1304
+ static get type() {
1305
+ return "AST_HTML_DOCTYPE_NODE";
1306
+ }
1264
1307
  static from(data) {
1265
1308
  return new HTMLDoctypeNode({
1266
1309
  type: data.type,
@@ -1310,12 +1353,132 @@ class HTMLDoctypeNode extends Node {
1310
1353
  output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
1311
1354
  output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
1312
1355
  output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1313
- // output += "\n";
1356
+ return output;
1357
+ }
1358
+ }
1359
+ class XMLDeclarationNode extends Node {
1360
+ tag_opening;
1361
+ children;
1362
+ tag_closing;
1363
+ static get type() {
1364
+ return "AST_XML_DECLARATION_NODE";
1365
+ }
1366
+ static from(data) {
1367
+ return new XMLDeclarationNode({
1368
+ type: data.type,
1369
+ location: Location.from(data.location),
1370
+ errors: (data.errors || []).map(error => HerbError.from(error)),
1371
+ tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
1372
+ children: (data.children || []).map(node => fromSerializedNode(node)),
1373
+ tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1374
+ });
1375
+ }
1376
+ constructor(props) {
1377
+ super(props.type, props.location, props.errors);
1378
+ this.tag_opening = props.tag_opening;
1379
+ this.children = props.children;
1380
+ this.tag_closing = props.tag_closing;
1381
+ }
1382
+ accept(visitor) {
1383
+ visitor.visitXMLDeclarationNode(this);
1384
+ }
1385
+ childNodes() {
1386
+ return [
1387
+ ...this.children,
1388
+ ];
1389
+ }
1390
+ compactChildNodes() {
1391
+ return this.childNodes().filter(node => node !== null && node !== undefined);
1392
+ }
1393
+ recursiveErrors() {
1394
+ return [
1395
+ ...this.errors,
1396
+ ...this.children.map(node => node.recursiveErrors()),
1397
+ ].flat();
1398
+ }
1399
+ toJSON() {
1400
+ return {
1401
+ ...super.toJSON(),
1402
+ type: "AST_XML_DECLARATION_NODE",
1403
+ tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
1404
+ children: this.children.map(node => node.toJSON()),
1405
+ tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
1406
+ };
1407
+ }
1408
+ treeInspect() {
1409
+ let output = "";
1410
+ output += `@ XMLDeclarationNode ${this.location.treeInspectWithLabel()}\n`;
1411
+ output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
1412
+ output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
1413
+ output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
1414
+ output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1415
+ return output;
1416
+ }
1417
+ }
1418
+ class CDATANode extends Node {
1419
+ tag_opening;
1420
+ children;
1421
+ tag_closing;
1422
+ static get type() {
1423
+ return "AST_CDATA_NODE";
1424
+ }
1425
+ static from(data) {
1426
+ return new CDATANode({
1427
+ type: data.type,
1428
+ location: Location.from(data.location),
1429
+ errors: (data.errors || []).map(error => HerbError.from(error)),
1430
+ tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
1431
+ children: (data.children || []).map(node => fromSerializedNode(node)),
1432
+ tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1433
+ });
1434
+ }
1435
+ constructor(props) {
1436
+ super(props.type, props.location, props.errors);
1437
+ this.tag_opening = props.tag_opening;
1438
+ this.children = props.children;
1439
+ this.tag_closing = props.tag_closing;
1440
+ }
1441
+ accept(visitor) {
1442
+ visitor.visitCDATANode(this);
1443
+ }
1444
+ childNodes() {
1445
+ return [
1446
+ ...this.children,
1447
+ ];
1448
+ }
1449
+ compactChildNodes() {
1450
+ return this.childNodes().filter(node => node !== null && node !== undefined);
1451
+ }
1452
+ recursiveErrors() {
1453
+ return [
1454
+ ...this.errors,
1455
+ ...this.children.map(node => node.recursiveErrors()),
1456
+ ].flat();
1457
+ }
1458
+ toJSON() {
1459
+ return {
1460
+ ...super.toJSON(),
1461
+ type: "AST_CDATA_NODE",
1462
+ tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
1463
+ children: this.children.map(node => node.toJSON()),
1464
+ tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
1465
+ };
1466
+ }
1467
+ treeInspect() {
1468
+ let output = "";
1469
+ output += `@ CDATANode ${this.location.treeInspectWithLabel()}\n`;
1470
+ output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
1471
+ output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
1472
+ output += `├── children: ${this.inspectArray(this.children, "│ ")}`;
1473
+ output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1314
1474
  return output;
1315
1475
  }
1316
1476
  }
1317
1477
  class WhitespaceNode extends Node {
1318
1478
  value;
1479
+ static get type() {
1480
+ return "AST_WHITESPACE_NODE";
1481
+ }
1319
1482
  static from(data) {
1320
1483
  return new WhitespaceNode({
1321
1484
  type: data.type,
@@ -1354,7 +1517,6 @@ class WhitespaceNode extends Node {
1354
1517
  output += `@ WhitespaceNode ${this.location.treeInspectWithLabel()}\n`;
1355
1518
  output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
1356
1519
  output += `└── value: ${this.value ? this.value.treeInspect() : "∅"}\n`;
1357
- // output += "\n";
1358
1520
  return output;
1359
1521
  }
1360
1522
  }
@@ -1365,6 +1527,9 @@ class ERBContentNode extends Node {
1365
1527
  // no-op for analyzed_ruby
1366
1528
  parsed;
1367
1529
  valid;
1530
+ static get type() {
1531
+ return "AST_ERB_CONTENT_NODE";
1532
+ }
1368
1533
  static from(data) {
1369
1534
  return new ERBContentNode({
1370
1535
  type: data.type,
@@ -1423,7 +1588,6 @@ class ERBContentNode extends Node {
1423
1588
  // no-op for analyzed_ruby
1424
1589
  output += `├── parsed: ${typeof this.parsed === 'boolean' ? String(this.parsed) : "∅"}\n`;
1425
1590
  output += `└── valid: ${typeof this.valid === 'boolean' ? String(this.valid) : "∅"}\n`;
1426
- // output += "\n";
1427
1591
  return output;
1428
1592
  }
1429
1593
  }
@@ -1431,6 +1595,9 @@ class ERBEndNode extends Node {
1431
1595
  tag_opening;
1432
1596
  content;
1433
1597
  tag_closing;
1598
+ static get type() {
1599
+ return "AST_ERB_END_NODE";
1600
+ }
1434
1601
  static from(data) {
1435
1602
  return new ERBEndNode({
1436
1603
  type: data.type,
@@ -1477,7 +1644,6 @@ class ERBEndNode extends Node {
1477
1644
  output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
1478
1645
  output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
1479
1646
  output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1480
- // output += "\n";
1481
1647
  return output;
1482
1648
  }
1483
1649
  }
@@ -1486,6 +1652,9 @@ class ERBElseNode extends Node {
1486
1652
  content;
1487
1653
  tag_closing;
1488
1654
  statements;
1655
+ static get type() {
1656
+ return "AST_ERB_ELSE_NODE";
1657
+ }
1489
1658
  static from(data) {
1490
1659
  return new ERBElseNode({
1491
1660
  type: data.type,
@@ -1539,7 +1708,6 @@ class ERBElseNode extends Node {
1539
1708
  output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
1540
1709
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1541
1710
  output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
1542
- // output += "\n";
1543
1711
  return output;
1544
1712
  }
1545
1713
  }
@@ -1550,6 +1718,9 @@ class ERBIfNode extends Node {
1550
1718
  statements;
1551
1719
  subsequent;
1552
1720
  end_node;
1721
+ static get type() {
1722
+ return "AST_ERB_IF_NODE";
1723
+ }
1553
1724
  static from(data) {
1554
1725
  return new ERBIfNode({
1555
1726
  type: data.type,
@@ -1559,8 +1730,8 @@ class ERBIfNode extends Node {
1559
1730
  content: data.content ? Token.from(data.content) : null,
1560
1731
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1561
1732
  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,
1733
+ subsequent: data.subsequent ? fromSerializedNode((data.subsequent)) : null,
1734
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
1564
1735
  });
1565
1736
  }
1566
1737
  constructor(props) {
@@ -1615,7 +1786,6 @@ class ERBIfNode extends Node {
1615
1786
  output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
1616
1787
  output += `├── subsequent: ${this.inspectNode(this.subsequent, "│ ")}`;
1617
1788
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
1618
- // output += "\n";
1619
1789
  return output;
1620
1790
  }
1621
1791
  }
@@ -1625,6 +1795,9 @@ class ERBBlockNode extends Node {
1625
1795
  tag_closing;
1626
1796
  body;
1627
1797
  end_node;
1798
+ static get type() {
1799
+ return "AST_ERB_BLOCK_NODE";
1800
+ }
1628
1801
  static from(data) {
1629
1802
  return new ERBBlockNode({
1630
1803
  type: data.type,
@@ -1634,7 +1807,7 @@ class ERBBlockNode extends Node {
1634
1807
  content: data.content ? Token.from(data.content) : null,
1635
1808
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1636
1809
  body: (data.body || []).map(node => fromSerializedNode(node)),
1637
- end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
1810
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
1638
1811
  });
1639
1812
  }
1640
1813
  constructor(props) {
@@ -1684,7 +1857,6 @@ class ERBBlockNode extends Node {
1684
1857
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1685
1858
  output += `├── body: ${this.inspectArray(this.body, "│ ")}`;
1686
1859
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
1687
- // output += "\n";
1688
1860
  return output;
1689
1861
  }
1690
1862
  }
@@ -1693,6 +1865,9 @@ class ERBWhenNode extends Node {
1693
1865
  content;
1694
1866
  tag_closing;
1695
1867
  statements;
1868
+ static get type() {
1869
+ return "AST_ERB_WHEN_NODE";
1870
+ }
1696
1871
  static from(data) {
1697
1872
  return new ERBWhenNode({
1698
1873
  type: data.type,
@@ -1746,7 +1921,6 @@ class ERBWhenNode extends Node {
1746
1921
  output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
1747
1922
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1748
1923
  output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
1749
- // output += "\n";
1750
1924
  return output;
1751
1925
  }
1752
1926
  }
@@ -1758,6 +1932,9 @@ class ERBCaseNode extends Node {
1758
1932
  conditions;
1759
1933
  else_clause;
1760
1934
  end_node;
1935
+ static get type() {
1936
+ return "AST_ERB_CASE_NODE";
1937
+ }
1761
1938
  static from(data) {
1762
1939
  return new ERBCaseNode({
1763
1940
  type: data.type,
@@ -1768,8 +1945,8 @@ class ERBCaseNode extends Node {
1768
1945
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1769
1946
  children: (data.children || []).map(node => fromSerializedNode(node)),
1770
1947
  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,
1948
+ else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
1949
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
1773
1950
  });
1774
1951
  }
1775
1952
  constructor(props) {
@@ -1829,7 +2006,6 @@ class ERBCaseNode extends Node {
1829
2006
  output += `├── conditions: ${this.inspectArray(this.conditions, "│ ")}`;
1830
2007
  output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
1831
2008
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
1832
- // output += "\n";
1833
2009
  return output;
1834
2010
  }
1835
2011
  }
@@ -1841,6 +2017,9 @@ class ERBCaseMatchNode extends Node {
1841
2017
  conditions;
1842
2018
  else_clause;
1843
2019
  end_node;
2020
+ static get type() {
2021
+ return "AST_ERB_CASE_MATCH_NODE";
2022
+ }
1844
2023
  static from(data) {
1845
2024
  return new ERBCaseMatchNode({
1846
2025
  type: data.type,
@@ -1851,8 +2030,8 @@ class ERBCaseMatchNode extends Node {
1851
2030
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1852
2031
  children: (data.children || []).map(node => fromSerializedNode(node)),
1853
2032
  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,
2033
+ else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
2034
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
1856
2035
  });
1857
2036
  }
1858
2037
  constructor(props) {
@@ -1912,7 +2091,6 @@ class ERBCaseMatchNode extends Node {
1912
2091
  output += `├── conditions: ${this.inspectArray(this.conditions, "│ ")}`;
1913
2092
  output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
1914
2093
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
1915
- // output += "\n";
1916
2094
  return output;
1917
2095
  }
1918
2096
  }
@@ -1922,6 +2100,9 @@ class ERBWhileNode extends Node {
1922
2100
  tag_closing;
1923
2101
  statements;
1924
2102
  end_node;
2103
+ static get type() {
2104
+ return "AST_ERB_WHILE_NODE";
2105
+ }
1925
2106
  static from(data) {
1926
2107
  return new ERBWhileNode({
1927
2108
  type: data.type,
@@ -1931,7 +2112,7 @@ class ERBWhileNode extends Node {
1931
2112
  content: data.content ? Token.from(data.content) : null,
1932
2113
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
1933
2114
  statements: (data.statements || []).map(node => fromSerializedNode(node)),
1934
- end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
2115
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
1935
2116
  });
1936
2117
  }
1937
2118
  constructor(props) {
@@ -1981,7 +2162,6 @@ class ERBWhileNode extends Node {
1981
2162
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
1982
2163
  output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
1983
2164
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
1984
- // output += "\n";
1985
2165
  return output;
1986
2166
  }
1987
2167
  }
@@ -1991,6 +2171,9 @@ class ERBUntilNode extends Node {
1991
2171
  tag_closing;
1992
2172
  statements;
1993
2173
  end_node;
2174
+ static get type() {
2175
+ return "AST_ERB_UNTIL_NODE";
2176
+ }
1994
2177
  static from(data) {
1995
2178
  return new ERBUntilNode({
1996
2179
  type: data.type,
@@ -2000,7 +2183,7 @@ class ERBUntilNode extends Node {
2000
2183
  content: data.content ? Token.from(data.content) : null,
2001
2184
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
2002
2185
  statements: (data.statements || []).map(node => fromSerializedNode(node)),
2003
- end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
2186
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
2004
2187
  });
2005
2188
  }
2006
2189
  constructor(props) {
@@ -2050,7 +2233,6 @@ class ERBUntilNode extends Node {
2050
2233
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
2051
2234
  output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
2052
2235
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
2053
- // output += "\n";
2054
2236
  return output;
2055
2237
  }
2056
2238
  }
@@ -2060,6 +2242,9 @@ class ERBForNode extends Node {
2060
2242
  tag_closing;
2061
2243
  statements;
2062
2244
  end_node;
2245
+ static get type() {
2246
+ return "AST_ERB_FOR_NODE";
2247
+ }
2063
2248
  static from(data) {
2064
2249
  return new ERBForNode({
2065
2250
  type: data.type,
@@ -2069,7 +2254,7 @@ class ERBForNode extends Node {
2069
2254
  content: data.content ? Token.from(data.content) : null,
2070
2255
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
2071
2256
  statements: (data.statements || []).map(node => fromSerializedNode(node)),
2072
- end_node: data.end_node ? fromSerializedNode(data.end_node) : null,
2257
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
2073
2258
  });
2074
2259
  }
2075
2260
  constructor(props) {
@@ -2119,7 +2304,6 @@ class ERBForNode extends Node {
2119
2304
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
2120
2305
  output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
2121
2306
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
2122
- // output += "\n";
2123
2307
  return output;
2124
2308
  }
2125
2309
  }
@@ -2129,6 +2313,9 @@ class ERBRescueNode extends Node {
2129
2313
  tag_closing;
2130
2314
  statements;
2131
2315
  subsequent;
2316
+ static get type() {
2317
+ return "AST_ERB_RESCUE_NODE";
2318
+ }
2132
2319
  static from(data) {
2133
2320
  return new ERBRescueNode({
2134
2321
  type: data.type,
@@ -2138,7 +2325,7 @@ class ERBRescueNode extends Node {
2138
2325
  content: data.content ? Token.from(data.content) : null,
2139
2326
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
2140
2327
  statements: (data.statements || []).map(node => fromSerializedNode(node)),
2141
- subsequent: data.subsequent ? fromSerializedNode(data.subsequent) : null,
2328
+ subsequent: data.subsequent ? fromSerializedNode((data.subsequent)) : null,
2142
2329
  });
2143
2330
  }
2144
2331
  constructor(props) {
@@ -2188,7 +2375,6 @@ class ERBRescueNode extends Node {
2188
2375
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
2189
2376
  output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
2190
2377
  output += `└── subsequent: ${this.inspectNode(this.subsequent, " ")}`;
2191
- // output += "\n";
2192
2378
  return output;
2193
2379
  }
2194
2380
  }
@@ -2197,6 +2383,9 @@ class ERBEnsureNode extends Node {
2197
2383
  content;
2198
2384
  tag_closing;
2199
2385
  statements;
2386
+ static get type() {
2387
+ return "AST_ERB_ENSURE_NODE";
2388
+ }
2200
2389
  static from(data) {
2201
2390
  return new ERBEnsureNode({
2202
2391
  type: data.type,
@@ -2250,7 +2439,6 @@ class ERBEnsureNode extends Node {
2250
2439
  output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
2251
2440
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
2252
2441
  output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
2253
- // output += "\n";
2254
2442
  return output;
2255
2443
  }
2256
2444
  }
@@ -2263,6 +2451,9 @@ class ERBBeginNode extends Node {
2263
2451
  else_clause;
2264
2452
  ensure_clause;
2265
2453
  end_node;
2454
+ static get type() {
2455
+ return "AST_ERB_BEGIN_NODE";
2456
+ }
2266
2457
  static from(data) {
2267
2458
  return new ERBBeginNode({
2268
2459
  type: data.type,
@@ -2272,10 +2463,10 @@ class ERBBeginNode extends Node {
2272
2463
  content: data.content ? Token.from(data.content) : null,
2273
2464
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
2274
2465
  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,
2466
+ rescue_clause: data.rescue_clause ? fromSerializedNode((data.rescue_clause)) : null,
2467
+ else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
2468
+ ensure_clause: data.ensure_clause ? fromSerializedNode((data.ensure_clause)) : null,
2469
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
2279
2470
  });
2280
2471
  }
2281
2472
  constructor(props) {
@@ -2340,7 +2531,6 @@ class ERBBeginNode extends Node {
2340
2531
  output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
2341
2532
  output += `├── ensure_clause: ${this.inspectNode(this.ensure_clause, "│ ")}`;
2342
2533
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
2343
- // output += "\n";
2344
2534
  return output;
2345
2535
  }
2346
2536
  }
@@ -2351,6 +2541,9 @@ class ERBUnlessNode extends Node {
2351
2541
  statements;
2352
2542
  else_clause;
2353
2543
  end_node;
2544
+ static get type() {
2545
+ return "AST_ERB_UNLESS_NODE";
2546
+ }
2354
2547
  static from(data) {
2355
2548
  return new ERBUnlessNode({
2356
2549
  type: data.type,
@@ -2360,8 +2553,8 @@ class ERBUnlessNode extends Node {
2360
2553
  content: data.content ? Token.from(data.content) : null,
2361
2554
  tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
2362
2555
  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,
2556
+ else_clause: data.else_clause ? fromSerializedNode((data.else_clause)) : null,
2557
+ end_node: data.end_node ? fromSerializedNode((data.end_node)) : null,
2365
2558
  });
2366
2559
  }
2367
2560
  constructor(props) {
@@ -2416,7 +2609,6 @@ class ERBUnlessNode extends Node {
2416
2609
  output += `├── statements: ${this.inspectArray(this.statements, "│ ")}`;
2417
2610
  output += `├── else_clause: ${this.inspectNode(this.else_clause, "│ ")}`;
2418
2611
  output += `└── end_node: ${this.inspectNode(this.end_node, " ")}`;
2419
- // output += "\n";
2420
2612
  return output;
2421
2613
  }
2422
2614
  }
@@ -2424,6 +2616,9 @@ class ERBYieldNode extends Node {
2424
2616
  tag_opening;
2425
2617
  content;
2426
2618
  tag_closing;
2619
+ static get type() {
2620
+ return "AST_ERB_YIELD_NODE";
2621
+ }
2427
2622
  static from(data) {
2428
2623
  return new ERBYieldNode({
2429
2624
  type: data.type,
@@ -2470,7 +2665,6 @@ class ERBYieldNode extends Node {
2470
2665
  output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
2471
2666
  output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
2472
2667
  output += `└── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
2473
- // output += "\n";
2474
2668
  return output;
2475
2669
  }
2476
2670
  }
@@ -2479,6 +2673,9 @@ class ERBInNode extends Node {
2479
2673
  content;
2480
2674
  tag_closing;
2481
2675
  statements;
2676
+ static get type() {
2677
+ return "AST_ERB_IN_NODE";
2678
+ }
2482
2679
  static from(data) {
2483
2680
  return new ERBInNode({
2484
2681
  type: data.type,
@@ -2532,7 +2729,6 @@ class ERBInNode extends Node {
2532
2729
  output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
2533
2730
  output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
2534
2731
  output += `└── statements: ${this.inspectArray(this.statements, " ")}`;
2535
- // output += "\n";
2536
2732
  return output;
2537
2733
  }
2538
2734
  }
@@ -2542,7 +2738,6 @@ function fromSerializedNode(node) {
2542
2738
  case "AST_LITERAL_NODE": return LiteralNode.from(node);
2543
2739
  case "AST_HTML_OPEN_TAG_NODE": return HTMLOpenTagNode.from(node);
2544
2740
  case "AST_HTML_CLOSE_TAG_NODE": return HTMLCloseTagNode.from(node);
2545
- case "AST_HTML_SELF_CLOSE_TAG_NODE": return HTMLSelfCloseTagNode.from(node);
2546
2741
  case "AST_HTML_ELEMENT_NODE": return HTMLElementNode.from(node);
2547
2742
  case "AST_HTML_ATTRIBUTE_VALUE_NODE": return HTMLAttributeValueNode.from(node);
2548
2743
  case "AST_HTML_ATTRIBUTE_NAME_NODE": return HTMLAttributeNameNode.from(node);
@@ -2550,6 +2745,8 @@ function fromSerializedNode(node) {
2550
2745
  case "AST_HTML_TEXT_NODE": return HTMLTextNode.from(node);
2551
2746
  case "AST_HTML_COMMENT_NODE": return HTMLCommentNode.from(node);
2552
2747
  case "AST_HTML_DOCTYPE_NODE": return HTMLDoctypeNode.from(node);
2748
+ case "AST_XML_DECLARATION_NODE": return XMLDeclarationNode.from(node);
2749
+ case "AST_CDATA_NODE": return CDATANode.from(node);
2553
2750
  case "AST_WHITESPACE_NODE": return WhitespaceNode.from(node);
2554
2751
  case "AST_ERB_CONTENT_NODE": return ERBContentNode.from(node);
2555
2752
  case "AST_ERB_END_NODE": return ERBEndNode.from(node);
@@ -2573,33 +2770,617 @@ function fromSerializedNode(node) {
2573
2770
  }
2574
2771
  }
2575
2772
 
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));
2773
+ class Result {
2774
+ source;
2775
+ warnings;
2776
+ errors;
2777
+ constructor(source, warnings = [], errors = []) {
2778
+ this.source = source;
2779
+ this.warnings = warnings || [];
2780
+ this.errors = errors || [];
2586
2781
  }
2587
- visitChildNodes(node) {
2588
- node.compactChildNodes().forEach(node => node.accept(this));
2782
+ /**
2783
+ * Determines if the parsing was successful.
2784
+ * @returns `true` if there are no errors, otherwise `false`.
2785
+ */
2786
+ get successful() {
2787
+ return this.errors.length === 0;
2589
2788
  }
2590
- visitDocumentNode(node) {
2591
- this.visitChildNodes(node);
2789
+ /**
2790
+ * Determines if the parsing failed.
2791
+ * @returns `true` if there are errors, otherwise `false`.
2792
+ */
2793
+ get failed() {
2794
+ return this.errors.length > 0;
2592
2795
  }
2593
- visitLiteralNode(node) {
2594
- this.visitChildNodes(node);
2796
+ }
2797
+
2798
+ class HerbWarning {
2799
+ message;
2800
+ location;
2801
+ static from(warning) {
2802
+ return new HerbWarning(warning.message, Location.from(warning.location));
2595
2803
  }
2596
- visitHTMLOpenTagNode(node) {
2597
- this.visitChildNodes(node);
2804
+ constructor(message, location) {
2805
+ this.message = message;
2806
+ this.location = location;
2598
2807
  }
2599
- visitHTMLCloseTagNode(node) {
2808
+ }
2809
+
2810
+ /**
2811
+ * Represents the result of a parsing operation, extending the base `Result` class.
2812
+ * It contains the parsed document node, source code, warnings, and errors.
2813
+ */
2814
+ class ParseResult extends Result {
2815
+ /** The document node generated from the source code. */
2816
+ value;
2817
+ /**
2818
+ * Creates a `ParseResult` instance from a serialized result.
2819
+ * @param result - The serialized parse result containing the value and source.
2820
+ * @returns A new `ParseResult` instance.
2821
+ */
2822
+ static from(result) {
2823
+ return new ParseResult(DocumentNode.from(result.value), result.source, result.warnings.map((warning) => HerbWarning.from(warning)), result.errors.map((error) => HerbError.from(error)));
2824
+ }
2825
+ /**
2826
+ * Constructs a new `ParseResult`.
2827
+ * @param value - The document node.
2828
+ * @param source - The source code that was parsed.
2829
+ * @param warnings - An array of warnings encountered during parsing.
2830
+ * @param errors - An array of errors encountered during parsing.
2831
+ */
2832
+ constructor(value, source, warnings = [], errors = []) {
2833
+ super(source, warnings, errors);
2834
+ this.value = value;
2835
+ }
2836
+ /**
2837
+ * Determines if the parsing failed.
2838
+ * @returns `true` if there are errors, otherwise `false`.
2839
+ */
2840
+ get failed() {
2841
+ // Consider errors on this result and recursively in the document tree
2842
+ return this.recursiveErrors().length > 0;
2843
+ }
2844
+ /**
2845
+ * Determines if the parsing was successful.
2846
+ * @returns `true` if there are no errors, otherwise `false`.
2847
+ */
2848
+ get successful() {
2849
+ return !this.failed;
2850
+ }
2851
+ /**
2852
+ * Returns a pretty-printed JSON string of the errors.
2853
+ * @returns A string representation of the errors.
2854
+ */
2855
+ prettyErrors() {
2856
+ return JSON.stringify([...this.errors, ...this.value.errors], null, 2);
2857
+ }
2858
+ recursiveErrors() {
2859
+ return [...this.errors, ...this.value.recursiveErrors()];
2860
+ }
2861
+ /**
2862
+ * Returns a pretty-printed string of the parse result.
2863
+ * @returns A string representation of the parse result.
2864
+ */
2865
+ inspect() {
2866
+ return this.value.inspect();
2867
+ }
2868
+ /**
2869
+ * Accepts a visitor to traverse the document node.
2870
+ * @param visitor - The visitor instance.
2871
+ */
2872
+ visit(visitor) {
2873
+ visitor.visit(this.value);
2874
+ }
2875
+ }
2876
+
2877
+ // NOTE: This file is generated by the templates/template.rb script and should not
2878
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.6.1/templates/javascript/packages/core/src/node-type-guards.ts.erb
2879
+ /**
2880
+ * Type guard functions for AST nodes.
2881
+ * These functions provide type checking by combining both instanceof
2882
+ * checks and type string comparisons for maximum reliability across different
2883
+ * runtime scenarios (e.g., serialized/deserialized nodes).
2884
+ */
2885
+ /**
2886
+ * Checks if a node is a DocumentNode
2887
+ */
2888
+ function isDocumentNode(node) {
2889
+ return node instanceof DocumentNode || node.type === "AST_DOCUMENT_NODE" || node.constructor.type === "AST_DOCUMENT_NODE";
2890
+ }
2891
+ /**
2892
+ * Checks if a node is a LiteralNode
2893
+ */
2894
+ function isLiteralNode(node) {
2895
+ return node instanceof LiteralNode || node.type === "AST_LITERAL_NODE" || node.constructor.type === "AST_LITERAL_NODE";
2896
+ }
2897
+ /**
2898
+ * Checks if a node is a HTMLOpenTagNode
2899
+ */
2900
+ function isHTMLOpenTagNode(node) {
2901
+ return node instanceof HTMLOpenTagNode || node.type === "AST_HTML_OPEN_TAG_NODE" || node.constructor.type === "AST_HTML_OPEN_TAG_NODE";
2902
+ }
2903
+ /**
2904
+ * Checks if a node is a HTMLCloseTagNode
2905
+ */
2906
+ function isHTMLCloseTagNode(node) {
2907
+ return node instanceof HTMLCloseTagNode || node.type === "AST_HTML_CLOSE_TAG_NODE" || node.constructor.type === "AST_HTML_CLOSE_TAG_NODE";
2908
+ }
2909
+ /**
2910
+ * Checks if a node is a HTMLElementNode
2911
+ */
2912
+ function isHTMLElementNode(node) {
2913
+ return node instanceof HTMLElementNode || node.type === "AST_HTML_ELEMENT_NODE" || node.constructor.type === "AST_HTML_ELEMENT_NODE";
2914
+ }
2915
+ /**
2916
+ * Checks if a node is a HTMLAttributeValueNode
2917
+ */
2918
+ function isHTMLAttributeValueNode(node) {
2919
+ return node instanceof HTMLAttributeValueNode || node.type === "AST_HTML_ATTRIBUTE_VALUE_NODE" || node.constructor.type === "AST_HTML_ATTRIBUTE_VALUE_NODE";
2920
+ }
2921
+ /**
2922
+ * Checks if a node is a HTMLAttributeNameNode
2923
+ */
2924
+ function isHTMLAttributeNameNode(node) {
2925
+ return node instanceof HTMLAttributeNameNode || node.type === "AST_HTML_ATTRIBUTE_NAME_NODE" || node.constructor.type === "AST_HTML_ATTRIBUTE_NAME_NODE";
2926
+ }
2927
+ /**
2928
+ * Checks if a node is a HTMLAttributeNode
2929
+ */
2930
+ function isHTMLAttributeNode(node) {
2931
+ return node instanceof HTMLAttributeNode || node.type === "AST_HTML_ATTRIBUTE_NODE" || node.constructor.type === "AST_HTML_ATTRIBUTE_NODE";
2932
+ }
2933
+ /**
2934
+ * Checks if a node is a HTMLTextNode
2935
+ */
2936
+ function isHTMLTextNode(node) {
2937
+ return node instanceof HTMLTextNode || node.type === "AST_HTML_TEXT_NODE" || node.constructor.type === "AST_HTML_TEXT_NODE";
2938
+ }
2939
+ /**
2940
+ * Checks if a node is a HTMLCommentNode
2941
+ */
2942
+ function isHTMLCommentNode(node) {
2943
+ return node instanceof HTMLCommentNode || node.type === "AST_HTML_COMMENT_NODE" || node.constructor.type === "AST_HTML_COMMENT_NODE";
2944
+ }
2945
+ /**
2946
+ * Checks if a node is a HTMLDoctypeNode
2947
+ */
2948
+ function isHTMLDoctypeNode(node) {
2949
+ return node instanceof HTMLDoctypeNode || node.type === "AST_HTML_DOCTYPE_NODE" || node.constructor.type === "AST_HTML_DOCTYPE_NODE";
2950
+ }
2951
+ /**
2952
+ * Checks if a node is a XMLDeclarationNode
2953
+ */
2954
+ function isXMLDeclarationNode(node) {
2955
+ return node instanceof XMLDeclarationNode || node.type === "AST_XML_DECLARATION_NODE" || node.constructor.type === "AST_XML_DECLARATION_NODE";
2956
+ }
2957
+ /**
2958
+ * Checks if a node is a CDATANode
2959
+ */
2960
+ function isCDATANode(node) {
2961
+ return node instanceof CDATANode || node.type === "AST_CDATA_NODE" || node.constructor.type === "AST_CDATA_NODE";
2962
+ }
2963
+ /**
2964
+ * Checks if a node is a WhitespaceNode
2965
+ */
2966
+ function isWhitespaceNode(node) {
2967
+ return node instanceof WhitespaceNode || node.type === "AST_WHITESPACE_NODE" || node.constructor.type === "AST_WHITESPACE_NODE";
2968
+ }
2969
+ /**
2970
+ * Checks if a node is a ERBContentNode
2971
+ */
2972
+ function isERBContentNode(node) {
2973
+ return node instanceof ERBContentNode || node.type === "AST_ERB_CONTENT_NODE" || node.constructor.type === "AST_ERB_CONTENT_NODE";
2974
+ }
2975
+ /**
2976
+ * Checks if a node is a ERBEndNode
2977
+ */
2978
+ function isERBEndNode(node) {
2979
+ return node instanceof ERBEndNode || node.type === "AST_ERB_END_NODE" || node.constructor.type === "AST_ERB_END_NODE";
2980
+ }
2981
+ /**
2982
+ * Checks if a node is a ERBElseNode
2983
+ */
2984
+ function isERBElseNode(node) {
2985
+ return node instanceof ERBElseNode || node.type === "AST_ERB_ELSE_NODE" || node.constructor.type === "AST_ERB_ELSE_NODE";
2986
+ }
2987
+ /**
2988
+ * Checks if a node is a ERBIfNode
2989
+ */
2990
+ function isERBIfNode(node) {
2991
+ return node instanceof ERBIfNode || node.type === "AST_ERB_IF_NODE" || node.constructor.type === "AST_ERB_IF_NODE";
2992
+ }
2993
+ /**
2994
+ * Checks if a node is a ERBBlockNode
2995
+ */
2996
+ function isERBBlockNode(node) {
2997
+ return node instanceof ERBBlockNode || node.type === "AST_ERB_BLOCK_NODE" || node.constructor.type === "AST_ERB_BLOCK_NODE";
2998
+ }
2999
+ /**
3000
+ * Checks if a node is a ERBWhenNode
3001
+ */
3002
+ function isERBWhenNode(node) {
3003
+ return node instanceof ERBWhenNode || node.type === "AST_ERB_WHEN_NODE" || node.constructor.type === "AST_ERB_WHEN_NODE";
3004
+ }
3005
+ /**
3006
+ * Checks if a node is a ERBCaseNode
3007
+ */
3008
+ function isERBCaseNode(node) {
3009
+ return node instanceof ERBCaseNode || node.type === "AST_ERB_CASE_NODE" || node.constructor.type === "AST_ERB_CASE_NODE";
3010
+ }
3011
+ /**
3012
+ * Checks if a node is a ERBCaseMatchNode
3013
+ */
3014
+ function isERBCaseMatchNode(node) {
3015
+ return node instanceof ERBCaseMatchNode || node.type === "AST_ERB_CASE_MATCH_NODE" || node.constructor.type === "AST_ERB_CASE_MATCH_NODE";
3016
+ }
3017
+ /**
3018
+ * Checks if a node is a ERBWhileNode
3019
+ */
3020
+ function isERBWhileNode(node) {
3021
+ return node instanceof ERBWhileNode || node.type === "AST_ERB_WHILE_NODE" || node.constructor.type === "AST_ERB_WHILE_NODE";
3022
+ }
3023
+ /**
3024
+ * Checks if a node is a ERBUntilNode
3025
+ */
3026
+ function isERBUntilNode(node) {
3027
+ return node instanceof ERBUntilNode || node.type === "AST_ERB_UNTIL_NODE" || node.constructor.type === "AST_ERB_UNTIL_NODE";
3028
+ }
3029
+ /**
3030
+ * Checks if a node is a ERBForNode
3031
+ */
3032
+ function isERBForNode(node) {
3033
+ return node instanceof ERBForNode || node.type === "AST_ERB_FOR_NODE" || node.constructor.type === "AST_ERB_FOR_NODE";
3034
+ }
3035
+ /**
3036
+ * Checks if a node is a ERBRescueNode
3037
+ */
3038
+ function isERBRescueNode(node) {
3039
+ return node instanceof ERBRescueNode || node.type === "AST_ERB_RESCUE_NODE" || node.constructor.type === "AST_ERB_RESCUE_NODE";
3040
+ }
3041
+ /**
3042
+ * Checks if a node is a ERBEnsureNode
3043
+ */
3044
+ function isERBEnsureNode(node) {
3045
+ return node instanceof ERBEnsureNode || node.type === "AST_ERB_ENSURE_NODE" || node.constructor.type === "AST_ERB_ENSURE_NODE";
3046
+ }
3047
+ /**
3048
+ * Checks if a node is a ERBBeginNode
3049
+ */
3050
+ function isERBBeginNode(node) {
3051
+ return node instanceof ERBBeginNode || node.type === "AST_ERB_BEGIN_NODE" || node.constructor.type === "AST_ERB_BEGIN_NODE";
3052
+ }
3053
+ /**
3054
+ * Checks if a node is a ERBUnlessNode
3055
+ */
3056
+ function isERBUnlessNode(node) {
3057
+ return node instanceof ERBUnlessNode || node.type === "AST_ERB_UNLESS_NODE" || node.constructor.type === "AST_ERB_UNLESS_NODE";
3058
+ }
3059
+ /**
3060
+ * Checks if a node is a ERBYieldNode
3061
+ */
3062
+ function isERBYieldNode(node) {
3063
+ return node instanceof ERBYieldNode || node.type === "AST_ERB_YIELD_NODE" || node.constructor.type === "AST_ERB_YIELD_NODE";
3064
+ }
3065
+ /**
3066
+ * Checks if a node is a ERBInNode
3067
+ */
3068
+ function isERBInNode(node) {
3069
+ return node instanceof ERBInNode || node.type === "AST_ERB_IN_NODE" || node.constructor.type === "AST_ERB_IN_NODE";
3070
+ }
3071
+ /**
3072
+ * Checks if a node is any ERB node type
3073
+ */
3074
+ function isERBNode(node) {
3075
+ return isERBContentNode(node) ||
3076
+ isERBEndNode(node) ||
3077
+ isERBElseNode(node) ||
3078
+ isERBIfNode(node) ||
3079
+ isERBBlockNode(node) ||
3080
+ isERBWhenNode(node) ||
3081
+ isERBCaseNode(node) ||
3082
+ isERBCaseMatchNode(node) ||
3083
+ isERBWhileNode(node) ||
3084
+ isERBUntilNode(node) ||
3085
+ isERBForNode(node) ||
3086
+ isERBRescueNode(node) ||
3087
+ isERBEnsureNode(node) ||
3088
+ isERBBeginNode(node) ||
3089
+ isERBUnlessNode(node) ||
3090
+ isERBYieldNode(node) ||
3091
+ isERBInNode(node);
3092
+ }
3093
+ /**
3094
+ * Map of node classes to their corresponding type guard functions
3095
+ *
3096
+ * @example
3097
+ * const guard = NODE_TYPE_GUARDS[HTMLTextNode]
3098
+ *
3099
+ * if (guard(node)) {
3100
+ * // node is HTMLTextNode
3101
+ * }
3102
+ */
3103
+ const NODE_TYPE_GUARDS = new Map([
3104
+ [DocumentNode, isDocumentNode],
3105
+ [LiteralNode, isLiteralNode],
3106
+ [HTMLOpenTagNode, isHTMLOpenTagNode],
3107
+ [HTMLCloseTagNode, isHTMLCloseTagNode],
3108
+ [HTMLElementNode, isHTMLElementNode],
3109
+ [HTMLAttributeValueNode, isHTMLAttributeValueNode],
3110
+ [HTMLAttributeNameNode, isHTMLAttributeNameNode],
3111
+ [HTMLAttributeNode, isHTMLAttributeNode],
3112
+ [HTMLTextNode, isHTMLTextNode],
3113
+ [HTMLCommentNode, isHTMLCommentNode],
3114
+ [HTMLDoctypeNode, isHTMLDoctypeNode],
3115
+ [XMLDeclarationNode, isXMLDeclarationNode],
3116
+ [CDATANode, isCDATANode],
3117
+ [WhitespaceNode, isWhitespaceNode],
3118
+ [ERBContentNode, isERBContentNode],
3119
+ [ERBEndNode, isERBEndNode],
3120
+ [ERBElseNode, isERBElseNode],
3121
+ [ERBIfNode, isERBIfNode],
3122
+ [ERBBlockNode, isERBBlockNode],
3123
+ [ERBWhenNode, isERBWhenNode],
3124
+ [ERBCaseNode, isERBCaseNode],
3125
+ [ERBCaseMatchNode, isERBCaseMatchNode],
3126
+ [ERBWhileNode, isERBWhileNode],
3127
+ [ERBUntilNode, isERBUntilNode],
3128
+ [ERBForNode, isERBForNode],
3129
+ [ERBRescueNode, isERBRescueNode],
3130
+ [ERBEnsureNode, isERBEnsureNode],
3131
+ [ERBBeginNode, isERBBeginNode],
3132
+ [ERBUnlessNode, isERBUnlessNode],
3133
+ [ERBYieldNode, isERBYieldNode],
3134
+ [ERBInNode, isERBInNode],
3135
+ ]);
3136
+ /**
3137
+ * Map of AST node type strings to their corresponding type guard functions
3138
+ *
3139
+ * @example
3140
+ * const guard = AST_TYPE_GUARDS["AST_HTML_TEXT_NODE"]
3141
+ *
3142
+ * if (guard(node)) {
3143
+ * // node is HTMLTextNode
3144
+ * }
3145
+ */
3146
+ const AST_TYPE_GUARDS = new Map([
3147
+ ["AST_DOCUMENT_NODE", isDocumentNode],
3148
+ ["AST_LITERAL_NODE", isLiteralNode],
3149
+ ["AST_HTML_OPEN_TAG_NODE", isHTMLOpenTagNode],
3150
+ ["AST_HTML_CLOSE_TAG_NODE", isHTMLCloseTagNode],
3151
+ ["AST_HTML_ELEMENT_NODE", isHTMLElementNode],
3152
+ ["AST_HTML_ATTRIBUTE_VALUE_NODE", isHTMLAttributeValueNode],
3153
+ ["AST_HTML_ATTRIBUTE_NAME_NODE", isHTMLAttributeNameNode],
3154
+ ["AST_HTML_ATTRIBUTE_NODE", isHTMLAttributeNode],
3155
+ ["AST_HTML_TEXT_NODE", isHTMLTextNode],
3156
+ ["AST_HTML_COMMENT_NODE", isHTMLCommentNode],
3157
+ ["AST_HTML_DOCTYPE_NODE", isHTMLDoctypeNode],
3158
+ ["AST_XML_DECLARATION_NODE", isXMLDeclarationNode],
3159
+ ["AST_CDATA_NODE", isCDATANode],
3160
+ ["AST_WHITESPACE_NODE", isWhitespaceNode],
3161
+ ["AST_ERB_CONTENT_NODE", isERBContentNode],
3162
+ ["AST_ERB_END_NODE", isERBEndNode],
3163
+ ["AST_ERB_ELSE_NODE", isERBElseNode],
3164
+ ["AST_ERB_IF_NODE", isERBIfNode],
3165
+ ["AST_ERB_BLOCK_NODE", isERBBlockNode],
3166
+ ["AST_ERB_WHEN_NODE", isERBWhenNode],
3167
+ ["AST_ERB_CASE_NODE", isERBCaseNode],
3168
+ ["AST_ERB_CASE_MATCH_NODE", isERBCaseMatchNode],
3169
+ ["AST_ERB_WHILE_NODE", isERBWhileNode],
3170
+ ["AST_ERB_UNTIL_NODE", isERBUntilNode],
3171
+ ["AST_ERB_FOR_NODE", isERBForNode],
3172
+ ["AST_ERB_RESCUE_NODE", isERBRescueNode],
3173
+ ["AST_ERB_ENSURE_NODE", isERBEnsureNode],
3174
+ ["AST_ERB_BEGIN_NODE", isERBBeginNode],
3175
+ ["AST_ERB_UNLESS_NODE", isERBUnlessNode],
3176
+ ["AST_ERB_YIELD_NODE", isERBYieldNode],
3177
+ ["AST_ERB_IN_NODE", isERBInNode],
3178
+ ]);
3179
+ /**
3180
+ * Checks if a node matches any of the provided type identifiers with proper type narrowing
3181
+ * Supports AST type strings, node classes, or type guard functions
3182
+ *
3183
+ * @example
3184
+ * if (isAnyOf(node, "AST_HTML_TEXT_NODE", "AST_LITERAL_NODE")) {
3185
+ * // node is narrowed to HTMLTextNode | LiteralNode
3186
+ * }
3187
+ *
3188
+ * @example
3189
+ * if (isAnyOf(node, HTMLTextNode, LiteralNode)) {
3190
+ * // node is narrowed to HTMLTextNode | LiteralNode
3191
+ * }
3192
+ */
3193
+ function isAnyOf(node, ...types) {
3194
+ return types.some(type => {
3195
+ if (typeof type === 'string') {
3196
+ return isNode(node, type);
3197
+ }
3198
+ else if (typeof type === 'function' && type.prototype && type.prototype.constructor === type && NODE_TYPE_GUARDS.has(type)) {
3199
+ return isNode(node, type);
3200
+ }
3201
+ else if (typeof type === 'function') {
3202
+ return type(node);
3203
+ }
3204
+ else {
3205
+ return false;
3206
+ }
3207
+ });
3208
+ }
3209
+ /**
3210
+ * Checks if a node does NOT match any of the provided type identifiers
3211
+ * Supports AST type strings, node classes, or type guard functions
3212
+ * This is the logical inverse of isAnyOf
3213
+ *
3214
+ * @example
3215
+ * if (isNoneOf(node, "AST_HTML_TEXT_NODE", "AST_LITERAL_NODE")) {
3216
+ * // node is neither HTMLTextNode nor LiteralNode
3217
+ * }
3218
+ *
3219
+ * @example
3220
+ * if (isNoneOf(node, HTMLTextNode, LiteralNode)) {
3221
+ * // node is neither HTMLTextNode nor LiteralNode
3222
+ * }
3223
+ *
3224
+ * @example
3225
+ * if (isNoneOf(node, isHTMLTextNode, isLiteralNode)) {
3226
+ * // node is neither HTMLTextNode nor LiteralNode
3227
+ * }
3228
+ */
3229
+ function isNoneOf(node, ...types) {
3230
+ return !isAnyOf(node, ...types);
3231
+ }
3232
+ function filterNodes(nodes, ...types) {
3233
+ if (!nodes)
3234
+ return [];
3235
+ return nodes.filter(node => isAnyOf(node, ...types));
3236
+ }
3237
+ function isNode(node, type) {
3238
+ if (!node)
3239
+ return false;
3240
+ if (typeof type === 'string') {
3241
+ const guard = AST_TYPE_GUARDS.get(type);
3242
+ return guard ? guard(node) : false;
3243
+ }
3244
+ else if (typeof type === 'function') {
3245
+ const guard = NODE_TYPE_GUARDS.get(type);
3246
+ return guard ? guard(node) : false;
3247
+ }
3248
+ else {
3249
+ return false;
3250
+ }
3251
+ }
3252
+ function isToken(object) {
3253
+ return (object instanceof Token) || (object?.constructor?.name === "Token" && "value" in object) || object.type?.startsWith('TOKEN_');
3254
+ }
3255
+ function isParseResult(object) {
3256
+ return (object instanceof ParseResult) || (object?.constructor?.name === "ParseResult" && "value" in object);
3257
+ }
3258
+
3259
+ /**
3260
+ * Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
3261
+ */
3262
+ function isERBOutputNode(node) {
3263
+ return isNode(node, ERBContentNode) && ["<%=", "<%=="].includes(node.tag_opening?.value);
3264
+ }
3265
+ /**
3266
+ * Checks if a node is a non-output ERB node (control flow: <% %>)
3267
+ */
3268
+ function isERBControlFlowNode(node) {
3269
+ return isAnyOf(node, ERBIfNode, ERBUnlessNode, ERBBlockNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBForNode, ERBBeginNode);
3270
+ }
3271
+ /**
3272
+ * Checks if an array of nodes contains any ERB output nodes (dynamic content)
3273
+ */
3274
+ function hasERBOutput(nodes) {
3275
+ return nodes.some(isERBOutputNode);
3276
+ }
3277
+ /**
3278
+ * Extracts a combined string from nodes, including ERB content
3279
+ * For ERB nodes, includes the full tag syntax (e.g., "<%= foo %>")
3280
+ * This is useful for debugging or displaying the full attribute name
3281
+ */
3282
+ function getCombinedStringFromNodes(nodes) {
3283
+ return nodes.map(node => {
3284
+ if (isLiteralNode(node)) {
3285
+ return node.content;
3286
+ }
3287
+ else if (isERBContentNode(node)) {
3288
+ const opening = node.tag_opening?.value || "";
3289
+ const content = node.content?.value || "";
3290
+ const closing = node.tag_closing?.value || "";
3291
+ return `${opening}${content}${closing}`;
3292
+ }
3293
+ else {
3294
+ // For other node types, return a placeholder or empty string
3295
+ return `[${node.type}]`;
3296
+ }
3297
+ }).join("");
3298
+ }
3299
+ /**
3300
+ * Gets the combined string representation of an HTML attribute name node
3301
+ * This includes both static and dynamic content, useful for debugging
3302
+ */
3303
+ function getCombinedAttributeName(attributeNameNode) {
3304
+ if (!attributeNameNode.children) {
3305
+ return "";
3306
+ }
3307
+ return getCombinedStringFromNodes(attributeNameNode.children);
3308
+ }
3309
+ /**
3310
+ * Gets the tag name of an HTML element node
3311
+ */
3312
+ function getTagName(node) {
3313
+ return node.tag_name?.value ?? "";
3314
+ }
3315
+ /**
3316
+ * Check if a node is a comment (HTML comment or ERB comment)
3317
+ */
3318
+ function isCommentNode(node) {
3319
+ return isNode(node, HTMLCommentNode) || (isERBNode(node) && !isERBControlFlowNode(node));
3320
+ }
3321
+ /**
3322
+ * Compares two positions to determine if the first comes before the second
3323
+ * Returns true if pos1 comes before pos2 in source order
3324
+ * @param inclusive - If true, returns true when positions are equal
3325
+ */
3326
+ function isPositionBefore(position1, position2, inclusive = false) {
3327
+ if (position1.line < position2.line)
3328
+ return true;
3329
+ if (position1.line > position2.line)
3330
+ return false;
3331
+ return inclusive ? position1.column <= position2.column : position1.column < position2.column;
3332
+ }
3333
+ /**
3334
+ * Compares two positions to determine if the first comes after the second
3335
+ * Returns true if pos1 comes after pos2 in source order
3336
+ * @param inclusive - If true, returns true when positions are equal
3337
+ */
3338
+ function isPositionAfter(position1, position2, inclusive = false) {
3339
+ if (position1.line > position2.line)
3340
+ return true;
3341
+ if (position1.line < position2.line)
3342
+ return false;
3343
+ return inclusive ? position1.column >= position2.column : position1.column > position2.column;
3344
+ }
3345
+ /**
3346
+ * Gets nodes that end before the specified position
3347
+ * @param inclusive - If true, includes nodes that end exactly at the position (default: false, matching half-open interval semantics)
3348
+ */
3349
+ function getNodesBeforePosition(nodes, position, inclusive = false) {
3350
+ return nodes.filter(node => node.location && isPositionBefore(node.location.end, position, inclusive));
3351
+ }
3352
+ /**
3353
+ * Gets nodes that start after the specified position
3354
+ * @param inclusive - If true, includes nodes that start exactly at the position (default: true, matching typical boundary behavior)
3355
+ */
3356
+ function getNodesAfterPosition(nodes, position, inclusive = true) {
3357
+ return nodes.filter(node => node.location && isPositionAfter(node.location.start, position, inclusive));
3358
+ }
3359
+
3360
+ // NOTE: This file is generated by the templates/template.rb script and should not
3361
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.6.1/templates/javascript/packages/core/src/visitor.ts.erb
3362
+ class Visitor {
3363
+ visit(node) {
3364
+ if (!node)
3365
+ return;
3366
+ node.accept(this);
3367
+ }
3368
+ visitAll(nodes) {
3369
+ nodes.forEach(node => node?.accept(this));
3370
+ }
3371
+ visitChildNodes(node) {
3372
+ node.compactChildNodes().forEach(node => node.accept(this));
3373
+ }
3374
+ visitDocumentNode(node) {
3375
+ this.visitChildNodes(node);
3376
+ }
3377
+ visitLiteralNode(node) {
3378
+ this.visitChildNodes(node);
3379
+ }
3380
+ visitHTMLOpenTagNode(node) {
2600
3381
  this.visitChildNodes(node);
2601
3382
  }
2602
- visitHTMLSelfCloseTagNode(node) {
3383
+ visitHTMLCloseTagNode(node) {
2603
3384
  this.visitChildNodes(node);
2604
3385
  }
2605
3386
  visitHTMLElementNode(node) {
@@ -2623,6 +3404,12 @@ class Visitor {
2623
3404
  visitHTMLDoctypeNode(node) {
2624
3405
  this.visitChildNodes(node);
2625
3406
  }
3407
+ visitXMLDeclarationNode(node) {
3408
+ this.visitChildNodes(node);
3409
+ }
3410
+ visitCDATANode(node) {
3411
+ this.visitChildNodes(node);
3412
+ }
2626
3413
  visitWhitespaceNode(node) {
2627
3414
  this.visitChildNodes(node);
2628
3415
  }
@@ -2674,11 +3461,450 @@ class Visitor {
2674
3461
  visitERBYieldNode(node) {
2675
3462
  this.visitChildNodes(node);
2676
3463
  }
2677
- visitERBInNode(node) {
2678
- this.visitChildNodes(node);
3464
+ visitERBInNode(node) {
3465
+ this.visitChildNodes(node);
3466
+ }
3467
+ }
3468
+
3469
+ class PrintContext {
3470
+ output = "";
3471
+ indentLevel = 0;
3472
+ currentColumn = 0;
3473
+ preserveStack = [];
3474
+ /**
3475
+ * Write text to the output
3476
+ */
3477
+ write(text) {
3478
+ this.output += text;
3479
+ this.currentColumn += text.length;
3480
+ }
3481
+ /**
3482
+ * Write text and update column tracking for newlines
3483
+ */
3484
+ writeWithColumnTracking(text) {
3485
+ this.output += text;
3486
+ const lines = text.split('\n');
3487
+ if (lines.length > 1) {
3488
+ this.currentColumn = lines[lines.length - 1].length;
3489
+ }
3490
+ else {
3491
+ this.currentColumn += text.length;
3492
+ }
3493
+ }
3494
+ /**
3495
+ * Increase indentation level
3496
+ */
3497
+ indent() {
3498
+ this.indentLevel++;
3499
+ }
3500
+ /**
3501
+ * Decrease indentation level
3502
+ */
3503
+ dedent() {
3504
+ if (this.indentLevel > 0) {
3505
+ this.indentLevel--;
3506
+ }
3507
+ }
3508
+ /**
3509
+ * Enter a tag that may preserve whitespace
3510
+ */
3511
+ enterTag(tagName) {
3512
+ this.preserveStack.push(tagName.toLowerCase());
3513
+ }
3514
+ /**
3515
+ * Exit the current tag
3516
+ */
3517
+ exitTag() {
3518
+ this.preserveStack.pop();
3519
+ }
3520
+ /**
3521
+ * Check if we're at the start of a line
3522
+ */
3523
+ isAtStartOfLine() {
3524
+ return this.currentColumn === 0;
3525
+ }
3526
+ /**
3527
+ * Get current indentation level
3528
+ */
3529
+ getCurrentIndentLevel() {
3530
+ return this.indentLevel;
3531
+ }
3532
+ /**
3533
+ * Get current column position
3534
+ */
3535
+ getCurrentColumn() {
3536
+ return this.currentColumn;
3537
+ }
3538
+ /**
3539
+ * Get the current tag stack (for debugging)
3540
+ */
3541
+ getTagStack() {
3542
+ return [...this.preserveStack];
3543
+ }
3544
+ /**
3545
+ * Get the complete output string
3546
+ */
3547
+ getOutput() {
3548
+ return this.output;
3549
+ }
3550
+ /**
3551
+ * Reset the context for reuse
3552
+ */
3553
+ reset() {
3554
+ this.output = "";
3555
+ this.indentLevel = 0;
3556
+ this.currentColumn = 0;
3557
+ this.preserveStack = [];
3558
+ }
3559
+ }
3560
+
3561
+ /**
3562
+ * Default print options used when none are provided
3563
+ */
3564
+ const DEFAULT_PRINT_OPTIONS = {
3565
+ ignoreErrors: false
3566
+ };
3567
+ class Printer extends Visitor {
3568
+ context = new PrintContext();
3569
+ /**
3570
+ * Static method to print a node without creating an instance
3571
+ *
3572
+ * @param input - The AST Node, Token, or ParseResult to print
3573
+ * @param options - Print options to control behavior
3574
+ * @returns The printed string representation of the input
3575
+ * @throws {Error} When node has parse errors and ignoreErrors is false
3576
+ */
3577
+ static print(input, options = DEFAULT_PRINT_OPTIONS) {
3578
+ const printer = new this();
3579
+ return printer.print(input, options);
3580
+ }
3581
+ /**
3582
+ * Print a node, token, or parse result to a string
3583
+ *
3584
+ * @param input - The AST Node, Token, or ParseResult to print
3585
+ * @param options - Print options to control behavior
3586
+ * @returns The printed string representation of the input
3587
+ * @throws {Error} When node has parse errors and ignoreErrors is false
3588
+ */
3589
+ print(input, options = DEFAULT_PRINT_OPTIONS) {
3590
+ if (isToken(input)) {
3591
+ return input.value;
3592
+ }
3593
+ if (Array.isArray(input)) {
3594
+ this.context.reset();
3595
+ input.forEach(node => this.visit(node));
3596
+ return this.context.getOutput();
3597
+ }
3598
+ const node = isParseResult(input) ? input.value : input;
3599
+ if (options.ignoreErrors === false && node.recursiveErrors().length > 0) {
3600
+ 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 })\``);
3601
+ }
3602
+ this.context.reset();
3603
+ this.visit(node);
3604
+ return this.context.getOutput();
3605
+ }
3606
+ write(content) {
3607
+ this.context.write(content);
3608
+ }
3609
+ }
3610
+
3611
+ /**
3612
+ * IdentityPrinter - Provides lossless reconstruction of the original source
3613
+ *
3614
+ * This printer aims to reconstruct the original input as faithfully as possible,
3615
+ * preserving all whitespace, formatting, and structure. It's useful for:
3616
+ * - Testing parser accuracy (input should equal output)
3617
+ * - Baseline printing before applying transformations
3618
+ * - Verifying AST round-trip fidelity
3619
+ */
3620
+ class IdentityPrinter extends Printer {
3621
+ visitLiteralNode(node) {
3622
+ this.write(node.content);
3623
+ }
3624
+ visitHTMLTextNode(node) {
3625
+ this.write(node.content);
3626
+ }
3627
+ visitWhitespaceNode(node) {
3628
+ if (node.value) {
3629
+ this.write(node.value.value);
3630
+ }
3631
+ }
3632
+ visitHTMLOpenTagNode(node) {
3633
+ if (node.tag_opening) {
3634
+ this.write(node.tag_opening.value);
3635
+ }
3636
+ if (node.tag_name) {
3637
+ this.write(node.tag_name.value);
3638
+ }
3639
+ this.visitChildNodes(node);
3640
+ if (node.tag_closing) {
3641
+ this.write(node.tag_closing.value);
3642
+ }
3643
+ }
3644
+ visitHTMLCloseTagNode(node) {
3645
+ if (node.tag_opening) {
3646
+ this.write(node.tag_opening.value);
3647
+ }
3648
+ if (node.tag_name) {
3649
+ const before = getNodesBeforePosition(node.children, node.tag_name.location.start, true);
3650
+ const after = getNodesAfterPosition(node.children, node.tag_name.location.end);
3651
+ this.visitAll(before);
3652
+ this.write(node.tag_name.value);
3653
+ this.visitAll(after);
3654
+ }
3655
+ else {
3656
+ this.visitAll(node.children);
3657
+ }
3658
+ if (node.tag_closing) {
3659
+ this.write(node.tag_closing.value);
3660
+ }
3661
+ }
3662
+ visitHTMLElementNode(node) {
3663
+ const tagName = node.tag_name?.value;
3664
+ if (tagName) {
3665
+ this.context.enterTag(tagName);
3666
+ }
3667
+ if (node.open_tag) {
3668
+ this.visit(node.open_tag);
3669
+ }
3670
+ if (node.body) {
3671
+ node.body.forEach(child => this.visit(child));
3672
+ }
3673
+ if (node.close_tag) {
3674
+ this.visit(node.close_tag);
3675
+ }
3676
+ if (tagName) {
3677
+ this.context.exitTag();
3678
+ }
3679
+ }
3680
+ visitHTMLAttributeNode(node) {
3681
+ if (node.name) {
3682
+ this.visit(node.name);
3683
+ }
3684
+ if (node.equals) {
3685
+ this.write(node.equals.value);
3686
+ }
3687
+ if (node.equals && node.value) {
3688
+ this.visit(node.value);
3689
+ }
3690
+ }
3691
+ visitHTMLAttributeNameNode(node) {
3692
+ this.visitChildNodes(node);
3693
+ }
3694
+ visitHTMLAttributeValueNode(node) {
3695
+ if (node.quoted && node.open_quote) {
3696
+ this.write(node.open_quote.value);
3697
+ }
3698
+ this.visitChildNodes(node);
3699
+ if (node.quoted && node.close_quote) {
3700
+ this.write(node.close_quote.value);
3701
+ }
3702
+ }
3703
+ visitHTMLCommentNode(node) {
3704
+ if (node.comment_start) {
3705
+ this.write(node.comment_start.value);
3706
+ }
3707
+ this.visitChildNodes(node);
3708
+ if (node.comment_end) {
3709
+ this.write(node.comment_end.value);
3710
+ }
3711
+ }
3712
+ visitHTMLDoctypeNode(node) {
3713
+ if (node.tag_opening) {
3714
+ this.write(node.tag_opening.value);
3715
+ }
3716
+ this.visitChildNodes(node);
3717
+ if (node.tag_closing) {
3718
+ this.write(node.tag_closing.value);
3719
+ }
3720
+ }
3721
+ visitXMLDeclarationNode(node) {
3722
+ if (node.tag_opening) {
3723
+ this.write(node.tag_opening.value);
3724
+ }
3725
+ this.visitChildNodes(node);
3726
+ if (node.tag_closing) {
3727
+ this.write(node.tag_closing.value);
3728
+ }
3729
+ }
3730
+ visitCDATANode(node) {
3731
+ if (node.tag_opening) {
3732
+ this.write(node.tag_opening.value);
3733
+ }
3734
+ this.visitChildNodes(node);
3735
+ if (node.tag_closing) {
3736
+ this.write(node.tag_closing.value);
3737
+ }
3738
+ }
3739
+ visitERBContentNode(node) {
3740
+ this.printERBNode(node);
3741
+ }
3742
+ visitERBIfNode(node) {
3743
+ this.printERBNode(node);
3744
+ if (node.statements) {
3745
+ node.statements.forEach(statement => this.visit(statement));
3746
+ }
3747
+ if (node.subsequent) {
3748
+ this.visit(node.subsequent);
3749
+ }
3750
+ if (node.end_node) {
3751
+ this.visit(node.end_node);
3752
+ }
3753
+ }
3754
+ visitERBElseNode(node) {
3755
+ this.printERBNode(node);
3756
+ if (node.statements) {
3757
+ node.statements.forEach(statement => this.visit(statement));
3758
+ }
3759
+ }
3760
+ visitERBEndNode(node) {
3761
+ this.printERBNode(node);
3762
+ }
3763
+ visitERBBlockNode(node) {
3764
+ this.printERBNode(node);
3765
+ if (node.body) {
3766
+ node.body.forEach(child => this.visit(child));
3767
+ }
3768
+ if (node.end_node) {
3769
+ this.visit(node.end_node);
3770
+ }
3771
+ }
3772
+ visitERBCaseNode(node) {
3773
+ this.printERBNode(node);
3774
+ if (node.children) {
3775
+ node.children.forEach(child => this.visit(child));
3776
+ }
3777
+ if (node.conditions) {
3778
+ node.conditions.forEach(condition => this.visit(condition));
3779
+ }
3780
+ if (node.else_clause) {
3781
+ this.visit(node.else_clause);
3782
+ }
3783
+ if (node.end_node) {
3784
+ this.visit(node.end_node);
3785
+ }
3786
+ }
3787
+ visitERBWhenNode(node) {
3788
+ this.printERBNode(node);
3789
+ if (node.statements) {
3790
+ node.statements.forEach(statement => this.visit(statement));
3791
+ }
3792
+ }
3793
+ visitERBWhileNode(node) {
3794
+ this.printERBNode(node);
3795
+ if (node.statements) {
3796
+ node.statements.forEach(statement => this.visit(statement));
3797
+ }
3798
+ if (node.end_node) {
3799
+ this.visit(node.end_node);
3800
+ }
3801
+ }
3802
+ visitERBUntilNode(node) {
3803
+ this.printERBNode(node);
3804
+ if (node.statements) {
3805
+ node.statements.forEach(statement => this.visit(statement));
3806
+ }
3807
+ if (node.end_node) {
3808
+ this.visit(node.end_node);
3809
+ }
3810
+ }
3811
+ visitERBForNode(node) {
3812
+ this.printERBNode(node);
3813
+ if (node.statements) {
3814
+ node.statements.forEach(statement => this.visit(statement));
3815
+ }
3816
+ if (node.end_node) {
3817
+ this.visit(node.end_node);
3818
+ }
3819
+ }
3820
+ visitERBBeginNode(node) {
3821
+ this.printERBNode(node);
3822
+ if (node.statements) {
3823
+ node.statements.forEach(statement => this.visit(statement));
3824
+ }
3825
+ if (node.rescue_clause) {
3826
+ this.visit(node.rescue_clause);
3827
+ }
3828
+ if (node.else_clause) {
3829
+ this.visit(node.else_clause);
3830
+ }
3831
+ if (node.ensure_clause) {
3832
+ this.visit(node.ensure_clause);
3833
+ }
3834
+ if (node.end_node) {
3835
+ this.visit(node.end_node);
3836
+ }
3837
+ }
3838
+ visitERBRescueNode(node) {
3839
+ this.printERBNode(node);
3840
+ if (node.statements) {
3841
+ node.statements.forEach(statement => this.visit(statement));
3842
+ }
3843
+ if (node.subsequent) {
3844
+ this.visit(node.subsequent);
3845
+ }
3846
+ }
3847
+ visitERBEnsureNode(node) {
3848
+ this.printERBNode(node);
3849
+ if (node.statements) {
3850
+ node.statements.forEach(statement => this.visit(statement));
3851
+ }
3852
+ }
3853
+ visitERBUnlessNode(node) {
3854
+ this.printERBNode(node);
3855
+ if (node.statements) {
3856
+ node.statements.forEach(statement => this.visit(statement));
3857
+ }
3858
+ if (node.else_clause) {
3859
+ this.visit(node.else_clause);
3860
+ }
3861
+ if (node.end_node) {
3862
+ this.visit(node.end_node);
3863
+ }
3864
+ }
3865
+ visitERBYieldNode(node) {
3866
+ this.printERBNode(node);
3867
+ }
3868
+ visitERBInNode(node) {
3869
+ this.printERBNode(node);
3870
+ if (node.statements) {
3871
+ node.statements.forEach(statement => this.visit(statement));
3872
+ }
3873
+ }
3874
+ visitERBCaseMatchNode(node) {
3875
+ this.printERBNode(node);
3876
+ if (node.children) {
3877
+ node.children.forEach(child => this.visit(child));
3878
+ }
3879
+ if (node.conditions) {
3880
+ node.conditions.forEach(condition => this.visit(condition));
3881
+ }
3882
+ if (node.else_clause) {
3883
+ this.visit(node.else_clause);
3884
+ }
3885
+ if (node.end_node) {
3886
+ this.visit(node.end_node);
3887
+ }
3888
+ }
3889
+ /**
3890
+ * Print ERB node tags and content
3891
+ */
3892
+ printERBNode(node) {
3893
+ if (node.tag_opening) {
3894
+ this.write(node.tag_opening.value);
3895
+ }
3896
+ if (node.content) {
3897
+ this.write(node.content.value);
3898
+ }
3899
+ if (node.tag_closing) {
3900
+ this.write(node.tag_closing.value);
3901
+ }
2679
3902
  }
2680
3903
  }
2681
3904
 
3905
+ ({
3906
+ ...DEFAULT_PRINT_OPTIONS});
3907
+
2682
3908
  // TODO: we can probably expand this list with more tags/attributes
2683
3909
  const FORMATTABLE_ATTRIBUTES = {
2684
3910
  '*': ['class'],
@@ -2688,53 +3914,153 @@ const FORMATTABLE_ATTRIBUTES = {
2688
3914
  * Printer traverses the Herb AST using the Visitor pattern
2689
3915
  * and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
2690
3916
  */
2691
- class Printer extends Visitor {
3917
+ class FormatPrinter extends Printer {
3918
+ /**
3919
+ * @deprecated integrate indentWidth into this.options and update FormatOptions to extend from @herb-tools/printer options
3920
+ */
2692
3921
  indentWidth;
3922
+ /**
3923
+ * @deprecated integrate maxLineLength into this.options and update FormatOptions to extend from @herb-tools/printer options
3924
+ */
2693
3925
  maxLineLength;
2694
- source;
3926
+ /**
3927
+ * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
3928
+ */
2695
3929
  lines = [];
2696
3930
  indentLevel = 0;
2697
3931
  inlineMode = false;
2698
- isInComplexNesting = false;
2699
- currentTagName = "";
3932
+ currentAttributeName = null;
3933
+ elementStack = [];
3934
+ elementFormattingAnalysis = new Map();
3935
+ source;
3936
+ // TODO: extract
2700
3937
  static INLINE_ELEMENTS = new Set([
2701
3938
  'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'cite', 'code',
2702
3939
  'dfn', 'em', 'i', 'img', 'kbd', 'label', 'map', 'object', 'q',
2703
3940
  'samp', 'small', 'span', 'strong', 'sub', 'sup',
2704
3941
  'tt', 'var', 'del', 'ins', 'mark', 's', 'u', 'time', 'wbr'
2705
3942
  ]);
3943
+ static CONTENT_PRESERVING_ELEMENTS = new Set([
3944
+ 'script', 'style', 'pre', 'textarea'
3945
+ ]);
3946
+ static SPACEABLE_CONTAINERS = new Set([
3947
+ 'div', 'section', 'article', 'main', 'header', 'footer', 'aside',
3948
+ 'figure', 'details', 'summary', 'dialog', 'fieldset'
3949
+ ]);
3950
+ static TIGHT_GROUP_PARENTS = new Set([
3951
+ 'ul', 'ol', 'nav', 'select', 'datalist', 'optgroup', 'tr', 'thead',
3952
+ 'tbody', 'tfoot'
3953
+ ]);
3954
+ static TIGHT_GROUP_CHILDREN = new Set([
3955
+ 'li', 'option', 'td', 'th', 'dt', 'dd'
3956
+ ]);
3957
+ static SPACING_THRESHOLD = 3;
2706
3958
  constructor(source, options) {
2707
3959
  super();
2708
3960
  this.source = source;
2709
3961
  this.indentWidth = options.indentWidth;
2710
3962
  this.maxLineLength = options.maxLineLength;
2711
3963
  }
2712
- print(object, indentLevel = 0) {
2713
- if (object instanceof Token || object.type?.startsWith('TOKEN_')) {
2714
- return object.value;
2715
- }
2716
- const node = object;
3964
+ print(input) {
3965
+ if (isToken(input))
3966
+ return input.value;
3967
+ const node = isParseResult(input) ? input.value : input;
3968
+ // TODO: refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
2717
3969
  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);
3970
+ this.indentLevel = 0;
3971
+ this.visit(node);
3972
+ return this.lines.join("\n");
3973
+ }
3974
+ /**
3975
+ * Get the current element (top of stack)
3976
+ */
3977
+ get currentElement() {
3978
+ return this.elementStack.length > 0 ? this.elementStack[this.elementStack.length - 1] : null;
3979
+ }
3980
+ /**
3981
+ * Get the current tag name from the current element context
3982
+ */
3983
+ get currentTagName() {
3984
+ return this.currentElement?.open_tag?.tag_name?.value ?? "";
3985
+ }
3986
+ /**
3987
+ * Append text to the last line instead of creating a new line
3988
+ */
3989
+ pushToLastLine(text) {
3990
+ if (this.lines.length > 0) {
3991
+ this.lines[this.lines.length - 1] += text;
2722
3992
  }
2723
3993
  else {
2724
- this.visit(node);
3994
+ this.lines.push(text);
3995
+ }
3996
+ }
3997
+ /**
3998
+ * Capture output from a callback into a separate lines array
3999
+ * Useful for testing what output would be generated without affecting the main output
4000
+ */
4001
+ capture(callback) {
4002
+ const previousLines = this.lines;
4003
+ const previousInlineMode = this.inlineMode;
4004
+ this.lines = [];
4005
+ try {
4006
+ callback();
4007
+ return this.lines;
4008
+ }
4009
+ finally {
4010
+ this.lines = previousLines;
4011
+ this.inlineMode = previousInlineMode;
4012
+ }
4013
+ }
4014
+ /**
4015
+ * Capture all nodes that would be visited during a callback
4016
+ * Returns a flat list of all nodes without generating any output
4017
+ */
4018
+ captureNodes(callback) {
4019
+ const capturedNodes = [];
4020
+ const previousLines = this.lines;
4021
+ const previousInlineMode = this.inlineMode;
4022
+ const originalPush = this.push.bind(this);
4023
+ const originalPushToLastLine = this.pushToLastLine.bind(this);
4024
+ const originalVisit = this.visit.bind(this);
4025
+ this.lines = [];
4026
+ this.push = () => { };
4027
+ this.pushToLastLine = () => { };
4028
+ this.visit = (node) => {
4029
+ capturedNodes.push(node);
4030
+ originalVisit(node);
4031
+ };
4032
+ try {
4033
+ callback();
4034
+ return capturedNodes;
4035
+ }
4036
+ finally {
4037
+ this.lines = previousLines;
4038
+ this.inlineMode = previousInlineMode;
4039
+ this.push = originalPush;
4040
+ this.pushToLastLine = originalPushToLastLine;
4041
+ this.visit = originalVisit;
2725
4042
  }
2726
- return this.lines.join("\n");
2727
4043
  }
4044
+ /**
4045
+ * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
4046
+ */
2728
4047
  push(line) {
2729
4048
  this.lines.push(line);
2730
4049
  }
4050
+ /**
4051
+ * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
4052
+ */
4053
+ pushWithIndent(line) {
4054
+ const indent = line.trim() === "" ? "" : this.indent;
4055
+ this.push(indent + line);
4056
+ }
2731
4057
  withIndent(callback) {
2732
4058
  this.indentLevel++;
2733
4059
  const result = callback();
2734
4060
  this.indentLevel--;
2735
4061
  return result;
2736
4062
  }
2737
- indent() {
4063
+ get indent() {
2738
4064
  return " ".repeat(this.indentLevel * this.indentWidth);
2739
4065
  }
2740
4066
  /**
@@ -2744,45 +4070,128 @@ class Printer extends Visitor {
2744
4070
  formatERBContent(content) {
2745
4071
  return content.trim() ? ` ${content.trim()} ` : "";
2746
4072
  }
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
4073
  /**
2760
4074
  * Count total attributes including those inside ERB conditionals
2761
4075
  */
2762
4076
  getTotalAttributeCount(attributes, inlineNodes = []) {
2763
4077
  let totalAttributeCount = attributes.length;
2764
4078
  inlineNodes.forEach(node => {
2765
- if (this.isERBControlFlow(node)) {
2766
- const erbNode = node;
2767
- if (erbNode.statements) {
2768
- totalAttributeCount += erbNode.statements.length;
2769
- }
4079
+ if (isERBControlFlowNode(node)) {
4080
+ const capturedNodes = this.captureNodes(() => this.visit(node));
4081
+ const attributeNodes = filterNodes(capturedNodes, HTMLAttributeNode);
4082
+ totalAttributeCount += attributeNodes.length;
2770
4083
  }
2771
4084
  });
2772
4085
  return totalAttributeCount;
2773
4086
  }
2774
4087
  /**
2775
- * Extract HTML attributes from a list of nodes
4088
+ * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
2776
4089
  */
2777
- extractAttributes(nodes) {
2778
- return nodes.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
4090
+ extractInlineNodes(nodes) {
4091
+ return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode));
2779
4092
  }
2780
4093
  /**
2781
- * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
4094
+ * Determine if spacing should be added between sibling elements
4095
+ *
4096
+ * This implements the "rule of three" intelligent spacing system:
4097
+ * - Adds spacing between 3 or more meaningful siblings
4098
+ * - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
4099
+ * - Groups comments with following elements
4100
+ * - Preserves user-added spacing
4101
+ *
4102
+ * @param parentElement - The parent element containing the siblings
4103
+ * @param siblings - Array of all sibling nodes
4104
+ * @param currentIndex - Index of the current node being evaluated
4105
+ * @param hasExistingSpacing - Whether user-added spacing already exists
4106
+ * @returns true if spacing should be added before the current element
2782
4107
  */
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'));
4108
+ shouldAddSpacingBetweenSiblings(parentElement, siblings, currentIndex, hasExistingSpacing) {
4109
+ if (hasExistingSpacing) {
4110
+ return true;
4111
+ }
4112
+ const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "");
4113
+ if (hasMixedContent) {
4114
+ return false;
4115
+ }
4116
+ const meaningfulSiblings = siblings.filter(child => this.isNonWhitespaceNode(child));
4117
+ if (meaningfulSiblings.length < FormatPrinter.SPACING_THRESHOLD) {
4118
+ return false;
4119
+ }
4120
+ const parentTagName = parentElement ? getTagName(parentElement) : null;
4121
+ if (parentTagName && FormatPrinter.TIGHT_GROUP_PARENTS.has(parentTagName)) {
4122
+ return false;
4123
+ }
4124
+ const isSpaceableContainer = !parentTagName || (parentTagName && FormatPrinter.SPACEABLE_CONTAINERS.has(parentTagName));
4125
+ if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
4126
+ return false;
4127
+ }
4128
+ const currentNode = siblings[currentIndex];
4129
+ const previousMeaningfulIndex = this.findPreviousMeaningfulSibling(siblings, currentIndex);
4130
+ const isCurrentComment = isCommentNode(currentNode);
4131
+ if (previousMeaningfulIndex !== -1) {
4132
+ const previousNode = siblings[previousMeaningfulIndex];
4133
+ const isPreviousComment = isCommentNode(previousNode);
4134
+ if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
4135
+ return false;
4136
+ }
4137
+ if (isPreviousComment && isCurrentComment) {
4138
+ return false;
4139
+ }
4140
+ }
4141
+ if (isNode(currentNode, HTMLElementNode)) {
4142
+ const currentTagName = getTagName(currentNode);
4143
+ if (FormatPrinter.INLINE_ELEMENTS.has(currentTagName)) {
4144
+ return false;
4145
+ }
4146
+ if (FormatPrinter.TIGHT_GROUP_CHILDREN.has(currentTagName)) {
4147
+ return false;
4148
+ }
4149
+ if (currentTagName === 'a' && parentTagName === 'nav') {
4150
+ return false;
4151
+ }
4152
+ }
4153
+ const isBlockElement = this.isBlockLevelNode(currentNode);
4154
+ const isERBBlock = isERBNode(currentNode) && isERBControlFlowNode(currentNode);
4155
+ const isComment = isCommentNode(currentNode);
4156
+ return isBlockElement || isERBBlock || isComment;
4157
+ }
4158
+ /**
4159
+ * Token list attributes that contain space-separated values and benefit from
4160
+ * spacing around ERB content for readability
4161
+ */
4162
+ static TOKEN_LIST_ATTRIBUTES = new Set([
4163
+ 'class', 'data-controller', 'data-action'
4164
+ ]);
4165
+ /**
4166
+ * Check if we're currently processing a token list attribute that needs spacing
4167
+ */
4168
+ isInTokenListAttribute() {
4169
+ return this.currentAttributeName !== null &&
4170
+ FormatPrinter.TOKEN_LIST_ATTRIBUTES.has(this.currentAttributeName);
4171
+ }
4172
+ /**
4173
+ * Find the previous meaningful (non-whitespace) sibling
4174
+ */
4175
+ findPreviousMeaningfulSibling(siblings, currentIndex) {
4176
+ for (let i = currentIndex - 1; i >= 0; i--) {
4177
+ if (this.isNonWhitespaceNode(siblings[i])) {
4178
+ return i;
4179
+ }
4180
+ }
4181
+ return -1;
4182
+ }
4183
+ /**
4184
+ * Check if a node represents a block-level element
4185
+ */
4186
+ isBlockLevelNode(node) {
4187
+ if (!isNode(node, HTMLElementNode)) {
4188
+ return false;
4189
+ }
4190
+ const tagName = getTagName(node);
4191
+ if (FormatPrinter.INLINE_ELEMENTS.has(tagName)) {
4192
+ return false;
4193
+ }
4194
+ return true;
2786
4195
  }
2787
4196
  /**
2788
4197
  * Render attributes as a space-separated string
@@ -2790,39 +4199,73 @@ class Printer extends Visitor {
2790
4199
  renderAttributesString(attributes) {
2791
4200
  if (attributes.length === 0)
2792
4201
  return "";
2793
- return ` ${attributes.map(attr => this.renderAttribute(attr)).join(" ")}`;
4202
+ return ` ${attributes.map(attribute => this.renderAttribute(attribute)).join(" ")}`;
2794
4203
  }
2795
4204
  /**
2796
4205
  * Determine if a tag should be rendered inline based on attribute count and other factors
2797
4206
  */
2798
- shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false, _nestingDepth = 0, _inlineNodesLength = 0, hasMultilineAttributes = false) {
4207
+ shouldRenderInline(totalAttributeCount, inlineLength, indentLength, maxLineLength = this.maxLineLength, hasComplexERB = false, hasMultilineAttributes = false, attributes = []) {
2799
4208
  if (hasComplexERB || hasMultilineAttributes)
2800
4209
  return false;
2801
4210
  if (totalAttributeCount === 0) {
2802
4211
  return inlineLength + indentLength <= maxLineLength;
2803
4212
  }
4213
+ if (totalAttributeCount === 1 && attributes.length === 1) {
4214
+ const attribute = attributes[0];
4215
+ const attributeName = this.getAttributeName(attribute);
4216
+ if (attributeName === 'class') {
4217
+ const attributeValue = this.getAttributeValue(attribute);
4218
+ const wouldBeMultiline = this.wouldClassAttributeBeMultiline(attributeValue, indentLength);
4219
+ if (!wouldBeMultiline) {
4220
+ return true;
4221
+ }
4222
+ else {
4223
+ return false;
4224
+ }
4225
+ }
4226
+ }
2804
4227
  if (totalAttributeCount > 3 || inlineLength + indentLength > maxLineLength) {
2805
4228
  return false;
2806
4229
  }
2807
4230
  return true;
2808
4231
  }
4232
+ getAttributeName(attribute) {
4233
+ return attribute.name ? getCombinedAttributeName(attribute.name) : "";
4234
+ }
4235
+ wouldClassAttributeBeMultiline(content, indentLength) {
4236
+ const normalizedContent = content.replace(/\s+/g, ' ').trim();
4237
+ const hasActualNewlines = /\r?\n/.test(content);
4238
+ if (hasActualNewlines && normalizedContent.length > 80) {
4239
+ const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line);
4240
+ if (lines.length > 1) {
4241
+ return true;
4242
+ }
4243
+ }
4244
+ const attributeLine = `class="${normalizedContent}"`;
4245
+ const currentIndent = indentLength;
4246
+ if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
4247
+ if (/<%[^%]*%>/.test(normalizedContent)) {
4248
+ return false;
4249
+ }
4250
+ const classes = normalizedContent.split(' ');
4251
+ const lines = this.breakTokensIntoLines(classes, currentIndent);
4252
+ return lines.length > 1;
4253
+ }
4254
+ return false;
4255
+ }
4256
+ getAttributeValue(attribute) {
4257
+ if (isNode(attribute.value, HTMLAttributeValueNode)) {
4258
+ return attribute.value.children.map(child => isNode(child, HTMLTextNode) ? child.content : IdentityPrinter.print(child)).join('');
4259
+ }
4260
+ return '';
4261
+ }
2809
4262
  hasMultilineAttributes(attributes) {
2810
4263
  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("");
4264
+ if (isNode(attribute.value, HTMLAttributeValueNode)) {
4265
+ const content = getCombinedStringFromNodes(attribute.value.children);
2823
4266
  if (/\r?\n/.test(content)) {
2824
- const name = attribute.name.name.value ?? "";
2825
- if (name === 'class') {
4267
+ const name = attribute.name ? getCombinedAttributeName(attribute.name) : "";
4268
+ if (name === "class") {
2826
4269
  const normalizedContent = content.replace(/\s+/g, ' ').trim();
2827
4270
  return normalizedContent.length > 80;
2828
4271
  }
@@ -2847,6 +4290,9 @@ class Printer extends Visitor {
2847
4290
  const currentIndent = this.indentLevel * this.indentWidth;
2848
4291
  const attributeLine = `${name}${equals}${open_quote}${normalizedContent}${close_quote}`;
2849
4292
  if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
4293
+ if (/<%[^%]*%>/.test(normalizedContent)) {
4294
+ return open_quote + normalizedContent + close_quote;
4295
+ }
2850
4296
  const classes = normalizedContent.split(' ');
2851
4297
  const lines = this.breakTokensIntoLines(classes, currentIndent);
2852
4298
  if (lines.length > 1) {
@@ -2860,7 +4306,7 @@ class Printer extends Visitor {
2860
4306
  const tagSpecificFormattable = FORMATTABLE_ATTRIBUTES[tagName.toLowerCase()] || [];
2861
4307
  return globalFormattable.includes(attributeName) || tagSpecificFormattable.includes(attributeName);
2862
4308
  }
2863
- formatMultilineAttribute(content, name, equals, open_quote, close_quote) {
4309
+ formatMultilineAttribute(content, name, open_quote, close_quote) {
2864
4310
  if (name === 'srcset' || name === 'sizes') {
2865
4311
  const normalizedContent = content.replace(/\s+/g, ' ').trim();
2866
4312
  return open_quote + normalizedContent + close_quote;
@@ -2902,42 +4348,43 @@ class Printer extends Visitor {
2902
4348
  /**
2903
4349
  * Render multiline attributes for a tag
2904
4350
  */
2905
- renderMultilineAttributes(tagName, _attributes, _inlineNodes = [], allChildren = [], isSelfClosing = false, isVoid = false, hasBodyContent = false) {
2906
- const indent = this.indent();
2907
- this.push(indent + `<${tagName}`);
4351
+ renderMultilineAttributes(tagName, allChildren = [], isSelfClosing = false) {
4352
+ this.pushWithIndent(`<${tagName}`);
2908
4353
  this.withIndent(() => {
2909
4354
  allChildren.forEach(child => {
2910
- if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
2911
- this.push(this.indent() + this.renderAttribute(child));
4355
+ if (isNode(child, HTMLAttributeNode)) {
4356
+ this.pushWithIndent(this.renderAttribute(child));
2912
4357
  }
2913
- else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
4358
+ else if (!isNode(child, WhitespaceNode)) {
2914
4359
  this.visit(child);
2915
4360
  }
2916
4361
  });
2917
4362
  });
2918
4363
  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}>`);
4364
+ this.pushWithIndent("/>");
2926
4365
  }
2927
4366
  else {
2928
- this.push(indent + ">");
4367
+ this.pushWithIndent(">");
2929
4368
  }
2930
4369
  }
2931
4370
  /**
2932
- * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
4371
+ * Reconstruct the text representation of an ERB node
4372
+ * @param withFormatting - if true, format the content; if false, preserve original
2933
4373
  */
2934
- printERBNode(node) {
2935
- const indent = this.inlineMode ? "" : this.indent();
4374
+ reconstructERBNode(node, withFormatting = true) {
2936
4375
  const open = node.tag_opening?.value ?? "";
2937
4376
  const close = node.tag_closing?.value ?? "";
2938
4377
  const content = node.content?.value ?? "";
2939
- const inner = this.formatERBContent(content);
2940
- this.push(indent + open + inner + close);
4378
+ const inner = withFormatting ? this.formatERBContent(content) : content;
4379
+ return open + inner + close;
4380
+ }
4381
+ /**
4382
+ * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
4383
+ */
4384
+ printERBNode(node) {
4385
+ const indent = this.inlineMode ? "" : this.indent;
4386
+ const erbText = this.reconstructERBNode(node, true);
4387
+ this.push(indent + erbText);
2941
4388
  }
2942
4389
  // --- Visitor methods ---
2943
4390
  visitDocumentNode(node) {
@@ -2945,14 +4392,13 @@ class Printer extends Visitor {
2945
4392
  let hasHandledSpacing = false;
2946
4393
  for (let i = 0; i < node.children.length; i++) {
2947
4394
  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() === "";
4395
+ if (isNode(child, HTMLTextNode)) {
4396
+ const isWhitespaceOnly = child.content.trim() === "";
2951
4397
  if (isWhitespaceOnly) {
2952
- const hasPrevNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1]);
4398
+ const hasPreviousNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1]);
2953
4399
  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) {
4400
+ const hasMultipleNewlines = child.content.includes('\n\n');
4401
+ if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
2956
4402
  this.push("");
2957
4403
  hasHandledSpacing = true;
2958
4404
  }
@@ -2970,332 +4416,149 @@ class Printer extends Visitor {
2970
4416
  }
2971
4417
  }
2972
4418
  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}`);
4419
+ this.elementStack.push(node);
4420
+ this.elementFormattingAnalysis.set(node, this.analyzeElementFormatting(node));
4421
+ this.visit(node.open_tag);
4422
+ if (node.body.length > 0) {
4423
+ this.visitHTMLElementBody(node.body, node);
4424
+ }
4425
+ if (node.close_tag) {
4426
+ this.visit(node.close_tag);
4427
+ }
4428
+ this.elementStack.pop();
4429
+ }
4430
+ visitHTMLElementBody(body, element) {
4431
+ if (this.isContentPreserving(element)) {
4432
+ element.body.map(child => this.pushToLastLine(IdentityPrinter.print(child)));
2998
4433
  return;
2999
4434
  }
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
- }
4435
+ const analysis = this.elementFormattingAnalysis.get(element);
4436
+ const hasTextFlow = this.isInTextFlowContext(null, body);
4437
+ const children = this.filterSignificantChildren(body, hasTextFlow);
4438
+ if (analysis?.elementContentInline) {
4439
+ if (children.length === 0)
3011
4440
  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');
4441
+ const oldInlineMode = this.inlineMode;
4442
+ const nodesToRender = hasTextFlow ? body : children;
4443
+ this.inlineMode = true;
4444
+ const lines = this.capture(() => {
4445
+ nodesToRender.forEach(child => {
4446
+ if (isNode(child, HTMLTextNode)) {
4447
+ if (hasTextFlow) {
4448
+ const normalizedContent = child.content.replace(/\s+/g, ' ');
4449
+ if (normalizedContent && normalizedContent !== ' ') {
4450
+ this.push(normalizedContent);
3037
4451
  }
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
- }
4452
+ else if (normalizedContent === ' ') {
4453
+ this.push(' ');
3050
4454
  }
3051
4455
  }
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;
4456
+ else {
4457
+ const normalizedContent = child.content.replace(/\s+/g, ' ').trim();
4458
+ if (normalizedContent) {
4459
+ this.push(normalizedContent);
3139
4460
  }
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;
4461
+ }
3147
4462
  }
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);
4463
+ else if (isNode(child, WhitespaceNode)) {
3159
4464
  return;
3160
4465
  }
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
4466
  else {
3170
- children.forEach(child => this.visit(child));
4467
+ this.visit(child);
3171
4468
  }
3172
4469
  });
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
4470
  });
3192
- if (!node.is_void && !isSelfClosing) {
3193
- this.push(indent + `</${tagName}>`);
4471
+ const content = lines.join('');
4472
+ const inlineContent = hasTextFlow ? content.replace(/\s+/g, ' ').trim() : content.trim();
4473
+ if (inlineContent) {
4474
+ this.pushToLastLine(inlineContent);
3194
4475
  }
4476
+ this.inlineMode = oldInlineMode;
3195
4477
  return;
3196
4478
  }
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}>`);
4479
+ if (children.length === 0)
4480
+ return;
4481
+ this.withIndent(() => {
4482
+ if (hasTextFlow) {
4483
+ this.visitTextFlowChildren(children);
3204
4484
  }
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}>`);
4485
+ else {
4486
+ this.visitElementChildren(body, element);
3213
4487
  }
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;
4488
+ });
4489
+ }
4490
+ /**
4491
+ * Visit element children with intelligent spacing logic
4492
+ */
4493
+ visitElementChildren(body, parentElement) {
4494
+ let lastWasMeaningful = false;
4495
+ let hasHandledSpacing = false;
4496
+ for (let i = 0; i < body.length; i++) {
4497
+ const child = body[i];
4498
+ if (isNode(child, HTMLTextNode)) {
4499
+ const isWhitespaceOnly = child.content.trim() === "";
4500
+ if (isWhitespaceOnly) {
4501
+ const hasPreviousNonWhitespace = i > 0 && this.isNonWhitespaceNode(body[i - 1]);
4502
+ const hasNextNonWhitespace = i < body.length - 1 && this.isNonWhitespaceNode(body[i + 1]);
4503
+ const hasMultipleNewlines = child.content.includes('\n\n');
4504
+ if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
4505
+ this.push("");
4506
+ hasHandledSpacing = true;
3223
4507
  }
4508
+ continue;
3224
4509
  }
3225
4510
  }
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;
4511
+ if (this.isNonWhitespaceNode(child) && lastWasMeaningful && !hasHandledSpacing) {
4512
+ const element = body[i - 1];
4513
+ const hasExistingSpacing = i > 0 && isNode(element, HTMLTextNode) && element.content.trim() === "" && (element.content.includes('\n\n') || element.content.split('\n').length > 2);
4514
+ const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, i, hasExistingSpacing);
4515
+ if (shouldAddSpacing) {
4516
+ this.push("");
3244
4517
  }
3245
4518
  }
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}>`);
4519
+ this.visit(child);
4520
+ if (this.isNonWhitespaceNode(child)) {
4521
+ lastWasMeaningful = true;
4522
+ hasHandledSpacing = false;
3257
4523
  }
3258
4524
  }
3259
4525
  }
3260
4526
  visitHTMLOpenTagNode(node) {
3261
- const tagName = node.tag_name?.value ?? "";
3262
- const indent = this.indent();
3263
- const attributes = this.extractAttributes(node.children);
4527
+ const attributes = filterNodes(node.children, HTMLAttributeNode);
3264
4528
  const inlineNodes = this.extractInlineNodes(node.children);
3265
- const hasClosing = node.tag_closing?.value === ">";
3266
- if (!hasClosing) {
3267
- this.push(indent + `<${tagName}`);
3268
- return;
4529
+ const isSelfClosing = node.tag_closing?.value === "/>";
4530
+ if (this.currentElement && this.elementFormattingAnalysis.has(this.currentElement)) {
4531
+ const analysis = this.elementFormattingAnalysis.get(this.currentElement);
4532
+ if (analysis.openTagInline) {
4533
+ const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children);
4534
+ this.push(this.inlineMode ? inline : this.indent + inline);
4535
+ return;
4536
+ }
4537
+ else {
4538
+ this.renderMultilineAttributes(getTagName(node), node.children, isSelfClosing);
4539
+ return;
4540
+ }
3269
4541
  }
3270
- const inline = this.renderInlineOpen(tagName, attributes, node.is_void, inlineNodes, node.children);
4542
+ const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children);
3271
4543
  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));
4544
+ const shouldKeepInline = this.shouldRenderInline(totalAttributeCount, inline.length, this.indent.length, this.maxLineLength, false, this.hasMultilineAttributes(attributes), attributes);
3273
4545
  if (shouldKeepInline) {
3274
- this.push(indent + inline);
3275
- return;
4546
+ this.push(this.inlineMode ? inline : this.indent + inline);
3276
4547
  }
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;
4548
+ else {
4549
+ this.renderMultilineAttributes(getTagName(node), node.children, isSelfClosing);
3290
4550
  }
3291
- this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false);
3292
4551
  }
3293
4552
  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);
4553
+ const closingTag = IdentityPrinter.print(node);
4554
+ const analysis = this.currentElement && this.elementFormattingAnalysis.get(this.currentElement);
4555
+ const closeTagInline = analysis?.closeTagInline;
4556
+ if (this.currentElement && closeTagInline) {
4557
+ this.pushToLastLine(closingTag);
4558
+ }
4559
+ else {
4560
+ this.pushWithIndent(closingTag);
4561
+ }
3299
4562
  }
3300
4563
  visitHTMLTextNode(node) {
3301
4564
  if (this.inlineMode) {
@@ -3305,17 +4568,16 @@ class Printer extends Visitor {
3305
4568
  }
3306
4569
  return;
3307
4570
  }
3308
- const indent = this.indent();
3309
4571
  let text = node.content.trim();
3310
4572
  if (!text)
3311
4573
  return;
3312
- const wrapWidth = this.maxLineLength - indent.length;
4574
+ const wrapWidth = this.maxLineLength - this.indent.length;
3313
4575
  const words = text.split(/\s+/);
3314
4576
  const lines = [];
3315
4577
  let line = "";
3316
4578
  for (const word of words) {
3317
4579
  if ((line + (line ? " " : "") + word).length > wrapWidth && line) {
3318
- lines.push(indent + line);
4580
+ lines.push(this.indent + line);
3319
4581
  line = word;
3320
4582
  }
3321
4583
  else {
@@ -3323,115 +4585,105 @@ class Printer extends Visitor {
3323
4585
  }
3324
4586
  }
3325
4587
  if (line)
3326
- lines.push(indent + line);
4588
+ lines.push(this.indent + line);
3327
4589
  lines.forEach(line => this.push(line));
3328
4590
  }
3329
4591
  visitHTMLAttributeNode(node) {
3330
- const indent = this.indent();
3331
- this.push(indent + this.renderAttribute(node));
4592
+ this.pushWithIndent(this.renderAttribute(node));
3332
4593
  }
3333
4594
  visitHTMLAttributeNameNode(node) {
3334
- const indent = this.indent();
3335
- const name = node.name?.value ?? "";
3336
- this.push(indent + name);
4595
+ this.pushWithIndent(getCombinedAttributeName(node));
3337
4596
  }
3338
4597
  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);
4598
+ this.pushWithIndent(IdentityPrinter.print(node));
3354
4599
  }
4600
+ // TODO: rework
3355
4601
  visitHTMLCommentNode(node) {
3356
- const indent = this.indent();
3357
4602
  const open = node.comment_start?.value ?? "";
3358
4603
  const close = node.comment_end?.value ?? "";
3359
4604
  let inner;
3360
4605
  if (node.children && node.children.length > 0) {
3361
4606
  inner = node.children.map(child => {
3362
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
4607
+ if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
3363
4608
  return child.content;
3364
4609
  }
3365
- else if (child instanceof LiteralNode || child.type === 'AST_LITERAL_NODE') {
3366
- return child.content;
4610
+ else if (isERBNode(child) || isNode(child, ERBContentNode)) {
4611
+ return this.reconstructERBNode(child, false);
3367
4612
  }
3368
4613
  else {
3369
- const prevLines = this.lines.length;
3370
- this.visit(child);
3371
- return this.lines.slice(prevLines).join("");
4614
+ return "";
3372
4615
  }
3373
4616
  }).join("");
3374
- inner = ` ${inner.trim()} `;
4617
+ const hasNewlines = inner.includes('\n');
4618
+ if (hasNewlines) {
4619
+ const lines = inner.split('\n');
4620
+ const childIndent = " ".repeat(this.indentWidth);
4621
+ const firstLineHasContent = lines[0].trim() !== '';
4622
+ if (firstLineHasContent && lines.length > 1) {
4623
+ const contentLines = lines.map(line => line.trim()).filter(line => line !== '');
4624
+ inner = '\n' + contentLines.map(line => childIndent + line).join('\n') + '\n';
4625
+ }
4626
+ else {
4627
+ const contentLines = lines.filter((line, index) => {
4628
+ return line.trim() !== '' && !(index === 0 || index === lines.length - 1);
4629
+ });
4630
+ const minIndent = contentLines.length > 0 ? Math.min(...contentLines.map(line => line.length - line.trimStart().length)) : 0;
4631
+ const processedLines = lines.map((line, index) => {
4632
+ const trimmedLine = line.trim();
4633
+ if ((index === 0 || index === lines.length - 1) && trimmedLine === '') {
4634
+ return line;
4635
+ }
4636
+ if (trimmedLine !== '') {
4637
+ const currentIndent = line.length - line.trimStart().length;
4638
+ const relativeIndent = Math.max(0, currentIndent - minIndent);
4639
+ return childIndent + " ".repeat(relativeIndent) + trimmedLine;
4640
+ }
4641
+ return line;
4642
+ });
4643
+ inner = processedLines.join('\n');
4644
+ }
4645
+ }
4646
+ else {
4647
+ inner = ` ${inner.trim()} `;
4648
+ }
3375
4649
  }
3376
4650
  else {
3377
4651
  inner = "";
3378
4652
  }
3379
- this.push(indent + open + inner + close);
4653
+ this.pushWithIndent(open + inner + close);
3380
4654
  }
3381
4655
  visitERBCommentNode(node) {
3382
- const indent = this.indent();
3383
- const open = node.tag_opening?.value ?? "";
3384
- const close = node.tag_closing?.value ?? "";
3385
- let inner;
3386
- if (node.content && node.content.value) {
3387
- const rawInner = node.content.value;
3388
- const lines = rawInner.split("\n");
3389
- if (lines.length > 2) {
3390
- const childIndent = indent + " ".repeat(this.indentWidth);
3391
- const innerLines = lines.slice(1, -1).map(line => childIndent + line.trim());
3392
- inner = "\n" + innerLines.join("\n") + "\n";
3393
- }
3394
- else {
3395
- inner = ` ${rawInner.trim()} `;
3396
- }
3397
- }
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("");
4656
+ const open = node.tag_opening?.value || "<%#";
4657
+ const content = node?.content?.value || "";
4658
+ const close = node.tag_closing?.value || "%>";
4659
+ const contentLines = content.split("\n");
4660
+ const contentTrimmedLines = content.trim().split("\n");
4661
+ if (contentLines.length === 1 && contentTrimmedLines.length === 1) {
4662
+ const startsWithSpace = content[0] === " ";
4663
+ const before = startsWithSpace ? "" : " ";
4664
+ this.pushWithIndent(open + before + content.trimEnd() + ' ' + close);
4665
+ return;
3404
4666
  }
3405
- else {
3406
- inner = "";
4667
+ if (contentTrimmedLines.length === 1) {
4668
+ this.pushWithIndent(open + ' ' + content.trim() + ' ' + close);
4669
+ return;
3407
4670
  }
3408
- this.push(indent + open + inner + close);
4671
+ const firstLineEmpty = contentLines[0].trim() === "";
4672
+ const dedentedContent = dedent(firstLineEmpty ? content : content.trimStart());
4673
+ this.pushWithIndent(open);
4674
+ this.withIndent(() => {
4675
+ dedentedContent.split("\n").forEach(line => this.pushWithIndent(line));
4676
+ });
4677
+ this.pushWithIndent(close);
3409
4678
  }
3410
4679
  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);
4680
+ this.pushWithIndent(IdentityPrinter.print(node));
4681
+ }
4682
+ visitXMLDeclarationNode(node) {
4683
+ this.pushWithIndent(IdentityPrinter.print(node));
4684
+ }
4685
+ visitCDATANode(node) {
4686
+ this.pushWithIndent(IdentityPrinter.print(node));
3435
4687
  }
3436
4688
  visitERBContentNode(node) {
3437
4689
  // TODO: this feels hacky
@@ -3450,109 +4702,81 @@ class Printer extends Visitor {
3450
4702
  }
3451
4703
  visitERBInNode(node) {
3452
4704
  this.printERBNode(node);
3453
- this.withIndent(() => {
3454
- node.statements.forEach(stmt => this.visit(stmt));
3455
- });
4705
+ this.withIndent(() => this.visitAll(node.statements));
3456
4706
  }
3457
4707
  visitERBCaseMatchNode(node) {
3458
4708
  this.printERBNode(node);
3459
- node.conditions.forEach(condition => this.visit(condition));
4709
+ this.visitAll(node.conditions);
3460
4710
  if (node.else_clause)
3461
4711
  this.visit(node.else_clause);
3462
4712
  if (node.end_node)
3463
4713
  this.visit(node.end_node);
3464
4714
  }
3465
4715
  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) {
4716
+ this.printERBNode(node);
4717
+ this.withIndent(() => this.visitElementChildren(node.body, null));
4718
+ if (node.end_node)
3475
4719
  this.visit(node.end_node);
3476
- }
3477
4720
  }
3478
4721
  visitERBIfNode(node) {
3479
4722
  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') {
4723
+ this.printERBNode(node);
4724
+ node.statements.forEach(child => {
4725
+ if (isNode(child, HTMLAttributeNode)) {
4726
+ this.lines.push(" ");
3488
4727
  this.lines.push(this.renderAttribute(child));
3489
4728
  }
3490
4729
  else {
4730
+ const shouldAddSpaces = this.isInTokenListAttribute();
4731
+ if (shouldAddSpaces) {
4732
+ this.lines.push(" ");
4733
+ }
3491
4734
  this.visit(child);
4735
+ if (shouldAddSpaces) {
4736
+ this.lines.push(" ");
4737
+ }
3492
4738
  }
3493
4739
  });
3494
- if (node.statements.length > 0 && node.end_node) {
4740
+ const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode));
4741
+ const isTokenList = this.isInTokenListAttribute();
4742
+ if ((hasHTMLAttributes || isTokenList) && node.end_node) {
3495
4743
  this.lines.push(" ");
3496
4744
  }
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
- }
4745
+ if (node.subsequent)
4746
+ this.visit(node.end_node);
4747
+ if (node.end_node)
4748
+ this.visit(node.end_node);
3508
4749
  }
3509
4750
  else {
3510
4751
  this.printERBNode(node);
3511
4752
  this.withIndent(() => {
3512
4753
  node.statements.forEach(child => this.visit(child));
3513
4754
  });
3514
- if (node.subsequent) {
4755
+ if (node.subsequent)
3515
4756
  this.visit(node.subsequent);
3516
- }
3517
- if (node.end_node) {
3518
- this.printERBNode(node.end_node);
3519
- }
4757
+ if (node.end_node)
4758
+ this.visit(node.end_node);
3520
4759
  }
3521
4760
  }
3522
4761
  visitERBElseNode(node) {
3523
4762
  this.printERBNode(node);
3524
- this.withIndent(() => {
3525
- node.statements.forEach(child => this.visit(child));
3526
- });
4763
+ this.withIndent(() => node.statements.forEach(statement => this.visit(statement)));
3527
4764
  }
3528
4765
  visitERBWhenNode(node) {
3529
4766
  this.printERBNode(node);
3530
- this.withIndent(() => {
3531
- node.statements.forEach(stmt => this.visit(stmt));
3532
- });
4767
+ this.withIndent(() => this.visitAll(node.statements));
3533
4768
  }
3534
4769
  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));
4770
+ this.printERBNode(node);
4771
+ this.visitAll(node.conditions);
3541
4772
  if (node.else_clause)
3542
4773
  this.visit(node.else_clause);
3543
- if (node.end_node) {
4774
+ if (node.end_node)
3544
4775
  this.visit(node.end_node);
3545
- }
3546
4776
  }
3547
4777
  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
- });
4778
+ this.printERBNode(node);
4779
+ this.withIndent(() => this.visitAll(node.statements));
3556
4780
  if (node.rescue_clause)
3557
4781
  this.visit(node.rescue_clause);
3558
4782
  if (node.else_clause)
@@ -3563,61 +4787,150 @@ class Printer extends Visitor {
3563
4787
  this.visit(node.end_node);
3564
4788
  }
3565
4789
  visitERBWhileNode(node) {
3566
- this.visitERBGeneric(node);
4790
+ this.printERBNode(node);
4791
+ this.withIndent(() => this.visitAll(node.statements));
4792
+ if (node.end_node)
4793
+ this.visit(node.end_node);
3567
4794
  }
3568
4795
  visitERBUntilNode(node) {
3569
- this.visitERBGeneric(node);
4796
+ this.printERBNode(node);
4797
+ this.withIndent(() => this.visitAll(node.statements));
4798
+ if (node.end_node)
4799
+ this.visit(node.end_node);
3570
4800
  }
3571
4801
  visitERBForNode(node) {
3572
- this.visitERBGeneric(node);
4802
+ this.printERBNode(node);
4803
+ this.withIndent(() => this.visitAll(node.statements));
4804
+ if (node.end_node)
4805
+ this.visit(node.end_node);
3573
4806
  }
3574
4807
  visitERBRescueNode(node) {
3575
- this.visitERBGeneric(node);
4808
+ this.printERBNode(node);
4809
+ this.withIndent(() => this.visitAll(node.statements));
3576
4810
  }
3577
4811
  visitERBEnsureNode(node) {
3578
- this.visitERBGeneric(node);
4812
+ this.printERBNode(node);
4813
+ this.withIndent(() => this.visitAll(node.statements));
3579
4814
  }
3580
4815
  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
- });
4816
+ this.printERBNode(node);
4817
+ this.withIndent(() => this.visitAll(node.statements));
4818
+ if (node.else_clause)
4819
+ this.visit(node.else_clause);
3594
4820
  if (node.end_node)
3595
4821
  this.visit(node.end_node);
3596
4822
  }
4823
+ // --- Element Formatting Analysis Helpers ---
4824
+ /**
4825
+ * Analyzes an HTMLElementNode and returns formatting decisions for all parts
4826
+ */
4827
+ analyzeElementFormatting(node) {
4828
+ const openTagInline = this.shouldRenderOpenTagInline(node);
4829
+ const elementContentInline = this.shouldRenderElementContentInline(node);
4830
+ const closeTagInline = this.shouldRenderCloseTagInline(node, elementContentInline);
4831
+ return {
4832
+ openTagInline,
4833
+ elementContentInline,
4834
+ closeTagInline
4835
+ };
4836
+ }
4837
+ /**
4838
+ * Determines if the open tag should be rendered inline
4839
+ */
4840
+ shouldRenderOpenTagInline(node) {
4841
+ const children = node.open_tag?.children || [];
4842
+ const attributes = filterNodes(children, HTMLAttributeNode);
4843
+ const inlineNodes = this.extractInlineNodes(children);
4844
+ const hasERBControlFlow = inlineNodes.some(node => isERBControlFlowNode(node)) || children.some(node => isERBControlFlowNode(node));
4845
+ const hasComplexERB = hasERBControlFlow && this.hasComplexERBControlFlow(inlineNodes);
4846
+ if (hasComplexERB)
4847
+ return false;
4848
+ const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes);
4849
+ const hasMultilineAttrs = this.hasMultilineAttributes(attributes);
4850
+ if (hasMultilineAttrs)
4851
+ return false;
4852
+ const inline = this.renderInlineOpen(getTagName(node), attributes, node.open_tag?.tag_closing?.value === "/>", inlineNodes, children);
4853
+ return this.shouldRenderInline(totalAttributeCount, inline.length, this.indent.length, this.maxLineLength, hasComplexERB, hasMultilineAttrs, attributes);
4854
+ }
4855
+ /**
4856
+ * Determines if the element content should be rendered inline
4857
+ */
4858
+ shouldRenderElementContentInline(node) {
4859
+ const tagName = getTagName(node);
4860
+ const children = this.filterSignificantChildren(node.body, this.isInTextFlowContext(null, node.body));
4861
+ const isInlineElement = this.isInlineElement(tagName);
4862
+ const openTagInline = this.shouldRenderOpenTagInline(node);
4863
+ if (!openTagInline)
4864
+ return false;
4865
+ if (children.length === 0)
4866
+ return true;
4867
+ if (isInlineElement) {
4868
+ const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), children);
4869
+ if (fullInlineResult) {
4870
+ const totalLength = this.indent.length + fullInlineResult.length;
4871
+ return totalLength <= this.maxLineLength || totalLength <= 120;
4872
+ }
4873
+ return false;
4874
+ }
4875
+ const allNestedAreInline = this.areAllNestedElementsInline(children);
4876
+ const hasMultilineText = this.hasMultilineTextContent(children);
4877
+ const hasMixedContent = this.hasMixedTextAndInlineContent(children);
4878
+ if (allNestedAreInline && (!hasMultilineText || hasMixedContent)) {
4879
+ const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), children);
4880
+ if (fullInlineResult) {
4881
+ const totalLength = this.indent.length + fullInlineResult.length;
4882
+ if (totalLength <= this.maxLineLength) {
4883
+ return true;
4884
+ }
4885
+ }
4886
+ }
4887
+ const inlineResult = this.tryRenderInline(children, tagName);
4888
+ if (inlineResult) {
4889
+ const openTagResult = this.renderInlineOpen(tagName, filterNodes(node.open_tag?.children, HTMLAttributeNode), false, [], node.open_tag?.children || []);
4890
+ const childrenContent = this.renderChildrenInline(children);
4891
+ const fullLine = openTagResult + childrenContent + `</${tagName}>`;
4892
+ if ((this.indent.length + fullLine.length) <= this.maxLineLength) {
4893
+ return true;
4894
+ }
4895
+ }
4896
+ return false;
4897
+ }
4898
+ /**
4899
+ * Determines if the close tag should be rendered inline (usually follows content decision)
4900
+ */
4901
+ shouldRenderCloseTagInline(node, elementContentInline) {
4902
+ if (node.is_void)
4903
+ return true;
4904
+ if (node.open_tag?.tag_closing?.value === "/>")
4905
+ return true;
4906
+ if (this.isContentPreserving(node))
4907
+ return true;
4908
+ const children = this.filterSignificantChildren(node.body, this.isInTextFlowContext(null, node.body));
4909
+ if (children.length === 0)
4910
+ return true;
4911
+ return elementContentInline;
4912
+ }
3597
4913
  // --- Utility methods ---
3598
4914
  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') {
4915
+ if (isNode(node, WhitespaceNode))
3603
4916
  return false;
3604
- }
4917
+ if (isNode(node, HTMLTextNode))
4918
+ return node.content.trim() !== "";
3605
4919
  return true;
3606
4920
  }
3607
4921
  /**
3608
4922
  * Check if an element should be treated as inline based on its tag name
3609
4923
  */
3610
4924
  isInlineElement(tagName) {
3611
- return Printer.INLINE_ELEMENTS.has(tagName.toLowerCase());
4925
+ return FormatPrinter.INLINE_ELEMENTS.has(tagName.toLowerCase());
3612
4926
  }
3613
4927
  /**
3614
4928
  * Check if we're in a text flow context (parent contains mixed text and inline elements)
3615
4929
  */
3616
4930
  visitTextFlowChildren(children) {
3617
- const indent = this.indent();
3618
4931
  let currentLineContent = "";
3619
4932
  for (const child of children) {
3620
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
4933
+ if (isNode(child, HTMLTextNode)) {
3621
4934
  const content = child.content;
3622
4935
  let processedContent = content.replace(/\s+/g, ' ').trim();
3623
4936
  if (processedContent) {
@@ -3630,29 +4943,26 @@ class Printer extends Visitor {
3630
4943
  if (hasTrailingSpace && !currentLineContent.endsWith(' ')) {
3631
4944
  currentLineContent += ' ';
3632
4945
  }
3633
- if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
3634
- this.visitTextFlowChildrenMultiline(children);
4946
+ if ((this.indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
4947
+ children.forEach(child => this.visit(child));
3635
4948
  return;
3636
4949
  }
3637
4950
  }
3638
4951
  }
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 || '';
4952
+ else if (isNode(child, HTMLElementNode)) {
4953
+ const childTagName = getTagName(child);
3643
4954
  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() === "")));
4955
+ const childInline = this.tryRenderInlineFull(child, childTagName, filterNodes(child.open_tag?.children, HTMLAttributeNode), this.filterEmptyNodes(child.body));
3646
4956
  if (childInline) {
3647
4957
  currentLineContent += childInline;
3648
- if ((indent.length + currentLineContent.length) > this.maxLineLength) {
3649
- this.visitTextFlowChildrenMultiline(children);
4958
+ if ((this.indent.length + currentLineContent.length) > this.maxLineLength) {
4959
+ children.forEach(child => this.visit(child));
3650
4960
  return;
3651
4961
  }
3652
4962
  }
3653
4963
  else {
3654
4964
  if (currentLineContent.trim()) {
3655
- this.push(indent + currentLineContent.trim());
4965
+ this.pushWithIndent(currentLineContent.trim());
3656
4966
  currentLineContent = "";
3657
4967
  }
3658
4968
  this.visit(child);
@@ -3660,25 +4970,26 @@ class Printer extends Visitor {
3660
4970
  }
3661
4971
  else {
3662
4972
  if (currentLineContent.trim()) {
3663
- this.push(indent + currentLineContent.trim());
4973
+ this.pushWithIndent(currentLineContent.trim());
3664
4974
  currentLineContent = "";
3665
4975
  }
3666
4976
  this.visit(child);
3667
4977
  }
3668
4978
  }
3669
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
4979
+ else if (isNode(child, ERBContentNode)) {
3670
4980
  const oldLines = this.lines;
3671
4981
  const oldInlineMode = this.inlineMode;
4982
+ // TODO: use this.capture
3672
4983
  try {
3673
4984
  this.lines = [];
3674
4985
  this.inlineMode = true;
3675
4986
  this.visit(child);
3676
4987
  const erbContent = this.lines.join("");
3677
4988
  currentLineContent += erbContent;
3678
- if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
4989
+ if ((this.indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
3679
4990
  this.lines = oldLines;
3680
4991
  this.inlineMode = oldInlineMode;
3681
- this.visitTextFlowChildrenMultiline(children);
4992
+ children.forEach(child => this.visit(child));
3682
4993
  return;
3683
4994
  }
3684
4995
  }
@@ -3689,53 +5000,38 @@ class Printer extends Visitor {
3689
5000
  }
3690
5001
  else {
3691
5002
  if (currentLineContent.trim()) {
3692
- this.push(indent + currentLineContent.trim());
5003
+ this.pushWithIndent(currentLineContent.trim());
3693
5004
  currentLineContent = "";
3694
5005
  }
3695
5006
  this.visit(child);
3696
5007
  }
3697
5008
  }
3698
5009
  if (currentLineContent.trim()) {
3699
- const finalLine = indent + currentLineContent.trim();
5010
+ const finalLine = this.indent + currentLineContent.trim();
3700
5011
  if (finalLine.length > Math.max(this.maxLineLength, 120)) {
3701
- this.visitTextFlowChildrenMultiline(children);
5012
+ this.visitAll(children);
3702
5013
  return;
3703
5014
  }
3704
5015
  this.push(finalLine);
3705
5016
  }
3706
5017
  }
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) {
5018
+ isInTextFlowContext(_parent, children) {
5019
+ const hasTextContent = children.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "");
5020
+ const nonTextChildren = children.filter(child => !isNode(child, HTMLTextNode));
5021
+ if (!hasTextContent)
3714
5022
  return false;
3715
- }
3716
- const nonTextChildren = children.filter(child => !(child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE'));
3717
- if (nonTextChildren.length === 0) {
5023
+ if (nonTextChildren.length === 0)
3718
5024
  return false;
3719
- }
3720
5025
  const allInline = nonTextChildren.every(child => {
3721
- if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
5026
+ if (isNode(child, ERBContentNode))
3722
5027
  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);
5028
+ if (isNode(child, HTMLElementNode)) {
5029
+ return this.isInlineElement(getTagName(child));
3729
5030
  }
3730
5031
  return false;
3731
5032
  });
3732
- if (!allInline) {
5033
+ if (!allInline)
3733
5034
  return false;
3734
- }
3735
- const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3736
- if (maxNestingDepth > 2) {
3737
- return false;
3738
- }
3739
5035
  return true;
3740
5036
  }
3741
5037
  renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
@@ -3743,73 +5039,68 @@ class Printer extends Visitor {
3743
5039
  if (inlineNodes.length > 0) {
3744
5040
  let result = `<${name}`;
3745
5041
  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
- }
5042
+ const lines = this.capture(() => {
5043
+ allChildren.forEach(child => {
5044
+ if (isNode(child, HTMLAttributeNode)) {
5045
+ this.lines.push(" " + this.renderAttribute(child));
5046
+ }
5047
+ else if (!(isNode(child, WhitespaceNode))) {
5048
+ const wasInlineMode = this.inlineMode;
5049
+ this.inlineMode = true;
5050
+ this.lines.push(" ");
5051
+ this.visit(child);
5052
+ this.inlineMode = wasInlineMode;
5053
+ }
5054
+ });
3761
5055
  });
3762
- const inlineContent = this.lines.join("");
3763
- this.lines = tempLines;
3764
- this.indentLevel = currentIndentLevel;
3765
- result += inlineContent;
5056
+ result += lines.join("");
3766
5057
  }
3767
5058
  else {
3768
5059
  if (parts.length > 0) {
3769
5060
  result += ` ${parts.join(" ")}`;
3770
5061
  }
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;
5062
+ const lines = this.capture(() => {
5063
+ inlineNodes.forEach(node => {
5064
+ const wasInlineMode = this.inlineMode;
5065
+ if (!isERBControlFlowNode(node)) {
5066
+ this.inlineMode = true;
5067
+ }
5068
+ this.visit(node);
5069
+ this.inlineMode = wasInlineMode;
5070
+ });
3782
5071
  });
3783
- const inlineContent = this.lines.join("");
3784
- this.lines = tempLines;
3785
- this.indentLevel = currentIndentLevel;
3786
- result += inlineContent;
5072
+ result += lines.join("");
3787
5073
  }
3788
5074
  result += selfClose ? " />" : ">";
3789
5075
  return result;
3790
5076
  }
3791
- return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " /" : ""}>`;
5077
+ return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " />" : ">"}`;
3792
5078
  }
3793
5079
  renderAttribute(attribute) {
3794
- const name = attribute.name.name.value ?? "";
5080
+ const name = attribute.name ? getCombinedAttributeName(attribute.name) : "";
3795
5081
  const equals = attribute.equals?.value ?? "";
5082
+ this.currentAttributeName = name;
3796
5083
  let value = "";
3797
- if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || attribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
5084
+ if (isNode(attribute.value, HTMLAttributeValueNode)) {
3798
5085
  const attributeValue = attribute.value;
3799
5086
  let open_quote = attributeValue.open_quote?.value ?? "";
3800
5087
  let close_quote = attributeValue.close_quote?.value ?? "";
3801
5088
  let htmlTextContent = "";
3802
5089
  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;
5090
+ if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
5091
+ htmlTextContent += child.content;
5092
+ return child.content;
3807
5093
  }
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;
5094
+ else if (isNode(child, ERBContentNode)) {
5095
+ return IdentityPrinter.print(child);
5096
+ }
5097
+ else {
5098
+ const printed = IdentityPrinter.print(child);
5099
+ if (this.currentAttributeName && FormatPrinter.TOKEN_LIST_ATTRIBUTES.has(this.currentAttributeName)) {
5100
+ return printed.replace(/%>([^<\s])/g, '%> $1').replace(/([^>\s])<%/g, '$1 <%');
5101
+ }
5102
+ return printed;
3811
5103
  }
3812
- return "";
3813
5104
  }).join("");
3814
5105
  if (open_quote === "" && close_quote === "") {
3815
5106
  open_quote = '"';
@@ -3824,13 +5115,14 @@ class Printer extends Visitor {
3824
5115
  value = this.formatClassAttribute(content, name, equals, open_quote, close_quote);
3825
5116
  }
3826
5117
  else {
3827
- value = this.formatMultilineAttribute(content, name, equals, open_quote, close_quote);
5118
+ value = this.formatMultilineAttribute(content, name, open_quote, close_quote);
3828
5119
  }
3829
5120
  }
3830
5121
  else {
3831
5122
  value = open_quote + content + close_quote;
3832
5123
  }
3833
5124
  }
5125
+ this.currentAttributeName = null;
3834
5126
  return name + equals + value;
3835
5127
  }
3836
5128
  /**
@@ -3841,9 +5133,8 @@ class Printer extends Visitor {
3841
5133
  result += this.renderAttributesString(attributes);
3842
5134
  result += ">";
3843
5135
  const childrenContent = this.tryRenderChildrenInline(children);
3844
- if (!childrenContent) {
5136
+ if (!childrenContent)
3845
5137
  return null;
3846
- }
3847
5138
  result += childrenContent;
3848
5139
  result += `</${tagName}>`;
3849
5140
  return result;
@@ -3854,11 +5145,10 @@ class Printer extends Visitor {
3854
5145
  tryRenderChildrenInline(children) {
3855
5146
  let result = "";
3856
5147
  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);
5148
+ if (isNode(child, HTMLTextNode)) {
5149
+ const normalizedContent = child.content.replace(/\s+/g, ' ');
5150
+ const hasLeadingSpace = /^\s/.test(child.content);
5151
+ const hasTrailingSpace = /\s$/.test(child.content);
3862
5152
  const trimmedContent = normalizedContent.trim();
3863
5153
  if (trimmedContent) {
3864
5154
  let finalContent = trimmedContent;
@@ -3876,36 +5166,19 @@ class Printer extends Visitor {
3876
5166
  }
3877
5167
  }
3878
5168
  }
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)) {
5169
+ else if (isNode(child, HTMLElementNode)) {
5170
+ const tagName = getTagName(child);
5171
+ if (!this.isInlineElement(tagName)) {
3884
5172
  return null;
3885
5173
  }
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() === "")));
5174
+ const childInline = this.tryRenderInlineFull(child, tagName, filterNodes(child.open_tag?.children, HTMLAttributeNode), this.filterEmptyNodes(child.body));
3888
5175
  if (!childInline) {
3889
5176
  return null;
3890
5177
  }
3891
5178
  result += childInline;
3892
5179
  }
3893
5180
  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
- }
5181
+ result += this.capture(() => this.visit(child)).join("");
3909
5182
  }
3910
5183
  }
3911
5184
  return result.trim();
@@ -3914,146 +5187,153 @@ class Printer extends Visitor {
3914
5187
  * Try to render children inline if they are simple enough.
3915
5188
  * Returns the inline string if possible, null otherwise.
3916
5189
  */
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
- }
5190
+ tryRenderInline(children, tagName) {
3933
5191
  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')) {
5192
+ if (isNode(child, HTMLTextNode)) {
5193
+ if (child.content.includes('\n')) {
3937
5194
  return null;
3938
5195
  }
3939
5196
  }
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);
5197
+ else if (isNode(child, HTMLElementNode)) {
5198
+ const isInlineElement = this.isInlineElement(getTagName(child));
3945
5199
  if (!isInlineElement) {
3946
5200
  return null;
3947
5201
  }
3948
5202
  }
3949
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
5203
+ else if (isNode(child, ERBContentNode)) ;
3950
5204
  else {
3951
5205
  return null;
3952
5206
  }
3953
5207
  }
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}>`;
5208
+ let content = "";
5209
+ this.capture(() => {
5210
+ content = this.renderChildrenInline(children);
5211
+ });
5212
+ return `<${tagName}>${content}</${tagName}>`;
5213
+ }
5214
+ /**
5215
+ * Check if children contain mixed text and inline elements (like "text<em>inline</em>text")
5216
+ * or mixed ERB output and text (like "<%= value %> text")
5217
+ * This indicates content that should be formatted inline even with structural newlines
5218
+ */
5219
+ hasMixedTextAndInlineContent(children) {
5220
+ let hasText = false;
5221
+ let hasInlineElements = false;
5222
+ for (const child of children) {
5223
+ if (isNode(child, HTMLTextNode)) {
5224
+ if (child.content.trim() !== "") {
5225
+ hasText = true;
3972
5226
  }
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}`;
5227
+ }
5228
+ else if (isNode(child, HTMLElementNode)) {
5229
+ if (this.isInlineElement(getTagName(child))) {
5230
+ hasInlineElements = true;
3979
5231
  }
3980
5232
  }
3981
- content = content.replace(/\s+/g, ' ').trim();
3982
- return `<${tagName}>${content}</${tagName}>`;
3983
- }
3984
- finally {
3985
- this.lines = oldLines;
3986
- this.inlineMode = oldInlineMode;
3987
5233
  }
5234
+ return (hasText && hasInlineElements) || (hasERBOutput(children) && hasText);
3988
5235
  }
3989
5236
  /**
3990
- * Estimate the total content length of children nodes for decision making.
5237
+ * Check if children contain any text content with newlines
3991
5238
  */
3992
- estimateContentLength(children) {
3993
- let length = 0;
5239
+ hasMultilineTextContent(children) {
3994
5240
  for (const child of children) {
3995
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3996
- length += child.content.length;
5241
+ if (isNode(child, HTMLTextNode)) {
5242
+ return child.content.includes('\n');
3997
5243
  }
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);
4004
- }
4005
- else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
4006
- length += child.content?.value.length || 0;
5244
+ if (isNode(child, HTMLElementNode)) {
5245
+ const nestedChildren = this.filterEmptyNodes(child.body);
5246
+ if (this.hasMultilineTextContent(nestedChildren)) {
5247
+ return true;
5248
+ }
4007
5249
  }
4008
5250
  }
4009
- return length;
5251
+ return false;
4010
5252
  }
4011
5253
  /**
4012
- * Calculate the maximum nesting depth in a subtree of nodes.
5254
+ * Check if all nested elements in the children are inline elements
4013
5255
  */
4014
- getMaxNestingDepth(children, currentDepth) {
4015
- let maxDepth = currentDepth;
5256
+ areAllNestedElementsInline(children) {
4016
5257
  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);
5258
+ if (isNode(child, HTMLElementNode)) {
5259
+ if (!this.isInlineElement(getTagName(child))) {
5260
+ return false;
5261
+ }
5262
+ const nestedChildren = this.filterEmptyNodes(child.body);
5263
+ if (!this.areAllNestedElementsInline(nestedChildren)) {
5264
+ return false;
5265
+ }
5266
+ }
5267
+ else if (isAnyOf(child, HTMLDoctypeNode, HTMLCommentNode, isERBControlFlowNode)) {
5268
+ return false;
4023
5269
  }
4024
5270
  }
4025
- return maxDepth;
5271
+ return true;
5272
+ }
5273
+ /**
5274
+ * Check if element has complex ERB control flow
5275
+ */
5276
+ hasComplexERBControlFlow(inlineNodes) {
5277
+ return inlineNodes.some(node => {
5278
+ if (isNode(node, ERBIfNode)) {
5279
+ if (node.statements.length > 0 && node.location) {
5280
+ const startLine = node.location.start.line;
5281
+ const endLine = node.location.end.line;
5282
+ return startLine !== endLine;
5283
+ }
5284
+ return false;
5285
+ }
5286
+ return false;
5287
+ });
5288
+ }
5289
+ /**
5290
+ * Filter children to remove insignificant whitespace
5291
+ */
5292
+ filterSignificantChildren(body, hasTextFlow) {
5293
+ return body.filter(child => {
5294
+ if (isNode(child, WhitespaceNode))
5295
+ return false;
5296
+ if (isNode(child, HTMLTextNode)) {
5297
+ if (hasTextFlow && child.content === " ")
5298
+ return true;
5299
+ return child.content.trim() !== "";
5300
+ }
5301
+ return true;
5302
+ });
4026
5303
  }
4027
5304
  /**
4028
- * Render an HTML element's content inline (without the wrapping tags).
5305
+ * Filter out empty text nodes and whitespace nodes
4029
5306
  */
5307
+ filterEmptyNodes(nodes) {
5308
+ return nodes.filter(child => !isNode(child, WhitespaceNode) && !(isNode(child, HTMLTextNode) && child.content.trim() === ""));
5309
+ }
4030
5310
  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() === ""));
5311
+ const children = this.filterEmptyNodes(element.body);
5312
+ return this.renderChildrenInline(children);
5313
+ }
5314
+ renderChildrenInline(children) {
4033
5315
  let content = '';
4034
5316
  for (const child of children) {
4035
- if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
5317
+ if (isNode(child, HTMLTextNode)) {
4036
5318
  content += child.content;
4037
5319
  }
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);
5320
+ else if (isNode(child, HTMLElementNode)) {
5321
+ const tagName = getTagName(child);
5322
+ const attributes = filterNodes(child.open_tag?.children, HTMLAttributeNode);
4043
5323
  const attributesString = this.renderAttributesString(attributes);
4044
- const childContent = this.renderElementInline(childElement);
4045
- content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
5324
+ const childContent = this.renderElementInline(child);
5325
+ content += `<${tagName}${attributesString}>${childContent}</${tagName}>`;
4046
5326
  }
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}`;
5327
+ else if (isNode(child, ERBContentNode)) {
5328
+ content += this.reconstructERBNode(child, true);
4053
5329
  }
4054
5330
  }
4055
5331
  return content.replace(/\s+/g, ' ').trim();
4056
5332
  }
5333
+ isContentPreserving(element) {
5334
+ const tagName = getTagName(element);
5335
+ return FormatPrinter.CONTENT_PRESERVING_ELEMENTS.has(tagName);
5336
+ }
4057
5337
  }
4058
5338
 
4059
5339
  /**
@@ -4094,7 +5374,7 @@ class Formatter {
4094
5374
  if (result.failed)
4095
5375
  return source;
4096
5376
  const resolvedOptions = resolveFormatOptions({ ...this.options, ...options });
4097
- return new Printer(source, resolvedOptions).print(result.value);
5377
+ return new FormatPrinter(source, resolvedOptions).print(result.value);
4098
5378
  }
4099
5379
  parse(source) {
4100
5380
  this.herb.ensureBackend();
@@ -4102,5 +5382,5 @@ class Formatter {
4102
5382
  }
4103
5383
  }
4104
5384
 
4105
- export { Formatter, defaultFormatOptions, resolveFormatOptions };
5385
+ export { FormatPrinter, Formatter, defaultFormatOptions, resolveFormatOptions };
4106
5386
  //# sourceMappingURL=index.esm.js.map