@herb-tools/printer 0.6.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.
package/src/printer.ts ADDED
@@ -0,0 +1,438 @@
1
+ import { Node, Visitor, Token, ParseResult, isToken, isParseResult } from "@herb-tools/core"
2
+ import { PrintContext } from "./print-context.js"
3
+
4
+ import type * as Nodes from "@herb-tools/core"
5
+
6
+ /**
7
+ * Options for controlling the printing behavior
8
+ */
9
+ export type PrintOptions = {
10
+ /**
11
+ * When true, allows printing nodes that have parse errors.
12
+ * When false (default), throws an error if attempting to print nodes with errors.
13
+ * @default false
14
+ */
15
+ ignoreErrors: boolean
16
+ }
17
+
18
+ /**
19
+ * Default print options used when none are provided
20
+ */
21
+ export const DEFAULT_PRINT_OPTIONS: PrintOptions = {
22
+ ignoreErrors: false
23
+ }
24
+
25
+ export abstract class Printer extends Visitor {
26
+ protected context: PrintContext = new PrintContext()
27
+
28
+ /**
29
+ * Static method to print a node without creating an instance
30
+ *
31
+ * @param input - The AST Node, Token, or ParseResult to print
32
+ * @param options - Print options to control behavior
33
+ * @returns The printed string representation of the input
34
+ * @throws {Error} When node has parse errors and ignoreErrors is false
35
+ */
36
+ static print(input: Token | Node | ParseResult, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string {
37
+ const printer = new (this as any)()
38
+
39
+ return printer.print(input, options)
40
+ }
41
+
42
+ /**
43
+ * Print a node, token, or parse result to a string
44
+ *
45
+ * @param input - The AST Node, Token, or ParseResult to print
46
+ * @param options - Print options to control behavior
47
+ * @returns The printed string representation of the input
48
+ * @throws {Error} When node has parse errors and ignoreErrors is false
49
+ */
50
+ print(input: Token | Node | ParseResult, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string {
51
+ if (isToken(input)) {
52
+ return input.value
53
+ }
54
+
55
+ const node: Node = isParseResult(input) ? input.value : input
56
+
57
+ if (options.ignoreErrors === false && node.recursiveErrors().length > 0) {
58
+ 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 })\``)
59
+ }
60
+
61
+ this.context.reset()
62
+
63
+ this.visit(node)
64
+
65
+ return this.context.getOutput()
66
+ }
67
+
68
+ visitDocumentNode(node: Nodes.DocumentNode): void {
69
+ this.visitChildNodes(node)
70
+ }
71
+
72
+ visitLiteralNode(node: Nodes.LiteralNode): void {
73
+ this.context.write(node.content)
74
+ }
75
+
76
+ visitHTMLTextNode(node: Nodes.HTMLTextNode): void {
77
+ this.write(node.content)
78
+ }
79
+
80
+ visitWhitespaceNode(node: Nodes.WhitespaceNode): void {
81
+ if (node.value) {
82
+ this.write(node.value.value)
83
+ }
84
+ }
85
+
86
+ visitHTMLOpenTagNode(node: Nodes.HTMLOpenTagNode): void {
87
+ if (node.tag_opening) {
88
+ this.context.write(node.tag_opening.value)
89
+ }
90
+
91
+ if (node.tag_name) {
92
+ this.context.write(node.tag_name.value)
93
+ }
94
+
95
+ this.visitChildNodes(node)
96
+
97
+ if (node.tag_closing) {
98
+ this.context.write(node.tag_closing.value)
99
+ }
100
+ }
101
+
102
+ visitHTMLCloseTagNode(node: Nodes.HTMLCloseTagNode): void {
103
+ if (node.tag_opening) {
104
+ this.context.write(node.tag_opening.value)
105
+ }
106
+
107
+ if (node.tag_name) {
108
+ this.context.write(node.tag_name.value)
109
+ }
110
+
111
+ if (node.tag_closing) {
112
+ this.context.write(node.tag_closing.value)
113
+ }
114
+ }
115
+
116
+ visitHTMLElementNode(node: Nodes.HTMLElementNode): void {
117
+ const tagName = node.tag_name?.value
118
+
119
+ if (tagName) {
120
+ this.context.enterTag(tagName)
121
+ }
122
+
123
+ if (node.open_tag) {
124
+ this.visit(node.open_tag)
125
+ }
126
+
127
+ if (node.body) {
128
+ node.body.forEach(child => this.visit(child))
129
+ }
130
+
131
+ if (node.close_tag) {
132
+ this.visit(node.close_tag)
133
+ }
134
+
135
+ if (tagName) {
136
+ this.context.exitTag()
137
+ }
138
+ }
139
+
140
+ visitHTMLAttributeNode(node: Nodes.HTMLAttributeNode): void {
141
+ if (node.name) {
142
+ this.visit(node.name)
143
+ }
144
+
145
+ if (node.equals) {
146
+ this.context.write(node.equals.value)
147
+ }
148
+
149
+ if (node.equals && node.value) {
150
+ this.visit(node.value)
151
+ }
152
+ }
153
+
154
+ visitHTMLAttributeNameNode(node: Nodes.HTMLAttributeNameNode): void {
155
+ this.visitChildNodes(node)
156
+ }
157
+
158
+ visitHTMLAttributeValueNode(node: Nodes.HTMLAttributeValueNode): void {
159
+ if (node.quoted && node.open_quote) {
160
+ this.context.write(node.open_quote.value)
161
+ }
162
+
163
+ this.visitChildNodes(node)
164
+
165
+ if (node.quoted && node.close_quote) {
166
+ this.context.write(node.close_quote.value)
167
+ }
168
+ }
169
+
170
+ visitHTMLCommentNode(node: Nodes.HTMLCommentNode): void {
171
+ if (node.comment_start) {
172
+ this.context.write(node.comment_start.value)
173
+ }
174
+
175
+ this.visitChildNodes(node)
176
+
177
+ if (node.comment_end) {
178
+ this.context.write(node.comment_end.value)
179
+ }
180
+ }
181
+
182
+ visitHTMLDoctypeNode(node: Nodes.HTMLDoctypeNode): void {
183
+ if (node.tag_opening) {
184
+ this.context.write(node.tag_opening.value)
185
+ }
186
+
187
+ this.visitChildNodes(node)
188
+
189
+ if (node.tag_closing) {
190
+ this.context.write(node.tag_closing.value)
191
+ }
192
+ }
193
+
194
+ visitXMLDeclarationNode(node: Nodes.XMLDeclarationNode): void {
195
+ if (node.tag_opening) {
196
+ this.context.write(node.tag_opening.value)
197
+ }
198
+
199
+ this.visitChildNodes(node)
200
+
201
+ if (node.tag_closing) {
202
+ this.context.write(node.tag_closing.value)
203
+ }
204
+ }
205
+
206
+ visitCDATANode(node: Nodes.CDATANode): void {
207
+ if (node.tag_opening) {
208
+ this.context.write(node.tag_opening.value)
209
+ }
210
+
211
+ this.visitChildNodes(node)
212
+
213
+ if (node.tag_closing) {
214
+ this.context.write(node.tag_closing.value)
215
+ }
216
+ }
217
+
218
+ visitERBContentNode(node: Nodes.ERBContentNode): void {
219
+ this.printERBNode(node)
220
+ }
221
+
222
+ visitERBIfNode(node: Nodes.ERBIfNode): void {
223
+ this.printERBNode(node)
224
+
225
+ if (node.statements) {
226
+ node.statements.forEach(statement => this.visit(statement))
227
+ }
228
+
229
+ if (node.subsequent) {
230
+ this.visit(node.subsequent)
231
+ }
232
+
233
+ if (node.end_node) {
234
+ this.visit(node.end_node)
235
+ }
236
+ }
237
+
238
+ visitERBElseNode(node: Nodes.ERBElseNode): void {
239
+ this.printERBNode(node)
240
+
241
+ if (node.statements) {
242
+ node.statements.forEach(statement => this.visit(statement))
243
+ }
244
+ }
245
+
246
+ visitERBEndNode(node: Nodes.ERBEndNode): void {
247
+ this.printERBNode(node)
248
+ }
249
+
250
+ visitERBBlockNode(node: Nodes.ERBBlockNode): void {
251
+ this.printERBNode(node)
252
+
253
+ if (node.body) {
254
+ node.body.forEach(child => this.visit(child))
255
+ }
256
+
257
+ if (node.end_node) {
258
+ this.visit(node.end_node)
259
+ }
260
+ }
261
+
262
+ visitERBCaseNode(node: Nodes.ERBCaseNode): void {
263
+ this.printERBNode(node)
264
+
265
+ if (node.children) {
266
+ node.children.forEach(child => this.visit(child))
267
+ }
268
+
269
+ if (node.conditions) {
270
+ node.conditions.forEach(condition => this.visit(condition))
271
+ }
272
+
273
+ if (node.else_clause) {
274
+ this.visit(node.else_clause)
275
+ }
276
+
277
+ if (node.end_node) {
278
+ this.visit(node.end_node)
279
+ }
280
+ }
281
+
282
+ visitERBWhenNode(node: Nodes.ERBWhenNode): void {
283
+ this.printERBNode(node)
284
+
285
+ if (node.statements) {
286
+ node.statements.forEach(statement => this.visit(statement))
287
+ }
288
+ }
289
+
290
+ visitERBWhileNode(node: Nodes.ERBWhileNode): void {
291
+ this.printERBNode(node)
292
+
293
+ if (node.statements) {
294
+ node.statements.forEach(statement => this.visit(statement))
295
+ }
296
+
297
+ if (node.end_node) {
298
+ this.visit(node.end_node)
299
+ }
300
+ }
301
+
302
+ visitERBUntilNode(node: Nodes.ERBUntilNode): void {
303
+ this.printERBNode(node)
304
+
305
+ if (node.statements) {
306
+ node.statements.forEach(statement => this.visit(statement))
307
+ }
308
+
309
+ if (node.end_node) {
310
+ this.visit(node.end_node)
311
+ }
312
+ }
313
+
314
+ visitERBForNode(node: Nodes.ERBForNode): void {
315
+ this.printERBNode(node)
316
+
317
+ if (node.statements) {
318
+ node.statements.forEach(statement => this.visit(statement))
319
+ }
320
+
321
+ if (node.end_node) {
322
+ this.visit(node.end_node)
323
+ }
324
+ }
325
+
326
+ visitERBBeginNode(node: Nodes.ERBBeginNode): void {
327
+ this.printERBNode(node)
328
+
329
+ if (node.statements) {
330
+ node.statements.forEach(statement => this.visit(statement))
331
+ }
332
+
333
+ if (node.rescue_clause) {
334
+ this.visit(node.rescue_clause)
335
+ }
336
+
337
+ if (node.else_clause) {
338
+ this.visit(node.else_clause)
339
+ }
340
+
341
+ if (node.ensure_clause) {
342
+ this.visit(node.ensure_clause)
343
+ }
344
+
345
+ if (node.end_node) {
346
+ this.visit(node.end_node)
347
+ }
348
+ }
349
+
350
+ visitERBRescueNode(node: Nodes.ERBRescueNode): void {
351
+ this.printERBNode(node)
352
+
353
+ if (node.statements) {
354
+ node.statements.forEach(statement => this.visit(statement))
355
+ }
356
+
357
+ if (node.subsequent) {
358
+ this.visit(node.subsequent)
359
+ }
360
+ }
361
+
362
+ visitERBEnsureNode(node: Nodes.ERBEnsureNode): void {
363
+ this.printERBNode(node)
364
+
365
+ if (node.statements) {
366
+ node.statements.forEach(statement => this.visit(statement))
367
+ }
368
+ }
369
+
370
+ visitERBUnlessNode(node: Nodes.ERBUnlessNode): void {
371
+ this.printERBNode(node)
372
+
373
+ if (node.statements) {
374
+ node.statements.forEach(statement => this.visit(statement))
375
+ }
376
+
377
+ if (node.else_clause) {
378
+ this.visit(node.else_clause)
379
+ }
380
+
381
+ if (node.end_node) {
382
+ this.visit(node.end_node)
383
+ }
384
+ }
385
+
386
+ visitERBYieldNode(node: Nodes.ERBYieldNode): void {
387
+ this.printERBNode(node)
388
+ }
389
+
390
+ visitERBInNode(node: Nodes.ERBInNode): void {
391
+ this.printERBNode(node)
392
+
393
+ if (node.statements) {
394
+ node.statements.forEach(statement => this.visit(statement))
395
+ }
396
+ }
397
+
398
+ visitERBCaseMatchNode(node: Nodes.ERBCaseMatchNode): void {
399
+ this.printERBNode(node)
400
+
401
+ if (node.children) {
402
+ node.children.forEach(child => this.visit(child))
403
+ }
404
+
405
+ if (node.conditions) {
406
+ node.conditions.forEach(condition => this.visit(condition))
407
+ }
408
+
409
+ if (node.else_clause) {
410
+ this.visit(node.else_clause)
411
+ }
412
+
413
+ if (node.end_node) {
414
+ this.visit(node.end_node)
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Print ERB node tags and content
420
+ */
421
+ protected printERBNode(node: Nodes.ERBNode): void {
422
+ if (node.tag_opening) {
423
+ this.context.write(node.tag_opening.value)
424
+ }
425
+
426
+ if (node.content) {
427
+ this.context.write(node.content.value)
428
+ }
429
+
430
+ if (node.tag_closing) {
431
+ this.context.write(node.tag_closing.value)
432
+ }
433
+ }
434
+
435
+ protected write(content: string): void {
436
+ this.context.write(content)
437
+ }
438
+ }