prettier 1.2.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +349 -358
- data/README.md +21 -93
- data/node_modules/prettier/index.js +54 -54
- data/package.json +1 -2
- data/rubocop.yml +26 -0
- data/src/haml/embed.js +87 -0
- data/src/haml/nodes/comment.js +27 -0
- data/src/haml/nodes/doctype.js +34 -0
- data/src/haml/nodes/filter.js +16 -0
- data/src/haml/nodes/hamlComment.js +21 -0
- data/src/haml/nodes/plain.js +6 -0
- data/src/haml/nodes/root.js +8 -0
- data/src/haml/nodes/script.js +33 -0
- data/src/haml/nodes/silentScript.js +59 -0
- data/src/haml/nodes/tag.js +193 -0
- data/src/haml/parser.js +22 -0
- data/src/haml/parser.rb +138 -0
- data/src/haml/printer.js +28 -0
- data/src/parser/getLang.js +32 -0
- data/src/parser/getNetcat.js +50 -0
- data/src/parser/netcat.js +15 -0
- data/src/parser/parseSync.js +33 -0
- data/src/parser/requestParse.js +74 -0
- data/src/parser/server.rb +61 -0
- data/src/plugin.js +26 -4
- data/src/prettier.js +1 -0
- data/src/rbs/parser.js +39 -0
- data/src/rbs/parser.rb +94 -0
- data/src/rbs/printer.js +605 -0
- data/src/ruby/embed.js +58 -8
- data/src/ruby/nodes/args.js +20 -6
- data/src/ruby/nodes/blocks.js +64 -59
- data/src/ruby/nodes/calls.js +12 -43
- data/src/ruby/nodes/class.js +17 -27
- data/src/ruby/nodes/commands.js +7 -2
- data/src/ruby/nodes/conditionals.js +1 -1
- data/src/ruby/nodes/hashes.js +28 -14
- data/src/ruby/nodes/hooks.js +9 -19
- data/src/ruby/nodes/loops.js +4 -10
- data/src/ruby/nodes/methods.js +8 -17
- data/src/ruby/nodes/params.js +22 -14
- data/src/ruby/nodes/patterns.js +9 -5
- data/src/ruby/nodes/rescue.js +32 -25
- data/src/ruby/nodes/return.js +0 -4
- data/src/ruby/nodes/statements.js +11 -13
- data/src/ruby/nodes/strings.js +27 -35
- data/src/ruby/parser.js +2 -49
- data/src/ruby/parser.rb +256 -232
- data/src/ruby/printer.js +0 -2
- data/src/ruby/toProc.js +4 -8
- data/src/utils.js +1 -0
- data/src/utils/isEmptyBodyStmt.js +7 -0
- data/src/utils/isEmptyStmts.js +9 -5
- data/src/utils/makeCall.js +3 -0
- data/src/utils/noIndent.js +1 -0
- data/src/utils/printEmptyCollection.js +9 -2
- metadata +26 -2
data/src/ruby/embed.js
CHANGED
@@ -20,8 +20,12 @@ const parsers = {
|
|
20
20
|
scss: "scss"
|
21
21
|
};
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
// This function is in here because it handles embedded parser values. I don't
|
24
|
+
// have a test that exercises it because I'm not sure for which parser it is
|
25
|
+
// necessary, but since it's in prettier core I'm keeping it here.
|
26
|
+
/* istanbul ignore next */
|
27
|
+
function replaceNewlines(doc) {
|
28
|
+
return mapDoc(doc, (currentDoc) =>
|
25
29
|
typeof currentDoc === "string" && currentDoc.includes("\n")
|
26
30
|
? concat(
|
27
31
|
currentDoc
|
@@ -30,8 +34,44 @@ const replaceNewlines = (doc) =>
|
|
30
34
|
)
|
31
35
|
: currentDoc
|
32
36
|
);
|
37
|
+
}
|
33
38
|
|
34
|
-
|
39
|
+
// Returns a number that represents the minimum amount of leading whitespace
|
40
|
+
// that is present on every line in the given string. So for example if you have
|
41
|
+
// the following heredoc:
|
42
|
+
//
|
43
|
+
// <<~HERE
|
44
|
+
// my
|
45
|
+
// content
|
46
|
+
// here
|
47
|
+
// HERE
|
48
|
+
//
|
49
|
+
// then the return value of this function would be 2. If you indented every line
|
50
|
+
// of the inner content 2 more spaces then this function would return 4.
|
51
|
+
function getCommonLeadingWhitespace(content) {
|
52
|
+
const pattern = /^\s+/;
|
53
|
+
|
54
|
+
return content
|
55
|
+
.split("\n")
|
56
|
+
.slice(0, -1)
|
57
|
+
.reduce((minimum, line) => {
|
58
|
+
const matched = pattern.exec(line);
|
59
|
+
const length = matched ? matched[0].length : 0;
|
60
|
+
|
61
|
+
return minimum === null ? length : Math.min(minimum, length);
|
62
|
+
}, null);
|
63
|
+
}
|
64
|
+
|
65
|
+
// Returns a new string with the common whitespace stripped out. Effectively it
|
66
|
+
// emulates what a squiggly heredoc does in Ruby.
|
67
|
+
function stripCommonLeadingWhitespace(content) {
|
68
|
+
const lines = content.split("\n");
|
69
|
+
const minimum = getCommonLeadingWhitespace(content);
|
70
|
+
|
71
|
+
return lines.map((line) => line.slice(minimum)).join("\n");
|
72
|
+
}
|
73
|
+
|
74
|
+
function embed(path, print, textToDoc, _opts) {
|
35
75
|
const node = path.getValue();
|
36
76
|
|
37
77
|
// Currently we only support embedded formatting on heredoc nodes
|
@@ -41,6 +81,8 @@ const embed = (path, print, textToDoc, _opts) => {
|
|
41
81
|
|
42
82
|
// First, ensure that we don't have any interpolation
|
43
83
|
const { beging, body, ending } = node;
|
84
|
+
const isSquiggly = beging.body[2] === "~";
|
85
|
+
|
44
86
|
if (body.some((part) => part.type !== "@tstring_content")) {
|
45
87
|
return null;
|
46
88
|
}
|
@@ -52,9 +94,17 @@ const embed = (path, print, textToDoc, _opts) => {
|
|
52
94
|
return null;
|
53
95
|
}
|
54
96
|
|
55
|
-
// Get the content as if it were a source string
|
56
|
-
|
57
|
-
|
97
|
+
// Get the content as if it were a source string.
|
98
|
+
let content = body.map((part) => part.body).join("");
|
99
|
+
|
100
|
+
// If we're using a squiggly heredoc, then we're going to manually strip off
|
101
|
+
// the leading whitespace of each line up to the minimum leading whitespace so
|
102
|
+
// that the embedded parser can handle that for us.
|
103
|
+
if (isSquiggly) {
|
104
|
+
content = stripCommonLeadingWhitespace(content);
|
105
|
+
}
|
106
|
+
|
107
|
+
// Pass that content into the embedded parser. Get back the doc node.
|
58
108
|
const formatted = concat([
|
59
109
|
literalLineNoBreak,
|
60
110
|
replaceNewlines(stripTrailingHardline(textToDoc(content, { parser })))
|
@@ -62,7 +112,7 @@ const embed = (path, print, textToDoc, _opts) => {
|
|
62
112
|
|
63
113
|
// If we're using a squiggly heredoc, then we can properly handle indentation
|
64
114
|
// ourselves.
|
65
|
-
if (
|
115
|
+
if (isSquiggly) {
|
66
116
|
return concat([
|
67
117
|
path.call(print, "beging"),
|
68
118
|
lineSuffix(
|
@@ -85,6 +135,6 @@ const embed = (path, print, textToDoc, _opts) => {
|
|
85
135
|
lineSuffix(group(concat([formatted, literalLineNoBreak, ending.trim()])))
|
86
136
|
])
|
87
137
|
);
|
88
|
-
}
|
138
|
+
}
|
89
139
|
|
90
140
|
module.exports = embed;
|
data/src/ruby/nodes/args.js
CHANGED
@@ -8,9 +8,26 @@ const {
|
|
8
8
|
softline
|
9
9
|
} = require("../../prettier");
|
10
10
|
const { getTrailingComma } = require("../../utils");
|
11
|
-
|
12
11
|
const toProc = require("../toProc");
|
13
12
|
|
13
|
+
const noTrailingComma = ["command", "command_call"];
|
14
|
+
|
15
|
+
function getArgParenTrailingComma(node) {
|
16
|
+
// If we have a block, then we don't want to add a trailing comma.
|
17
|
+
if (node.type === "args_add_block" && node.body[1]) {
|
18
|
+
return "";
|
19
|
+
}
|
20
|
+
|
21
|
+
// If we only have one argument and that first argument necessitates that we
|
22
|
+
// skip putting a comma (because it would interfere with parsing the argument)
|
23
|
+
// then we don't want to add a trailing comma.
|
24
|
+
if (node.body.length === 1 && noTrailingComma.includes(node.body[0].type)) {
|
25
|
+
return "";
|
26
|
+
}
|
27
|
+
|
28
|
+
return ifBreak(",", "");
|
29
|
+
}
|
30
|
+
|
14
31
|
function printArgParen(path, opts, print) {
|
15
32
|
const argsNode = path.getValue().body[0];
|
16
33
|
|
@@ -32,9 +49,6 @@ function printArgParen(path, opts, print) {
|
|
32
49
|
);
|
33
50
|
}
|
34
51
|
|
35
|
-
const args = path.call(print, "body", 0);
|
36
|
-
const hasBlock = argsNode.type === "args_add_block" && argsNode.body[1];
|
37
|
-
|
38
52
|
// Now here we return a doc that represents the whole grouped expression,
|
39
53
|
// including the surrouding parentheses.
|
40
54
|
return group(
|
@@ -43,8 +57,8 @@ function printArgParen(path, opts, print) {
|
|
43
57
|
indent(
|
44
58
|
concat([
|
45
59
|
softline,
|
46
|
-
join(concat([",", line]),
|
47
|
-
getTrailingComma(opts) &&
|
60
|
+
join(concat([",", line]), path.call(print, "body", 0)),
|
61
|
+
getTrailingComma(opts) && getArgParenTrailingComma(argsNode)
|
48
62
|
])
|
49
63
|
),
|
50
64
|
softline,
|
data/src/ruby/nodes/blocks.js
CHANGED
@@ -10,76 +10,81 @@ const {
|
|
10
10
|
} = require("../../prettier");
|
11
11
|
const { hasAncestor } = require("../../utils");
|
12
12
|
|
13
|
-
|
14
|
-
const [
|
15
|
-
const stmts =
|
16
|
-
statements.type === "stmts" ? statements.body : statements.body[0].body;
|
13
|
+
function printBlockVar(path, opts, print) {
|
14
|
+
const parts = ["|", removeLines(path.call(print, "body", 0))];
|
17
15
|
|
18
|
-
|
19
|
-
if (
|
20
|
-
|
21
|
-
stmts[0].type !== "void_stmt" ||
|
22
|
-
stmts[0].comments
|
23
|
-
) {
|
24
|
-
doBlockBody = indent(concat([softline, path.call(print, "body", 1)]));
|
16
|
+
// The second part of this node is a list of optional block-local variables
|
17
|
+
if (path.getValue().body[1]) {
|
18
|
+
parts.push("; ", join(", ", path.map(print, "body", 1)));
|
25
19
|
}
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
// precedence). Instead, we still use a multi-line format but switch to using
|
31
|
-
// braces instead.
|
32
|
-
const useBraces = braces && hasAncestor(path, ["command", "command_call"]);
|
21
|
+
parts.push("| ");
|
22
|
+
return concat(parts);
|
23
|
+
}
|
33
24
|
|
34
|
-
|
35
|
-
|
36
|
-
variables
|
37
|
-
|
38
|
-
|
39
|
-
]);
|
25
|
+
function printBlock(braces) {
|
26
|
+
return function printBlockWithBraces(path, opts, print) {
|
27
|
+
const [variables, statements] = path.getValue().body;
|
28
|
+
const stmts =
|
29
|
+
statements.type === "stmts" ? statements.body : statements.body[0].body;
|
40
30
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
}
|
31
|
+
let doBlockBody = "";
|
32
|
+
if (
|
33
|
+
stmts.length !== 1 ||
|
34
|
+
stmts[0].type !== "void_stmt" ||
|
35
|
+
stmts[0].comments
|
36
|
+
) {
|
37
|
+
doBlockBody = indent(concat([softline, path.call(print, "body", 1)]));
|
38
|
+
}
|
50
39
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
40
|
+
// If this block is nested underneath a command or command_call node, then
|
41
|
+
// we can't use `do...end` because that will get associated with the parent
|
42
|
+
// node as opposed to the current node (because of the difference in
|
43
|
+
// operator precedence). Instead, we still use a multi-line format but
|
44
|
+
// switch to using braces instead.
|
45
|
+
const useBraces = braces && hasAncestor(path, ["command", "command_call"]);
|
56
46
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
hasBody ? " " : "",
|
64
|
-
"}"
|
65
|
-
]);
|
47
|
+
const doBlock = concat([
|
48
|
+
useBraces ? " {" : " do",
|
49
|
+
variables ? concat([" ", path.call(print, "body", 0)]) : "",
|
50
|
+
doBlockBody,
|
51
|
+
concat([softline, useBraces ? "}" : "end"])
|
52
|
+
]);
|
66
53
|
|
67
|
-
|
68
|
-
|
54
|
+
// We can hit this next pattern if within the block the only statement is a
|
55
|
+
// comment.
|
56
|
+
if (
|
57
|
+
stmts.length === 1 &&
|
58
|
+
stmts[0].type === "void_stmt" &&
|
59
|
+
stmts[0].comments
|
60
|
+
) {
|
61
|
+
return concat([breakParent, doBlock]);
|
62
|
+
}
|
69
63
|
|
70
|
-
|
71
|
-
block_var: (path, opts, print) => {
|
72
|
-
const parts = ["|", removeLines(path.call(print, "body", 0))];
|
64
|
+
const blockReceiver = path.getParentNode().body[0];
|
73
65
|
|
74
|
-
//
|
75
|
-
|
76
|
-
|
66
|
+
// If the parent node is a command node, then there are no parentheses
|
67
|
+
// around the arguments to that command, so we need to break the block
|
68
|
+
if (["command", "command_call"].includes(blockReceiver.type)) {
|
69
|
+
return concat([breakParent, doBlock]);
|
77
70
|
}
|
78
71
|
|
79
|
-
|
80
|
-
|
81
|
-
|
72
|
+
const hasBody = stmts.some(({ type }) => type !== "void_stmt");
|
73
|
+
const braceBlock = concat([
|
74
|
+
" {",
|
75
|
+
hasBody || variables ? " " : "",
|
76
|
+
variables ? path.call(print, "body", 0) : "",
|
77
|
+
path.call(print, "body", 1),
|
78
|
+
hasBody ? " " : "",
|
79
|
+
"}"
|
80
|
+
]);
|
81
|
+
|
82
|
+
return group(ifBreak(doBlock, braceBlock));
|
83
|
+
};
|
84
|
+
}
|
85
|
+
|
86
|
+
module.exports = {
|
87
|
+
block_var: printBlockVar,
|
82
88
|
brace_block: printBlock(true),
|
83
|
-
do_block: printBlock(false)
|
84
|
-
excessed_comma: () => ""
|
89
|
+
do_block: printBlock(false)
|
85
90
|
};
|
data/src/ruby/nodes/calls.js
CHANGED
@@ -24,6 +24,12 @@ function printCall(path, opts, print) {
|
|
24
24
|
// call syntax so if `call` is implicit, we don't print it out.
|
25
25
|
const messageDoc = messageNode === "call" ? "" : path.call(print, "body", 2);
|
26
26
|
|
27
|
+
// For certain left sides of the call nodes, we want to attach directly to
|
28
|
+
// the } or end.
|
29
|
+
if (noIndent.includes(receiverNode.type)) {
|
30
|
+
return concat([receiverDoc, operatorDoc, messageDoc]);
|
31
|
+
}
|
32
|
+
|
27
33
|
// The right side of the call node, as in everything including the operator
|
28
34
|
// and beyond.
|
29
35
|
const rightSideDoc = concat([
|
@@ -37,7 +43,7 @@ function printCall(path, opts, print) {
|
|
37
43
|
|
38
44
|
// If our parent node is a chained node then we're not going to group the
|
39
45
|
// right side of the expression, as we want to have a nice multi-line layout.
|
40
|
-
if (chained.includes(parentNode.type)) {
|
46
|
+
if (chained.includes(parentNode.type) && !node.comments) {
|
41
47
|
parentNode.chain = (node.chain || 0) + 1;
|
42
48
|
parentNode.callChain = (node.callChain || 0) + 1;
|
43
49
|
parentNode.breakDoc = (node.breakDoc || [receiverDoc]).concat(rightSideDoc);
|
@@ -47,18 +53,10 @@ function printCall(path, opts, print) {
|
|
47
53
|
// If we're at the top of a chain, then we're going to print out a nice
|
48
54
|
// multi-line layout if this doesn't break into multiple lines.
|
49
55
|
if (!chained.includes(parentNode.type) && (node.chain || 0) >= 3) {
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
return ifBreak(group(breakDoc), concat([receiverDoc, group(rightSideDoc)]));
|
56
|
-
}
|
57
|
-
|
58
|
-
// For certain left sides of the call nodes, we want to attach directly to
|
59
|
-
// the } or end.
|
60
|
-
if (noIndent.includes(receiverNode.type)) {
|
61
|
-
return concat([receiverDoc, operatorDoc, messageDoc]);
|
56
|
+
return ifBreak(
|
57
|
+
group(indent(concat(node.breakDoc.concat(rightSideDoc)))),
|
58
|
+
concat([receiverDoc, group(rightSideDoc)])
|
59
|
+
);
|
62
60
|
}
|
63
61
|
|
64
62
|
return group(concat([receiverDoc, group(indent(rightSideDoc))]));
|
@@ -105,7 +103,7 @@ function printMethodAddArg(path, opts, print) {
|
|
105
103
|
|
106
104
|
// If our parent node is a chained node then we're not going to group the
|
107
105
|
// right side of the expression, as we want to have a nice multi-line layout.
|
108
|
-
if (chained.includes(parentNode.type)) {
|
106
|
+
if (chained.includes(parentNode.type) && !node.comments) {
|
109
107
|
parentNode.chain = (node.chain || 0) + 1;
|
110
108
|
parentNode.breakDoc = (node.breakDoc || [methodDoc]).concat(argsDoc);
|
111
109
|
parentNode.firstReceiverType = node.firstReceiverType;
|
@@ -138,41 +136,12 @@ function printMethodAddArg(path, opts, print) {
|
|
138
136
|
return concat([methodDoc, argsDoc]);
|
139
137
|
}
|
140
138
|
|
141
|
-
// Sorbet type annotations look like the following:
|
142
|
-
//
|
143
|
-
// {method_add_block
|
144
|
-
// [{method_add_arg
|
145
|
-
// [{fcall
|
146
|
-
// [{@ident "sig"}]},
|
147
|
-
// {args []}]},
|
148
|
-
// {brace_block [nil, {stmts}]}}]}
|
149
|
-
//
|
150
|
-
function isSorbetTypeAnnotation(node) {
|
151
|
-
const [callNode, blockNode] = node.body;
|
152
|
-
|
153
|
-
return (
|
154
|
-
callNode.type === "method_add_arg" &&
|
155
|
-
callNode.body[0].type === "fcall" &&
|
156
|
-
callNode.body[0].body[0].body === "sig" &&
|
157
|
-
callNode.body[1].type === "args" &&
|
158
|
-
callNode.body[1].body.length === 0 &&
|
159
|
-
blockNode
|
160
|
-
);
|
161
|
-
}
|
162
|
-
|
163
139
|
function printMethodAddBlock(path, opts, print) {
|
164
140
|
const node = path.getValue();
|
165
141
|
|
166
142
|
const [callNode, blockNode] = node.body;
|
167
143
|
const [callDoc, blockDoc] = path.map(print, "body");
|
168
144
|
|
169
|
-
// Very special handling here for sorbet type annotations. They look like Ruby
|
170
|
-
// code, but they're not actually Ruby code, so we're not going to mess with
|
171
|
-
// them at all.
|
172
|
-
if (isSorbetTypeAnnotation(node)) {
|
173
|
-
return opts.originalText.slice(opts.locStart(node), opts.locEnd(node));
|
174
|
-
}
|
175
|
-
|
176
145
|
// Don't bother trying to do any kind of fancy toProc transform if the option
|
177
146
|
// is disabled.
|
178
147
|
if (opts.rubyToProc) {
|
data/src/ruby/nodes/class.js
CHANGED
@@ -1,34 +1,22 @@
|
|
1
|
-
const {
|
2
|
-
|
3
|
-
group,
|
4
|
-
hardline,
|
5
|
-
ifBreak,
|
6
|
-
indent,
|
7
|
-
line
|
8
|
-
} = require("../../prettier");
|
1
|
+
const { concat, group, hardline, indent } = require("../../prettier");
|
2
|
+
const { isEmptyBodyStmt } = require("../../utils");
|
9
3
|
|
10
4
|
function printClass(path, opts, print) {
|
11
5
|
const [_constant, superclass, bodystmt] = path.getValue().body;
|
12
|
-
const stmts = bodystmt.body[0];
|
13
6
|
|
14
7
|
const parts = ["class ", path.call(print, "body", 0)];
|
15
8
|
if (superclass) {
|
16
9
|
parts.push(" < ", path.call(print, "body", 1));
|
17
10
|
}
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
stmts.body.length === 1 &&
|
23
|
-
stmts.body[0].type === "void_stmt" &&
|
24
|
-
!stmts.body[0].comments
|
25
|
-
) {
|
26
|
-
return group(concat([concat(parts), ifBreak(line, "; "), "end"]));
|
12
|
+
const declaration = group(concat(parts));
|
13
|
+
if (isEmptyBodyStmt(bodystmt)) {
|
14
|
+
return group(concat([declaration, hardline, "end"]));
|
27
15
|
}
|
28
16
|
|
29
17
|
return group(
|
30
18
|
concat([
|
31
|
-
|
19
|
+
declaration,
|
32
20
|
indent(concat([hardline, path.call(print, "body", 2)])),
|
33
21
|
concat([hardline, "end"])
|
34
22
|
])
|
@@ -36,16 +24,11 @@ function printClass(path, opts, print) {
|
|
36
24
|
}
|
37
25
|
|
38
26
|
function printModule(path, opts, print) {
|
27
|
+
const node = path.getValue();
|
39
28
|
const declaration = group(concat(["module ", path.call(print, "body", 0)]));
|
40
29
|
|
41
|
-
|
42
|
-
|
43
|
-
if (
|
44
|
-
stmts.body.length === 1 &&
|
45
|
-
stmts.body[0].type === "void_stmt" &&
|
46
|
-
!stmts.body[0].comments
|
47
|
-
) {
|
48
|
-
return group(concat([declaration, ifBreak(line, "; "), "end"]));
|
30
|
+
if (isEmptyBodyStmt(node.body[1])) {
|
31
|
+
return group(concat([declaration, hardline, "end"]));
|
49
32
|
}
|
50
33
|
|
51
34
|
return group(
|
@@ -58,9 +41,16 @@ function printModule(path, opts, print) {
|
|
58
41
|
}
|
59
42
|
|
60
43
|
function printSClass(path, opts, print) {
|
44
|
+
const bodystmt = path.getValue().body[1];
|
45
|
+
const declaration = concat(["class << ", path.call(print, "body", 0)]);
|
46
|
+
|
47
|
+
if (isEmptyBodyStmt(bodystmt)) {
|
48
|
+
return group(concat([declaration, hardline, "end"]));
|
49
|
+
}
|
50
|
+
|
61
51
|
return group(
|
62
52
|
concat([
|
63
|
-
|
53
|
+
declaration,
|
64
54
|
indent(concat([hardline, path.call(print, "body", 1)])),
|
65
55
|
concat([hardline, "end"])
|
66
56
|
])
|