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