@herb-tools/formatter 0.4.2 → 0.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.
- package/README.md +113 -15
- package/dist/herb-format.js +784 -179
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +776 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +776 -171
- package/dist/index.esm.js.map +1 -1
- package/dist/types/printer.d.ts +64 -0
- package/package.json +3 -2
- package/src/cli.ts +2 -2
- package/src/printer.ts +1006 -189
package/src/printer.ts
CHANGED
|
@@ -57,6 +57,12 @@ type ERBNode =
|
|
|
57
57
|
|
|
58
58
|
import type { FormatOptions } from "./options.js"
|
|
59
59
|
|
|
60
|
+
// TODO: we can probably expand this list with more tags/attributes
|
|
61
|
+
const FORMATTABLE_ATTRIBUTES: Record<string, string[]> = {
|
|
62
|
+
'*': ['class'],
|
|
63
|
+
'img': ['srcset', 'sizes']
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
/**
|
|
61
67
|
* Printer traverses the Herb AST using the Visitor pattern
|
|
62
68
|
* and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
|
|
@@ -69,9 +75,18 @@ export class Printer extends Visitor {
|
|
|
69
75
|
private indentLevel: number = 0
|
|
70
76
|
private inlineMode: boolean = false
|
|
71
77
|
private isInComplexNesting: boolean = false
|
|
78
|
+
private currentTagName: string = ""
|
|
79
|
+
|
|
80
|
+
private static readonly INLINE_ELEMENTS = new Set([
|
|
81
|
+
'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'cite', 'code',
|
|
82
|
+
'dfn', 'em', 'i', 'img', 'kbd', 'label', 'map', 'object', 'q',
|
|
83
|
+
'samp', 'small', 'span', 'strong', 'sub', 'sup',
|
|
84
|
+
'tt', 'var', 'del', 'ins', 'mark', 's', 'u', 'time', 'wbr'
|
|
85
|
+
])
|
|
72
86
|
|
|
73
87
|
constructor(source: string, options: Required<FormatOptions>) {
|
|
74
88
|
super()
|
|
89
|
+
|
|
75
90
|
this.source = source
|
|
76
91
|
this.indentWidth = options.indentWidth
|
|
77
92
|
this.maxLineLength = options.maxLineLength
|
|
@@ -94,7 +109,7 @@ export class Printer extends Visitor {
|
|
|
94
109
|
this.visit(node)
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
return this.lines.
|
|
112
|
+
return this.lines.join("\n")
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
private push(line: string) {
|
|
@@ -105,6 +120,7 @@ export class Printer extends Visitor {
|
|
|
105
120
|
this.indentLevel++
|
|
106
121
|
const result = callback()
|
|
107
122
|
this.indentLevel--
|
|
123
|
+
|
|
108
124
|
return result
|
|
109
125
|
}
|
|
110
126
|
|
|
@@ -112,30 +128,307 @@ export class Printer extends Visitor {
|
|
|
112
128
|
return " ".repeat(this.indentLevel * this.indentWidth)
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Format ERB content with proper spacing around the inner content.
|
|
133
|
+
* Returns empty string if content is empty, otherwise wraps content with single spaces.
|
|
134
|
+
*/
|
|
135
|
+
private formatERBContent(content: string): string {
|
|
136
|
+
return content.trim() ? ` ${content.trim()} ` : ""
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a node is an ERB control flow node (if, unless, block, case, while, for)
|
|
141
|
+
*/
|
|
142
|
+
private isERBControlFlow(node: Node): boolean {
|
|
143
|
+
return node instanceof ERBIfNode || (node as any).type === 'AST_ERB_IF_NODE' ||
|
|
144
|
+
node instanceof ERBUnlessNode || (node as any).type === 'AST_ERB_UNLESS_NODE' ||
|
|
145
|
+
node instanceof ERBBlockNode || (node as any).type === 'AST_ERB_BLOCK_NODE' ||
|
|
146
|
+
node instanceof ERBCaseNode || (node as any).type === 'AST_ERB_CASE_NODE' ||
|
|
147
|
+
node instanceof ERBCaseMatchNode || (node as any).type === 'AST_ERB_CASE_MATCH_NODE' ||
|
|
148
|
+
node instanceof ERBWhileNode || (node as any).type === 'AST_ERB_WHILE_NODE' ||
|
|
149
|
+
node instanceof ERBForNode || (node as any).type === 'AST_ERB_FOR_NODE'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Count total attributes including those inside ERB conditionals
|
|
154
|
+
*/
|
|
155
|
+
private getTotalAttributeCount(attributes: HTMLAttributeNode[], inlineNodes: Node[] = []): number {
|
|
156
|
+
let totalAttributeCount = attributes.length
|
|
157
|
+
|
|
158
|
+
inlineNodes.forEach(node => {
|
|
159
|
+
if (this.isERBControlFlow(node)) {
|
|
160
|
+
const erbNode = node as any
|
|
161
|
+
if (erbNode.statements) {
|
|
162
|
+
totalAttributeCount += erbNode.statements.length
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return totalAttributeCount
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract HTML attributes from a list of nodes
|
|
172
|
+
*/
|
|
173
|
+
private extractAttributes(nodes: Node[]): HTMLAttributeNode[] {
|
|
174
|
+
return nodes.filter((child): child is HTMLAttributeNode =>
|
|
175
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
|
|
181
|
+
*/
|
|
182
|
+
private extractInlineNodes(nodes: Node[]): Node[] {
|
|
183
|
+
return nodes.filter(child =>
|
|
184
|
+
!(child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') &&
|
|
185
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Render attributes as a space-separated string
|
|
191
|
+
*/
|
|
192
|
+
private renderAttributesString(attributes: HTMLAttributeNode[]): string {
|
|
193
|
+
if (attributes.length === 0) return ""
|
|
194
|
+
return ` ${attributes.map(attr => this.renderAttribute(attr)).join(" ")}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Determine if a tag should be rendered inline based on attribute count and other factors
|
|
199
|
+
*/
|
|
200
|
+
private shouldRenderInline(
|
|
201
|
+
totalAttributeCount: number,
|
|
202
|
+
inlineLength: number,
|
|
203
|
+
indentLength: number,
|
|
204
|
+
maxLineLength: number = this.maxLineLength,
|
|
205
|
+
hasComplexERB: boolean = false,
|
|
206
|
+
_nestingDepth: number = 0,
|
|
207
|
+
_inlineNodesLength: number = 0,
|
|
208
|
+
hasMultilineAttributes: boolean = false
|
|
209
|
+
): boolean {
|
|
210
|
+
if (hasComplexERB || hasMultilineAttributes) return false
|
|
211
|
+
|
|
212
|
+
if (totalAttributeCount === 0) {
|
|
213
|
+
return inlineLength + indentLength <= maxLineLength
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (totalAttributeCount > 3 || inlineLength + indentLength > maxLineLength) {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private hasMultilineAttributes(attributes: HTMLAttributeNode[]): boolean {
|
|
224
|
+
return attributes.some(attribute => {
|
|
225
|
+
if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || (attribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
|
|
226
|
+
const attributeValue = attribute.value as HTMLAttributeValueNode
|
|
227
|
+
|
|
228
|
+
const content = attributeValue.children.map((child: Node) => {
|
|
229
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE' || child instanceof LiteralNode || (child as any).type === 'AST_LITERAL_NODE') {
|
|
230
|
+
return (child as HTMLTextNode | LiteralNode).content
|
|
231
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
232
|
+
const erbAttribute = child as ERBContentNode
|
|
233
|
+
|
|
234
|
+
return erbAttribute.tag_opening!.value + erbAttribute.content!.value + erbAttribute.tag_closing!.value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return ""
|
|
238
|
+
}).join("")
|
|
239
|
+
|
|
240
|
+
if (/\r?\n/.test(content)) {
|
|
241
|
+
const name = (attribute.name as HTMLAttributeNameNode)!.name!.value ?? ""
|
|
242
|
+
|
|
243
|
+
if (name === 'class') {
|
|
244
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim()
|
|
245
|
+
|
|
246
|
+
return normalizedContent.length > 80
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const lines = content.split(/\r?\n/)
|
|
250
|
+
|
|
251
|
+
if (lines.length > 1) {
|
|
252
|
+
return lines.slice(1).some(line => /^\s+/.test(line))
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return false
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private formatClassAttribute(content: string, name: string, equals: string, open_quote: string, close_quote: string): string {
|
|
262
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim()
|
|
263
|
+
const hasActualNewlines = /\r?\n/.test(content)
|
|
264
|
+
|
|
265
|
+
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
266
|
+
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line)
|
|
267
|
+
|
|
268
|
+
if (lines.length > 1) {
|
|
269
|
+
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const currentIndent = this.indentLevel * this.indentWidth
|
|
274
|
+
const attributeLine = `${name}${equals}${open_quote}${normalizedContent}${close_quote}`
|
|
275
|
+
|
|
276
|
+
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
277
|
+
const classes = normalizedContent.split(' ')
|
|
278
|
+
const lines = this.breakTokensIntoLines(classes, currentIndent)
|
|
279
|
+
|
|
280
|
+
if (lines.length > 1) {
|
|
281
|
+
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return open_quote + normalizedContent + close_quote
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private isFormattableAttribute(attributeName: string, tagName: string): boolean {
|
|
289
|
+
const globalFormattable = FORMATTABLE_ATTRIBUTES['*'] || []
|
|
290
|
+
const tagSpecificFormattable = FORMATTABLE_ATTRIBUTES[tagName.toLowerCase()] || []
|
|
291
|
+
|
|
292
|
+
return globalFormattable.includes(attributeName) || tagSpecificFormattable.includes(attributeName)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private formatMultilineAttribute(content: string, name: string, equals: string, open_quote: string, close_quote: string): string {
|
|
296
|
+
if (name === 'srcset' || name === 'sizes') {
|
|
297
|
+
const normalizedContent = content.replace(/\s+/g, ' ').trim()
|
|
298
|
+
|
|
299
|
+
return open_quote + normalizedContent + close_quote
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const lines = content.split('\n')
|
|
303
|
+
|
|
304
|
+
if (lines.length <= 1) {
|
|
305
|
+
return open_quote + content + close_quote
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const formattedContent = this.formatMultilineAttributeValue(lines)
|
|
309
|
+
|
|
310
|
+
return open_quote + formattedContent + close_quote
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private formatMultilineAttributeValue(lines: string[]): string {
|
|
314
|
+
const indent = " ".repeat((this.indentLevel + 1) * this.indentWidth)
|
|
315
|
+
const closeIndent = " ".repeat(this.indentLevel * this.indentWidth)
|
|
316
|
+
|
|
317
|
+
return "\n" + lines.map(line => indent + line).join("\n") + "\n" + closeIndent
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private breakTokensIntoLines(tokens: string[], currentIndent: number, separator: string = ' '): string[] {
|
|
321
|
+
const lines: string[] = []
|
|
322
|
+
let currentLine = ''
|
|
323
|
+
|
|
324
|
+
for (const token of tokens) {
|
|
325
|
+
const testLine = currentLine ? currentLine + separator + token : token
|
|
326
|
+
|
|
327
|
+
if (testLine.length > (this.maxLineLength - currentIndent - 6)) {
|
|
328
|
+
if (currentLine) {
|
|
329
|
+
lines.push(currentLine)
|
|
330
|
+
currentLine = token
|
|
331
|
+
} else {
|
|
332
|
+
lines.push(token)
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
currentLine = testLine
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (currentLine) lines.push(currentLine)
|
|
340
|
+
|
|
341
|
+
return lines
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Render multiline attributes for a tag
|
|
346
|
+
*/
|
|
347
|
+
private renderMultilineAttributes(
|
|
348
|
+
tagName: string,
|
|
349
|
+
_attributes: HTMLAttributeNode[],
|
|
350
|
+
_inlineNodes: Node[] = [],
|
|
351
|
+
allChildren: Node[] = [],
|
|
352
|
+
isSelfClosing: boolean = false,
|
|
353
|
+
isVoid: boolean = false,
|
|
354
|
+
hasBodyContent: boolean = false
|
|
355
|
+
): void {
|
|
356
|
+
const indent = this.indent()
|
|
357
|
+
this.push(indent + `<${tagName}`)
|
|
358
|
+
|
|
359
|
+
this.withIndent(() => {
|
|
360
|
+
allChildren.forEach(child => {
|
|
361
|
+
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
362
|
+
this.push(this.indent() + this.renderAttribute(child as HTMLAttributeNode))
|
|
363
|
+
} else if (!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')) {
|
|
364
|
+
this.visit(child)
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
if (isSelfClosing) {
|
|
370
|
+
this.push(indent + "/>")
|
|
371
|
+
} else if (isVoid) {
|
|
372
|
+
this.push(indent + ">")
|
|
373
|
+
} else if (!hasBodyContent) {
|
|
374
|
+
this.push(indent + `></${tagName}>`)
|
|
375
|
+
} else {
|
|
376
|
+
this.push(indent + ">")
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
115
380
|
/**
|
|
116
381
|
* Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
|
|
117
382
|
*/
|
|
118
383
|
private printERBNode(node: ERBNode): void {
|
|
119
|
-
const indent = this.indent()
|
|
384
|
+
const indent = this.inlineMode ? "" : this.indent()
|
|
120
385
|
const open = node.tag_opening?.value ?? ""
|
|
121
386
|
const close = node.tag_closing?.value ?? ""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const [closingStart] = node.tag_closing.range.toArray()
|
|
126
|
-
const rawInner = this.source.slice(openingEnd, closingStart)
|
|
127
|
-
inner = ` ${rawInner.trim()} `
|
|
128
|
-
} else {
|
|
129
|
-
const txt = node.content?.value ?? ""
|
|
130
|
-
inner = txt.trim() ? ` ${txt.trim()} ` : ""
|
|
131
|
-
}
|
|
387
|
+
const content = node.content?.value ?? ""
|
|
388
|
+
const inner = this.formatERBContent(content)
|
|
389
|
+
|
|
132
390
|
this.push(indent + open + inner + close)
|
|
133
391
|
}
|
|
134
392
|
|
|
135
393
|
// --- Visitor methods ---
|
|
136
394
|
|
|
137
395
|
visitDocumentNode(node: DocumentNode): void {
|
|
138
|
-
|
|
396
|
+
let lastWasMeaningful = false
|
|
397
|
+
let hasHandledSpacing = false
|
|
398
|
+
|
|
399
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
400
|
+
const child = node.children[i]
|
|
401
|
+
|
|
402
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
403
|
+
const textNode = child as HTMLTextNode
|
|
404
|
+
const isWhitespaceOnly = textNode.content.trim() === ""
|
|
405
|
+
|
|
406
|
+
if (isWhitespaceOnly) {
|
|
407
|
+
const hasPrevNonWhitespace = i > 0 && this.isNonWhitespaceNode(node.children[i - 1])
|
|
408
|
+
const hasNextNonWhitespace = i < node.children.length - 1 && this.isNonWhitespaceNode(node.children[i + 1])
|
|
409
|
+
|
|
410
|
+
const hasMultipleNewlines = textNode.content.includes('\n\n')
|
|
411
|
+
|
|
412
|
+
if (hasPrevNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
|
|
413
|
+
this.push("")
|
|
414
|
+
hasHandledSpacing = true
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (this.isNonWhitespaceNode(child) && lastWasMeaningful && !hasHandledSpacing) {
|
|
422
|
+
this.push("")
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.visit(child)
|
|
426
|
+
|
|
427
|
+
if (this.isNonWhitespaceNode(child)) {
|
|
428
|
+
lastWasMeaningful = true
|
|
429
|
+
hasHandledSpacing = false
|
|
430
|
+
}
|
|
431
|
+
}
|
|
139
432
|
}
|
|
140
433
|
|
|
141
434
|
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
@@ -143,21 +436,32 @@ export class Printer extends Visitor {
|
|
|
143
436
|
const tagName = open.tag_name?.value ?? ""
|
|
144
437
|
const indent = this.indent()
|
|
145
438
|
|
|
146
|
-
|
|
147
|
-
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
148
|
-
)
|
|
439
|
+
this.currentTagName = tagName
|
|
149
440
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')
|
|
153
|
-
)
|
|
441
|
+
const attributes = this.extractAttributes(open.children)
|
|
442
|
+
const inlineNodes = this.extractInlineNodes(open.children)
|
|
154
443
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
444
|
+
const hasTextFlow = this.isInTextFlowContext(null, node.body)
|
|
445
|
+
|
|
446
|
+
const children = node.body.filter(child => {
|
|
447
|
+
if (child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') {
|
|
448
|
+
return false
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
452
|
+
const content = (child as HTMLTextNode).content
|
|
453
|
+
|
|
454
|
+
if (hasTextFlow && content === " ") {
|
|
455
|
+
return true
|
|
456
|
+
}
|
|
160
457
|
|
|
458
|
+
return content.trim() !== ""
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return true
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const isInlineElement = this.isInlineElement(tagName)
|
|
161
465
|
const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>"
|
|
162
466
|
const isSelfClosing = open.tag_closing?.value === "/>"
|
|
163
467
|
|
|
@@ -191,15 +495,59 @@ export class Printer extends Visitor {
|
|
|
191
495
|
|
|
192
496
|
if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
|
|
193
497
|
this.push(indent + singleLine)
|
|
498
|
+
|
|
194
499
|
return
|
|
195
500
|
}
|
|
196
501
|
}
|
|
197
502
|
}
|
|
198
503
|
} else {
|
|
199
|
-
const inlineResult = this.tryRenderInline(children, tagName)
|
|
504
|
+
const inlineResult = this.tryRenderInline(children, tagName, 0, false, hasTextFlow)
|
|
200
505
|
|
|
201
506
|
if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
|
|
202
507
|
this.push(indent + inlineResult)
|
|
508
|
+
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (hasTextFlow) {
|
|
513
|
+
const hasAnyNewlines = children.some(child => {
|
|
514
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
515
|
+
return (child as HTMLTextNode).content.includes('\n')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return false
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
if (!hasAnyNewlines) {
|
|
522
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children)
|
|
523
|
+
|
|
524
|
+
if (fullInlineResult) {
|
|
525
|
+
const totalLength = indent.length + fullInlineResult.length
|
|
526
|
+
const maxNesting = this.getMaxNestingDepth(children, 0)
|
|
527
|
+
const maxInlineLength = maxNesting <= 1 ? this.maxLineLength : 60
|
|
528
|
+
|
|
529
|
+
if (totalLength <= maxInlineLength) {
|
|
530
|
+
this.push(indent + fullInlineResult)
|
|
531
|
+
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (hasTextFlow) {
|
|
541
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, [], children)
|
|
542
|
+
|
|
543
|
+
if (fullInlineResult) {
|
|
544
|
+
const totalLength = indent.length + fullInlineResult.length
|
|
545
|
+
const maxNesting = this.getMaxNestingDepth(children, 0)
|
|
546
|
+
const maxInlineLength = maxNesting <= 1 ? this.maxLineLength : 60
|
|
547
|
+
|
|
548
|
+
if (totalLength <= maxInlineLength) {
|
|
549
|
+
this.push(indent + fullInlineResult)
|
|
550
|
+
|
|
203
551
|
return
|
|
204
552
|
}
|
|
205
553
|
}
|
|
@@ -208,7 +556,11 @@ export class Printer extends Visitor {
|
|
|
208
556
|
this.push(indent + `<${tagName}>`)
|
|
209
557
|
|
|
210
558
|
this.withIndent(() => {
|
|
211
|
-
|
|
559
|
+
if (hasTextFlow) {
|
|
560
|
+
this.visitTextFlowChildren(children)
|
|
561
|
+
} else {
|
|
562
|
+
children.forEach(child => this.visit(child))
|
|
563
|
+
}
|
|
212
564
|
})
|
|
213
565
|
|
|
214
566
|
if (!node.is_void && !isSelfClosing) {
|
|
@@ -242,25 +594,40 @@ export class Printer extends Visitor {
|
|
|
242
594
|
return
|
|
243
595
|
}
|
|
244
596
|
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
const hasEmptyValue =
|
|
248
|
-
singleAttribute &&
|
|
249
|
-
(singleAttribute.value instanceof HTMLAttributeValueNode || (singleAttribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
250
|
-
(singleAttribute.value as any)?.children.length === 0
|
|
597
|
+
const hasERBControlFlow = inlineNodes.some(node => this.isERBControlFlow(node)) ||
|
|
598
|
+
open.children.some(node => this.isERBControlFlow(node))
|
|
251
599
|
|
|
252
|
-
const
|
|
253
|
-
node instanceof ERBIfNode || (node as any).type === 'AST_ERB_IF_NODE'
|
|
254
|
-
|
|
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
|
-
)
|
|
600
|
+
const hasComplexERB = hasERBControlFlow && inlineNodes.some(node => {
|
|
601
|
+
if (node instanceof ERBIfNode || (node as any).type === 'AST_ERB_IF_NODE') {
|
|
602
|
+
const erbNode = node as ERBIfNode
|
|
260
603
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
604
|
+
if (erbNode.statements.length > 0 && erbNode.location) {
|
|
605
|
+
const startLine = erbNode.location.start.line
|
|
606
|
+
const endLine = erbNode.location.end.line
|
|
607
|
+
|
|
608
|
+
return startLine !== endLine
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return false
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return false
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
const inline = hasComplexERB ? "" : this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children)
|
|
618
|
+
const nestingDepth = this.getMaxNestingDepth(children, 0)
|
|
619
|
+
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
620
|
+
|
|
621
|
+
const shouldKeepInline = this.shouldRenderInline(
|
|
622
|
+
totalAttributeCount,
|
|
623
|
+
inline.length,
|
|
624
|
+
indent.length,
|
|
625
|
+
this.maxLineLength,
|
|
626
|
+
hasComplexERB,
|
|
627
|
+
nestingDepth,
|
|
628
|
+
inlineNodes.length,
|
|
629
|
+
this.hasMultilineAttributes(attributes)
|
|
630
|
+
)
|
|
264
631
|
|
|
265
632
|
if (shouldKeepInline) {
|
|
266
633
|
if (children.length === 0) {
|
|
@@ -269,7 +636,69 @@ export class Printer extends Visitor {
|
|
|
269
636
|
} else if (node.is_void) {
|
|
270
637
|
this.push(indent + inline)
|
|
271
638
|
} else {
|
|
272
|
-
|
|
639
|
+
let result = `<${tagName}`
|
|
640
|
+
|
|
641
|
+
result += this.renderAttributesString(attributes)
|
|
642
|
+
|
|
643
|
+
if (inlineNodes.length > 0) {
|
|
644
|
+
const currentIndentLevel = this.indentLevel
|
|
645
|
+
this.indentLevel = 0
|
|
646
|
+
const tempLines = this.lines
|
|
647
|
+
this.lines = []
|
|
648
|
+
|
|
649
|
+
inlineNodes.forEach(node => {
|
|
650
|
+
const wasInlineMode = this.inlineMode
|
|
651
|
+
|
|
652
|
+
if (!this.isERBControlFlow(node)) {
|
|
653
|
+
this.inlineMode = true
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
this.visit(node)
|
|
657
|
+
this.inlineMode = wasInlineMode
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
const inlineContent = this.lines.join("")
|
|
661
|
+
|
|
662
|
+
this.lines = tempLines
|
|
663
|
+
this.indentLevel = currentIndentLevel
|
|
664
|
+
|
|
665
|
+
result += inlineContent
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
result += `></${tagName}>`
|
|
669
|
+
this.push(indent + result)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (isInlineElement && children.length > 0 && !hasERBControlFlow) {
|
|
676
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children)
|
|
677
|
+
|
|
678
|
+
if (fullInlineResult) {
|
|
679
|
+
const totalLength = indent.length + fullInlineResult.length
|
|
680
|
+
|
|
681
|
+
if (totalLength <= this.maxLineLength || totalLength <= 120) {
|
|
682
|
+
this.push(indent + fullInlineResult)
|
|
683
|
+
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (!isInlineElement && children.length > 0 && !hasERBControlFlow) {
|
|
690
|
+
this.push(indent + inline)
|
|
691
|
+
|
|
692
|
+
this.withIndent(() => {
|
|
693
|
+
if (hasTextFlow) {
|
|
694
|
+
this.visitTextFlowChildren(children)
|
|
695
|
+
} else {
|
|
696
|
+
children.forEach(child => this.visit(child))
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
if (!node.is_void && !isSelfClosing) {
|
|
701
|
+
this.push(indent + `</${tagName}>`)
|
|
273
702
|
}
|
|
274
703
|
|
|
275
704
|
return
|
|
@@ -282,7 +711,11 @@ export class Printer extends Visitor {
|
|
|
282
711
|
}
|
|
283
712
|
|
|
284
713
|
this.withIndent(() => {
|
|
285
|
-
|
|
714
|
+
if (hasTextFlow) {
|
|
715
|
+
this.visitTextFlowChildren(children)
|
|
716
|
+
} else {
|
|
717
|
+
children.forEach(child => this.visit(child))
|
|
718
|
+
}
|
|
286
719
|
})
|
|
287
720
|
|
|
288
721
|
if (!node.is_void && !isSelfClosing) {
|
|
@@ -293,25 +726,9 @@ export class Printer extends Visitor {
|
|
|
293
726
|
}
|
|
294
727
|
|
|
295
728
|
if (inlineNodes.length > 0 && hasERBControlFlow) {
|
|
296
|
-
this.
|
|
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
|
-
})
|
|
305
|
-
})
|
|
729
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0)
|
|
306
730
|
|
|
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 + ">")
|
|
731
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
315
732
|
this.withIndent(() => {
|
|
316
733
|
children.forEach(child => this.visit(child))
|
|
317
734
|
})
|
|
@@ -327,24 +744,57 @@ export class Printer extends Visitor {
|
|
|
327
744
|
this.push(indent + `</${tagName}>`)
|
|
328
745
|
}
|
|
329
746
|
} else {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
attributes.forEach(attribute => {
|
|
333
|
-
this.push(this.indent() + this.renderAttribute(attribute))
|
|
334
|
-
})
|
|
335
|
-
})
|
|
747
|
+
if (isInlineElement && children.length > 0) {
|
|
748
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, attributes, children)
|
|
336
749
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
750
|
+
if (fullInlineResult) {
|
|
751
|
+
const totalLength = indent.length + fullInlineResult.length
|
|
752
|
+
|
|
753
|
+
if (totalLength <= this.maxLineLength || totalLength <= 120) {
|
|
754
|
+
this.push(indent + fullInlineResult)
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (isInlineElement && children.length === 0) {
|
|
761
|
+
const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children)
|
|
762
|
+
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
763
|
+
const shouldKeepInline = this.shouldRenderInline(
|
|
764
|
+
totalAttributeCount,
|
|
765
|
+
inline.length,
|
|
766
|
+
indent.length,
|
|
767
|
+
this.maxLineLength,
|
|
768
|
+
false,
|
|
769
|
+
0,
|
|
770
|
+
inlineNodes.length,
|
|
771
|
+
this.hasMultilineAttributes(attributes)
|
|
772
|
+
)
|
|
345
773
|
|
|
774
|
+
if (shouldKeepInline) {
|
|
775
|
+
let result = `<${tagName}`
|
|
776
|
+
result += this.renderAttributesString(attributes)
|
|
777
|
+
if (isSelfClosing) {
|
|
778
|
+
result += " />"
|
|
779
|
+
} else if (node.is_void) {
|
|
780
|
+
result += ">"
|
|
781
|
+
} else {
|
|
782
|
+
result += `></${tagName}>`
|
|
783
|
+
}
|
|
784
|
+
this.push(indent + result)
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, open.children, isSelfClosing, node.is_void, children.length > 0)
|
|
790
|
+
|
|
791
|
+
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
346
792
|
this.withIndent(() => {
|
|
347
|
-
|
|
793
|
+
if (hasTextFlow) {
|
|
794
|
+
this.visitTextFlowChildren(children)
|
|
795
|
+
} else {
|
|
796
|
+
children.forEach(child => this.visit(child))
|
|
797
|
+
}
|
|
348
798
|
})
|
|
349
799
|
|
|
350
800
|
this.push(indent + `</${tagName}>`)
|
|
@@ -355,9 +805,8 @@ export class Printer extends Visitor {
|
|
|
355
805
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
356
806
|
const tagName = node.tag_name?.value ?? ""
|
|
357
807
|
const indent = this.indent()
|
|
358
|
-
const attributes = node.children
|
|
359
|
-
|
|
360
|
-
)
|
|
808
|
+
const attributes = this.extractAttributes(node.children)
|
|
809
|
+
const inlineNodes = this.extractInlineNodes(node.children)
|
|
361
810
|
|
|
362
811
|
const hasClosing = node.tag_closing?.value === ">"
|
|
363
812
|
|
|
@@ -366,52 +815,55 @@ export class Printer extends Visitor {
|
|
|
366
815
|
return
|
|
367
816
|
}
|
|
368
817
|
|
|
369
|
-
const inline = this.renderInlineOpen(tagName, attributes, node.is_void)
|
|
818
|
+
const inline = this.renderInlineOpen(tagName, attributes, node.is_void, inlineNodes, node.children)
|
|
819
|
+
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
820
|
+
const shouldKeepInline = this.shouldRenderInline(
|
|
821
|
+
totalAttributeCount,
|
|
822
|
+
inline.length,
|
|
823
|
+
indent.length,
|
|
824
|
+
this.maxLineLength,
|
|
825
|
+
false,
|
|
826
|
+
0,
|
|
827
|
+
inlineNodes.length,
|
|
828
|
+
this.hasMultilineAttributes(attributes)
|
|
829
|
+
)
|
|
370
830
|
|
|
371
|
-
if (
|
|
831
|
+
if (shouldKeepInline) {
|
|
372
832
|
this.push(indent + inline)
|
|
373
833
|
|
|
374
834
|
return
|
|
375
835
|
}
|
|
376
836
|
|
|
377
|
-
this.
|
|
378
|
-
this.withIndent(() => {
|
|
379
|
-
attributes.forEach(attribute => {
|
|
380
|
-
this.push(this.indent() + this.renderAttribute(attribute))
|
|
381
|
-
})
|
|
382
|
-
})
|
|
383
|
-
this.push(indent + (node.is_void ? "/>" : ">"))
|
|
837
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.children, false, node.is_void, false)
|
|
384
838
|
}
|
|
385
839
|
|
|
386
840
|
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
387
841
|
const tagName = node.tag_name?.value ?? ""
|
|
388
842
|
const indent = this.indent()
|
|
389
|
-
const attributes = node.attributes.filter((attribute): attribute is HTMLAttributeNode =>
|
|
390
|
-
attribute instanceof HTMLAttributeNode || (attribute as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
391
|
-
)
|
|
392
|
-
const inline = this.renderInlineOpen(tagName, attributes, true)
|
|
393
|
-
|
|
394
|
-
const singleAttribute = attributes[0]
|
|
395
|
-
const hasEmptyValue =
|
|
396
|
-
singleAttribute &&
|
|
397
|
-
(singleAttribute.value instanceof HTMLAttributeValueNode || (singleAttribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
398
|
-
(singleAttribute.value as any)?.children.length === 0
|
|
399
843
|
|
|
400
|
-
const
|
|
401
|
-
|
|
844
|
+
const attributes = this.extractAttributes(node.attributes)
|
|
845
|
+
const inlineNodes = this.extractInlineNodes(node.attributes)
|
|
846
|
+
|
|
847
|
+
const inline = this.renderInlineOpen(tagName, attributes, true, inlineNodes, node.attributes)
|
|
848
|
+
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
849
|
+
const shouldKeepInline = this.shouldRenderInline(
|
|
850
|
+
totalAttributeCount,
|
|
851
|
+
inline.length,
|
|
852
|
+
indent.length,
|
|
853
|
+
this.maxLineLength,
|
|
854
|
+
false,
|
|
855
|
+
0,
|
|
856
|
+
inlineNodes.length,
|
|
857
|
+
this.hasMultilineAttributes(attributes)
|
|
858
|
+
)
|
|
402
859
|
|
|
403
860
|
if (shouldKeepInline) {
|
|
404
861
|
this.push(indent + inline)
|
|
862
|
+
|
|
405
863
|
return
|
|
406
864
|
}
|
|
407
865
|
|
|
408
|
-
this.
|
|
409
|
-
this.withIndent(() => {
|
|
410
|
-
attributes.forEach(attribute => {
|
|
411
|
-
this.push(this.indent() + this.renderAttribute(attribute))
|
|
412
|
-
})
|
|
413
|
-
})
|
|
414
|
-
this.push(indent + "/>")
|
|
866
|
+
this.renderMultilineAttributes(tagName, attributes, inlineNodes, node.attributes, true, false, false)
|
|
415
867
|
}
|
|
416
868
|
|
|
417
869
|
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
|
|
@@ -424,6 +876,16 @@ export class Printer extends Visitor {
|
|
|
424
876
|
}
|
|
425
877
|
|
|
426
878
|
visitHTMLTextNode(node: HTMLTextNode): void {
|
|
879
|
+
if (this.inlineMode) {
|
|
880
|
+
const normalizedContent = node.content.replace(/\s+/g, ' ').trim()
|
|
881
|
+
|
|
882
|
+
if (normalizedContent) {
|
|
883
|
+
this.push(normalizedContent)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return
|
|
887
|
+
}
|
|
888
|
+
|
|
427
889
|
const indent = this.indent()
|
|
428
890
|
let text = node.content.trim()
|
|
429
891
|
|
|
@@ -464,16 +926,21 @@ export class Printer extends Visitor {
|
|
|
464
926
|
const indent = this.indent()
|
|
465
927
|
const open_quote = node.open_quote?.value ?? ""
|
|
466
928
|
const close_quote = node.close_quote?.value ?? ""
|
|
929
|
+
|
|
467
930
|
const attribute_value = node.children.map(child => {
|
|
468
931
|
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE' ||
|
|
469
932
|
child instanceof LiteralNode || (child as any).type === 'AST_LITERAL_NODE') {
|
|
933
|
+
|
|
470
934
|
return (child as HTMLTextNode | LiteralNode).content
|
|
471
935
|
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
472
936
|
const erbChild = child as ERBContentNode
|
|
937
|
+
|
|
473
938
|
return (erbChild.tag_opening!.value + erbChild.content!.value + erbChild.tag_closing!.value)
|
|
474
939
|
}
|
|
940
|
+
|
|
475
941
|
return ""
|
|
476
942
|
}).join("")
|
|
943
|
+
|
|
477
944
|
this.push(indent + open_quote + attribute_value + close_quote)
|
|
478
945
|
}
|
|
479
946
|
|
|
@@ -481,20 +948,25 @@ export class Printer extends Visitor {
|
|
|
481
948
|
const indent = this.indent()
|
|
482
949
|
const open = node.comment_start?.value ?? ""
|
|
483
950
|
const close = node.comment_end?.value ?? ""
|
|
951
|
+
|
|
484
952
|
let inner: string
|
|
485
953
|
|
|
486
|
-
if (node.
|
|
487
|
-
// TODO: use .value
|
|
488
|
-
const [_, startIndex] = node.comment_start.range.toArray()
|
|
489
|
-
const [endIndex] = node.comment_end.range.toArray()
|
|
490
|
-
const rawInner = this.source.slice(startIndex, endIndex)
|
|
491
|
-
inner = ` ${rawInner.trim()} `
|
|
492
|
-
} else {
|
|
954
|
+
if (node.children && node.children.length > 0) {
|
|
493
955
|
inner = node.children.map(child => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
956
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
957
|
+
return (child as HTMLTextNode).content
|
|
958
|
+
} else if (child instanceof LiteralNode || (child as any).type === 'AST_LITERAL_NODE') {
|
|
959
|
+
return (child as LiteralNode).content
|
|
960
|
+
} else {
|
|
961
|
+
const prevLines = this.lines.length
|
|
962
|
+
this.visit(child)
|
|
963
|
+
return this.lines.slice(prevLines).join("")
|
|
964
|
+
}
|
|
497
965
|
}).join("")
|
|
966
|
+
|
|
967
|
+
inner = ` ${inner.trim()} `
|
|
968
|
+
} else {
|
|
969
|
+
inner = ""
|
|
498
970
|
}
|
|
499
971
|
|
|
500
972
|
this.push(indent + open + inner + close)
|
|
@@ -506,26 +978,28 @@ export class Printer extends Visitor {
|
|
|
506
978
|
const close = node.tag_closing?.value ?? ""
|
|
507
979
|
let inner: string
|
|
508
980
|
|
|
509
|
-
if (node.
|
|
510
|
-
const
|
|
511
|
-
const [closingStart] = node.tag_closing.range.toArray()
|
|
512
|
-
const rawInner = this.source.slice(openingEnd, closingStart)
|
|
981
|
+
if (node.content && node.content.value) {
|
|
982
|
+
const rawInner = node.content.value
|
|
513
983
|
const lines = rawInner.split("\n")
|
|
984
|
+
|
|
514
985
|
if (lines.length > 2) {
|
|
515
986
|
const childIndent = indent + " ".repeat(this.indentWidth)
|
|
516
987
|
const innerLines = lines.slice(1, -1).map(line => childIndent + line.trim())
|
|
988
|
+
|
|
517
989
|
inner = "\n" + innerLines.join("\n") + "\n"
|
|
518
990
|
} else {
|
|
519
991
|
inner = ` ${rawInner.trim()} `
|
|
520
992
|
}
|
|
993
|
+
} else if ((node as any).children) {
|
|
994
|
+
inner = (node as any).children.map((child: any) => {
|
|
995
|
+
const prevLines = this.lines.length
|
|
996
|
+
|
|
997
|
+
this.visit(child)
|
|
998
|
+
|
|
999
|
+
return this.lines.slice(prevLines).join("")
|
|
1000
|
+
}).join("")
|
|
521
1001
|
} else {
|
|
522
|
-
inner =
|
|
523
|
-
.map((child: any) => {
|
|
524
|
-
const prevLines = this.lines.length
|
|
525
|
-
this.visit(child)
|
|
526
|
-
return this.lines.slice(prevLines).join("")
|
|
527
|
-
})
|
|
528
|
-
.join("")
|
|
1002
|
+
inner = ""
|
|
529
1003
|
}
|
|
530
1004
|
|
|
531
1005
|
this.push(indent + open + inner + close)
|
|
@@ -534,22 +1008,30 @@ export class Printer extends Visitor {
|
|
|
534
1008
|
visitHTMLDoctypeNode(node: HTMLDoctypeNode): void {
|
|
535
1009
|
const indent = this.indent()
|
|
536
1010
|
const open = node.tag_opening?.value ?? ""
|
|
537
|
-
let innerDoctype: string
|
|
538
1011
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1012
|
+
let innerDoctype = node.children.map(child => {
|
|
1013
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
1014
|
+
return (child as HTMLTextNode).content
|
|
1015
|
+
} else if (child instanceof LiteralNode || (child as any).type === 'AST_LITERAL_NODE') {
|
|
1016
|
+
return (child as LiteralNode).content
|
|
1017
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
1018
|
+
const erbNode = child as ERBContentNode
|
|
1019
|
+
const erbOpen = erbNode.tag_opening?.value ?? ""
|
|
1020
|
+
const erbContent = erbNode.content?.value ?? ""
|
|
1021
|
+
const erbClose = erbNode.tag_closing?.value ?? ""
|
|
1022
|
+
|
|
1023
|
+
return erbOpen + (erbContent ? ` ${erbContent.trim()} ` : "") + erbClose
|
|
1024
|
+
} else {
|
|
1025
|
+
const prevLines = this.lines.length
|
|
1026
|
+
|
|
1027
|
+
this.visit(child)
|
|
1028
|
+
|
|
1029
|
+
return this.lines.slice(prevLines).join("")
|
|
1030
|
+
}
|
|
1031
|
+
}).join("")
|
|
551
1032
|
|
|
552
1033
|
const close = node.tag_closing?.value ?? ""
|
|
1034
|
+
|
|
553
1035
|
this.push(indent + open + innerDoctype + close)
|
|
554
1036
|
}
|
|
555
1037
|
|
|
@@ -572,10 +1054,19 @@ export class Printer extends Visitor {
|
|
|
572
1054
|
|
|
573
1055
|
visitERBInNode(node: ERBInNode): void {
|
|
574
1056
|
this.printERBNode(node)
|
|
1057
|
+
|
|
1058
|
+
this.withIndent(() => {
|
|
1059
|
+
node.statements.forEach(stmt => this.visit(stmt))
|
|
1060
|
+
})
|
|
575
1061
|
}
|
|
576
1062
|
|
|
577
1063
|
visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
|
|
578
1064
|
this.printERBNode(node)
|
|
1065
|
+
|
|
1066
|
+
node.conditions.forEach(condition => this.visit(condition))
|
|
1067
|
+
|
|
1068
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
1069
|
+
if (node.end_node) this.visit(node.end_node)
|
|
579
1070
|
}
|
|
580
1071
|
|
|
581
1072
|
visitERBBlockNode(node: ERBBlockNode): void {
|
|
@@ -600,26 +1091,21 @@ export class Printer extends Visitor {
|
|
|
600
1091
|
const open = node.tag_opening?.value ?? ""
|
|
601
1092
|
const content = node.content?.value ?? ""
|
|
602
1093
|
const close = node.tag_closing?.value ?? ""
|
|
1094
|
+
const inner = this.formatERBContent(content)
|
|
603
1095
|
|
|
604
|
-
this.lines.push(open +
|
|
1096
|
+
this.lines.push(open + inner + close)
|
|
605
1097
|
|
|
606
|
-
|
|
1098
|
+
node.statements.forEach((child, _index) => {
|
|
607
1099
|
this.lines.push(" ")
|
|
608
|
-
}
|
|
609
1100
|
|
|
610
|
-
node.statements.forEach((child, index) => {
|
|
611
1101
|
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
612
1102
|
this.lines.push(this.renderAttribute(child as HTMLAttributeNode))
|
|
613
1103
|
} else {
|
|
614
1104
|
this.visit(child)
|
|
615
1105
|
}
|
|
616
|
-
|
|
617
|
-
if (index < node.statements.length - 1) {
|
|
618
|
-
this.lines.push(" ")
|
|
619
|
-
}
|
|
620
1106
|
})
|
|
621
1107
|
|
|
622
|
-
if (node.statements.length > 0) {
|
|
1108
|
+
if (node.statements.length > 0 && node.end_node) {
|
|
623
1109
|
this.lines.push(" ")
|
|
624
1110
|
}
|
|
625
1111
|
|
|
@@ -632,8 +1118,9 @@ export class Printer extends Visitor {
|
|
|
632
1118
|
const endOpen = endNode.tag_opening?.value ?? ""
|
|
633
1119
|
const endContent = endNode.content?.value ?? ""
|
|
634
1120
|
const endClose = endNode.tag_closing?.value ?? ""
|
|
1121
|
+
const endInner = this.formatERBContent(endContent)
|
|
635
1122
|
|
|
636
|
-
this.lines.push(endOpen +
|
|
1123
|
+
this.lines.push(endOpen + endInner + endClose)
|
|
637
1124
|
}
|
|
638
1125
|
} else {
|
|
639
1126
|
this.printERBNode(node)
|
|
@@ -669,11 +1156,11 @@ export class Printer extends Visitor {
|
|
|
669
1156
|
}
|
|
670
1157
|
|
|
671
1158
|
visitERBCaseNode(node: ERBCaseNode): void {
|
|
672
|
-
const baseLevel = this.indentLevel
|
|
673
1159
|
const indent = this.indent()
|
|
674
1160
|
const open = node.tag_opening?.value ?? ""
|
|
675
1161
|
const content = node.content?.value ?? ""
|
|
676
1162
|
const close = node.tag_closing?.value ?? ""
|
|
1163
|
+
|
|
677
1164
|
this.push(indent + open + content + close)
|
|
678
1165
|
|
|
679
1166
|
node.conditions.forEach(condition => this.visit(condition))
|
|
@@ -746,6 +1233,188 @@ export class Printer extends Visitor {
|
|
|
746
1233
|
|
|
747
1234
|
// --- Utility methods ---
|
|
748
1235
|
|
|
1236
|
+
private isNonWhitespaceNode(node: Node): boolean {
|
|
1237
|
+
if (node instanceof HTMLTextNode || (node as any).type === 'AST_HTML_TEXT_NODE') {
|
|
1238
|
+
return (node as HTMLTextNode).content.trim() !== ""
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (node instanceof WhitespaceNode || (node as any).type === 'AST_WHITESPACE_NODE') {
|
|
1242
|
+
return false
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return true
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Check if an element should be treated as inline based on its tag name
|
|
1250
|
+
*/
|
|
1251
|
+
private isInlineElement(tagName: string): boolean {
|
|
1252
|
+
return Printer.INLINE_ELEMENTS.has(tagName.toLowerCase())
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Check if we're in a text flow context (parent contains mixed text and inline elements)
|
|
1257
|
+
*/
|
|
1258
|
+
private visitTextFlowChildren(children: Node[]): void {
|
|
1259
|
+
const indent = this.indent()
|
|
1260
|
+
let currentLineContent = ""
|
|
1261
|
+
|
|
1262
|
+
for (const child of children) {
|
|
1263
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
1264
|
+
const content = (child as HTMLTextNode).content
|
|
1265
|
+
|
|
1266
|
+
let processedContent = content.replace(/\s+/g, ' ').trim()
|
|
1267
|
+
|
|
1268
|
+
if (processedContent) {
|
|
1269
|
+
const hasLeadingSpace = /^\s/.test(content)
|
|
1270
|
+
|
|
1271
|
+
if (currentLineContent && hasLeadingSpace && !currentLineContent.endsWith(' ')) {
|
|
1272
|
+
currentLineContent += ' '
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
currentLineContent += processedContent
|
|
1276
|
+
|
|
1277
|
+
const hasTrailingSpace = /\s$/.test(content)
|
|
1278
|
+
|
|
1279
|
+
if (hasTrailingSpace && !currentLineContent.endsWith(' ')) {
|
|
1280
|
+
currentLineContent += ' '
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
|
|
1284
|
+
this.visitTextFlowChildrenMultiline(children)
|
|
1285
|
+
|
|
1286
|
+
return
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
1290
|
+
const element = child as HTMLElementNode
|
|
1291
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
1292
|
+
const childTagName = openTag?.tag_name?.value || ''
|
|
1293
|
+
|
|
1294
|
+
if (this.isInlineElement(childTagName)) {
|
|
1295
|
+
const childInline = this.tryRenderInlineFull(element, childTagName,
|
|
1296
|
+
this.extractAttributes(openTag.children),
|
|
1297
|
+
element.body.filter(c => !(c instanceof WhitespaceNode || (c as any).type === 'AST_WHITESPACE_NODE') &&
|
|
1298
|
+
!((c instanceof HTMLTextNode || (c as any).type === 'AST_HTML_TEXT_NODE') && (c as any)?.content.trim() === "")))
|
|
1299
|
+
|
|
1300
|
+
if (childInline) {
|
|
1301
|
+
currentLineContent += childInline
|
|
1302
|
+
|
|
1303
|
+
if ((indent.length + currentLineContent.length) > this.maxLineLength) {
|
|
1304
|
+
this.visitTextFlowChildrenMultiline(children)
|
|
1305
|
+
|
|
1306
|
+
return
|
|
1307
|
+
}
|
|
1308
|
+
} else {
|
|
1309
|
+
if (currentLineContent.trim()) {
|
|
1310
|
+
this.push(indent + currentLineContent.trim())
|
|
1311
|
+
currentLineContent = ""
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
this.visit(child)
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
if (currentLineContent.trim()) {
|
|
1318
|
+
this.push(indent + currentLineContent.trim())
|
|
1319
|
+
currentLineContent = ""
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
this.visit(child)
|
|
1323
|
+
}
|
|
1324
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
1325
|
+
const oldLines = this.lines
|
|
1326
|
+
const oldInlineMode = this.inlineMode
|
|
1327
|
+
|
|
1328
|
+
try {
|
|
1329
|
+
this.lines = []
|
|
1330
|
+
this.inlineMode = true
|
|
1331
|
+
this.visit(child)
|
|
1332
|
+
const erbContent = this.lines.join("")
|
|
1333
|
+
currentLineContent += erbContent
|
|
1334
|
+
|
|
1335
|
+
if ((indent.length + currentLineContent.length) > Math.max(this.maxLineLength, 120)) {
|
|
1336
|
+
this.lines = oldLines
|
|
1337
|
+
this.inlineMode = oldInlineMode
|
|
1338
|
+
this.visitTextFlowChildrenMultiline(children)
|
|
1339
|
+
|
|
1340
|
+
return
|
|
1341
|
+
}
|
|
1342
|
+
} finally {
|
|
1343
|
+
this.lines = oldLines
|
|
1344
|
+
this.inlineMode = oldInlineMode
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
if (currentLineContent.trim()) {
|
|
1348
|
+
this.push(indent + currentLineContent.trim())
|
|
1349
|
+
currentLineContent = ""
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
this.visit(child)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (currentLineContent.trim()) {
|
|
1357
|
+
const finalLine = indent + currentLineContent.trim()
|
|
1358
|
+
if (finalLine.length > Math.max(this.maxLineLength, 120)) {
|
|
1359
|
+
this.visitTextFlowChildrenMultiline(children)
|
|
1360
|
+
|
|
1361
|
+
return
|
|
1362
|
+
}
|
|
1363
|
+
this.push(finalLine)
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
private visitTextFlowChildrenMultiline(children: Node[]): void {
|
|
1368
|
+
children.forEach(child => this.visit(child))
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
private isInTextFlowContext(parent: Node | null, children: Node[]): boolean {
|
|
1372
|
+
const hasTextContent = children.some(child =>
|
|
1373
|
+
(child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') &&
|
|
1374
|
+
(child as HTMLTextNode).content.trim() !== ""
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
if (!hasTextContent) {
|
|
1378
|
+
return false
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const nonTextChildren = children.filter(child =>
|
|
1382
|
+
!(child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE')
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
if (nonTextChildren.length === 0) {
|
|
1386
|
+
return false
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const allInline = nonTextChildren.every(child => {
|
|
1390
|
+
if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
1391
|
+
return true
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
1395
|
+
const element = child as HTMLElementNode
|
|
1396
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
1397
|
+
const tagName = openTag?.tag_name?.value || ''
|
|
1398
|
+
|
|
1399
|
+
return this.isInlineElement(tagName)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
return false
|
|
1403
|
+
})
|
|
1404
|
+
|
|
1405
|
+
if (!allInline) {
|
|
1406
|
+
return false
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const maxNestingDepth = this.getMaxNestingDepth(children, 0)
|
|
1410
|
+
|
|
1411
|
+
if (maxNestingDepth > 2) {
|
|
1412
|
+
return false
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return true
|
|
1416
|
+
}
|
|
1417
|
+
|
|
749
1418
|
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean, inlineNodes: Node[] = [], allChildren: Node[] = []): string {
|
|
750
1419
|
const parts = attributes.map(attribute => this.renderAttribute(attribute))
|
|
751
1420
|
|
|
@@ -763,10 +1432,10 @@ export class Printer extends Visitor {
|
|
|
763
1432
|
this.lines.push(" " + this.renderAttribute(child as HTMLAttributeNode))
|
|
764
1433
|
} else if (!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')) {
|
|
765
1434
|
const wasInlineMode = this.inlineMode
|
|
1435
|
+
|
|
766
1436
|
this.inlineMode = true
|
|
767
1437
|
|
|
768
1438
|
this.lines.push(" ")
|
|
769
|
-
|
|
770
1439
|
this.visit(child)
|
|
771
1440
|
this.inlineMode = wasInlineMode
|
|
772
1441
|
}
|
|
@@ -789,8 +1458,13 @@ export class Printer extends Visitor {
|
|
|
789
1458
|
|
|
790
1459
|
inlineNodes.forEach(node => {
|
|
791
1460
|
const wasInlineMode = this.inlineMode
|
|
792
|
-
|
|
1461
|
+
|
|
1462
|
+
if (!this.isERBControlFlow(node)) {
|
|
1463
|
+
this.inlineMode = true
|
|
1464
|
+
}
|
|
1465
|
+
|
|
793
1466
|
this.visit(node)
|
|
1467
|
+
|
|
794
1468
|
this.inlineMode = wasInlineMode
|
|
795
1469
|
})
|
|
796
1470
|
|
|
@@ -816,41 +1490,169 @@ export class Printer extends Visitor {
|
|
|
816
1490
|
let value = ""
|
|
817
1491
|
|
|
818
1492
|
if (attribute.value && (attribute.value instanceof HTMLAttributeValueNode || (attribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE')) {
|
|
819
|
-
const
|
|
820
|
-
const open_quote = (attrValue.open_quote?.value ?? "")
|
|
821
|
-
const close_quote = (attrValue.close_quote?.value ?? "")
|
|
822
|
-
const attribute_value = attrValue.children.map((attr: any) => {
|
|
823
|
-
if (attr instanceof HTMLTextNode || (attr as any).type === 'AST_HTML_TEXT_NODE' || attr instanceof LiteralNode || (attr as any).type === 'AST_LITERAL_NODE') {
|
|
1493
|
+
const attributeValue = attribute.value as HTMLAttributeValueNode
|
|
824
1494
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1495
|
+
let open_quote = attributeValue.open_quote?.value ?? ""
|
|
1496
|
+
let close_quote = attributeValue.close_quote?.value ?? ""
|
|
1497
|
+
let htmlTextContent = ""
|
|
828
1498
|
|
|
829
|
-
|
|
1499
|
+
const content = attributeValue.children.map((child: Node) => {
|
|
1500
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE' || child instanceof LiteralNode || (child as any).type === 'AST_LITERAL_NODE') {
|
|
1501
|
+
const textContent = (child as HTMLTextNode | LiteralNode).content
|
|
1502
|
+
htmlTextContent += textContent
|
|
1503
|
+
|
|
1504
|
+
return textContent
|
|
1505
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
1506
|
+
const erbAttribute = child as ERBContentNode
|
|
1507
|
+
|
|
1508
|
+
return erbAttribute.tag_opening!.value + erbAttribute.content!.value + erbAttribute.tag_closing!.value
|
|
830
1509
|
}
|
|
831
1510
|
|
|
832
1511
|
return ""
|
|
833
1512
|
}).join("")
|
|
834
1513
|
|
|
835
|
-
|
|
1514
|
+
if (open_quote === "" && close_quote === "") {
|
|
1515
|
+
open_quote = '"'
|
|
1516
|
+
close_quote = '"'
|
|
1517
|
+
} else if (open_quote === "'" && close_quote === "'" && !htmlTextContent.includes('"')) {
|
|
1518
|
+
open_quote = '"'
|
|
1519
|
+
close_quote = '"'
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (this.isFormattableAttribute(name, this.currentTagName)) {
|
|
1523
|
+
if (name === 'class') {
|
|
1524
|
+
value = this.formatClassAttribute(content, name, equals, open_quote, close_quote)
|
|
1525
|
+
} else {
|
|
1526
|
+
value = this.formatMultilineAttribute(content, name, equals, open_quote, close_quote)
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
value = open_quote + content + close_quote
|
|
1530
|
+
}
|
|
836
1531
|
}
|
|
837
1532
|
|
|
838
1533
|
return name + equals + value
|
|
839
1534
|
}
|
|
840
1535
|
|
|
1536
|
+
/**
|
|
1537
|
+
* Try to render a complete element inline including opening tag, children, and closing tag
|
|
1538
|
+
*/
|
|
1539
|
+
private tryRenderInlineFull(_node: HTMLElementNode, tagName: string, attributes: HTMLAttributeNode[], children: Node[]): string | null {
|
|
1540
|
+
let result = `<${tagName}`
|
|
1541
|
+
|
|
1542
|
+
result += this.renderAttributesString(attributes)
|
|
1543
|
+
|
|
1544
|
+
result += ">"
|
|
1545
|
+
|
|
1546
|
+
const childrenContent = this.tryRenderChildrenInline(children)
|
|
1547
|
+
if (!childrenContent) {
|
|
1548
|
+
return null
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
result += childrenContent
|
|
1552
|
+
result += `</${tagName}>`
|
|
1553
|
+
|
|
1554
|
+
return result
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Try to render just the children inline (without tags)
|
|
1559
|
+
*/
|
|
1560
|
+
private tryRenderChildrenInline(children: Node[]): string | null {
|
|
1561
|
+
let result = ""
|
|
1562
|
+
|
|
1563
|
+
for (const child of children) {
|
|
1564
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
1565
|
+
const content = (child as HTMLTextNode).content
|
|
1566
|
+
const normalizedContent = content.replace(/\s+/g, ' ')
|
|
1567
|
+
const hasLeadingSpace = /^\s/.test(content)
|
|
1568
|
+
const hasTrailingSpace = /\s$/.test(content)
|
|
1569
|
+
const trimmedContent = normalizedContent.trim()
|
|
1570
|
+
|
|
1571
|
+
if (trimmedContent) {
|
|
1572
|
+
let finalContent = trimmedContent
|
|
1573
|
+
|
|
1574
|
+
if (hasLeadingSpace && result && !result.endsWith(' ')) {
|
|
1575
|
+
finalContent = ' ' + finalContent
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
if (hasTrailingSpace) {
|
|
1579
|
+
finalContent = finalContent + ' '
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
result += finalContent
|
|
1583
|
+
} else if (hasLeadingSpace || hasTrailingSpace) {
|
|
1584
|
+
if (result && !result.endsWith(' ')) {
|
|
1585
|
+
result += ' '
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
1590
|
+
const element = child as HTMLElementNode
|
|
1591
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
1592
|
+
const childTagName = openTag?.tag_name?.value || ''
|
|
1593
|
+
|
|
1594
|
+
if (!this.isInlineElement(childTagName)) {
|
|
1595
|
+
return null
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const childInline = this.tryRenderInlineFull(element, childTagName,
|
|
1599
|
+
this.extractAttributes(openTag.children),
|
|
1600
|
+
element.body.filter(c => !(c instanceof WhitespaceNode || (c as any).type === 'AST_WHITESPACE_NODE') &&
|
|
1601
|
+
!((c instanceof HTMLTextNode || (c as any).type === 'AST_HTML_TEXT_NODE') && (c as any)?.content.trim() === ""))
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
if (!childInline) {
|
|
1605
|
+
return null
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
result += childInline
|
|
1609
|
+
} else {
|
|
1610
|
+
const oldLines = this.lines
|
|
1611
|
+
const oldInlineMode = this.inlineMode
|
|
1612
|
+
const oldIndentLevel = this.indentLevel
|
|
1613
|
+
|
|
1614
|
+
try {
|
|
1615
|
+
this.lines = []
|
|
1616
|
+
this.inlineMode = true
|
|
1617
|
+
this.indentLevel = 0
|
|
1618
|
+
this.visit(child)
|
|
1619
|
+
|
|
1620
|
+
result += this.lines.join("")
|
|
1621
|
+
} finally {
|
|
1622
|
+
this.lines = oldLines
|
|
1623
|
+
this.inlineMode = oldInlineMode
|
|
1624
|
+
this.indentLevel = oldIndentLevel
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return result.trim()
|
|
1630
|
+
}
|
|
1631
|
+
|
|
841
1632
|
/**
|
|
842
1633
|
* Try to render children inline if they are simple enough.
|
|
843
1634
|
* Returns the inline string if possible, null otherwise.
|
|
844
1635
|
*/
|
|
845
|
-
private tryRenderInline(children: Node[], tagName: string, depth: number = 0): string | null {
|
|
846
|
-
if (children.length > 10) {
|
|
1636
|
+
private tryRenderInline(children: Node[], tagName: string, depth: number = 0, forceInline: boolean = false, hasTextFlow: boolean = false): string | null {
|
|
1637
|
+
if (!forceInline && children.length > 10) {
|
|
847
1638
|
return null
|
|
848
1639
|
}
|
|
849
1640
|
|
|
850
1641
|
const maxNestingDepth = this.getMaxNestingDepth(children, 0)
|
|
851
1642
|
|
|
852
|
-
|
|
1643
|
+
let maxAllowedDepth = forceInline ? 5 : (tagName && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div'].includes(tagName) ? 1 : 2)
|
|
1644
|
+
|
|
1645
|
+
if (hasTextFlow && maxNestingDepth >= 2) {
|
|
1646
|
+
const roughContentLength = this.estimateContentLength(children)
|
|
1647
|
+
|
|
1648
|
+
if (roughContentLength > 47) {
|
|
1649
|
+
maxAllowedDepth = 1
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (!forceInline && maxNestingDepth > maxAllowedDepth) {
|
|
853
1654
|
this.isInComplexNesting = true
|
|
1655
|
+
|
|
854
1656
|
return null
|
|
855
1657
|
}
|
|
856
1658
|
|
|
@@ -864,12 +1666,10 @@ export class Printer extends Visitor {
|
|
|
864
1666
|
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
865
1667
|
const element = child as HTMLElementNode
|
|
866
1668
|
const openTag = element.open_tag as HTMLOpenTagNode
|
|
1669
|
+
const elementTagName = openTag?.tag_name?.value || ''
|
|
1670
|
+
const isInlineElement = this.isInlineElement(elementTagName)
|
|
867
1671
|
|
|
868
|
-
|
|
869
|
-
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
870
|
-
)
|
|
871
|
-
|
|
872
|
-
if (attributes.length > 0) {
|
|
1672
|
+
if (!isInlineElement) {
|
|
873
1673
|
return null
|
|
874
1674
|
}
|
|
875
1675
|
|
|
@@ -897,13 +1697,9 @@ export class Printer extends Visitor {
|
|
|
897
1697
|
const openTag = element.open_tag as HTMLOpenTagNode
|
|
898
1698
|
const childTagName = openTag?.tag_name?.value || ''
|
|
899
1699
|
|
|
900
|
-
const attributes = openTag.children
|
|
901
|
-
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
902
|
-
)
|
|
1700
|
+
const attributes = this.extractAttributes(openTag.children)
|
|
903
1701
|
|
|
904
|
-
const attributesString = attributes
|
|
905
|
-
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
906
|
-
: ''
|
|
1702
|
+
const attributesString = this.renderAttributesString(attributes)
|
|
907
1703
|
|
|
908
1704
|
const elementContent = this.renderElementInline(element)
|
|
909
1705
|
|
|
@@ -914,7 +1710,7 @@ export class Printer extends Visitor {
|
|
|
914
1710
|
const erbContent = erbNode.content?.value ?? ""
|
|
915
1711
|
const close = erbNode.tag_closing?.value ?? ""
|
|
916
1712
|
|
|
917
|
-
content += `${open}
|
|
1713
|
+
content += `${open}${this.formatERBContent(erbContent)}${close}`
|
|
918
1714
|
}
|
|
919
1715
|
}
|
|
920
1716
|
|
|
@@ -928,6 +1724,29 @@ export class Printer extends Visitor {
|
|
|
928
1724
|
}
|
|
929
1725
|
}
|
|
930
1726
|
|
|
1727
|
+
/**
|
|
1728
|
+
* Estimate the total content length of children nodes for decision making.
|
|
1729
|
+
*/
|
|
1730
|
+
private estimateContentLength(children: Node[]): number {
|
|
1731
|
+
let length = 0
|
|
1732
|
+
|
|
1733
|
+
for (const child of children) {
|
|
1734
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
1735
|
+
length += (child as HTMLTextNode).content.length
|
|
1736
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
1737
|
+
const element = child as HTMLElementNode
|
|
1738
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
1739
|
+
const tagName = openTag?.tag_name?.value || ''
|
|
1740
|
+
|
|
1741
|
+
length += tagName.length + 5 // Rough estimate for tag overhead
|
|
1742
|
+
length += this.estimateContentLength(element.body)
|
|
1743
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
1744
|
+
length += (child as ERBContentNode).content?.value.length || 0
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return length
|
|
1748
|
+
}
|
|
1749
|
+
|
|
931
1750
|
/**
|
|
932
1751
|
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
933
1752
|
*/
|
|
@@ -962,6 +1781,7 @@ export class Printer extends Visitor {
|
|
|
962
1781
|
)
|
|
963
1782
|
|
|
964
1783
|
let content = ''
|
|
1784
|
+
|
|
965
1785
|
for (const child of children) {
|
|
966
1786
|
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
967
1787
|
content += (child as HTMLTextNode).content
|
|
@@ -970,15 +1790,12 @@ export class Printer extends Visitor {
|
|
|
970
1790
|
const openTag = childElement.open_tag as HTMLOpenTagNode
|
|
971
1791
|
const childTagName = openTag?.tag_name?.value || ''
|
|
972
1792
|
|
|
973
|
-
const attributes = openTag.children
|
|
974
|
-
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
975
|
-
)
|
|
1793
|
+
const attributes = this.extractAttributes(openTag.children)
|
|
976
1794
|
|
|
977
|
-
const attributesString = attributes
|
|
978
|
-
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
979
|
-
: ''
|
|
1795
|
+
const attributesString = this.renderAttributesString(attributes)
|
|
980
1796
|
|
|
981
1797
|
const childContent = this.renderElementInline(childElement)
|
|
1798
|
+
|
|
982
1799
|
content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`
|
|
983
1800
|
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
984
1801
|
const erbNode = child as ERBContentNode
|
|
@@ -986,7 +1803,7 @@ export class Printer extends Visitor {
|
|
|
986
1803
|
const erbContent = erbNode.content?.value ?? ""
|
|
987
1804
|
const close = erbNode.tag_closing?.value ?? ""
|
|
988
1805
|
|
|
989
|
-
content += `${open}
|
|
1806
|
+
content += `${open}${this.formatERBContent(erbContent)}${close}`
|
|
990
1807
|
}
|
|
991
1808
|
}
|
|
992
1809
|
|