@herb-tools/formatter 0.4.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/README.md +59 -0
- package/bin/herb-formatter +3 -0
- package/dist/herb-formatter.js +9070 -0
- package/dist/herb-formatter.js.map +1 -0
- package/dist/index.cjs +3215 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +3211 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/formatter.d.ts +16 -0
- package/dist/types/herb-formatter.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/options.d.ts +22 -0
- package/dist/types/printer.d.ts +55 -0
- package/package.json +47 -0
- package/src/cli.ts +70 -0
- package/src/formatter.ts +36 -0
- package/src/herb-formatter.ts +6 -0
- package/src/index.ts +5 -0
- package/src/options.ts +34 -0
- package/src/printer.ts +635 -0
package/src/printer.ts
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { Visitor } from "@herb-tools/core"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Node,
|
|
5
|
+
DocumentNode,
|
|
6
|
+
HTMLOpenTagNode,
|
|
7
|
+
HTMLCloseTagNode,
|
|
8
|
+
HTMLSelfCloseTagNode,
|
|
9
|
+
HTMLElementNode,
|
|
10
|
+
HTMLAttributeNode,
|
|
11
|
+
HTMLAttributeValueNode,
|
|
12
|
+
HTMLAttributeNameNode,
|
|
13
|
+
HTMLTextNode,
|
|
14
|
+
HTMLCommentNode,
|
|
15
|
+
HTMLDoctypeNode,
|
|
16
|
+
LiteralNode,
|
|
17
|
+
WhitespaceNode,
|
|
18
|
+
ERBContentNode,
|
|
19
|
+
ERBBlockNode,
|
|
20
|
+
ERBEndNode,
|
|
21
|
+
ERBElseNode,
|
|
22
|
+
ERBIfNode,
|
|
23
|
+
ERBWhenNode,
|
|
24
|
+
ERBCaseNode,
|
|
25
|
+
ERBCaseMatchNode,
|
|
26
|
+
ERBWhileNode,
|
|
27
|
+
ERBUntilNode,
|
|
28
|
+
ERBForNode,
|
|
29
|
+
ERBRescueNode,
|
|
30
|
+
ERBEnsureNode,
|
|
31
|
+
ERBBeginNode,
|
|
32
|
+
ERBUnlessNode,
|
|
33
|
+
ERBYieldNode,
|
|
34
|
+
ERBInNode,
|
|
35
|
+
Token
|
|
36
|
+
} from "@herb-tools/core"
|
|
37
|
+
|
|
38
|
+
type ERBNode =
|
|
39
|
+
ERBContentNode
|
|
40
|
+
| ERBBlockNode
|
|
41
|
+
| ERBEndNode
|
|
42
|
+
| ERBElseNode
|
|
43
|
+
| ERBIfNode
|
|
44
|
+
| ERBWhenNode
|
|
45
|
+
| ERBCaseNode
|
|
46
|
+
| ERBCaseMatchNode
|
|
47
|
+
| ERBWhileNode
|
|
48
|
+
| ERBUntilNode
|
|
49
|
+
| ERBForNode
|
|
50
|
+
| ERBRescueNode
|
|
51
|
+
| ERBEnsureNode
|
|
52
|
+
| ERBBeginNode
|
|
53
|
+
| ERBUnlessNode
|
|
54
|
+
| ERBYieldNode
|
|
55
|
+
| ERBInNode
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
import type { FormatOptions } from "./options.js"
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Printer traverses the Herb AST using the Visitor pattern
|
|
62
|
+
* and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
|
|
63
|
+
*/
|
|
64
|
+
export class Printer extends Visitor {
|
|
65
|
+
private indentWidth: number
|
|
66
|
+
private maxLineLength: number
|
|
67
|
+
private source: string
|
|
68
|
+
private lines: string[] = []
|
|
69
|
+
private indentLevel: number = 0
|
|
70
|
+
|
|
71
|
+
constructor(source: string, options: Required<FormatOptions>) {
|
|
72
|
+
super()
|
|
73
|
+
this.source = source
|
|
74
|
+
this.indentWidth = options.indentWidth
|
|
75
|
+
this.maxLineLength = options.maxLineLength
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
print(object: Node | Token, indentLevel: number = 0): string {
|
|
79
|
+
if (object instanceof Token || (object as any).type?.startsWith('TOKEN_')) {
|
|
80
|
+
return (object as Token).value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const node: Node = object
|
|
84
|
+
|
|
85
|
+
this.lines = []
|
|
86
|
+
this.indentLevel = indentLevel
|
|
87
|
+
|
|
88
|
+
if (typeof (node as any).accept === 'function') {
|
|
89
|
+
node.accept(this)
|
|
90
|
+
} else {
|
|
91
|
+
this.visit(node)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return this.lines.filter(Boolean).join("\n")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private push(line: string) {
|
|
98
|
+
this.lines.push(line)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private withIndent<T>(callback: () => T): T {
|
|
102
|
+
this.indentLevel++
|
|
103
|
+
const result = callback()
|
|
104
|
+
this.indentLevel--
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private indent(): string {
|
|
109
|
+
return " ".repeat(this.indentLevel * this.indentWidth)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
|
|
114
|
+
*/
|
|
115
|
+
private printERBNode(node: ERBNode): void {
|
|
116
|
+
const indent = this.indent()
|
|
117
|
+
const open = node.tag_opening?.value ?? ""
|
|
118
|
+
const close = node.tag_closing?.value ?? ""
|
|
119
|
+
let inner: string
|
|
120
|
+
if (node.tag_opening && node.tag_closing) {
|
|
121
|
+
const [, openingEnd] = node.tag_opening.range.toArray()
|
|
122
|
+
const [closingStart] = node.tag_closing.range.toArray()
|
|
123
|
+
const rawInner = this.source.slice(openingEnd, closingStart)
|
|
124
|
+
inner = ` ${rawInner.trim()} `
|
|
125
|
+
} else {
|
|
126
|
+
const txt = node.content?.value ?? ""
|
|
127
|
+
inner = txt.trim() ? ` ${txt.trim()} ` : ""
|
|
128
|
+
}
|
|
129
|
+
this.push(indent + open + inner + close)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- Visitor methods ---
|
|
133
|
+
|
|
134
|
+
visitDocumentNode(node: DocumentNode): void {
|
|
135
|
+
node.children.forEach(child => this.visit(child))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
139
|
+
const open = node.open_tag as HTMLOpenTagNode
|
|
140
|
+
const tagName = open.tag_name?.value ?? ""
|
|
141
|
+
const indent = this.indent()
|
|
142
|
+
|
|
143
|
+
const attributes = open.children.filter((child): child is HTMLAttributeNode =>
|
|
144
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
145
|
+
)
|
|
146
|
+
const children = node.body.filter(
|
|
147
|
+
child =>
|
|
148
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') &&
|
|
149
|
+
!((child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') && (child as any)?.content.trim() === ""),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>"
|
|
153
|
+
const isSelfClosing = open.tag_closing?.value === "/>"
|
|
154
|
+
|
|
155
|
+
if (!hasClosing) {
|
|
156
|
+
this.push(indent + `<${tagName}`)
|
|
157
|
+
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (attributes.length === 0) {
|
|
162
|
+
if (children.length === 0) {
|
|
163
|
+
if (isSelfClosing) {
|
|
164
|
+
this.push(indent + `<${tagName} />`)
|
|
165
|
+
} else if (node.is_void) {
|
|
166
|
+
this.push(indent + `<${tagName}>`)
|
|
167
|
+
} else {
|
|
168
|
+
this.push(indent + `<${tagName}></${tagName}>`)
|
|
169
|
+
}
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.push(indent + `<${tagName}>`)
|
|
174
|
+
|
|
175
|
+
this.withIndent(() => {
|
|
176
|
+
children.forEach(child => this.visit(child))
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (!node.is_void && !isSelfClosing) {
|
|
180
|
+
this.push(indent + `</${tagName}>`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing)
|
|
187
|
+
const singleAttribute = attributes[0]
|
|
188
|
+
const hasEmptyValue =
|
|
189
|
+
singleAttribute &&
|
|
190
|
+
(singleAttribute.value instanceof HTMLAttributeValueNode || (singleAttribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
191
|
+
(singleAttribute.value as any)?.children.length === 0
|
|
192
|
+
|
|
193
|
+
const shouldKeepInline = attributes.length <= 3 &&
|
|
194
|
+
!hasEmptyValue &&
|
|
195
|
+
inline.length + indent.length <= this.maxLineLength
|
|
196
|
+
|
|
197
|
+
if (shouldKeepInline) {
|
|
198
|
+
if (children.length === 0) {
|
|
199
|
+
if (isSelfClosing) {
|
|
200
|
+
this.push(indent + inline)
|
|
201
|
+
} else if (node.is_void) {
|
|
202
|
+
this.push(indent + inline)
|
|
203
|
+
} else {
|
|
204
|
+
this.push(indent + inline.replace('>', `></${tagName}>`))
|
|
205
|
+
}
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isSelfClosing) {
|
|
210
|
+
this.push(indent + inline.replace(' />', '>'))
|
|
211
|
+
} else {
|
|
212
|
+
this.push(indent + inline)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.withIndent(() => {
|
|
216
|
+
children.forEach(child => this.visit(child))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if (!node.is_void && !isSelfClosing) {
|
|
220
|
+
this.push(indent + `</${tagName}>`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.push(indent + `<${tagName}`)
|
|
227
|
+
this.withIndent(() => {
|
|
228
|
+
attributes.forEach(attribute => {
|
|
229
|
+
this.push(this.indent() + this.renderAttribute(attribute))
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
if (isSelfClosing) {
|
|
234
|
+
this.push(indent + "/>")
|
|
235
|
+
} else if (node.is_void) {
|
|
236
|
+
this.push(indent + ">")
|
|
237
|
+
} else if (children.length === 0) {
|
|
238
|
+
this.push(indent + ">" + `</${tagName}>`)
|
|
239
|
+
} else {
|
|
240
|
+
this.push(indent + ">")
|
|
241
|
+
|
|
242
|
+
this.withIndent(() => {
|
|
243
|
+
children.forEach(child => this.visit(child))
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
this.push(indent + `</${tagName}>`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
251
|
+
const tagName = node.tag_name?.value ?? ""
|
|
252
|
+
const indent = this.indent()
|
|
253
|
+
const attributes = node.children.filter((attribute): attribute is HTMLAttributeNode =>
|
|
254
|
+
attribute instanceof HTMLAttributeNode || (attribute as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const hasClosing = node.tag_closing?.value === ">"
|
|
258
|
+
|
|
259
|
+
if (!hasClosing) {
|
|
260
|
+
this.push(indent + `<${tagName}`)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const inline = this.renderInlineOpen(tagName, attributes, node.is_void)
|
|
265
|
+
|
|
266
|
+
if (attributes.length === 0 || inline.length + indent.length <= this.maxLineLength) {
|
|
267
|
+
this.push(indent + inline)
|
|
268
|
+
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.push(indent + `<${tagName}`)
|
|
273
|
+
this.withIndent(() => {
|
|
274
|
+
attributes.forEach(attribute => {
|
|
275
|
+
this.push(this.indent() + this.renderAttribute(attribute))
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
this.push(indent + (node.is_void ? "/>" : ">"))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
282
|
+
const tagName = node.tag_name?.value ?? ""
|
|
283
|
+
const indent = this.indent()
|
|
284
|
+
const attributes = node.attributes.filter((attribute): attribute is HTMLAttributeNode =>
|
|
285
|
+
attribute instanceof HTMLAttributeNode || (attribute as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
286
|
+
)
|
|
287
|
+
const inline = this.renderInlineOpen(tagName, attributes, true)
|
|
288
|
+
|
|
289
|
+
const singleAttribute = attributes[0]
|
|
290
|
+
const hasEmptyValue =
|
|
291
|
+
singleAttribute &&
|
|
292
|
+
(singleAttribute.value instanceof HTMLAttributeValueNode || (singleAttribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
293
|
+
(singleAttribute.value as any)?.children.length === 0
|
|
294
|
+
|
|
295
|
+
const shouldKeepInline = attributes.length <= 3 &&
|
|
296
|
+
!hasEmptyValue &&
|
|
297
|
+
inline.length + indent.length <= this.maxLineLength
|
|
298
|
+
|
|
299
|
+
if (shouldKeepInline) {
|
|
300
|
+
this.push(indent + inline)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.push(indent + `<${tagName}`)
|
|
305
|
+
this.withIndent(() => {
|
|
306
|
+
attributes.forEach(attribute => {
|
|
307
|
+
this.push(this.indent() + this.renderAttribute(attribute))
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
this.push(indent + "/>")
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
|
|
314
|
+
const indent = this.indent()
|
|
315
|
+
const open = node.tag_opening?.value ?? ""
|
|
316
|
+
const name = node.tag_name?.value ?? ""
|
|
317
|
+
const close = node.tag_closing?.value ?? ""
|
|
318
|
+
|
|
319
|
+
this.push(indent + open + name + close)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
visitHTMLTextNode(node: HTMLTextNode): void {
|
|
323
|
+
const indent = this.indent()
|
|
324
|
+
let text = node.content.trim()
|
|
325
|
+
|
|
326
|
+
if (!text) return
|
|
327
|
+
|
|
328
|
+
const wrapWidth = this.maxLineLength - indent.length
|
|
329
|
+
const words = text.split(/\s+/)
|
|
330
|
+
const lines: string[] = []
|
|
331
|
+
|
|
332
|
+
let line = ""
|
|
333
|
+
|
|
334
|
+
for (const word of words) {
|
|
335
|
+
if ((line + (line ? " " : "") + word).length > wrapWidth && line) {
|
|
336
|
+
lines.push(indent + line)
|
|
337
|
+
line = word
|
|
338
|
+
} else {
|
|
339
|
+
line += (line ? " " : "") + word
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (line) lines.push(indent + line)
|
|
344
|
+
|
|
345
|
+
lines.forEach(line => this.push(line))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
visitHTMLAttributeNode(node: HTMLAttributeNode): void {
|
|
349
|
+
const indent = this.indent()
|
|
350
|
+
this.push(indent + this.renderAttribute(node))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode): void {
|
|
354
|
+
const indent = this.indent()
|
|
355
|
+
const name = node.name?.value ?? ""
|
|
356
|
+
this.push(indent + name)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void {
|
|
360
|
+
const indent = this.indent()
|
|
361
|
+
const open_quote = node.open_quote?.value ?? ""
|
|
362
|
+
const close_quote = node.close_quote?.value ?? ""
|
|
363
|
+
const attribute_value = node.children.map(child => {
|
|
364
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE' ||
|
|
365
|
+
child instanceof LiteralNode || (child as any).type === 'AST_LITERAL_NODE') {
|
|
366
|
+
return (child as HTMLTextNode | LiteralNode).content
|
|
367
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
368
|
+
const erbChild = child as ERBContentNode
|
|
369
|
+
return (erbChild.tag_opening!.value + erbChild.content!.value + erbChild.tag_closing!.value)
|
|
370
|
+
}
|
|
371
|
+
return ""
|
|
372
|
+
}).join("")
|
|
373
|
+
this.push(indent + open_quote + attribute_value + close_quote)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
visitHTMLCommentNode(node: HTMLCommentNode): void {
|
|
377
|
+
const indent = this.indent()
|
|
378
|
+
const open = node.comment_start?.value ?? ""
|
|
379
|
+
const close = node.comment_end?.value ?? ""
|
|
380
|
+
let inner: string
|
|
381
|
+
|
|
382
|
+
if (node.comment_start && node.comment_end) {
|
|
383
|
+
// TODO: use .value
|
|
384
|
+
const [_, startIndex] = node.comment_start.range.toArray()
|
|
385
|
+
const [endIndex] = node.comment_end.range.toArray()
|
|
386
|
+
const rawInner = this.source.slice(startIndex, endIndex)
|
|
387
|
+
inner = ` ${rawInner.trim()} `
|
|
388
|
+
} else {
|
|
389
|
+
inner = node.children.map(child => {
|
|
390
|
+
const prevLines = this.lines.length
|
|
391
|
+
this.visit(child)
|
|
392
|
+
return this.lines.slice(prevLines).join("")
|
|
393
|
+
}).join("")
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.push(indent + open + inner + close)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
visitERBCommentNode(node: ERBContentNode): void {
|
|
400
|
+
const indent = this.indent()
|
|
401
|
+
const open = node.tag_opening?.value ?? ""
|
|
402
|
+
const close = node.tag_closing?.value ?? ""
|
|
403
|
+
let inner: string
|
|
404
|
+
|
|
405
|
+
if (node.tag_opening && node.tag_closing) {
|
|
406
|
+
const [, openingEnd] = node.tag_opening.range.toArray()
|
|
407
|
+
const [closingStart] = node.tag_closing.range.toArray()
|
|
408
|
+
const rawInner = this.source.slice(openingEnd, closingStart)
|
|
409
|
+
const lines = rawInner.split("\n")
|
|
410
|
+
if (lines.length > 2) {
|
|
411
|
+
const childIndent = indent + " ".repeat(this.indentWidth)
|
|
412
|
+
const innerLines = lines.slice(1, -1).map(line => childIndent + line.trim())
|
|
413
|
+
inner = "\n" + innerLines.join("\n") + "\n"
|
|
414
|
+
} else {
|
|
415
|
+
inner = ` ${rawInner.trim()} `
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
inner = (node as any).children
|
|
419
|
+
.map((child: any) => {
|
|
420
|
+
const prevLines = this.lines.length
|
|
421
|
+
this.visit(child)
|
|
422
|
+
return this.lines.slice(prevLines).join("")
|
|
423
|
+
})
|
|
424
|
+
.join("")
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
this.push(indent + open + inner + close)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
visitHTMLDoctypeNode(node: HTMLDoctypeNode): void {
|
|
431
|
+
const indent = this.indent()
|
|
432
|
+
const open = node.tag_opening?.value ?? ""
|
|
433
|
+
let innerDoctype: string
|
|
434
|
+
|
|
435
|
+
if (node.tag_opening && node.tag_closing) {
|
|
436
|
+
// TODO: use .value
|
|
437
|
+
const [, openingEnd] = node.tag_opening.range.toArray()
|
|
438
|
+
const [closingStart] = node.tag_closing.range.toArray()
|
|
439
|
+
innerDoctype = this.source.slice(openingEnd, closingStart)
|
|
440
|
+
} else {
|
|
441
|
+
innerDoctype = node.children
|
|
442
|
+
.map(child =>
|
|
443
|
+
child instanceof HTMLTextNode ? child.content : (() => { const prevLines = this.lines.length; this.visit(child); return this.lines.slice(prevLines).join("") })(),
|
|
444
|
+
)
|
|
445
|
+
.join("")
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const close = node.tag_closing?.value ?? ""
|
|
449
|
+
this.push(indent + open + innerDoctype + close)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
visitERBContentNode(node: ERBContentNode): void {
|
|
453
|
+
// TODO: this feels hacky
|
|
454
|
+
if (node.tag_opening?.value === "<%#") {
|
|
455
|
+
this.visitERBCommentNode(node)
|
|
456
|
+
} else {
|
|
457
|
+
this.printERBNode(node)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
visitERBEndNode(node: ERBEndNode): void {
|
|
462
|
+
this.printERBNode(node)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
visitERBYieldNode(node: ERBYieldNode): void {
|
|
466
|
+
this.printERBNode(node)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
visitERBInNode(node: ERBInNode): void {
|
|
470
|
+
this.printERBNode(node)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
|
|
474
|
+
this.printERBNode(node)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
visitERBBlockNode(node: ERBBlockNode): void {
|
|
478
|
+
const indent = this.indent()
|
|
479
|
+
const open = node.tag_opening?.value ?? ""
|
|
480
|
+
const content = node.content?.value ?? ""
|
|
481
|
+
const close = node.tag_closing?.value ?? ""
|
|
482
|
+
|
|
483
|
+
this.push(indent + open + content + close)
|
|
484
|
+
|
|
485
|
+
this.withIndent(() => {
|
|
486
|
+
node.body.forEach(child => this.visit(child))
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
if (node.end_node) {
|
|
490
|
+
this.visit(node.end_node)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
visitERBIfNode(node: ERBIfNode): void {
|
|
495
|
+
this.printERBNode(node)
|
|
496
|
+
|
|
497
|
+
this.withIndent(() => {
|
|
498
|
+
node.statements.forEach(child => this.visit(child))
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (node.subsequent) {
|
|
502
|
+
this.visit(node.subsequent)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (node.end_node) {
|
|
506
|
+
this.printERBNode(node.end_node as any)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
visitERBElseNode(node: ERBElseNode): void {
|
|
511
|
+
this.printERBNode(node)
|
|
512
|
+
|
|
513
|
+
this.withIndent(() => {
|
|
514
|
+
node.statements.forEach(child => this.visit(child))
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
visitERBWhenNode(node: ERBWhenNode): void {
|
|
519
|
+
this.printERBNode(node)
|
|
520
|
+
|
|
521
|
+
this.withIndent(() => {
|
|
522
|
+
node.statements.forEach(stmt => this.visit(stmt))
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
visitERBCaseNode(node: ERBCaseNode): void {
|
|
527
|
+
const baseLevel = this.indentLevel
|
|
528
|
+
const indent = this.indent()
|
|
529
|
+
const open = node.tag_opening?.value ?? ""
|
|
530
|
+
const content = node.content?.value ?? ""
|
|
531
|
+
const close = node.tag_closing?.value ?? ""
|
|
532
|
+
this.push(indent + open + content + close)
|
|
533
|
+
|
|
534
|
+
node.conditions.forEach(condition => this.visit(condition))
|
|
535
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
536
|
+
|
|
537
|
+
if (node.end_node) {
|
|
538
|
+
this.visit(node.end_node)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
visitERBBeginNode(node: ERBBeginNode): void {
|
|
543
|
+
const indent = this.indent()
|
|
544
|
+
const open = node.tag_opening?.value ?? ""
|
|
545
|
+
const content = node.content?.value ?? ""
|
|
546
|
+
const close = node.tag_closing?.value ?? ""
|
|
547
|
+
|
|
548
|
+
this.push(indent + open + content + close)
|
|
549
|
+
|
|
550
|
+
this.withIndent(() => {
|
|
551
|
+
node.statements.forEach(statement => this.visit(statement))
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
if (node.rescue_clause) this.visit(node.rescue_clause)
|
|
555
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
556
|
+
if (node.ensure_clause) this.visit(node.ensure_clause)
|
|
557
|
+
if (node.end_node) this.visit(node.end_node)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
visitERBWhileNode(node: ERBWhileNode): void {
|
|
561
|
+
this.visitERBGeneric(node)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
visitERBUntilNode(node: ERBUntilNode): void {
|
|
565
|
+
this.visitERBGeneric(node)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
visitERBForNode(node: ERBForNode): void {
|
|
569
|
+
this.visitERBGeneric(node)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
visitERBRescueNode(node: ERBRescueNode): void {
|
|
573
|
+
this.visitERBGeneric(node)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
visitERBEnsureNode(node: ERBEnsureNode): void {
|
|
577
|
+
this.visitERBGeneric(node)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
visitERBUnlessNode(node: ERBUnlessNode): void {
|
|
581
|
+
this.visitERBGeneric(node)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// TODO: don't use any
|
|
585
|
+
private visitERBGeneric(node: any): void {
|
|
586
|
+
const indent = this.indent()
|
|
587
|
+
const open = node.tag_opening?.value ?? ""
|
|
588
|
+
const content = node.content?.value ?? ""
|
|
589
|
+
const close = node.tag_closing?.value ?? ""
|
|
590
|
+
|
|
591
|
+
this.push(indent + open + content + close)
|
|
592
|
+
|
|
593
|
+
this.withIndent(() => {
|
|
594
|
+
const statements: any[] = node.statements ?? node.body ?? node.children ?? []
|
|
595
|
+
|
|
596
|
+
statements.forEach(statement => this.visit(statement))
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
if (node.end_node) this.visit(node.end_node)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// --- Utility methods ---
|
|
603
|
+
|
|
604
|
+
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean): string {
|
|
605
|
+
const parts = attributes.map(attribute => this.renderAttribute(attribute))
|
|
606
|
+
|
|
607
|
+
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " /" : ""}>`
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
renderAttribute(attribute: HTMLAttributeNode): string {
|
|
611
|
+
const name = (attribute.name as HTMLAttributeNameNode)!.name!.value ?? ""
|
|
612
|
+
const equals = attribute.equals?.value ?? ""
|
|
613
|
+
let value = ""
|
|
614
|
+
|
|
615
|
+
if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || (attribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
|
|
616
|
+
const attrValue = attribute.value as HTMLAttributeValueNode
|
|
617
|
+
const open_quote = (attrValue.open_quote?.value ?? "")
|
|
618
|
+
const close_quote = (attrValue.close_quote?.value ?? "")
|
|
619
|
+
const attribute_value = attrValue.children.map((attr: any) => {
|
|
620
|
+
if (attr instanceof HTMLTextNode || (attr as any).type === 'AST_HTML_TEXT_NODE' ||
|
|
621
|
+
attr instanceof LiteralNode || (attr as any).type === 'AST_LITERAL_NODE') {
|
|
622
|
+
return (attr as HTMLTextNode | LiteralNode).content
|
|
623
|
+
} else if (attr instanceof ERBContentNode || (attr as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
624
|
+
const erbAttr = attr as ERBContentNode
|
|
625
|
+
return (erbAttr.tag_opening!.value + erbAttr.content!.value + erbAttr.tag_closing!.value)
|
|
626
|
+
}
|
|
627
|
+
return ""
|
|
628
|
+
}).join("")
|
|
629
|
+
|
|
630
|
+
value = open_quote + attribute_value + close_quote
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return name + equals + value
|
|
634
|
+
}
|
|
635
|
+
}
|