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