@herb-tools/printer 0.8.10 → 0.9.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/cli.js.map +1 -0
- package/dist/{src/erb-to-ruby-string-printer.js → erb-to-ruby-string-printer.js} +20 -22
- package/dist/erb-to-ruby-string-printer.js.map +1 -0
- package/dist/herb-print.js +55369 -15024
- package/dist/herb-print.js.map +1 -1
- package/dist/{src/identity-printer.js → identity-printer.js} +39 -0
- package/dist/identity-printer.js.map +1 -0
- package/dist/indent-printer.js +256 -0
- package/dist/indent-printer.js.map +1 -0
- package/dist/index.cjs +315 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +316 -24
- package/dist/index.js.map +1 -1
- package/dist/print-context.js.map +1 -0
- package/dist/{src/printer.js → printer.js} +3 -1
- package/dist/printer.js.map +1 -0
- package/dist/types/identity-printer.d.ts +8 -0
- package/dist/types/indent-printer.d.ts +41 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/printer.d.ts +1 -1
- package/package.json +2 -2
- package/src/erb-to-ruby-string-printer.ts +25 -27
- package/src/identity-printer.ts +52 -0
- package/src/indent-printer.ts +313 -0
- package/src/index.ts +1 -0
- package/src/printer.ts +6 -6
- package/dist/package.json +0 -50
- package/dist/src/cli.js.map +0 -1
- package/dist/src/erb-to-ruby-string-printer.js.map +0 -1
- package/dist/src/herb-print.js +0 -5
- package/dist/src/herb-print.js.map +0 -1
- package/dist/src/identity-printer.js.map +0 -1
- package/dist/src/index.js +0 -5
- package/dist/src/index.js.map +0 -1
- package/dist/src/print-context.js.map +0 -1
- package/dist/src/printer.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/types/src/cli.d.ts +0 -6
- package/dist/types/src/erb-to-ruby-string-printer.d.ts +0 -42
- package/dist/types/src/herb-print.d.ts +0 -2
- package/dist/types/src/identity-printer.d.ts +0 -48
- package/dist/types/src/index.d.ts +0 -5
- package/dist/types/src/print-context.d.ts +0 -54
- package/dist/types/src/printer.d.ts +0 -39
- /package/dist/{src/cli.js → cli.js} +0 -0
- /package/dist/{src/print-context.js → print-context.js} +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IdentityPrinter } from "./identity-printer.js";
|
|
2
|
+
import type * as Nodes from "@herb-tools/core";
|
|
3
|
+
/**
|
|
4
|
+
* IndentPrinter - Re-indentation printer that preserves content but adjusts indentation
|
|
5
|
+
*
|
|
6
|
+
* Extends IdentityPrinter to preserve all content as-is while replacing
|
|
7
|
+
* leading whitespace on each line with the correct indentation based on
|
|
8
|
+
* the AST nesting depth.
|
|
9
|
+
*/
|
|
10
|
+
export declare class IndentPrinter extends IdentityPrinter {
|
|
11
|
+
protected indentLevel: number;
|
|
12
|
+
protected indentWidth: number;
|
|
13
|
+
private pendingIndent;
|
|
14
|
+
constructor(indentWidth?: number);
|
|
15
|
+
protected get indent(): string;
|
|
16
|
+
protected write(content: string): void;
|
|
17
|
+
visitLiteralNode(node: Nodes.LiteralNode): void;
|
|
18
|
+
visitHTMLTextNode(node: Nodes.HTMLTextNode): void;
|
|
19
|
+
visitHTMLElementNode(node: Nodes.HTMLElementNode): void;
|
|
20
|
+
visitERBIfNode(node: Nodes.ERBIfNode): void;
|
|
21
|
+
visitERBElseNode(node: Nodes.ERBElseNode): void;
|
|
22
|
+
visitERBBlockNode(node: Nodes.ERBBlockNode): void;
|
|
23
|
+
visitERBCaseNode(node: Nodes.ERBCaseNode): void;
|
|
24
|
+
visitERBWhenNode(node: Nodes.ERBWhenNode): void;
|
|
25
|
+
visitERBWhileNode(node: Nodes.ERBWhileNode): void;
|
|
26
|
+
visitERBUntilNode(node: Nodes.ERBUntilNode): void;
|
|
27
|
+
visitERBForNode(node: Nodes.ERBForNode): void;
|
|
28
|
+
visitERBBeginNode(node: Nodes.ERBBeginNode): void;
|
|
29
|
+
visitERBRescueNode(node: Nodes.ERBRescueNode): void;
|
|
30
|
+
visitERBEnsureNode(node: Nodes.ERBEnsureNode): void;
|
|
31
|
+
visitERBUnlessNode(node: Nodes.ERBUnlessNode): void;
|
|
32
|
+
/**
|
|
33
|
+
* Write content, replacing leading whitespace on each line with the current indent.
|
|
34
|
+
*
|
|
35
|
+
* Uses a pendingIndent mechanism: when content ends with a newline followed by
|
|
36
|
+
* whitespace-only, sets pendingIndent=true instead of writing the indent immediately.
|
|
37
|
+
* The indent is then applied at the correct level when the next node writes content
|
|
38
|
+
* (via the overridden write() method).
|
|
39
|
+
*/
|
|
40
|
+
protected writeWithIndent(content: string): void;
|
|
41
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { IdentityPrinter } from "./identity-printer.js";
|
|
2
|
+
export { IndentPrinter } from "./indent-printer.js";
|
|
2
3
|
export { ERBToRubyStringPrinter } from "./erb-to-ruby-string-printer.js";
|
|
3
4
|
export { PrintContext } from "./print-context.js";
|
|
4
5
|
export { Printer, DEFAULT_PRINT_OPTIONS } from "./printer.js";
|
package/dist/types/printer.d.ts
CHANGED
|
@@ -35,5 +35,5 @@ export declare abstract class Printer extends Visitor {
|
|
|
35
35
|
* @throws {Error} When node has parse errors and ignoreErrors is false
|
|
36
36
|
*/
|
|
37
37
|
print(input: Token | Node | ParseResult | Node[] | undefined | null, options?: PrintOptions): string;
|
|
38
|
-
protected write(content: string): void;
|
|
38
|
+
protected write(content: string | null | undefined): void;
|
|
39
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/printer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "AST printer infrastructure and lossless reconstruction tool for HTML+ERB templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://herb-tools.dev",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "yarn clean && yarn build && yarn test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@herb-tools/core": "0.
|
|
40
|
+
"@herb-tools/core": "0.9.1",
|
|
41
41
|
"tinyglobby": "^0.2.15"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IdentityPrinter } from "./identity-printer.js"
|
|
2
2
|
import { PrintOptions, DEFAULT_PRINT_OPTIONS } from "./printer.js"
|
|
3
|
-
import { isERBOutputNode, filterNodes, ERBContentNode, } from "@herb-tools/core"
|
|
3
|
+
import { isERBOutputNode, filterNodes, ERBContentNode, isERBIfNode, isERBUnlessNode, isERBElseNode, isHTMLTextNode } from "@herb-tools/core"
|
|
4
4
|
|
|
5
|
-
import { HTMLTextNode, ERBIfNode,
|
|
5
|
+
import { HTMLTextNode, ERBIfNode, ERBUnlessNode, Node, HTMLAttributeValueNode } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
export interface ERBToRubyStringOptions extends PrintOptions {
|
|
8
8
|
/**
|
|
@@ -34,8 +34,6 @@ export const DEFAULT_ERB_TO_RUBY_STRING_OPTIONS: ERBToRubyStringOptions = {
|
|
|
34
34
|
* - `<% if logged_in? %>Welcome<% else %>Login<% end %>!` => `"#{logged_in? ? "Welcome" : "Login"}!"`
|
|
35
35
|
*/
|
|
36
36
|
export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
37
|
-
|
|
38
|
-
// TODO: cleanup `.type === "AST_*" checks`
|
|
39
37
|
static print(node: Node, options: Partial<ERBToRubyStringOptions> = DEFAULT_ERB_TO_RUBY_STRING_OPTIONS): string {
|
|
40
38
|
const erbNodes = filterNodes([node], ERBContentNode)
|
|
41
39
|
|
|
@@ -51,22 +49,22 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
51
49
|
return (childErbNodes[0].content?.value || "").trim()
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const firstChild = node.children[0]
|
|
53
|
+
|
|
54
|
+
if (node.children.length === 1 && isERBIfNode(firstChild) && !options.forceQuotes) {
|
|
56
55
|
const printer = new ERBToRubyStringPrinter()
|
|
57
56
|
|
|
58
|
-
if (printer.canConvertToTernary(
|
|
59
|
-
printer.convertToTernaryWithoutWrapper(
|
|
57
|
+
if (printer.canConvertToTernary(firstChild)) {
|
|
58
|
+
printer.convertToTernaryWithoutWrapper(firstChild)
|
|
60
59
|
return printer.context.getOutput()
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
if (node.children.length === 1 &&
|
|
65
|
-
const unlessNode = node.children[0] as ERBUnlessNode
|
|
63
|
+
if (node.children.length === 1 && isERBUnlessNode(firstChild) && !options.forceQuotes) {
|
|
66
64
|
const printer = new ERBToRubyStringPrinter()
|
|
67
65
|
|
|
68
|
-
if (printer.canConvertUnlessToTernary(
|
|
69
|
-
printer.convertUnlessToTernaryWithoutWrapper(
|
|
66
|
+
if (printer.canConvertUnlessToTernary(firstChild)) {
|
|
67
|
+
printer.convertUnlessToTernaryWithoutWrapper(firstChild)
|
|
70
68
|
return printer.context.getOutput()
|
|
71
69
|
}
|
|
72
70
|
}
|
|
@@ -119,16 +117,16 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
private canConvertToTernary(node: ERBIfNode): boolean {
|
|
122
|
-
if (node.subsequent && node.subsequent
|
|
120
|
+
if (node.subsequent && !isERBElseNode(node.subsequent)) {
|
|
123
121
|
return false
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
const ifOnlyText = node.statements ? node.statements.every(
|
|
124
|
+
const ifOnlyText = node.statements ? node.statements.every(isHTMLTextNode) : true
|
|
127
125
|
if (!ifOnlyText) return false
|
|
128
126
|
|
|
129
|
-
if (node.subsequent
|
|
130
|
-
return
|
|
131
|
-
?
|
|
127
|
+
if (isERBElseNode(node.subsequent)) {
|
|
128
|
+
return node.subsequent.statements
|
|
129
|
+
? node.subsequent.statements.every(isHTMLTextNode)
|
|
132
130
|
: true
|
|
133
131
|
}
|
|
134
132
|
|
|
@@ -165,8 +163,8 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
165
163
|
this.context.write(" : ")
|
|
166
164
|
this.context.write('"')
|
|
167
165
|
|
|
168
|
-
if (node.subsequent && node.subsequent.
|
|
169
|
-
|
|
166
|
+
if (isERBElseNode(node.subsequent) && node.subsequent.statements) {
|
|
167
|
+
node.subsequent.statements.forEach(statement => this.visit(statement))
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
this.context.write('"')
|
|
@@ -174,7 +172,7 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
private convertToTernaryWithoutWrapper(node: ERBIfNode) {
|
|
177
|
-
if (node.subsequent && node.subsequent
|
|
175
|
+
if (node.subsequent && !isERBElseNode(node.subsequent)) {
|
|
178
176
|
return false
|
|
179
177
|
}
|
|
180
178
|
|
|
@@ -205,21 +203,21 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
205
203
|
this.context.write(" : ")
|
|
206
204
|
this.context.write('"')
|
|
207
205
|
|
|
208
|
-
if (node.subsequent && node.subsequent.
|
|
209
|
-
|
|
206
|
+
if (isERBElseNode(node.subsequent) && node.subsequent.statements) {
|
|
207
|
+
node.subsequent.statements.forEach(statement => this.visit(statement))
|
|
210
208
|
}
|
|
211
209
|
|
|
212
210
|
this.context.write('"')
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
private canConvertUnlessToTernary(node: ERBUnlessNode): boolean {
|
|
216
|
-
const unlessOnlyText = node.statements ? node.statements.every(
|
|
214
|
+
const unlessOnlyText = node.statements ? node.statements.every(isHTMLTextNode) : true
|
|
217
215
|
|
|
218
216
|
if (!unlessOnlyText) return false
|
|
219
217
|
|
|
220
|
-
if (node.else_clause
|
|
218
|
+
if (isERBElseNode(node.else_clause)) {
|
|
221
219
|
return node.else_clause.statements
|
|
222
|
-
? node.else_clause.statements.every(
|
|
220
|
+
? node.else_clause.statements.every(isHTMLTextNode)
|
|
223
221
|
: true
|
|
224
222
|
}
|
|
225
223
|
|
|
@@ -260,7 +258,7 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
260
258
|
this.context.write(" : ")
|
|
261
259
|
this.context.write('"')
|
|
262
260
|
|
|
263
|
-
if (node.else_clause
|
|
261
|
+
if (isERBElseNode(node.else_clause)) {
|
|
264
262
|
node.else_clause.statements.forEach(statement => this.visit(statement))
|
|
265
263
|
}
|
|
266
264
|
|
|
@@ -300,7 +298,7 @@ export class ERBToRubyStringPrinter extends IdentityPrinter {
|
|
|
300
298
|
this.context.write(" : ")
|
|
301
299
|
this.context.write('"')
|
|
302
300
|
|
|
303
|
-
if (node.else_clause
|
|
301
|
+
if (isERBElseNode(node.else_clause)) {
|
|
304
302
|
node.else_clause.statements.forEach(statement => this.visit(statement))
|
|
305
303
|
}
|
|
306
304
|
|
package/src/identity-printer.ts
CHANGED
|
@@ -72,6 +72,14 @@ export class IdentityPrinter extends Printer {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
visitHTMLVirtualCloseTagNode(_node: Nodes.HTMLVirtualCloseTagNode): void {
|
|
76
|
+
// Virtual closing tags don't print anything (they are synthetic)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
visitHTMLOmittedCloseTagNode(_node: Nodes.HTMLOmittedCloseTagNode): void {
|
|
80
|
+
// Omitted closing tags don't print anything
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
visitHTMLElementNode(node: Nodes.HTMLElementNode): void {
|
|
76
84
|
const tagName = node.tag_name?.value
|
|
77
85
|
|
|
@@ -96,6 +104,30 @@ export class IdentityPrinter extends Printer {
|
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
visitHTMLConditionalElementNode(node: Nodes.HTMLConditionalElementNode): void {
|
|
108
|
+
const tagName = node.tag_name?.value
|
|
109
|
+
|
|
110
|
+
if (tagName) {
|
|
111
|
+
this.context.enterTag(tagName)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (node.open_conditional) {
|
|
115
|
+
this.visit(node.open_conditional)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (node.body) {
|
|
119
|
+
node.body.forEach(child => this.visit(child))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (node.close_conditional) {
|
|
123
|
+
this.visit(node.close_conditional)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (tagName) {
|
|
127
|
+
this.context.exitTag()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
visitHTMLAttributeNode(node: Nodes.HTMLAttributeNode): void {
|
|
100
132
|
if (node.name) {
|
|
101
133
|
this.visit(node.name)
|
|
@@ -126,6 +158,14 @@ export class IdentityPrinter extends Printer {
|
|
|
126
158
|
}
|
|
127
159
|
}
|
|
128
160
|
|
|
161
|
+
visitRubyLiteralNode(node: Nodes.RubyLiteralNode): void {
|
|
162
|
+
this.write(node.content)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
visitRubyHTMLAttributesSplatNode(node: Nodes.RubyHTMLAttributesSplatNode): void {
|
|
166
|
+
this.write(node.content)
|
|
167
|
+
}
|
|
168
|
+
|
|
129
169
|
visitHTMLCommentNode(node: Nodes.HTMLCommentNode): void {
|
|
130
170
|
if (node.comment_start) {
|
|
131
171
|
this.write(node.comment_start.value)
|
|
@@ -174,6 +214,10 @@ export class IdentityPrinter extends Printer {
|
|
|
174
214
|
}
|
|
175
215
|
}
|
|
176
216
|
|
|
217
|
+
visitERBOpenTagNode(node: Nodes.ERBOpenTagNode): void {
|
|
218
|
+
this.printERBNode(node)
|
|
219
|
+
}
|
|
220
|
+
|
|
177
221
|
visitERBContentNode(node: Nodes.ERBContentNode): void {
|
|
178
222
|
this.printERBNode(node)
|
|
179
223
|
}
|
|
@@ -342,6 +386,14 @@ export class IdentityPrinter extends Printer {
|
|
|
342
386
|
}
|
|
343
387
|
}
|
|
344
388
|
|
|
389
|
+
visitERBRenderNode(node: Nodes.ERBRenderNode): void {
|
|
390
|
+
this.printERBNode(node)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
visitRubyRenderLocalNode(_node: Nodes.RubyRenderLocalNode): void {
|
|
394
|
+
// extracted metadata, nothing to print
|
|
395
|
+
}
|
|
396
|
+
|
|
345
397
|
visitERBYieldNode(node: Nodes.ERBYieldNode): void {
|
|
346
398
|
this.printERBNode(node)
|
|
347
399
|
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { IdentityPrinter } from "./identity-printer.js"
|
|
2
|
+
|
|
3
|
+
import type * as Nodes from "@herb-tools/core"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IndentPrinter - Re-indentation printer that preserves content but adjusts indentation
|
|
7
|
+
*
|
|
8
|
+
* Extends IdentityPrinter to preserve all content as-is while replacing
|
|
9
|
+
* leading whitespace on each line with the correct indentation based on
|
|
10
|
+
* the AST nesting depth.
|
|
11
|
+
*/
|
|
12
|
+
export class IndentPrinter extends IdentityPrinter {
|
|
13
|
+
protected indentLevel: number = 0
|
|
14
|
+
protected indentWidth: number
|
|
15
|
+
private pendingIndent: boolean = false
|
|
16
|
+
|
|
17
|
+
constructor(indentWidth: number = 2) {
|
|
18
|
+
super()
|
|
19
|
+
|
|
20
|
+
this.indentWidth = indentWidth
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected get indent(): string {
|
|
24
|
+
return " ".repeat(this.indentLevel * this.indentWidth)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected write(content: string): void {
|
|
28
|
+
if (this.pendingIndent && content.length > 0) {
|
|
29
|
+
this.pendingIndent = false
|
|
30
|
+
this.context.write(this.indent + content)
|
|
31
|
+
} else {
|
|
32
|
+
this.context.write(content)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
visitLiteralNode(node: Nodes.LiteralNode): void {
|
|
37
|
+
this.writeWithIndent(node.content)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
visitHTMLTextNode(node: Nodes.HTMLTextNode): void {
|
|
41
|
+
this.writeWithIndent(node.content)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
visitHTMLElementNode(node: Nodes.HTMLElementNode): void {
|
|
45
|
+
const tagName = node.tag_name?.value
|
|
46
|
+
|
|
47
|
+
if (tagName) {
|
|
48
|
+
this.context.enterTag(tagName)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (node.open_tag) {
|
|
52
|
+
this.visit(node.open_tag)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (node.body) {
|
|
56
|
+
this.indentLevel++
|
|
57
|
+
node.body.forEach(child => this.visit(child))
|
|
58
|
+
this.indentLevel--
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (node.close_tag) {
|
|
62
|
+
this.visit(node.close_tag)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (tagName) {
|
|
66
|
+
this.context.exitTag()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
visitERBIfNode(node: Nodes.ERBIfNode): void {
|
|
71
|
+
this.printERBNode(node)
|
|
72
|
+
|
|
73
|
+
if (node.statements) {
|
|
74
|
+
this.indentLevel++
|
|
75
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
76
|
+
this.indentLevel--
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node.subsequent) {
|
|
80
|
+
this.visit(node.subsequent)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (node.end_node) {
|
|
84
|
+
this.visit(node.end_node)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
visitERBElseNode(node: Nodes.ERBElseNode): void {
|
|
89
|
+
this.printERBNode(node)
|
|
90
|
+
|
|
91
|
+
if (node.statements) {
|
|
92
|
+
this.indentLevel++
|
|
93
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
94
|
+
this.indentLevel--
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
visitERBBlockNode(node: Nodes.ERBBlockNode): void {
|
|
99
|
+
this.printERBNode(node)
|
|
100
|
+
|
|
101
|
+
if (node.body) {
|
|
102
|
+
this.indentLevel++
|
|
103
|
+
node.body.forEach(child => this.visit(child))
|
|
104
|
+
this.indentLevel--
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (node.end_node) {
|
|
108
|
+
this.visit(node.end_node)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
visitERBCaseNode(node: Nodes.ERBCaseNode): void {
|
|
113
|
+
this.printERBNode(node)
|
|
114
|
+
|
|
115
|
+
if (node.children) {
|
|
116
|
+
this.indentLevel++
|
|
117
|
+
node.children.forEach(child => this.visit(child))
|
|
118
|
+
this.indentLevel--
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (node.conditions) {
|
|
122
|
+
this.indentLevel++
|
|
123
|
+
node.conditions.forEach(condition => this.visit(condition))
|
|
124
|
+
this.indentLevel--
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (node.else_clause) {
|
|
128
|
+
this.indentLevel++
|
|
129
|
+
this.visit(node.else_clause)
|
|
130
|
+
this.indentLevel--
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (node.end_node) {
|
|
134
|
+
this.visit(node.end_node)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
visitERBWhenNode(node: Nodes.ERBWhenNode): void {
|
|
139
|
+
this.printERBNode(node)
|
|
140
|
+
|
|
141
|
+
if (node.statements) {
|
|
142
|
+
this.indentLevel++
|
|
143
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
144
|
+
this.indentLevel--
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
visitERBWhileNode(node: Nodes.ERBWhileNode): void {
|
|
149
|
+
this.printERBNode(node)
|
|
150
|
+
|
|
151
|
+
if (node.statements) {
|
|
152
|
+
this.indentLevel++
|
|
153
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
154
|
+
this.indentLevel--
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (node.end_node) {
|
|
158
|
+
this.visit(node.end_node)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
visitERBUntilNode(node: Nodes.ERBUntilNode): void {
|
|
163
|
+
this.printERBNode(node)
|
|
164
|
+
|
|
165
|
+
if (node.statements) {
|
|
166
|
+
this.indentLevel++
|
|
167
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
168
|
+
this.indentLevel--
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (node.end_node) {
|
|
172
|
+
this.visit(node.end_node)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
visitERBForNode(node: Nodes.ERBForNode): void {
|
|
177
|
+
this.printERBNode(node)
|
|
178
|
+
|
|
179
|
+
if (node.statements) {
|
|
180
|
+
this.indentLevel++
|
|
181
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
182
|
+
this.indentLevel--
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (node.end_node) {
|
|
186
|
+
this.visit(node.end_node)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
visitERBBeginNode(node: Nodes.ERBBeginNode): void {
|
|
191
|
+
this.printERBNode(node)
|
|
192
|
+
|
|
193
|
+
if (node.statements) {
|
|
194
|
+
this.indentLevel++
|
|
195
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
196
|
+
this.indentLevel--
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (node.rescue_clause) {
|
|
200
|
+
this.visit(node.rescue_clause)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (node.else_clause) {
|
|
204
|
+
this.visit(node.else_clause)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (node.ensure_clause) {
|
|
208
|
+
this.visit(node.ensure_clause)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (node.end_node) {
|
|
212
|
+
this.visit(node.end_node)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
visitERBRescueNode(node: Nodes.ERBRescueNode): void {
|
|
217
|
+
this.printERBNode(node)
|
|
218
|
+
|
|
219
|
+
if (node.statements) {
|
|
220
|
+
this.indentLevel++
|
|
221
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
222
|
+
this.indentLevel--
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (node.subsequent) {
|
|
226
|
+
this.visit(node.subsequent)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
visitERBEnsureNode(node: Nodes.ERBEnsureNode): void {
|
|
231
|
+
this.printERBNode(node)
|
|
232
|
+
|
|
233
|
+
if (node.statements) {
|
|
234
|
+
this.indentLevel++
|
|
235
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
236
|
+
this.indentLevel--
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
visitERBUnlessNode(node: Nodes.ERBUnlessNode): void {
|
|
241
|
+
this.printERBNode(node)
|
|
242
|
+
|
|
243
|
+
if (node.statements) {
|
|
244
|
+
this.indentLevel++
|
|
245
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
246
|
+
this.indentLevel--
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (node.else_clause) {
|
|
250
|
+
this.visit(node.else_clause)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (node.end_node) {
|
|
254
|
+
this.visit(node.end_node)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Write content, replacing leading whitespace on each line with the current indent.
|
|
260
|
+
*
|
|
261
|
+
* Uses a pendingIndent mechanism: when content ends with a newline followed by
|
|
262
|
+
* whitespace-only, sets pendingIndent=true instead of writing the indent immediately.
|
|
263
|
+
* The indent is then applied at the correct level when the next node writes content
|
|
264
|
+
* (via the overridden write() method).
|
|
265
|
+
*/
|
|
266
|
+
protected writeWithIndent(content: string): void {
|
|
267
|
+
if (!content.includes("\n")) {
|
|
268
|
+
if (this.pendingIndent) {
|
|
269
|
+
this.pendingIndent = false
|
|
270
|
+
|
|
271
|
+
const trimmed = content.replace(/^[ \t]+/, "")
|
|
272
|
+
|
|
273
|
+
if (trimmed.length > 0) {
|
|
274
|
+
this.context.write(this.indent + trimmed)
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
this.context.write(content)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const lines = content.split("\n")
|
|
284
|
+
const lastIndex = lines.length - 1
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < lines.length; i++) {
|
|
287
|
+
if (i > 0) {
|
|
288
|
+
this.context.write("\n")
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const line = lines[i]
|
|
292
|
+
const trimmed = line.replace(/^[ \t]+/, "")
|
|
293
|
+
|
|
294
|
+
if (i === 0) {
|
|
295
|
+
if (this.pendingIndent) {
|
|
296
|
+
this.pendingIndent = false
|
|
297
|
+
|
|
298
|
+
if (trimmed.length > 0) {
|
|
299
|
+
this.context.write(this.indent + trimmed)
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
this.context.write(line)
|
|
303
|
+
}
|
|
304
|
+
} else if (i === lastIndex && trimmed.length === 0) {
|
|
305
|
+
this.pendingIndent = true
|
|
306
|
+
} else if (trimmed.length === 0) {
|
|
307
|
+
// Middle whitespace-only line: write nothing (newline already written above)
|
|
308
|
+
} else {
|
|
309
|
+
this.context.write(this.indent + trimmed)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { IdentityPrinter } from "./identity-printer.js"
|
|
2
|
+
export { IndentPrinter } from "./indent-printer.js"
|
|
2
3
|
export { ERBToRubyStringPrinter } from "./erb-to-ruby-string-printer.js"
|
|
3
4
|
export { PrintContext } from "./print-context.js"
|
|
4
5
|
export { Printer, DEFAULT_PRINT_OPTIONS } from "./printer.js"
|
package/src/printer.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Node, Visitor, Token, ParseResult, isToken, isParseResult } from "@herb-tools/core"
|
|
2
2
|
import { PrintContext } from "./print-context.js"
|
|
3
3
|
|
|
4
|
-
import type { ERBNode } from "@herb-tools/core"
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* Options for controlling the printing behavior
|
|
8
6
|
*/
|
|
@@ -33,7 +31,7 @@ export abstract class Printer extends Visitor {
|
|
|
33
31
|
* @returns The printed string representation of the input
|
|
34
32
|
* @throws {Error} When node has parse errors and ignoreErrors is false
|
|
35
33
|
*/
|
|
36
|
-
static print(input: Token | Node | ParseResult | Node[] | undefined |
|
|
34
|
+
static print(input: Token | Node | ParseResult | Node[] | undefined | null, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string {
|
|
37
35
|
const printer = new (this as any)()
|
|
38
36
|
|
|
39
37
|
return printer.print(input, options)
|
|
@@ -47,7 +45,7 @@ export abstract class Printer extends Visitor {
|
|
|
47
45
|
* @returns The printed string representation of the input
|
|
48
46
|
* @throws {Error} When node has parse errors and ignoreErrors is false
|
|
49
47
|
*/
|
|
50
|
-
print(input: Token | Node | ParseResult | Node[] | undefined |
|
|
48
|
+
print(input: Token | Node | ParseResult | Node[] | undefined | null, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string {
|
|
51
49
|
if (!input) return ""
|
|
52
50
|
|
|
53
51
|
if (isToken(input)) {
|
|
@@ -73,7 +71,9 @@ export abstract class Printer extends Visitor {
|
|
|
73
71
|
return this.context.getOutput()
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
protected write(content: string): void {
|
|
77
|
-
|
|
74
|
+
protected write(content: string | null | undefined): void {
|
|
75
|
+
if (content !== null && content !== undefined) {
|
|
76
|
+
this.context.write(content)
|
|
77
|
+
}
|
|
78
78
|
}
|
|
79
79
|
}
|