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