@herb-tools/formatter 0.4.0 → 0.4.2
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 +8 -5
- package/bin/herb-format +3 -0
- package/dist/herb-format.js +17444 -0
- package/dist/herb-format.js.map +1 -0
- package/dist/index.cjs +327 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +327 -37
- package/dist/index.esm.js.map +1 -1
- package/dist/types/printer.d.ts +15 -0
- package/package.json +4 -4
- package/src/cli.ts +175 -23
- package/src/printer.ts +393 -33
- package/bin/herb-formatter +0 -3
- package/dist/herb-formatter.js +0 -9070
- package/dist/herb-formatter.js.map +0 -1
- /package/dist/types/{herb-formatter.d.ts → herb-format.d.ts} +0 -0
- /package/src/{herb-formatter.ts → herb-format.ts} +0 -0
package/src/printer.ts
CHANGED
|
@@ -67,6 +67,8 @@ export class Printer extends Visitor {
|
|
|
67
67
|
private source: string
|
|
68
68
|
private lines: string[] = []
|
|
69
69
|
private indentLevel: number = 0
|
|
70
|
+
private inlineMode: boolean = false
|
|
71
|
+
private isInComplexNesting: boolean = false
|
|
70
72
|
|
|
71
73
|
constructor(source: string, options: Required<FormatOptions>) {
|
|
72
74
|
super()
|
|
@@ -84,6 +86,7 @@ export class Printer extends Visitor {
|
|
|
84
86
|
|
|
85
87
|
this.lines = []
|
|
86
88
|
this.indentLevel = indentLevel
|
|
89
|
+
this.isInComplexNesting = false // Reset for each top-level element
|
|
87
90
|
|
|
88
91
|
if (typeof (node as any).accept === 'function') {
|
|
89
92
|
node.accept(this)
|
|
@@ -143,6 +146,12 @@ export class Printer extends Visitor {
|
|
|
143
146
|
const attributes = open.children.filter((child): child is HTMLAttributeNode =>
|
|
144
147
|
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
145
148
|
)
|
|
149
|
+
|
|
150
|
+
const inlineNodes = open.children.filter(child =>
|
|
151
|
+
!(child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') &&
|
|
152
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')
|
|
153
|
+
)
|
|
154
|
+
|
|
146
155
|
const children = node.body.filter(
|
|
147
156
|
child =>
|
|
148
157
|
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') &&
|
|
@@ -158,7 +167,7 @@ export class Printer extends Visitor {
|
|
|
158
167
|
return
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
if (attributes.length === 0) {
|
|
170
|
+
if (attributes.length === 0 && inlineNodes.length === 0) {
|
|
162
171
|
if (children.length === 0) {
|
|
163
172
|
if (isSelfClosing) {
|
|
164
173
|
this.push(indent + `<${tagName} />`)
|
|
@@ -167,9 +176,35 @@ export class Printer extends Visitor {
|
|
|
167
176
|
} else {
|
|
168
177
|
this.push(indent + `<${tagName}></${tagName}>`)
|
|
169
178
|
}
|
|
179
|
+
|
|
170
180
|
return
|
|
171
181
|
}
|
|
172
182
|
|
|
183
|
+
if (children.length >= 1) {
|
|
184
|
+
if (this.isInComplexNesting) {
|
|
185
|
+
if (children.length === 1) {
|
|
186
|
+
const child = children[0]
|
|
187
|
+
|
|
188
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
189
|
+
const textContent = (child as HTMLTextNode).content.trim()
|
|
190
|
+
const singleLine = `<${tagName}>${textContent}</${tagName}>`
|
|
191
|
+
|
|
192
|
+
if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
|
|
193
|
+
this.push(indent + singleLine)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const inlineResult = this.tryRenderInline(children, tagName)
|
|
200
|
+
|
|
201
|
+
if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
|
|
202
|
+
this.push(indent + inlineResult)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
173
208
|
this.push(indent + `<${tagName}>`)
|
|
174
209
|
|
|
175
210
|
this.withIndent(() => {
|
|
@@ -183,16 +218,49 @@ export class Printer extends Visitor {
|
|
|
183
218
|
return
|
|
184
219
|
}
|
|
185
220
|
|
|
186
|
-
|
|
221
|
+
if (attributes.length === 0 && inlineNodes.length > 0) {
|
|
222
|
+
const inline = this.renderInlineOpen(tagName, [], isSelfClosing, inlineNodes, open.children)
|
|
223
|
+
|
|
224
|
+
if (children.length === 0) {
|
|
225
|
+
if (isSelfClosing || node.is_void) {
|
|
226
|
+
this.push(indent + inline)
|
|
227
|
+
} else {
|
|
228
|
+
this.push(indent + inline + `</${tagName}>`)
|
|
229
|
+
}
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.push(indent + inline)
|
|
234
|
+
this.withIndent(() => {
|
|
235
|
+
children.forEach(child => this.visit(child))
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
if (!node.is_void && !isSelfClosing) {
|
|
239
|
+
this.push(indent + `</${tagName}>`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children)
|
|
187
246
|
const singleAttribute = attributes[0]
|
|
188
247
|
const hasEmptyValue =
|
|
189
248
|
singleAttribute &&
|
|
190
249
|
(singleAttribute.value instanceof HTMLAttributeValueNode || (singleAttribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
191
250
|
(singleAttribute.value as any)?.children.length === 0
|
|
192
251
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
252
|
+
const hasERBControlFlow = inlineNodes.some(node =>
|
|
253
|
+
node instanceof ERBIfNode || (node as any).type === 'AST_ERB_IF_NODE' ||
|
|
254
|
+
node instanceof ERBUnlessNode || (node as any).type === 'AST_ERB_UNLESS_NODE' ||
|
|
255
|
+
node instanceof ERBBlockNode || (node as any).type === 'AST_ERB_BLOCK_NODE' ||
|
|
256
|
+
node instanceof ERBCaseNode || (node as any).type === 'AST_ERB_CASE_NODE' ||
|
|
257
|
+
node instanceof ERBWhileNode || (node as any).type === 'AST_ERB_WHILE_NODE' ||
|
|
258
|
+
node instanceof ERBForNode || (node as any).type === 'AST_ERB_FOR_NODE'
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const shouldKeepInline = (attributes.length <= 3 &&
|
|
262
|
+
inline.length + indent.length <= this.maxLineLength) ||
|
|
263
|
+
(inlineNodes.length > 0 && !hasERBControlFlow)
|
|
196
264
|
|
|
197
265
|
if (shouldKeepInline) {
|
|
198
266
|
if (children.length === 0) {
|
|
@@ -203,6 +271,7 @@ export class Printer extends Visitor {
|
|
|
203
271
|
} else {
|
|
204
272
|
this.push(indent + inline.replace('>', `></${tagName}>`))
|
|
205
273
|
}
|
|
274
|
+
|
|
206
275
|
return
|
|
207
276
|
}
|
|
208
277
|
|
|
@@ -223,27 +292,63 @@ export class Printer extends Visitor {
|
|
|
223
292
|
return
|
|
224
293
|
}
|
|
225
294
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
295
|
+
if (inlineNodes.length > 0 && hasERBControlFlow) {
|
|
296
|
+
this.push(indent + `<${tagName}`)
|
|
297
|
+
this.withIndent(() => {
|
|
298
|
+
open.children.forEach(child => {
|
|
299
|
+
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
300
|
+
this.push(this.indent() + this.renderAttribute(child as HTMLAttributeNode))
|
|
301
|
+
} else if (!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')) {
|
|
302
|
+
this.visit(child)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
230
305
|
})
|
|
231
|
-
})
|
|
232
306
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
307
|
+
if (isSelfClosing) {
|
|
308
|
+
this.push(indent + "/>")
|
|
309
|
+
} else if (node.is_void) {
|
|
310
|
+
this.push(indent + ">")
|
|
311
|
+
} else if (children.length === 0) {
|
|
312
|
+
this.push(indent + ">" + `</${tagName}>`)
|
|
313
|
+
} else {
|
|
314
|
+
this.push(indent + ">")
|
|
315
|
+
this.withIndent(() => {
|
|
316
|
+
children.forEach(child => this.visit(child))
|
|
317
|
+
})
|
|
318
|
+
this.push(indent + `</${tagName}>`)
|
|
319
|
+
}
|
|
320
|
+
} else if (inlineNodes.length > 0) {
|
|
321
|
+
this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children))
|
|
241
322
|
|
|
323
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
324
|
+
this.withIndent(() => {
|
|
325
|
+
children.forEach(child => this.visit(child))
|
|
326
|
+
})
|
|
327
|
+
this.push(indent + `</${tagName}>`)
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
this.push(indent + `<${tagName}`)
|
|
242
331
|
this.withIndent(() => {
|
|
243
|
-
|
|
332
|
+
attributes.forEach(attribute => {
|
|
333
|
+
this.push(this.indent() + this.renderAttribute(attribute))
|
|
334
|
+
})
|
|
244
335
|
})
|
|
245
336
|
|
|
246
|
-
|
|
337
|
+
if (isSelfClosing) {
|
|
338
|
+
this.push(indent + "/>")
|
|
339
|
+
} else if (node.is_void) {
|
|
340
|
+
this.push(indent + ">")
|
|
341
|
+
} else if (children.length === 0) {
|
|
342
|
+
this.push(indent + ">" + `</${tagName}>`)
|
|
343
|
+
} else {
|
|
344
|
+
this.push(indent + ">")
|
|
345
|
+
|
|
346
|
+
this.withIndent(() => {
|
|
347
|
+
children.forEach(child => this.visit(child))
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
this.push(indent + `</${tagName}>`)
|
|
351
|
+
}
|
|
247
352
|
}
|
|
248
353
|
}
|
|
249
354
|
|
|
@@ -293,7 +398,6 @@ export class Printer extends Visitor {
|
|
|
293
398
|
(singleAttribute.value as any)?.children.length === 0
|
|
294
399
|
|
|
295
400
|
const shouldKeepInline = attributes.length <= 3 &&
|
|
296
|
-
!hasEmptyValue &&
|
|
297
401
|
inline.length + indent.length <= this.maxLineLength
|
|
298
402
|
|
|
299
403
|
if (shouldKeepInline) {
|
|
@@ -492,18 +596,59 @@ export class Printer extends Visitor {
|
|
|
492
596
|
}
|
|
493
597
|
|
|
494
598
|
visitERBIfNode(node: ERBIfNode): void {
|
|
495
|
-
this.
|
|
599
|
+
if (this.inlineMode) {
|
|
600
|
+
const open = node.tag_opening?.value ?? ""
|
|
601
|
+
const content = node.content?.value ?? ""
|
|
602
|
+
const close = node.tag_closing?.value ?? ""
|
|
496
603
|
|
|
497
|
-
|
|
498
|
-
node.statements.forEach(child => this.visit(child))
|
|
499
|
-
})
|
|
604
|
+
this.lines.push(open + content + close)
|
|
500
605
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
606
|
+
if (node.statements.length > 0) {
|
|
607
|
+
this.lines.push(" ")
|
|
608
|
+
}
|
|
504
609
|
|
|
505
|
-
|
|
506
|
-
|
|
610
|
+
node.statements.forEach((child, index) => {
|
|
611
|
+
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
612
|
+
this.lines.push(this.renderAttribute(child as HTMLAttributeNode))
|
|
613
|
+
} else {
|
|
614
|
+
this.visit(child)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (index < node.statements.length - 1) {
|
|
618
|
+
this.lines.push(" ")
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
if (node.statements.length > 0) {
|
|
623
|
+
this.lines.push(" ")
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (node.subsequent) {
|
|
627
|
+
this.visit(node.subsequent)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (node.end_node) {
|
|
631
|
+
const endNode = node.end_node as any
|
|
632
|
+
const endOpen = endNode.tag_opening?.value ?? ""
|
|
633
|
+
const endContent = endNode.content?.value ?? ""
|
|
634
|
+
const endClose = endNode.tag_closing?.value ?? ""
|
|
635
|
+
|
|
636
|
+
this.lines.push(endOpen + endContent + endClose)
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
this.printERBNode(node)
|
|
640
|
+
|
|
641
|
+
this.withIndent(() => {
|
|
642
|
+
node.statements.forEach(child => this.visit(child))
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
if (node.subsequent) {
|
|
646
|
+
this.visit(node.subsequent)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (node.end_node) {
|
|
650
|
+
this.printERBNode(node.end_node as any)
|
|
651
|
+
}
|
|
507
652
|
}
|
|
508
653
|
}
|
|
509
654
|
|
|
@@ -601,15 +746,73 @@ export class Printer extends Visitor {
|
|
|
601
746
|
|
|
602
747
|
// --- Utility methods ---
|
|
603
748
|
|
|
604
|
-
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean): string {
|
|
749
|
+
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean, inlineNodes: Node[] = [], allChildren: Node[] = []): string {
|
|
605
750
|
const parts = attributes.map(attribute => this.renderAttribute(attribute))
|
|
606
751
|
|
|
752
|
+
if (inlineNodes.length > 0) {
|
|
753
|
+
let result = `<${name}`
|
|
754
|
+
|
|
755
|
+
if (allChildren.length > 0) {
|
|
756
|
+
const currentIndentLevel = this.indentLevel
|
|
757
|
+
this.indentLevel = 0
|
|
758
|
+
const tempLines = this.lines
|
|
759
|
+
this.lines = []
|
|
760
|
+
|
|
761
|
+
allChildren.forEach(child => {
|
|
762
|
+
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
763
|
+
this.lines.push(" " + this.renderAttribute(child as HTMLAttributeNode))
|
|
764
|
+
} else if (!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')) {
|
|
765
|
+
const wasInlineMode = this.inlineMode
|
|
766
|
+
this.inlineMode = true
|
|
767
|
+
|
|
768
|
+
this.lines.push(" ")
|
|
769
|
+
|
|
770
|
+
this.visit(child)
|
|
771
|
+
this.inlineMode = wasInlineMode
|
|
772
|
+
}
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
const inlineContent = this.lines.join("")
|
|
776
|
+
this.lines = tempLines
|
|
777
|
+
this.indentLevel = currentIndentLevel
|
|
778
|
+
|
|
779
|
+
result += inlineContent
|
|
780
|
+
} else {
|
|
781
|
+
if (parts.length > 0) {
|
|
782
|
+
result += ` ${parts.join(" ")}`
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const currentIndentLevel = this.indentLevel
|
|
786
|
+
this.indentLevel = 0
|
|
787
|
+
const tempLines = this.lines
|
|
788
|
+
this.lines = []
|
|
789
|
+
|
|
790
|
+
inlineNodes.forEach(node => {
|
|
791
|
+
const wasInlineMode = this.inlineMode
|
|
792
|
+
this.inlineMode = true
|
|
793
|
+
this.visit(node)
|
|
794
|
+
this.inlineMode = wasInlineMode
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
const inlineContent = this.lines.join("")
|
|
798
|
+
this.lines = tempLines
|
|
799
|
+
this.indentLevel = currentIndentLevel
|
|
800
|
+
|
|
801
|
+
result += inlineContent
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
result += selfClose ? " />" : ">"
|
|
805
|
+
|
|
806
|
+
return result
|
|
807
|
+
}
|
|
808
|
+
|
|
607
809
|
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " /" : ""}>`
|
|
608
810
|
}
|
|
609
811
|
|
|
610
812
|
renderAttribute(attribute: HTMLAttributeNode): string {
|
|
611
813
|
const name = (attribute.name as HTMLAttributeNameNode)!.name!.value ?? ""
|
|
612
814
|
const equals = attribute.equals?.value ?? ""
|
|
815
|
+
|
|
613
816
|
let value = ""
|
|
614
817
|
|
|
615
818
|
if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || (attribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
|
|
@@ -617,13 +820,15 @@ export class Printer extends Visitor {
|
|
|
617
820
|
const open_quote = (attrValue.open_quote?.value ?? "")
|
|
618
821
|
const close_quote = (attrValue.close_quote?.value ?? "")
|
|
619
822
|
const attribute_value = attrValue.children.map((attr: any) => {
|
|
620
|
-
if (attr instanceof HTMLTextNode || (attr as any).type === 'AST_HTML_TEXT_NODE' ||
|
|
621
|
-
|
|
823
|
+
if (attr instanceof HTMLTextNode || (attr as any).type === 'AST_HTML_TEXT_NODE' || attr instanceof LiteralNode || (attr as any).type === 'AST_LITERAL_NODE') {
|
|
824
|
+
|
|
622
825
|
return (attr as HTMLTextNode | LiteralNode).content
|
|
623
826
|
} else if (attr instanceof ERBContentNode || (attr as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
624
827
|
const erbAttr = attr as ERBContentNode
|
|
828
|
+
|
|
625
829
|
return (erbAttr.tag_opening!.value + erbAttr.content!.value + erbAttr.tag_closing!.value)
|
|
626
830
|
}
|
|
831
|
+
|
|
627
832
|
return ""
|
|
628
833
|
}).join("")
|
|
629
834
|
|
|
@@ -632,4 +837,159 @@ export class Printer extends Visitor {
|
|
|
632
837
|
|
|
633
838
|
return name + equals + value
|
|
634
839
|
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Try to render children inline if they are simple enough.
|
|
843
|
+
* Returns the inline string if possible, null otherwise.
|
|
844
|
+
*/
|
|
845
|
+
private tryRenderInline(children: Node[], tagName: string, depth: number = 0): string | null {
|
|
846
|
+
if (children.length > 10) {
|
|
847
|
+
return null
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const maxNestingDepth = this.getMaxNestingDepth(children, 0)
|
|
851
|
+
|
|
852
|
+
if (maxNestingDepth > 1) {
|
|
853
|
+
this.isInComplexNesting = true
|
|
854
|
+
return null
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for (const child of children) {
|
|
858
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
859
|
+
const textContent = (child as HTMLTextNode).content
|
|
860
|
+
|
|
861
|
+
if (textContent.includes('\n')) {
|
|
862
|
+
return null
|
|
863
|
+
}
|
|
864
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
865
|
+
const element = child as HTMLElementNode
|
|
866
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
867
|
+
|
|
868
|
+
const attributes = openTag.children.filter((child): child is HTMLAttributeNode =>
|
|
869
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
if (attributes.length > 0) {
|
|
873
|
+
return null
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
877
|
+
// ERB content nodes are allowed in inline rendering
|
|
878
|
+
} else {
|
|
879
|
+
return null
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const oldLines = this.lines
|
|
884
|
+
const oldInlineMode = this.inlineMode
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
this.lines = []
|
|
888
|
+
this.inlineMode = true
|
|
889
|
+
|
|
890
|
+
let content = ''
|
|
891
|
+
|
|
892
|
+
for (const child of children) {
|
|
893
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
894
|
+
content += (child as HTMLTextNode).content
|
|
895
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
896
|
+
const element = child as HTMLElementNode
|
|
897
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
898
|
+
const childTagName = openTag?.tag_name?.value || ''
|
|
899
|
+
|
|
900
|
+
const attributes = openTag.children.filter((child): child is HTMLAttributeNode =>
|
|
901
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
const attributesString = attributes.length > 0
|
|
905
|
+
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
906
|
+
: ''
|
|
907
|
+
|
|
908
|
+
const elementContent = this.renderElementInline(element)
|
|
909
|
+
|
|
910
|
+
content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`
|
|
911
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
912
|
+
const erbNode = child as ERBContentNode
|
|
913
|
+
const open = erbNode.tag_opening?.value ?? ""
|
|
914
|
+
const erbContent = erbNode.content?.value ?? ""
|
|
915
|
+
const close = erbNode.tag_closing?.value ?? ""
|
|
916
|
+
|
|
917
|
+
content += `${open} ${erbContent.trim()} ${close}`
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
content = content.replace(/\s+/g, ' ').trim()
|
|
922
|
+
|
|
923
|
+
return `<${tagName}>${content}</${tagName}>`
|
|
924
|
+
|
|
925
|
+
} finally {
|
|
926
|
+
this.lines = oldLines
|
|
927
|
+
this.inlineMode = oldInlineMode
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
933
|
+
*/
|
|
934
|
+
private getMaxNestingDepth(children: Node[], currentDepth: number): number {
|
|
935
|
+
let maxDepth = currentDepth
|
|
936
|
+
|
|
937
|
+
for (const child of children) {
|
|
938
|
+
if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
939
|
+
const element = child as HTMLElementNode
|
|
940
|
+
const elementChildren = element.body.filter(
|
|
941
|
+
child =>
|
|
942
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') &&
|
|
943
|
+
!((child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') && (child as any)?.content.trim() === ""),
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1)
|
|
947
|
+
maxDepth = Math.max(maxDepth, childDepth)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return maxDepth
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Render an HTML element's content inline (without the wrapping tags).
|
|
956
|
+
*/
|
|
957
|
+
private renderElementInline(element: HTMLElementNode): string {
|
|
958
|
+
const children = element.body.filter(
|
|
959
|
+
child =>
|
|
960
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') &&
|
|
961
|
+
!((child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') && (child as any)?.content.trim() === ""),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
let content = ''
|
|
965
|
+
for (const child of children) {
|
|
966
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
967
|
+
content += (child as HTMLTextNode).content
|
|
968
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
969
|
+
const childElement = child as HTMLElementNode
|
|
970
|
+
const openTag = childElement.open_tag as HTMLOpenTagNode
|
|
971
|
+
const childTagName = openTag?.tag_name?.value || ''
|
|
972
|
+
|
|
973
|
+
const attributes = openTag.children.filter((child): child is HTMLAttributeNode =>
|
|
974
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
const attributesString = attributes.length > 0
|
|
978
|
+
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
979
|
+
: ''
|
|
980
|
+
|
|
981
|
+
const childContent = this.renderElementInline(childElement)
|
|
982
|
+
content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`
|
|
983
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
984
|
+
const erbNode = child as ERBContentNode
|
|
985
|
+
const open = erbNode.tag_opening?.value ?? ""
|
|
986
|
+
const erbContent = erbNode.content?.value ?? ""
|
|
987
|
+
const close = erbNode.tag_closing?.value ?? ""
|
|
988
|
+
|
|
989
|
+
content += `${open} ${erbContent.trim()} ${close}`
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return content.replace(/\s+/g, ' ').trim()
|
|
994
|
+
}
|
|
635
995
|
}
|
package/bin/herb-formatter
DELETED