@herb-tools/formatter 0.8.10 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/herb-format.js +57800 -16767
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +24404 -3522
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +24404 -3522
- package/dist/index.esm.js.map +1 -1
- package/dist/types/attribute-renderer.d.ts +44 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/comment-helpers.d.ts +45 -0
- package/dist/types/format-helpers.d.ts +15 -11
- package/dist/types/format-printer.d.ts +33 -137
- package/dist/types/formatter.d.ts +3 -2
- package/dist/types/spacing-analyzer.d.ts +47 -0
- package/dist/types/text-flow-analyzer.d.ts +22 -0
- package/dist/types/text-flow-engine.d.ts +37 -0
- package/dist/types/text-flow-helpers.d.ts +58 -0
- package/package.json +5 -5
- package/src/attribute-renderer.ts +309 -0
- package/src/cli.ts +32 -11
- package/src/comment-helpers.ts +129 -0
- package/src/format-helpers.ts +73 -29
- package/src/format-printer.ts +448 -1459
- package/src/formatter.ts +10 -4
- package/src/spacing-analyzer.ts +244 -0
- package/src/text-flow-analyzer.ts +212 -0
- package/src/text-flow-engine.ts +311 -0
- package/src/text-flow-helpers.ts +319 -0
package/src/format-printer.ts
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
|
-
import dedent from "dedent"
|
|
2
|
-
|
|
3
1
|
import { Printer, IdentityPrinter } from "@herb-tools/printer"
|
|
2
|
+
import { TextFlowEngine } from "./text-flow-engine.js"
|
|
3
|
+
import { AttributeRenderer } from "./attribute-renderer.js"
|
|
4
|
+
import { SpacingAnalyzer } from "./spacing-analyzer.js"
|
|
5
|
+
import { isTextFlowNode } from "./text-flow-helpers.js"
|
|
6
|
+
import { extractHTMLCommentContent, formatHTMLCommentInner, formatERBCommentLines } from "./comment-helpers.js"
|
|
7
|
+
|
|
8
|
+
import type { ERBNode } from "@herb-tools/core"
|
|
9
|
+
import type { FormatOptions } from "./options.js"
|
|
10
|
+
import type { TextFlowDelegate } from "./text-flow-engine.js"
|
|
11
|
+
import type { AttributeRendererDelegate } from "./attribute-renderer.js"
|
|
12
|
+
import type { ElementFormattingAnalysis } from "./format-helpers.js"
|
|
13
|
+
|
|
14
|
+
interface ChildVisitResult {
|
|
15
|
+
newIndex: number
|
|
16
|
+
lastMeaningfulNode: Node | null
|
|
17
|
+
hasHandledSpacing: boolean
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
import {
|
|
6
21
|
getTagName,
|
|
7
22
|
getCombinedAttributeName,
|
|
8
|
-
getCombinedStringFromNodes,
|
|
9
23
|
isNode,
|
|
10
24
|
isToken,
|
|
11
25
|
isParseResult,
|
|
12
26
|
isNoneOf,
|
|
13
27
|
isERBNode,
|
|
14
|
-
isCommentNode,
|
|
15
28
|
isERBControlFlowNode,
|
|
16
29
|
isERBCommentNode,
|
|
17
|
-
|
|
30
|
+
isHTMLOpenTagNode,
|
|
31
|
+
isPureWhitespaceNode,
|
|
18
32
|
filterNodes,
|
|
19
33
|
} from "@herb-tools/core"
|
|
20
34
|
|
|
21
35
|
import {
|
|
22
36
|
areAllNestedElementsInline,
|
|
23
|
-
buildLineWithWord,
|
|
24
|
-
countAdjacentInlineElements,
|
|
25
|
-
endsWithWhitespace,
|
|
26
37
|
filterEmptyNodesForHerbDisable,
|
|
27
38
|
filterSignificantChildren,
|
|
28
|
-
findPreviousMeaningfulSibling,
|
|
29
39
|
hasComplexERBControlFlow,
|
|
30
40
|
hasMixedTextAndInlineContent,
|
|
31
41
|
hasMultilineTextContent,
|
|
32
|
-
hasWhitespaceBetween,
|
|
33
|
-
isBlockLevelNode,
|
|
34
|
-
isClosingPunctuation,
|
|
35
42
|
isContentPreserving,
|
|
36
43
|
isFrontmatter,
|
|
44
|
+
hasLeadingHerbDisable,
|
|
37
45
|
isHerbDisableComment,
|
|
38
46
|
isInlineElement,
|
|
39
|
-
isLineBreakingElement,
|
|
40
47
|
isNonWhitespaceNode,
|
|
41
|
-
isPureWhitespaceNode,
|
|
42
|
-
needsSpaceBetween,
|
|
43
|
-
normalizeAndSplitWords,
|
|
44
48
|
shouldAppendToLastLine,
|
|
45
49
|
shouldPreserveUserSpacing,
|
|
46
50
|
} from "./format-helpers.js"
|
|
47
51
|
|
|
48
52
|
import {
|
|
49
|
-
|
|
50
|
-
INLINE_ELEMENTS,
|
|
53
|
+
ASCII_WHITESPACE,
|
|
51
54
|
SPACEABLE_CONTAINERS,
|
|
52
|
-
TOKEN_LIST_ATTRIBUTES,
|
|
53
|
-
} from "./format-helpers.js"
|
|
54
|
-
|
|
55
|
-
import type {
|
|
56
|
-
ContentUnitWithNode,
|
|
57
|
-
ElementFormattingAnalysis,
|
|
58
55
|
} from "./format-helpers.js"
|
|
59
56
|
|
|
60
57
|
import {
|
|
@@ -62,15 +59,16 @@ import {
|
|
|
62
59
|
Node,
|
|
63
60
|
DocumentNode,
|
|
64
61
|
HTMLOpenTagNode,
|
|
62
|
+
HTMLConditionalOpenTagNode,
|
|
65
63
|
HTMLCloseTagNode,
|
|
66
64
|
HTMLElementNode,
|
|
65
|
+
HTMLConditionalElementNode,
|
|
67
66
|
HTMLAttributeNode,
|
|
68
67
|
HTMLAttributeValueNode,
|
|
69
68
|
HTMLAttributeNameNode,
|
|
70
69
|
HTMLTextNode,
|
|
71
70
|
HTMLCommentNode,
|
|
72
71
|
HTMLDoctypeNode,
|
|
73
|
-
LiteralNode,
|
|
74
72
|
WhitespaceNode,
|
|
75
73
|
ERBContentNode,
|
|
76
74
|
ERBBlockNode,
|
|
@@ -89,25 +87,36 @@ import {
|
|
|
89
87
|
ERBUnlessNode,
|
|
90
88
|
ERBYieldNode,
|
|
91
89
|
ERBInNode,
|
|
90
|
+
ERBRenderNode,
|
|
91
|
+
RubyRenderLocalNode,
|
|
92
|
+
ERBOpenTagNode,
|
|
93
|
+
HTMLVirtualCloseTagNode,
|
|
92
94
|
XMLDeclarationNode,
|
|
93
95
|
CDATANode,
|
|
94
96
|
Token
|
|
95
97
|
} from "@herb-tools/core"
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Gets the children of an open tag, narrowing from the union type.
|
|
101
|
+
* Returns empty array for conditional open tags.
|
|
102
|
+
*/
|
|
103
|
+
function getOpenTagChildren(element: HTMLElementNode): Node[] {
|
|
104
|
+
return isHTMLOpenTagNode(element.open_tag) ? element.open_tag.children : []
|
|
105
|
+
}
|
|
99
106
|
|
|
100
107
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
108
|
+
* Gets the tag_closing token of an open tag, narrowing from the union type.
|
|
109
|
+
* Returns null for conditional open tags.
|
|
103
110
|
*/
|
|
104
|
-
|
|
111
|
+
function getOpenTagClosing(element: HTMLElementNode): Token | null {
|
|
112
|
+
return isHTMLOpenTagNode(element.open_tag) ? element.open_tag.tag_closing : null
|
|
113
|
+
}
|
|
105
114
|
|
|
106
115
|
/**
|
|
107
116
|
* Printer traverses the Herb AST using the Visitor pattern
|
|
108
117
|
* and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
|
|
109
118
|
*/
|
|
110
|
-
export class FormatPrinter extends Printer {
|
|
119
|
+
export class FormatPrinter extends Printer implements TextFlowDelegate, AttributeRendererDelegate {
|
|
111
120
|
/**
|
|
112
121
|
* @deprecated integrate indentWidth into this.options and update FormatOptions to extend from @herb-tools/printer options
|
|
113
122
|
*/
|
|
@@ -116,7 +125,7 @@ export class FormatPrinter extends Printer {
|
|
|
116
125
|
/**
|
|
117
126
|
* @deprecated integrate maxLineLength into this.options and update FormatOptions to extend from @herb-tools/printer options
|
|
118
127
|
*/
|
|
119
|
-
|
|
128
|
+
maxLineLength: number
|
|
120
129
|
|
|
121
130
|
/**
|
|
122
131
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
@@ -124,14 +133,15 @@ export class FormatPrinter extends Printer {
|
|
|
124
133
|
private lines: string[] = []
|
|
125
134
|
private indentLevel: number = 0
|
|
126
135
|
private inlineMode: boolean = false
|
|
127
|
-
private
|
|
136
|
+
private inContentPreservingContext: boolean = false
|
|
137
|
+
private inConditionalOpenTagContext: boolean = false
|
|
128
138
|
private elementStack: HTMLElementNode[] = []
|
|
129
139
|
private elementFormattingAnalysis = new Map<HTMLElementNode, ElementFormattingAnalysis>()
|
|
130
140
|
private nodeIsMultiline = new Map<Node, boolean>()
|
|
131
141
|
private stringLineCount: number = 0
|
|
132
|
-
private
|
|
133
|
-
private
|
|
134
|
-
|
|
142
|
+
private textFlow: TextFlowEngine
|
|
143
|
+
private attributeRenderer: AttributeRenderer
|
|
144
|
+
private spacingAnalyzer: SpacingAnalyzer
|
|
135
145
|
|
|
136
146
|
public source: string
|
|
137
147
|
|
|
@@ -141,6 +151,9 @@ export class FormatPrinter extends Printer {
|
|
|
141
151
|
this.source = source
|
|
142
152
|
this.indentWidth = options.indentWidth
|
|
143
153
|
this.maxLineLength = options.maxLineLength
|
|
154
|
+
this.textFlow = new TextFlowEngine(this)
|
|
155
|
+
this.attributeRenderer = new AttributeRenderer(this, this.maxLineLength, this.indentWidth)
|
|
156
|
+
this.spacingAnalyzer = new SpacingAnalyzer(this.nodeIsMultiline)
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
print(input: Node | ParseResult | Token): string {
|
|
@@ -153,8 +166,7 @@ export class FormatPrinter extends Printer {
|
|
|
153
166
|
this.indentLevel = 0
|
|
154
167
|
this.stringLineCount = 0
|
|
155
168
|
this.nodeIsMultiline.clear()
|
|
156
|
-
this.
|
|
157
|
-
this.allSingleLineCache.clear()
|
|
169
|
+
this.spacingAnalyzer.clear()
|
|
158
170
|
|
|
159
171
|
this.visit(node)
|
|
160
172
|
|
|
@@ -172,7 +184,7 @@ export class FormatPrinter extends Printer {
|
|
|
172
184
|
* Get the current tag name from the current element context
|
|
173
185
|
*/
|
|
174
186
|
private get currentTagName(): string {
|
|
175
|
-
return this.currentElement?.
|
|
187
|
+
return this.currentElement?.tag_name?.value ?? ""
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
/**
|
|
@@ -257,7 +269,7 @@ export class FormatPrinter extends Printer {
|
|
|
257
269
|
/**
|
|
258
270
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
259
271
|
*/
|
|
260
|
-
|
|
272
|
+
push(line: string) {
|
|
261
273
|
this.lines.push(line)
|
|
262
274
|
this.stringLineCount++
|
|
263
275
|
}
|
|
@@ -265,7 +277,7 @@ export class FormatPrinter extends Printer {
|
|
|
265
277
|
/**
|
|
266
278
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
267
279
|
*/
|
|
268
|
-
|
|
280
|
+
pushWithIndent(line: string) {
|
|
269
281
|
const indent = line.trim() === "" ? "" : this.indent
|
|
270
282
|
|
|
271
283
|
this.push(indent + line)
|
|
@@ -279,7 +291,25 @@ export class FormatPrinter extends Printer {
|
|
|
279
291
|
return result
|
|
280
292
|
}
|
|
281
293
|
|
|
282
|
-
private
|
|
294
|
+
private withInlineMode<T>(callback: () => T): T {
|
|
295
|
+
const was = this.inlineMode
|
|
296
|
+
this.inlineMode = true
|
|
297
|
+
const result = callback()
|
|
298
|
+
this.inlineMode = was
|
|
299
|
+
|
|
300
|
+
return result
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private withContentPreserving<T>(callback: () => T): T {
|
|
304
|
+
const was = this.inContentPreservingContext
|
|
305
|
+
this.inContentPreservingContext = true
|
|
306
|
+
const result = callback()
|
|
307
|
+
this.inContentPreservingContext = was
|
|
308
|
+
|
|
309
|
+
return result
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
get indent(): string {
|
|
283
313
|
return " ".repeat(this.indentLevel * this.indentWidth)
|
|
284
314
|
}
|
|
285
315
|
|
|
@@ -289,11 +319,11 @@ export class FormatPrinter extends Printer {
|
|
|
289
319
|
* and a trailing space (or newline for heredoc content starting with "<<").
|
|
290
320
|
*/
|
|
291
321
|
private formatERBContent(content: string): string {
|
|
292
|
-
|
|
322
|
+
const trimmedContent = content.trim();
|
|
293
323
|
|
|
294
324
|
// See: https://github.com/marcoroth/herb/issues/476
|
|
295
325
|
// TODO: revisit once we have access to Prism nodes
|
|
296
|
-
|
|
326
|
+
const suffix = trimmedContent.startsWith("<<") ? "\n" : " "
|
|
297
327
|
|
|
298
328
|
return trimmedContent ? ` ${trimmedContent}${suffix}` : ""
|
|
299
329
|
}
|
|
@@ -323,401 +353,6 @@ export class FormatPrinter extends Printer {
|
|
|
323
353
|
return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode))
|
|
324
354
|
}
|
|
325
355
|
|
|
326
|
-
/**
|
|
327
|
-
* Check if a node will render as multiple lines when formatted.
|
|
328
|
-
*/
|
|
329
|
-
private isMultilineElement(node: Node): boolean {
|
|
330
|
-
if (isNode(node, ERBContentNode)) {
|
|
331
|
-
return (node.content?.value || "").includes("\n")
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (isNode(node, HTMLElementNode) && isContentPreserving(node)) {
|
|
335
|
-
return true
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const tracked = this.nodeIsMultiline.get(node)
|
|
339
|
-
|
|
340
|
-
if (tracked !== undefined) {
|
|
341
|
-
return tracked
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return false
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Get a grouping key for a node (tag name for HTML, ERB type for ERB)
|
|
349
|
-
*/
|
|
350
|
-
private getGroupingKey(node: Node): string | null {
|
|
351
|
-
if (isNode(node, HTMLElementNode)) {
|
|
352
|
-
return getTagName(node)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (isERBOutputNode(node)) return "erb-output"
|
|
356
|
-
if (isERBCommentNode(node)) return "erb-comment"
|
|
357
|
-
if (isERBNode(node)) return "erb-code"
|
|
358
|
-
|
|
359
|
-
return null
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Detect groups of consecutive same-tag/same-type single-line elements
|
|
364
|
-
* Returns a map of index -> group info for efficient lookup
|
|
365
|
-
*/
|
|
366
|
-
private detectTagGroups(siblings: Node[]): Map<number, { tagName: string; groupStart: number; groupEnd: number }> {
|
|
367
|
-
const cached = this.tagGroupsCache.get(siblings)
|
|
368
|
-
if (cached) return cached
|
|
369
|
-
|
|
370
|
-
const groupMap = new Map<number, { tagName: string; groupStart: number; groupEnd: number }>()
|
|
371
|
-
const meaningfulNodes: Array<{ index: number; groupKey: string }> = []
|
|
372
|
-
|
|
373
|
-
for (let i = 0; i < siblings.length; i++) {
|
|
374
|
-
const node = siblings[i]
|
|
375
|
-
|
|
376
|
-
if (!this.isMultilineElement(node)) {
|
|
377
|
-
const groupKey = this.getGroupingKey(node)
|
|
378
|
-
|
|
379
|
-
if (groupKey) {
|
|
380
|
-
meaningfulNodes.push({ index: i, groupKey })
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
let groupStart = 0
|
|
386
|
-
|
|
387
|
-
while (groupStart < meaningfulNodes.length) {
|
|
388
|
-
const startGroupKey = meaningfulNodes[groupStart].groupKey
|
|
389
|
-
let groupEnd = groupStart
|
|
390
|
-
|
|
391
|
-
while (groupEnd + 1 < meaningfulNodes.length && meaningfulNodes[groupEnd + 1].groupKey === startGroupKey) {
|
|
392
|
-
groupEnd++
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (groupEnd > groupStart) {
|
|
396
|
-
const groupStartIndex = meaningfulNodes[groupStart].index
|
|
397
|
-
const groupEndIndex = meaningfulNodes[groupEnd].index
|
|
398
|
-
|
|
399
|
-
for (let i = groupStart; i <= groupEnd; i++) {
|
|
400
|
-
groupMap.set(meaningfulNodes[i].index, {
|
|
401
|
-
tagName: startGroupKey,
|
|
402
|
-
groupStart: groupStartIndex,
|
|
403
|
-
groupEnd: groupEndIndex
|
|
404
|
-
})
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
groupStart = groupEnd + 1
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
this.tagGroupsCache.set(siblings, groupMap)
|
|
412
|
-
|
|
413
|
-
return groupMap
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Determine if spacing should be added between sibling elements
|
|
418
|
-
*
|
|
419
|
-
* This implements the "rule of three" intelligent spacing system:
|
|
420
|
-
* - Adds spacing between 3 or more meaningful siblings
|
|
421
|
-
* - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
|
|
422
|
-
* - Groups comments with following elements
|
|
423
|
-
* - Preserves user-added spacing
|
|
424
|
-
*
|
|
425
|
-
* @param parentElement - The parent element containing the siblings
|
|
426
|
-
* @param siblings - Array of all sibling nodes
|
|
427
|
-
* @param currentIndex - Index of the current node being evaluated
|
|
428
|
-
* @param hasExistingSpacing - Whether user-added spacing already exists
|
|
429
|
-
* @returns true if spacing should be added before the current element
|
|
430
|
-
*/
|
|
431
|
-
private shouldAddSpacingBetweenSiblings(parentElement: HTMLElementNode | null, siblings: Node[], currentIndex: number): boolean {
|
|
432
|
-
const currentNode = siblings[currentIndex]
|
|
433
|
-
const previousMeaningfulIndex = findPreviousMeaningfulSibling(siblings, currentIndex)
|
|
434
|
-
const previousNode = previousMeaningfulIndex !== -1 ? siblings[previousMeaningfulIndex] : null
|
|
435
|
-
|
|
436
|
-
if (previousNode && (isNode(previousNode, XMLDeclarationNode) || isNode(previousNode, HTMLDoctypeNode))) {
|
|
437
|
-
return true
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "")
|
|
441
|
-
|
|
442
|
-
if (hasMixedContent) return false
|
|
443
|
-
|
|
444
|
-
const isCurrentComment = isCommentNode(currentNode)
|
|
445
|
-
const isPreviousComment = previousNode ? isCommentNode(previousNode) : false
|
|
446
|
-
const isCurrentMultiline = this.isMultilineElement(currentNode)
|
|
447
|
-
const isPreviousMultiline = previousNode ? this.isMultilineElement(previousNode) : false
|
|
448
|
-
|
|
449
|
-
if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
|
|
450
|
-
return isPreviousMultiline && isCurrentMultiline
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (isPreviousComment && isCurrentComment) {
|
|
454
|
-
return false
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (isCurrentMultiline || isPreviousMultiline) {
|
|
458
|
-
return true
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const meaningfulSiblings = siblings.filter(child => isNonWhitespaceNode(child))
|
|
462
|
-
const parentTagName = parentElement ? getTagName(parentElement) : null
|
|
463
|
-
const isSpaceableContainer = !parentTagName || SPACEABLE_CONTAINERS.has(parentTagName)
|
|
464
|
-
const tagGroups = this.detectTagGroups(siblings)
|
|
465
|
-
|
|
466
|
-
const cached = this.allSingleLineCache.get(siblings)
|
|
467
|
-
let allSingleLineHTMLElements: boolean
|
|
468
|
-
if (cached !== undefined) {
|
|
469
|
-
allSingleLineHTMLElements = cached
|
|
470
|
-
} else {
|
|
471
|
-
allSingleLineHTMLElements = meaningfulSiblings.every(node => isNode(node, HTMLElementNode) && !this.isMultilineElement(node))
|
|
472
|
-
this.allSingleLineCache.set(siblings, allSingleLineHTMLElements)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
|
|
476
|
-
return false
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const currentGroup = tagGroups.get(currentIndex)
|
|
480
|
-
const previousGroup = previousNode ? tagGroups.get(previousMeaningfulIndex) : undefined
|
|
481
|
-
|
|
482
|
-
if (currentGroup && previousGroup && currentGroup.groupStart === previousGroup.groupStart && currentGroup.groupEnd === previousGroup.groupEnd) {
|
|
483
|
-
return false
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (previousGroup && previousGroup.groupEnd === previousMeaningfulIndex) {
|
|
487
|
-
return true
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (allSingleLineHTMLElements && tagGroups.size === 0) {
|
|
491
|
-
return false
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (isNode(currentNode, HTMLElementNode)) {
|
|
495
|
-
const currentTagName = getTagName(currentNode)
|
|
496
|
-
|
|
497
|
-
if (currentTagName && INLINE_ELEMENTS.has(currentTagName)) {
|
|
498
|
-
return false
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const isBlockElement = isBlockLevelNode(currentNode)
|
|
503
|
-
const isERBBlock = isERBNode(currentNode) && isERBControlFlowNode(currentNode)
|
|
504
|
-
const isComment = isCommentNode(currentNode)
|
|
505
|
-
|
|
506
|
-
return isBlockElement || isERBBlock || isComment
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Check if we're currently processing a token list attribute that needs spacing
|
|
511
|
-
*/
|
|
512
|
-
private get isInTokenListAttribute(): boolean {
|
|
513
|
-
return this.currentAttributeName !== null && TOKEN_LIST_ATTRIBUTES.has(this.currentAttributeName)
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Render attributes as a space-separated string
|
|
518
|
-
*/
|
|
519
|
-
private renderAttributesString(attributes: HTMLAttributeNode[]): string {
|
|
520
|
-
if (attributes.length === 0) return ""
|
|
521
|
-
|
|
522
|
-
return ` ${attributes.map(attribute => this.renderAttribute(attribute)).join(" ")}`
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Determine if a tag should be rendered inline based on attribute count and other factors
|
|
527
|
-
*/
|
|
528
|
-
private shouldRenderInline(
|
|
529
|
-
totalAttributeCount: number,
|
|
530
|
-
inlineLength: number,
|
|
531
|
-
indentLength: number,
|
|
532
|
-
maxLineLength: number = this.maxLineLength,
|
|
533
|
-
hasComplexERB: boolean = false,
|
|
534
|
-
hasMultilineAttributes: boolean = false,
|
|
535
|
-
attributes: HTMLAttributeNode[] = []
|
|
536
|
-
): boolean {
|
|
537
|
-
if (hasComplexERB || hasMultilineAttributes) return false
|
|
538
|
-
|
|
539
|
-
if (totalAttributeCount === 0) {
|
|
540
|
-
return inlineLength + indentLength <= maxLineLength
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (totalAttributeCount === 1 && attributes.length === 1) {
|
|
544
|
-
const attribute = attributes[0]
|
|
545
|
-
const attributeName = this.getAttributeName(attribute)
|
|
546
|
-
|
|
547
|
-
if (attributeName === 'class') {
|
|
548
|
-
const attributeValue = this.getAttributeValue(attribute)
|
|
549
|
-
const wouldBeMultiline = this.wouldClassAttributeBeMultiline(attributeValue, indentLength)
|
|
550
|
-
|
|
551
|
-
if (!wouldBeMultiline) {
|
|
552
|
-
return true
|
|
553
|
-
} else {
|
|
554
|
-
return false
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (totalAttributeCount > 3 || inlineLength + indentLength > maxLineLength) {
|
|
560
|
-
return false
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return true
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
private wouldClassAttributeBeMultiline(content: string, indentLength: number): boolean {
|
|
567
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
568
|
-
const hasActualNewlines = /\r?\n/.test(content)
|
|
569
|
-
|
|
570
|
-
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
571
|
-
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line)
|
|
572
|
-
|
|
573
|
-
if (lines.length > 1) {
|
|
574
|
-
return true
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const attributeLine = `class="${normalizedContent}"`
|
|
579
|
-
const currentIndent = indentLength
|
|
580
|
-
|
|
581
|
-
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
582
|
-
if (/<%[^%]*%>/.test(normalizedContent)) {
|
|
583
|
-
return false
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const classes = normalizedContent.split(' ')
|
|
587
|
-
const lines = this.breakTokensIntoLines(classes, currentIndent)
|
|
588
|
-
return lines.length > 1
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return false
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// TOOD: extract to core or reuse function from core
|
|
595
|
-
private getAttributeName(attribute: HTMLAttributeNode): string {
|
|
596
|
-
return attribute.name ? getCombinedAttributeName(attribute.name) : ""
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// TOOD: extract to core or reuse function from core
|
|
600
|
-
private getAttributeValue(attribute: HTMLAttributeNode): string {
|
|
601
|
-
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
602
|
-
return attribute.value.children.map(child => isNode(child, HTMLTextNode) ? child.content : IdentityPrinter.print(child)).join('')
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return ''
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private hasMultilineAttributes(attributes: HTMLAttributeNode[]): boolean {
|
|
609
|
-
return attributes.some(attribute => {
|
|
610
|
-
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
611
|
-
const content = getCombinedStringFromNodes(attribute.value.children)
|
|
612
|
-
|
|
613
|
-
if (/\r?\n/.test(content)) {
|
|
614
|
-
const name = attribute.name ? getCombinedAttributeName(attribute.name) : ""
|
|
615
|
-
|
|
616
|
-
if (name === "class") {
|
|
617
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
618
|
-
|
|
619
|
-
return normalizedContent.length > 80
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const lines = content.split(/\r?\n/)
|
|
623
|
-
|
|
624
|
-
if (lines.length > 1) {
|
|
625
|
-
return lines.slice(1).some(line => /^[ \t\n\r]+/.test(line))
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
return false
|
|
631
|
-
})
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
private formatClassAttribute(content: string, name: string, equals: string, open_quote: string, close_quote: string): string {
|
|
635
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
636
|
-
const hasActualNewlines = /\r?\n/.test(content)
|
|
637
|
-
|
|
638
|
-
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
639
|
-
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line)
|
|
640
|
-
|
|
641
|
-
if (lines.length > 1) {
|
|
642
|
-
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const currentIndent = this.indentLevel * this.indentWidth
|
|
647
|
-
const attributeLine = `${name}${equals}${open_quote}${normalizedContent}${close_quote}`
|
|
648
|
-
|
|
649
|
-
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
650
|
-
if (/<%[^%]*%>/.test(normalizedContent)) {
|
|
651
|
-
return open_quote + normalizedContent + close_quote
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const classes = normalizedContent.split(' ')
|
|
655
|
-
const lines = this.breakTokensIntoLines(classes, currentIndent)
|
|
656
|
-
|
|
657
|
-
if (lines.length > 1) {
|
|
658
|
-
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return open_quote + normalizedContent + close_quote
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
private isFormattableAttribute(attributeName: string, tagName: string): boolean {
|
|
666
|
-
const globalFormattable = FORMATTABLE_ATTRIBUTES['*'] || []
|
|
667
|
-
const tagSpecificFormattable = FORMATTABLE_ATTRIBUTES[tagName.toLowerCase()] || []
|
|
668
|
-
|
|
669
|
-
return globalFormattable.includes(attributeName) || tagSpecificFormattable.includes(attributeName)
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
private formatMultilineAttribute(content: string, name: string, open_quote: string, close_quote: string): string {
|
|
673
|
-
if (name === 'srcset' || name === 'sizes') {
|
|
674
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
675
|
-
|
|
676
|
-
return open_quote + normalizedContent + close_quote
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const lines = content.split('\n')
|
|
680
|
-
|
|
681
|
-
if (lines.length <= 1) {
|
|
682
|
-
return open_quote + content + close_quote
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const formattedContent = this.formatMultilineAttributeValue(lines)
|
|
686
|
-
|
|
687
|
-
return open_quote + formattedContent + close_quote
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
private formatMultilineAttributeValue(lines: string[]): string {
|
|
691
|
-
const indent = " ".repeat((this.indentLevel + 1) * this.indentWidth)
|
|
692
|
-
const closeIndent = " ".repeat(this.indentLevel * this.indentWidth)
|
|
693
|
-
|
|
694
|
-
return "\n" + lines.map(line => indent + line).join("\n") + "\n" + closeIndent
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
private breakTokensIntoLines(tokens: string[], currentIndent: number, separator: string = ' '): string[] {
|
|
698
|
-
const lines: string[] = []
|
|
699
|
-
let currentLine = ''
|
|
700
|
-
|
|
701
|
-
for (const token of tokens) {
|
|
702
|
-
const testLine = currentLine ? currentLine + separator + token : token
|
|
703
|
-
|
|
704
|
-
if (testLine.length > (this.maxLineLength - currentIndent - 6)) {
|
|
705
|
-
if (currentLine) {
|
|
706
|
-
lines.push(currentLine)
|
|
707
|
-
currentLine = token
|
|
708
|
-
} else {
|
|
709
|
-
lines.push(token)
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
currentLine = testLine
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
if (currentLine) lines.push(currentLine)
|
|
717
|
-
|
|
718
|
-
return lines
|
|
719
|
-
}
|
|
720
|
-
|
|
721
356
|
/**
|
|
722
357
|
* Render multiline attributes for a tag
|
|
723
358
|
*/
|
|
@@ -731,11 +366,10 @@ export class FormatPrinter extends Printer {
|
|
|
731
366
|
if (herbDisableComments.length > 0) {
|
|
732
367
|
const commentLines = this.capture(() => {
|
|
733
368
|
herbDisableComments.forEach(comment => {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.inlineMode = wasInlineMode
|
|
369
|
+
this.withInlineMode(() => {
|
|
370
|
+
this.lines.push(" ")
|
|
371
|
+
this.visit(comment)
|
|
372
|
+
})
|
|
739
373
|
})
|
|
740
374
|
})
|
|
741
375
|
|
|
@@ -745,9 +379,10 @@ export class FormatPrinter extends Printer {
|
|
|
745
379
|
this.pushWithIndent(openingLine)
|
|
746
380
|
|
|
747
381
|
this.withIndent(() => {
|
|
382
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
748
383
|
allChildren.forEach(child => {
|
|
749
384
|
if (isNode(child, HTMLAttributeNode)) {
|
|
750
|
-
this.pushWithIndent(this.renderAttribute(child))
|
|
385
|
+
this.pushWithIndent(this.attributeRenderer.renderAttribute(child, tagName))
|
|
751
386
|
} else if (!isNode(child, WhitespaceNode)) {
|
|
752
387
|
if (isNode(child, ERBContentNode) && isHerbDisableComment(child)) {
|
|
753
388
|
return
|
|
@@ -769,7 +404,7 @@ export class FormatPrinter extends Printer {
|
|
|
769
404
|
* Reconstruct the text representation of an ERB node
|
|
770
405
|
* @param withFormatting - if true, format the content; if false, preserve original
|
|
771
406
|
*/
|
|
772
|
-
|
|
407
|
+
reconstructERBNode(node: ERBNode, withFormatting: boolean = true): string {
|
|
773
408
|
const open = node.tag_opening?.value ?? ""
|
|
774
409
|
const close = node.tag_closing?.value ?? ""
|
|
775
410
|
const content = node.content?.value ?? ""
|
|
@@ -792,15 +427,12 @@ export class FormatPrinter extends Printer {
|
|
|
792
427
|
|
|
793
428
|
visitDocumentNode(node: DocumentNode) {
|
|
794
429
|
const children = this.formatFrontmatter(node)
|
|
795
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
430
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(children)
|
|
796
431
|
|
|
797
432
|
if (hasTextFlow) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
this.visitTextFlowChildren(children)
|
|
802
|
-
|
|
803
|
-
this.inlineMode = wasInlineMode
|
|
433
|
+
this.withInlineMode(() => {
|
|
434
|
+
this.textFlow.visitTextFlowChildren(children)
|
|
435
|
+
})
|
|
804
436
|
|
|
805
437
|
return
|
|
806
438
|
}
|
|
@@ -834,7 +466,7 @@ export class FormatPrinter extends Printer {
|
|
|
834
466
|
this.visit(child)
|
|
835
467
|
|
|
836
468
|
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
837
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
469
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
838
470
|
|
|
839
471
|
if (shouldAddSpacing) {
|
|
840
472
|
this.lines.splice(childStartLine, 0, "")
|
|
@@ -872,43 +504,104 @@ export class FormatPrinter extends Printer {
|
|
|
872
504
|
this.elementStack.pop()
|
|
873
505
|
}
|
|
874
506
|
|
|
875
|
-
|
|
876
|
-
|
|
507
|
+
visitHTMLConditionalElementNode(node: HTMLConditionalElementNode) {
|
|
508
|
+
this.trackBoundary(node, () => {
|
|
509
|
+
if (node.open_conditional) {
|
|
510
|
+
this.visit(node.open_conditional)
|
|
511
|
+
}
|
|
877
512
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (isNode(child, HTMLElementNode)) {
|
|
881
|
-
const wasInlineMode = this.inlineMode
|
|
882
|
-
this.inlineMode = true
|
|
513
|
+
if (node.body.length > 0) {
|
|
514
|
+
this.push("")
|
|
883
515
|
|
|
884
|
-
|
|
885
|
-
|
|
516
|
+
this.withIndent(() => {
|
|
517
|
+
for (const child of node.body) {
|
|
518
|
+
if (!isPureWhitespaceNode(child)) {
|
|
519
|
+
this.visit(child)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
})
|
|
886
523
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
524
|
+
this.push("")
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (node.close_conditional) {
|
|
528
|
+
this.visit(node.close_conditional)
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode) {
|
|
534
|
+
const wasInConditionalOpenTagContext = this.inConditionalOpenTagContext
|
|
535
|
+
this.inConditionalOpenTagContext = true
|
|
892
536
|
|
|
537
|
+
this.trackBoundary(node, () => {
|
|
538
|
+
if (node.conditional) {
|
|
539
|
+
this.visit(node.conditional)
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
this.inConditionalOpenTagContext = wasInConditionalOpenTagContext
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
visitHTMLElementBody(body: Node[], element: HTMLElementNode) {
|
|
547
|
+
if (isContentPreserving(element) || this.inContentPreservingContext) {
|
|
548
|
+
this.visitContentPreservingBody(element)
|
|
893
549
|
return
|
|
894
550
|
}
|
|
895
551
|
|
|
552
|
+
const tagName = getTagName(element)
|
|
896
553
|
const analysis = this.elementFormattingAnalysis.get(element)
|
|
897
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
554
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(body)
|
|
898
555
|
const children = filterSignificantChildren(body)
|
|
899
556
|
|
|
900
557
|
if (analysis?.elementContentInline) {
|
|
901
|
-
|
|
558
|
+
this.visitInlineElementBody(body, tagName, hasTextFlow, children)
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (children.length === 0) return
|
|
563
|
+
|
|
564
|
+
const { comment, hasLeadingWhitespace, remainingChildren, remainingBody } = this.stripLeadingHerbDisable(children, body)
|
|
565
|
+
|
|
566
|
+
if (comment) {
|
|
567
|
+
const herbDisableString = this.captureHerbDisableInline(comment)
|
|
568
|
+
this.pushToLastLine((hasLeadingWhitespace ? ' ' : '') + herbDisableString)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (remainingChildren.length === 0) return
|
|
572
|
+
|
|
573
|
+
this.withIndent(() => {
|
|
574
|
+
if (hasTextFlow) {
|
|
575
|
+
this.textFlow.visitTextFlowChildren(remainingBody)
|
|
576
|
+
} else {
|
|
577
|
+
this.visitElementChildren(comment ? remainingChildren : body, element)
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private visitContentPreservingBody(element: HTMLElementNode) {
|
|
583
|
+
this.withContentPreserving(() => {
|
|
584
|
+
element.body.map(child => {
|
|
585
|
+
if (isNode(child, HTMLElementNode)) {
|
|
586
|
+
const formattedElement = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
587
|
+
this.pushToLastLine(formattedElement)
|
|
588
|
+
} else {
|
|
589
|
+
this.pushToLastLine(IdentityPrinter.print(child))
|
|
590
|
+
}
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
}
|
|
902
594
|
|
|
903
|
-
|
|
904
|
-
|
|
595
|
+
private visitInlineElementBody(body: Node[], tagName: string, hasTextFlow: boolean, children: Node[]) {
|
|
596
|
+
if (children.length === 0) return
|
|
905
597
|
|
|
906
|
-
|
|
907
|
-
const shouldPreserveSpaces = hasOnlyTextContent && isInlineElement(tagName)
|
|
598
|
+
const nodesToRender = hasTextFlow ? body : children
|
|
908
599
|
|
|
909
|
-
|
|
600
|
+
const hasOnlyTextContent = nodesToRender.every(child => isNode(child, HTMLTextNode) || isNode(child, WhitespaceNode))
|
|
601
|
+
const shouldPreserveSpaces = hasOnlyTextContent && isInlineElement(tagName)
|
|
910
602
|
|
|
911
|
-
|
|
603
|
+
const lines = this.withInlineMode(() => {
|
|
604
|
+
return this.capture(() => {
|
|
912
605
|
nodesToRender.forEach(child => {
|
|
913
606
|
if (isNode(child, HTMLTextNode)) {
|
|
914
607
|
if (hasTextFlow) {
|
|
@@ -941,29 +634,28 @@ export class FormatPrinter extends Printer {
|
|
|
941
634
|
}
|
|
942
635
|
})
|
|
943
636
|
})
|
|
637
|
+
})
|
|
944
638
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const inlineContent = shouldPreserveSpaces
|
|
948
|
-
? (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ') : content)
|
|
949
|
-
: (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ').trim() : content.trim())
|
|
950
|
-
|
|
951
|
-
if (inlineContent) {
|
|
952
|
-
this.pushToLastLine(inlineContent)
|
|
953
|
-
}
|
|
639
|
+
const content = lines.join('')
|
|
954
640
|
|
|
955
|
-
|
|
641
|
+
const inlineContent = shouldPreserveSpaces
|
|
642
|
+
? (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ') : content)
|
|
643
|
+
: (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ').trim() : content.trim())
|
|
956
644
|
|
|
957
|
-
|
|
645
|
+
if (inlineContent) {
|
|
646
|
+
this.pushToLastLine(inlineContent)
|
|
958
647
|
}
|
|
648
|
+
}
|
|
959
649
|
|
|
960
|
-
|
|
961
|
-
|
|
650
|
+
private stripLeadingHerbDisable(children: Node[], body: Node[]): {
|
|
651
|
+
comment: Node | null
|
|
652
|
+
hasLeadingWhitespace: boolean
|
|
653
|
+
remainingChildren: Node[]
|
|
654
|
+
remainingBody: Node[]
|
|
655
|
+
} {
|
|
962
656
|
let leadingHerbDisableComment: Node | null = null
|
|
963
657
|
let leadingHerbDisableIndex = -1
|
|
964
658
|
let firstWhitespaceIndex = -1
|
|
965
|
-
let remainingChildren = children
|
|
966
|
-
let remainingBodyUnfiltered = body
|
|
967
659
|
|
|
968
660
|
for (let i = 0; i < children.length; i++) {
|
|
969
661
|
const child = children[i]
|
|
@@ -984,60 +676,30 @@ export class FormatPrinter extends Printer {
|
|
|
984
676
|
break
|
|
985
677
|
}
|
|
986
678
|
|
|
987
|
-
if (leadingHerbDisableComment
|
|
988
|
-
remainingChildren
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
if (firstWhitespaceIndex >= 0 && index === leadingHerbDisableIndex - 1) {
|
|
992
|
-
const child = children[index]
|
|
993
|
-
|
|
994
|
-
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
995
|
-
return false
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
return true
|
|
1000
|
-
})
|
|
679
|
+
if (!leadingHerbDisableComment || leadingHerbDisableIndex < 0) {
|
|
680
|
+
return { comment: null, hasLeadingWhitespace: false, remainingChildren: children, remainingBody: body }
|
|
681
|
+
}
|
|
1001
682
|
|
|
1002
|
-
|
|
1003
|
-
|
|
683
|
+
const filterOut = (nodes: Node[]) => nodes.filter((_, index) => {
|
|
684
|
+
if (index === leadingHerbDisableIndex) return false
|
|
1004
685
|
|
|
1005
|
-
|
|
1006
|
-
|
|
686
|
+
if (firstWhitespaceIndex >= 0 && index === leadingHerbDisableIndex - 1) {
|
|
687
|
+
const child = nodes[index]
|
|
1007
688
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
689
|
+
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
690
|
+
return false
|
|
1011
691
|
}
|
|
692
|
+
}
|
|
1012
693
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (leadingHerbDisableComment) {
|
|
1018
|
-
const herbDisableString = this.capture(() => {
|
|
1019
|
-
const savedIndentLevel = this.indentLevel
|
|
1020
|
-
this.indentLevel = 0
|
|
1021
|
-
this.inlineMode = true
|
|
1022
|
-
this.visit(leadingHerbDisableComment)
|
|
1023
|
-
this.inlineMode = false
|
|
1024
|
-
this.indentLevel = savedIndentLevel
|
|
1025
|
-
}).join("")
|
|
1026
|
-
|
|
1027
|
-
const hasLeadingWhitespace = firstWhitespaceIndex >= 0 && firstWhitespaceIndex < leadingHerbDisableIndex
|
|
694
|
+
return true
|
|
695
|
+
})
|
|
1028
696
|
|
|
1029
|
-
|
|
697
|
+
return {
|
|
698
|
+
comment: leadingHerbDisableComment,
|
|
699
|
+
hasLeadingWhitespace: firstWhitespaceIndex >= 0 && firstWhitespaceIndex < leadingHerbDisableIndex,
|
|
700
|
+
remainingChildren: filterOut(children),
|
|
701
|
+
remainingBody: filterOut(body),
|
|
1030
702
|
}
|
|
1031
|
-
|
|
1032
|
-
if (remainingChildren.length === 0) return
|
|
1033
|
-
|
|
1034
|
-
this.withIndent(() => {
|
|
1035
|
-
if (hasTextFlow) {
|
|
1036
|
-
this.visitTextFlowChildren(remainingBodyUnfiltered)
|
|
1037
|
-
} else {
|
|
1038
|
-
this.visitElementChildren(leadingHerbDisableComment ? remainingChildren : body, element)
|
|
1039
|
-
}
|
|
1040
|
-
})
|
|
1041
703
|
}
|
|
1042
704
|
|
|
1043
705
|
/**
|
|
@@ -1053,9 +715,7 @@ export class FormatPrinter extends Printer {
|
|
|
1053
715
|
const child = body[index]
|
|
1054
716
|
|
|
1055
717
|
if (isNode(child, HTMLTextNode)) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (isWhitespaceOnly) {
|
|
718
|
+
if (isPureWhitespaceNode(child)) {
|
|
1059
719
|
const hasPreviousNonWhitespace = index > 0 && isNonWhitespaceNode(body[index - 1])
|
|
1060
720
|
const hasNextNonWhitespace = index < body.length - 1 && isNonWhitespaceNode(body[index + 1])
|
|
1061
721
|
const hasMultipleNewlines = child.content.includes('\n\n')
|
|
@@ -1071,59 +731,99 @@ export class FormatPrinter extends Printer {
|
|
|
1071
731
|
|
|
1072
732
|
if (!isNonWhitespaceNode(child)) continue
|
|
1073
733
|
|
|
1074
|
-
|
|
734
|
+
const textFlowResult = this.visitTextFlowRunInChildren(body, index, lastMeaningfulNode, hasHandledSpacing)
|
|
1075
735
|
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
736
|
+
if (textFlowResult) {
|
|
737
|
+
index = textFlowResult.newIndex
|
|
738
|
+
lastMeaningfulNode = textFlowResult.lastMeaningfulNode
|
|
739
|
+
hasHandledSpacing = textFlowResult.hasHandledSpacing
|
|
740
|
+
continue
|
|
741
|
+
}
|
|
1079
742
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
743
|
+
const herbDisableResult: ChildVisitResult | null =
|
|
744
|
+
isNode(child, HTMLElementNode) && child.close_tag
|
|
745
|
+
? this.visitChildWithTrailingHerbDisable(child, body, index, parentElement, lastMeaningfulNode, hasHandledSpacing)
|
|
746
|
+
: null
|
|
1083
747
|
|
|
1084
|
-
|
|
1085
|
-
|
|
748
|
+
if (herbDisableResult) {
|
|
749
|
+
index = herbDisableResult.newIndex
|
|
750
|
+
lastMeaningfulNode = herbDisableResult.lastMeaningfulNode
|
|
751
|
+
hasHandledSpacing = herbDisableResult.hasHandledSpacing
|
|
752
|
+
continue
|
|
753
|
+
}
|
|
1086
754
|
|
|
1087
|
-
|
|
1088
|
-
|
|
755
|
+
if (shouldAppendToLastLine(child, body, index)) {
|
|
756
|
+
this.appendChildToLastLine(child, body, index)
|
|
757
|
+
lastMeaningfulNode = child
|
|
758
|
+
hasHandledSpacing = false
|
|
759
|
+
continue
|
|
760
|
+
}
|
|
1089
761
|
|
|
1090
|
-
|
|
1091
|
-
|
|
762
|
+
const childStartLine = this.stringLineCount
|
|
763
|
+
this.visit(child)
|
|
1092
764
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
765
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
766
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
767
|
+
|
|
768
|
+
if (shouldAddSpacing) {
|
|
769
|
+
this.lines.splice(childStartLine, 0, "")
|
|
770
|
+
this.stringLineCount++
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
lastMeaningfulNode = child
|
|
775
|
+
hasHandledSpacing = false
|
|
776
|
+
}
|
|
777
|
+
}
|
|
1098
778
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
this.indentLevel = 0
|
|
1102
|
-
this.inlineMode = true
|
|
1103
|
-
this.visit(nextChild)
|
|
1104
|
-
this.inlineMode = false
|
|
1105
|
-
this.indentLevel = savedIndentLevel
|
|
1106
|
-
}).join("")
|
|
779
|
+
private visitTextFlowRunInChildren(body: Node[], index: number, lastMeaningfulNode: Node | null, hasHandledSpacing: boolean): ChildVisitResult | null {
|
|
780
|
+
const child = body[index]
|
|
1107
781
|
|
|
1108
|
-
|
|
782
|
+
if (!isTextFlowNode(child)) return null
|
|
1109
783
|
|
|
1110
|
-
|
|
1111
|
-
lastMeaningfulNode = child
|
|
1112
|
-
hasHandledSpacing = false
|
|
784
|
+
const run = this.textFlow.collectTextFlowRun(body, index)
|
|
1113
785
|
|
|
1114
|
-
|
|
1115
|
-
}
|
|
786
|
+
if (!run) return null
|
|
1116
787
|
|
|
1117
|
-
|
|
1118
|
-
|
|
788
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
789
|
+
const hasBlankLineBefore = this.spacingAnalyzer.hasBlankLineBetween(body, index)
|
|
790
|
+
|
|
791
|
+
if (hasBlankLineBefore) {
|
|
792
|
+
this.push("")
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
this.textFlow.visitTextFlowChildren(run.nodes)
|
|
797
|
+
|
|
798
|
+
const lastRunNode = run.nodes[run.nodes.length - 1]
|
|
799
|
+
const hasBlankLineInTrailing = isNode(lastRunNode, HTMLTextNode) && lastRunNode.content.includes('\n\n')
|
|
800
|
+
const hasBlankLineAfter = hasBlankLineInTrailing || this.spacingAnalyzer.hasBlankLineBetween(body, run.endIndex)
|
|
801
|
+
|
|
802
|
+
if (hasBlankLineAfter) {
|
|
803
|
+
this.push("")
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
newIndex: run.endIndex - 1,
|
|
808
|
+
lastMeaningfulNode: run.nodes[run.nodes.length - 1],
|
|
809
|
+
hasHandledSpacing: hasBlankLineAfter,
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private visitChildWithTrailingHerbDisable(child: HTMLElementNode, body: Node[], index: number, parentElement: HTMLElementNode | null, lastMeaningfulNode: Node | null, hasHandledSpacing: boolean): ChildVisitResult | null {
|
|
814
|
+
for (let j = index + 1; j < body.length; j++) {
|
|
815
|
+
const nextChild = body[j]
|
|
816
|
+
|
|
817
|
+
if (isNode(nextChild, WhitespaceNode) || isPureWhitespaceNode(nextChild)) {
|
|
818
|
+
continue
|
|
1119
819
|
}
|
|
1120
820
|
|
|
1121
|
-
if (
|
|
821
|
+
if (isNode(nextChild, ERBContentNode) && isHerbDisableComment(nextChild)) {
|
|
1122
822
|
const childStartLine = this.stringLineCount
|
|
1123
823
|
this.visit(child)
|
|
1124
824
|
|
|
1125
825
|
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
1126
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
826
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
1127
827
|
|
|
1128
828
|
if (shouldAddSpacing) {
|
|
1129
829
|
this.lines.splice(childStartLine, 0, "")
|
|
@@ -1131,10 +831,21 @@ export class FormatPrinter extends Printer {
|
|
|
1131
831
|
}
|
|
1132
832
|
}
|
|
1133
833
|
|
|
1134
|
-
|
|
1135
|
-
|
|
834
|
+
const herbDisableString = this.captureHerbDisableInline(nextChild)
|
|
835
|
+
|
|
836
|
+
this.pushToLastLine(' ' + herbDisableString)
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
newIndex: j,
|
|
840
|
+
lastMeaningfulNode: child,
|
|
841
|
+
hasHandledSpacing: false,
|
|
842
|
+
}
|
|
1136
843
|
}
|
|
844
|
+
|
|
845
|
+
break
|
|
1137
846
|
}
|
|
847
|
+
|
|
848
|
+
return null
|
|
1138
849
|
}
|
|
1139
850
|
|
|
1140
851
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode) {
|
|
@@ -1142,6 +853,12 @@ export class FormatPrinter extends Printer {
|
|
|
1142
853
|
const inlineNodes = this.extractInlineNodes(node.children)
|
|
1143
854
|
const isSelfClosing = node.tag_closing?.value === "/>"
|
|
1144
855
|
|
|
856
|
+
if (this.inConditionalOpenTagContext) {
|
|
857
|
+
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children)
|
|
858
|
+
this.push(this.indent + inline)
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
|
|
1145
862
|
if (this.currentElement && this.elementFormattingAnalysis.has(this.currentElement)) {
|
|
1146
863
|
const analysis = this.elementFormattingAnalysis.get(this.currentElement)!
|
|
1147
864
|
|
|
@@ -1159,13 +876,14 @@ export class FormatPrinter extends Printer {
|
|
|
1159
876
|
|
|
1160
877
|
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children)
|
|
1161
878
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
1162
|
-
|
|
879
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
880
|
+
const shouldKeepInline = this.attributeRenderer.shouldRenderInline(
|
|
1163
881
|
totalAttributeCount,
|
|
1164
882
|
inline.length,
|
|
1165
883
|
this.indent.length,
|
|
1166
884
|
this.maxLineLength,
|
|
1167
885
|
false,
|
|
1168
|
-
this.hasMultilineAttributes(attributes),
|
|
886
|
+
this.attributeRenderer.hasMultilineAttributes(attributes),
|
|
1169
887
|
attributes
|
|
1170
888
|
)
|
|
1171
889
|
|
|
@@ -1199,7 +917,7 @@ export class FormatPrinter extends Printer {
|
|
|
1199
917
|
return
|
|
1200
918
|
}
|
|
1201
919
|
|
|
1202
|
-
|
|
920
|
+
const text = node.content.trim()
|
|
1203
921
|
|
|
1204
922
|
if (!text) return
|
|
1205
923
|
|
|
@@ -1224,7 +942,8 @@ export class FormatPrinter extends Printer {
|
|
|
1224
942
|
}
|
|
1225
943
|
|
|
1226
944
|
visitHTMLAttributeNode(node: HTMLAttributeNode) {
|
|
1227
|
-
this.
|
|
945
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
946
|
+
this.pushWithIndent(this.attributeRenderer.renderAttribute(node, this.currentTagName))
|
|
1228
947
|
}
|
|
1229
948
|
|
|
1230
949
|
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode) {
|
|
@@ -1235,119 +954,39 @@ export class FormatPrinter extends Printer {
|
|
|
1235
954
|
this.pushWithIndent(IdentityPrinter.print(node))
|
|
1236
955
|
}
|
|
1237
956
|
|
|
1238
|
-
// TODO: rework
|
|
1239
957
|
visitHTMLCommentNode(node: HTMLCommentNode) {
|
|
1240
958
|
const open = node.comment_start?.value ?? ""
|
|
1241
959
|
const close = node.comment_end?.value ?? ""
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
inner = node.children.map(child => {
|
|
1247
|
-
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
1248
|
-
return child.content
|
|
1249
|
-
} else if (isERBNode(child)) {
|
|
1250
|
-
return IdentityPrinter.print(child)
|
|
1251
|
-
} else {
|
|
1252
|
-
return ""
|
|
1253
|
-
}
|
|
1254
|
-
}).join("")
|
|
1255
|
-
|
|
1256
|
-
const trimmedInner = inner.trim()
|
|
1257
|
-
|
|
1258
|
-
if (trimmedInner.startsWith('[if ') && trimmedInner.endsWith('<![endif]')) {
|
|
1259
|
-
this.pushWithIndent(open + inner + close)
|
|
1260
|
-
|
|
1261
|
-
return
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
const hasNewlines = inner.includes('\n')
|
|
1265
|
-
|
|
1266
|
-
if (hasNewlines) {
|
|
1267
|
-
const lines = inner.split('\n')
|
|
1268
|
-
const childIndent = " ".repeat(this.indentWidth)
|
|
1269
|
-
const firstLineHasContent = lines[0].trim() !== ''
|
|
1270
|
-
|
|
1271
|
-
if (firstLineHasContent && lines.length > 1) {
|
|
1272
|
-
const contentLines = lines.map(line => line.trim()).filter(line => line !== '')
|
|
1273
|
-
inner = '\n' + contentLines.map(line => childIndent + line).join('\n') + '\n'
|
|
1274
|
-
} else {
|
|
1275
|
-
const contentLines = lines.filter((line, index) => {
|
|
1276
|
-
return line.trim() !== '' && !(index === 0 || index === lines.length - 1)
|
|
1277
|
-
})
|
|
1278
|
-
|
|
1279
|
-
const minIndent = contentLines.length > 0 ? Math.min(...contentLines.map(line => line.length - line.trimStart().length)) : 0
|
|
1280
|
-
|
|
1281
|
-
const processedLines = lines.map((line, index) => {
|
|
1282
|
-
const trimmedLine = line.trim()
|
|
1283
|
-
|
|
1284
|
-
if ((index === 0 || index === lines.length - 1) && trimmedLine === '') {
|
|
1285
|
-
return line
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
if (trimmedLine !== '') {
|
|
1289
|
-
const currentIndent = line.length - line.trimStart().length
|
|
1290
|
-
const relativeIndent = Math.max(0, currentIndent - minIndent)
|
|
1291
|
-
|
|
1292
|
-
return childIndent + " ".repeat(relativeIndent) + trimmedLine
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
return line
|
|
1296
|
-
})
|
|
1297
|
-
|
|
1298
|
-
inner = processedLines.join('\n')
|
|
1299
|
-
}
|
|
1300
|
-
} else {
|
|
1301
|
-
inner = ` ${inner.trim()} `
|
|
1302
|
-
}
|
|
1303
|
-
} else {
|
|
1304
|
-
inner = ""
|
|
1305
|
-
}
|
|
960
|
+
const rawInner = node.children && node.children.length > 0
|
|
961
|
+
? extractHTMLCommentContent(node.children)
|
|
962
|
+
: ""
|
|
963
|
+
const inner = rawInner ? formatHTMLCommentInner(rawInner, this.indentWidth) : ""
|
|
1306
964
|
|
|
1307
965
|
this.pushWithIndent(open + inner + close)
|
|
1308
966
|
}
|
|
1309
967
|
|
|
1310
968
|
visitERBCommentNode(node: ERBContentNode) {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
const contentTrimmedLines = content.trim().split("\n")
|
|
1317
|
-
|
|
1318
|
-
if (contentLines.length === 1 && contentTrimmedLines.length === 1) {
|
|
1319
|
-
const startsWithSpace = content[0] === " "
|
|
1320
|
-
const before = startsWithSpace ? "" : " "
|
|
969
|
+
const result = formatERBCommentLines(
|
|
970
|
+
node.tag_opening?.value || "<%#",
|
|
971
|
+
node?.content?.value || "",
|
|
972
|
+
node.tag_closing?.value || "%>"
|
|
973
|
+
)
|
|
1321
974
|
|
|
975
|
+
if (result.type === 'single-line') {
|
|
1322
976
|
if (this.inlineMode) {
|
|
1323
|
-
this.push(
|
|
977
|
+
this.push(result.text)
|
|
1324
978
|
} else {
|
|
1325
|
-
this.pushWithIndent(
|
|
979
|
+
this.pushWithIndent(result.text)
|
|
1326
980
|
}
|
|
981
|
+
} else {
|
|
982
|
+
this.pushWithIndent(result.header)
|
|
1327
983
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
if (contentTrimmedLines.length === 1) {
|
|
1332
|
-
if (this.inlineMode) {
|
|
1333
|
-
this.push(open + ' ' + content.trim() + ' ' + close)
|
|
1334
|
-
} else {
|
|
1335
|
-
this.pushWithIndent(open + ' ' + content.trim() + ' ' + close)
|
|
1336
|
-
}
|
|
984
|
+
this.withIndent(() => {
|
|
985
|
+
result.contentLines.forEach(line => this.pushWithIndent(line))
|
|
986
|
+
})
|
|
1337
987
|
|
|
1338
|
-
|
|
988
|
+
this.pushWithIndent(result.footer)
|
|
1339
989
|
}
|
|
1340
|
-
|
|
1341
|
-
const firstLineEmpty = contentLines[0].trim() === ""
|
|
1342
|
-
const dedentedContent = dedent(firstLineEmpty ? content : content.trimStart())
|
|
1343
|
-
|
|
1344
|
-
this.pushWithIndent(open)
|
|
1345
|
-
|
|
1346
|
-
this.withIndent(() => {
|
|
1347
|
-
dedentedContent.split("\n").forEach(line => this.pushWithIndent(line))
|
|
1348
|
-
})
|
|
1349
|
-
|
|
1350
|
-
this.pushWithIndent(close)
|
|
1351
990
|
}
|
|
1352
991
|
|
|
1353
992
|
visitHTMLDoctypeNode(node: HTMLDoctypeNode) {
|
|
@@ -1370,10 +1009,26 @@ export class FormatPrinter extends Printer {
|
|
|
1370
1009
|
}
|
|
1371
1010
|
}
|
|
1372
1011
|
|
|
1012
|
+
visitERBOpenTagNode(node: ERBOpenTagNode) {
|
|
1013
|
+
this.printERBNode(node)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
visitHTMLVirtualCloseTagNode(_node: HTMLVirtualCloseTagNode) {
|
|
1017
|
+
// Virtual closing tags don't print anything (they are synthetic)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1373
1020
|
visitERBEndNode(node: ERBEndNode) {
|
|
1374
1021
|
this.printERBNode(node)
|
|
1375
1022
|
}
|
|
1376
1023
|
|
|
1024
|
+
visitERBRenderNode(node: ERBRenderNode) {
|
|
1025
|
+
this.printERBNode(node)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
visitRubyRenderLocalNode(_node: RubyRenderLocalNode) {
|
|
1029
|
+
// extracted metadata, nothing to print
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1377
1032
|
visitERBYieldNode(node: ERBYieldNode) {
|
|
1378
1033
|
this.trackBoundary(node, () => {
|
|
1379
1034
|
this.printERBNode(node)
|
|
@@ -1404,10 +1059,10 @@ export class FormatPrinter extends Printer {
|
|
|
1404
1059
|
this.printERBNode(node)
|
|
1405
1060
|
|
|
1406
1061
|
this.withIndent(() => {
|
|
1407
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
1062
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(node.body)
|
|
1408
1063
|
|
|
1409
1064
|
if (hasTextFlow) {
|
|
1410
|
-
this.visitTextFlowChildren(node.body)
|
|
1065
|
+
this.textFlow.visitTextFlowChildren(node.body)
|
|
1411
1066
|
} else {
|
|
1412
1067
|
this.visitElementChildren(node.body, null)
|
|
1413
1068
|
}
|
|
@@ -1425,9 +1080,10 @@ export class FormatPrinter extends Printer {
|
|
|
1425
1080
|
node.statements.forEach(child => {
|
|
1426
1081
|
if (isNode(child, HTMLAttributeNode)) {
|
|
1427
1082
|
this.lines.push(" ")
|
|
1428
|
-
this.
|
|
1083
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1084
|
+
this.lines.push(this.attributeRenderer.renderAttribute(child, this.currentTagName))
|
|
1429
1085
|
} else {
|
|
1430
|
-
const shouldAddSpaces = this.isInTokenListAttribute
|
|
1086
|
+
const shouldAddSpaces = this.attributeRenderer.isInTokenListAttribute
|
|
1431
1087
|
|
|
1432
1088
|
if (shouldAddSpaces) {
|
|
1433
1089
|
this.lines.push(" ")
|
|
@@ -1442,7 +1098,7 @@ export class FormatPrinter extends Printer {
|
|
|
1442
1098
|
})
|
|
1443
1099
|
|
|
1444
1100
|
const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode))
|
|
1445
|
-
const isTokenList = this.isInTokenListAttribute
|
|
1101
|
+
const isTokenList = this.attributeRenderer.isInTokenListAttribute
|
|
1446
1102
|
|
|
1447
1103
|
if ((hasHTMLAttributes || isTokenList) && node.end_node) {
|
|
1448
1104
|
this.lines.push(" ")
|
|
@@ -1570,7 +1226,12 @@ export class FormatPrinter extends Printer {
|
|
|
1570
1226
|
* Determines if the open tag should be rendered inline
|
|
1571
1227
|
*/
|
|
1572
1228
|
private shouldRenderOpenTagInline(node: HTMLElementNode): boolean {
|
|
1573
|
-
|
|
1229
|
+
if (isNode(node.open_tag, HTMLConditionalOpenTagNode)) {
|
|
1230
|
+
return false
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const openTag = node.open_tag
|
|
1234
|
+
const children = openTag?.children || []
|
|
1574
1235
|
const attributes = filterNodes(children, HTMLAttributeNode)
|
|
1575
1236
|
const inlineNodes = this.extractInlineNodes(children)
|
|
1576
1237
|
const hasERBControlFlow = inlineNodes.some(node => isERBControlFlowNode(node)) || children.some(node => isERBControlFlowNode(node))
|
|
@@ -1579,19 +1240,20 @@ export class FormatPrinter extends Printer {
|
|
|
1579
1240
|
if (hasComplexERB) return false
|
|
1580
1241
|
|
|
1581
1242
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
1582
|
-
|
|
1243
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1244
|
+
const hasMultilineAttrs = this.attributeRenderer.hasMultilineAttributes(attributes)
|
|
1583
1245
|
|
|
1584
1246
|
if (hasMultilineAttrs) return false
|
|
1585
1247
|
|
|
1586
1248
|
const inline = this.renderInlineOpen(
|
|
1587
1249
|
getTagName(node),
|
|
1588
1250
|
attributes,
|
|
1589
|
-
|
|
1251
|
+
openTag?.tag_closing?.value === "/>",
|
|
1590
1252
|
inlineNodes,
|
|
1591
1253
|
children
|
|
1592
1254
|
)
|
|
1593
1255
|
|
|
1594
|
-
return this.shouldRenderInline(
|
|
1256
|
+
return this.attributeRenderer.shouldRenderInline(
|
|
1595
1257
|
totalAttributeCount,
|
|
1596
1258
|
inline.length,
|
|
1597
1259
|
this.indent.length,
|
|
@@ -1609,6 +1271,7 @@ export class FormatPrinter extends Printer {
|
|
|
1609
1271
|
const tagName = getTagName(node)
|
|
1610
1272
|
const children = filterSignificantChildren(node.body)
|
|
1611
1273
|
const openTagInline = this.shouldRenderOpenTagInline(node)
|
|
1274
|
+
const openTagClosing = getOpenTagClosing(node)
|
|
1612
1275
|
|
|
1613
1276
|
if (!openTagInline) return false
|
|
1614
1277
|
if (children.length === 0) return true
|
|
@@ -1623,46 +1286,46 @@ export class FormatPrinter extends Printer {
|
|
|
1623
1286
|
|
|
1624
1287
|
if (hasNonInlineChildElements) return false
|
|
1625
1288
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
for (const child of node.body) {
|
|
1629
|
-
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
1630
|
-
continue
|
|
1631
|
-
}
|
|
1632
|
-
if (isNode(child, ERBContentNode) && isHerbDisableComment(child)) {
|
|
1633
|
-
hasLeadingHerbDisable = true
|
|
1634
|
-
}
|
|
1635
|
-
break
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
if (hasLeadingHerbDisable && !isInlineElement(tagName)) {
|
|
1289
|
+
if (hasLeadingHerbDisable(node.body) && !isInlineElement(tagName)) {
|
|
1639
1290
|
return false
|
|
1640
1291
|
}
|
|
1641
1292
|
|
|
1642
1293
|
if (isInlineElement(tagName)) {
|
|
1643
|
-
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node
|
|
1294
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(getOpenTagChildren(node), HTMLAttributeNode), node.body)
|
|
1644
1295
|
|
|
1645
1296
|
if (fullInlineResult) {
|
|
1646
|
-
|
|
1647
|
-
return totalLength <= this.maxLineLength
|
|
1297
|
+
return this.fitsOnCurrentLine(fullInlineResult)
|
|
1648
1298
|
}
|
|
1649
1299
|
|
|
1650
1300
|
return false
|
|
1651
1301
|
}
|
|
1652
1302
|
|
|
1303
|
+
if (SPACEABLE_CONTAINERS.has(tagName)) {
|
|
1304
|
+
const allChildrenAreERB = children.length > 1 && children.every(child => isERBNode(child))
|
|
1305
|
+
|
|
1306
|
+
if (allChildrenAreERB) return false
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (!isInlineElement(tagName) && openTagClosing) {
|
|
1310
|
+
const first = children[0]
|
|
1311
|
+
const startsOnNewLine = first.location.start.line > openTagClosing.location.end.line
|
|
1312
|
+
const hasLeadingNewline = isNode(first, HTMLTextNode) && /^\s*\n/.test(first.content)
|
|
1313
|
+
const contentStartsOnNewLine = startsOnNewLine || hasLeadingNewline
|
|
1314
|
+
|
|
1315
|
+
if (contentStartsOnNewLine) {
|
|
1316
|
+
return false
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1653
1320
|
const allNestedAreInline = areAllNestedElementsInline(children)
|
|
1654
1321
|
const hasMultilineText = hasMultilineTextContent(children)
|
|
1655
1322
|
const hasMixedContent = hasMixedTextAndInlineContent(children)
|
|
1656
1323
|
|
|
1657
1324
|
if (allNestedAreInline && (!hasMultilineText || hasMixedContent)) {
|
|
1658
|
-
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node
|
|
1659
|
-
|
|
1660
|
-
if (fullInlineResult) {
|
|
1661
|
-
const totalLength = this.indent.length + fullInlineResult.length
|
|
1325
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(getOpenTagChildren(node), HTMLAttributeNode), node.body)
|
|
1662
1326
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1327
|
+
if (fullInlineResult && this.fitsOnCurrentLine(fullInlineResult)) {
|
|
1328
|
+
return true
|
|
1666
1329
|
}
|
|
1667
1330
|
}
|
|
1668
1331
|
|
|
@@ -1671,17 +1334,16 @@ export class FormatPrinter extends Printer {
|
|
|
1671
1334
|
if (inlineResult) {
|
|
1672
1335
|
const openTagResult = this.renderInlineOpen(
|
|
1673
1336
|
tagName,
|
|
1674
|
-
filterNodes(node
|
|
1337
|
+
filterNodes(getOpenTagChildren(node), HTMLAttributeNode),
|
|
1675
1338
|
false,
|
|
1676
1339
|
[],
|
|
1677
|
-
node
|
|
1340
|
+
getOpenTagChildren(node)
|
|
1678
1341
|
)
|
|
1679
1342
|
|
|
1680
1343
|
const childrenContent = this.renderChildrenInline(children)
|
|
1681
1344
|
const fullLine = openTagResult + childrenContent + `</${tagName}>`
|
|
1682
|
-
const totalLength = this.indent.length + fullLine.length
|
|
1683
1345
|
|
|
1684
|
-
if (
|
|
1346
|
+
if (this.fitsOnCurrentLine(fullLine)) {
|
|
1685
1347
|
return true
|
|
1686
1348
|
}
|
|
1687
1349
|
}
|
|
@@ -1694,7 +1356,7 @@ export class FormatPrinter extends Printer {
|
|
|
1694
1356
|
*/
|
|
1695
1357
|
private shouldRenderCloseTagInline(node: HTMLElementNode, elementContentInline: boolean): boolean {
|
|
1696
1358
|
if (node.is_void) return true
|
|
1697
|
-
if (node
|
|
1359
|
+
if (getOpenTagClosing(node)?.value === "/>") return true
|
|
1698
1360
|
if (isContentPreserving(node)) return true
|
|
1699
1361
|
|
|
1700
1362
|
const children = filterSignificantChildren(node.body)
|
|
@@ -1707,6 +1369,19 @@ export class FormatPrinter extends Printer {
|
|
|
1707
1369
|
|
|
1708
1370
|
// --- Utility methods ---
|
|
1709
1371
|
|
|
1372
|
+
private captureHerbDisableInline(node: Node): string {
|
|
1373
|
+
return this.capture(() => {
|
|
1374
|
+
const savedIndentLevel = this.indentLevel
|
|
1375
|
+
this.indentLevel = 0
|
|
1376
|
+
this.withInlineMode(() => this.visit(node))
|
|
1377
|
+
this.indentLevel = savedIndentLevel
|
|
1378
|
+
}).join("")
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private fitsOnCurrentLine(content: string): boolean {
|
|
1382
|
+
return this.indent.length + content.length <= this.maxLineLength
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1710
1385
|
private formatFrontmatter(node: DocumentNode): Node[] {
|
|
1711
1386
|
const firstChild = node.children[0]
|
|
1712
1387
|
const hasFrontmatter = firstChild && isFrontmatter(firstChild)
|
|
@@ -1739,682 +1414,75 @@ export class FormatPrinter extends Printer {
|
|
|
1739
1414
|
}
|
|
1740
1415
|
}
|
|
1741
1416
|
|
|
1742
|
-
const
|
|
1743
|
-
this.inlineMode = true
|
|
1744
|
-
const inlineContent = this.capture(() => this.visit(child)).join("")
|
|
1745
|
-
this.inlineMode = oldInlineMode
|
|
1417
|
+
const inlineContent = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
1746
1418
|
this.pushToLastLine((hasSpaceBefore ? " " : "") + inlineContent)
|
|
1747
1419
|
}
|
|
1748
1420
|
}
|
|
1749
1421
|
|
|
1750
|
-
/**
|
|
1751
|
-
* Visit children in a text flow context (mixed text and inline elements)
|
|
1752
|
-
* Handles word wrapping and keeps adjacent inline elements together
|
|
1753
|
-
*/
|
|
1754
|
-
private visitTextFlowChildren(children: Node[]) {
|
|
1755
|
-
const adjacentInlineCount = countAdjacentInlineElements(children)
|
|
1756
|
-
|
|
1757
|
-
if (adjacentInlineCount >= 2) {
|
|
1758
|
-
const { processedIndices } = this.renderAdjacentInlineElements(children, adjacentInlineCount)
|
|
1759
|
-
this.visitRemainingChildren(children, processedIndices)
|
|
1760
1422
|
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
this.buildAndWrapTextFlow(children)
|
|
1765
|
-
}
|
|
1423
|
+
// --- TextFlowDelegate implementation ---
|
|
1766
1424
|
|
|
1767
1425
|
/**
|
|
1768
|
-
*
|
|
1769
|
-
* Returns the wrapped lines with proper indentation
|
|
1426
|
+
* Render an inline element as a string
|
|
1770
1427
|
*/
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1428
|
+
renderInlineElementAsString(element: HTMLElementNode): string {
|
|
1429
|
+
const tagName = getTagName(element)
|
|
1430
|
+
const tagClosing = getOpenTagClosing(element)
|
|
1774
1431
|
|
|
1775
|
-
|
|
1776
|
-
const
|
|
1432
|
+
if (element.is_void || tagClosing?.value === "/>") {
|
|
1433
|
+
const attributes = filterNodes(getOpenTagChildren(element), HTMLAttributeNode)
|
|
1434
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1435
|
+
const attributesString = this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
1436
|
+
const isSelfClosing = tagClosing?.value === "/>"
|
|
1777
1437
|
|
|
1778
|
-
|
|
1779
|
-
lines.push(this.indent + line)
|
|
1780
|
-
line = word
|
|
1781
|
-
} else {
|
|
1782
|
-
line = testLine
|
|
1783
|
-
}
|
|
1438
|
+
return `<${tagName}${attributesString}${isSelfClosing ? " />" : ">"}`
|
|
1784
1439
|
}
|
|
1785
1440
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1441
|
+
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1442
|
+
|
|
1443
|
+
const childInline = this.tryRenderInlineFull(element, tagName,
|
|
1444
|
+
filterNodes(getOpenTagChildren(element), HTMLAttributeNode),
|
|
1445
|
+
childrenToRender
|
|
1446
|
+
)
|
|
1789
1447
|
|
|
1790
|
-
return
|
|
1448
|
+
return childInline !== null ? childInline : ""
|
|
1791
1449
|
}
|
|
1792
1450
|
|
|
1793
1451
|
/**
|
|
1794
|
-
*
|
|
1795
|
-
* Returns object with merged content and whether processing should stop
|
|
1452
|
+
* Render an ERB node as a string
|
|
1796
1453
|
*/
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
if (combined.length <= wrapWidth) {
|
|
1801
|
-
return {
|
|
1802
|
-
mergedContent: inlineContent + trimmedText,
|
|
1803
|
-
shouldStop: false,
|
|
1804
|
-
wrappedLines: []
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
const match = trimmedText.match(/^[.!?:;]+/)
|
|
1809
|
-
|
|
1810
|
-
if (!match) {
|
|
1811
|
-
return {
|
|
1812
|
-
mergedContent: inlineContent,
|
|
1813
|
-
shouldStop: false,
|
|
1814
|
-
wrappedLines: []
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
const punctuation = match[0]
|
|
1819
|
-
const restText = trimmedText.substring(punctuation.length).trim()
|
|
1454
|
+
renderERBAsString(node: ERBContentNode): string {
|
|
1455
|
+
return this.withInlineMode(() => this.capture(() => this.visit(node)).join(""))
|
|
1456
|
+
}
|
|
1820
1457
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Try to render an inline element, returning the full inline string or null if it can't be inlined.
|
|
1460
|
+
*/
|
|
1461
|
+
tryRenderInlineElement(element: HTMLElementNode): string | null {
|
|
1462
|
+
const tagName = getTagName(element)
|
|
1463
|
+
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1828
1464
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
let mergedWordCount = 0
|
|
1465
|
+
return this.tryRenderInlineFull(element, tagName, filterNodes(getOpenTagChildren(element), HTMLAttributeNode), childrenToRender)
|
|
1466
|
+
}
|
|
1832
1467
|
|
|
1833
|
-
for (const word of words) {
|
|
1834
|
-
const testMerge = toMerge + ' ' + word
|
|
1835
1468
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
} else {
|
|
1840
|
-
break
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1469
|
+
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean, inlineNodes: Node[] = [], allChildren: Node[] = []): string {
|
|
1470
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1471
|
+
const parts = attributes.map(attribute => this.attributeRenderer.renderAttribute(attribute, name))
|
|
1843
1472
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
if (mergedWordCount >= words.length) {
|
|
1847
|
-
return {
|
|
1848
|
-
mergedContent,
|
|
1849
|
-
shouldStop: false,
|
|
1850
|
-
wrappedLines: []
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
const remainingWords = words.slice(mergedWordCount)
|
|
1855
|
-
const wrappedLines = this.wrapRemainingWords(remainingWords, wrapWidth)
|
|
1856
|
-
|
|
1857
|
-
return {
|
|
1858
|
-
mergedContent,
|
|
1859
|
-
shouldStop: true,
|
|
1860
|
-
wrappedLines
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
/**
|
|
1865
|
-
* Render adjacent inline elements together on one line
|
|
1866
|
-
*/
|
|
1867
|
-
private renderAdjacentInlineElements(children: Node[], count: number): { processedIndices: Set<number> } {
|
|
1868
|
-
let inlineContent = ""
|
|
1869
|
-
let processedCount = 0
|
|
1870
|
-
let lastProcessedIndex = -1
|
|
1871
|
-
const processedIndices = new Set<number>()
|
|
1872
|
-
|
|
1873
|
-
for (let index = 0; index < children.length && processedCount < count; index++) {
|
|
1874
|
-
const child = children[index]
|
|
1875
|
-
|
|
1876
|
-
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
1877
|
-
continue
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
if (isNode(child, HTMLElementNode) && isInlineElement(getTagName(child))) {
|
|
1881
|
-
inlineContent += this.renderInlineElementAsString(child)
|
|
1882
|
-
processedCount++
|
|
1883
|
-
lastProcessedIndex = index
|
|
1884
|
-
processedIndices.add(index)
|
|
1885
|
-
|
|
1886
|
-
if (inlineContent && isLineBreakingElement(child)) {
|
|
1887
|
-
this.pushWithIndent(inlineContent)
|
|
1888
|
-
inlineContent = ""
|
|
1889
|
-
}
|
|
1890
|
-
} else if (isNode(child, ERBContentNode)) {
|
|
1891
|
-
inlineContent += this.renderERBAsString(child)
|
|
1892
|
-
processedCount++
|
|
1893
|
-
lastProcessedIndex = index
|
|
1894
|
-
processedIndices.add(index)
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
if (lastProcessedIndex >= 0) {
|
|
1899
|
-
for (let index = lastProcessedIndex + 1; index < children.length; index++) {
|
|
1900
|
-
const child = children[index]
|
|
1901
|
-
|
|
1902
|
-
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
1903
|
-
continue
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
if (isNode(child, ERBContentNode)) {
|
|
1907
|
-
inlineContent += this.renderERBAsString(child)
|
|
1908
|
-
processedIndices.add(index)
|
|
1909
|
-
continue
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
if (isNode(child, HTMLTextNode)) {
|
|
1913
|
-
const trimmed = child.content.trim()
|
|
1914
|
-
|
|
1915
|
-
if (trimmed && /^[.!?:;]/.test(trimmed)) {
|
|
1916
|
-
const wrapWidth = this.maxLineLength - this.indent.length
|
|
1917
|
-
const result = this.tryMergePunctuationText(inlineContent, trimmed, wrapWidth)
|
|
1918
|
-
|
|
1919
|
-
inlineContent = result.mergedContent
|
|
1920
|
-
processedIndices.add(index)
|
|
1921
|
-
|
|
1922
|
-
if (result.shouldStop) {
|
|
1923
|
-
if (inlineContent) {
|
|
1924
|
-
this.pushWithIndent(inlineContent)
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
result.wrappedLines.forEach(line => this.push(line))
|
|
1928
|
-
|
|
1929
|
-
return { processedIndices }
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
break
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
if (inlineContent) {
|
|
1939
|
-
this.pushWithIndent(inlineContent)
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
return { processedIndices }
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
/**
|
|
1946
|
-
* Render an inline element as a string
|
|
1947
|
-
*/
|
|
1948
|
-
private renderInlineElementAsString(element: HTMLElementNode): string {
|
|
1949
|
-
const tagName = getTagName(element)
|
|
1950
|
-
|
|
1951
|
-
if (element.is_void || element.open_tag?.tag_closing?.value === "/>") {
|
|
1952
|
-
const attributes = filterNodes(element.open_tag?.children, HTMLAttributeNode)
|
|
1953
|
-
const attributesString = this.renderAttributesString(attributes)
|
|
1954
|
-
const isSelfClosing = element.open_tag?.tag_closing?.value === "/>"
|
|
1955
|
-
|
|
1956
|
-
return `<${tagName}${attributesString}${isSelfClosing ? " />" : ">"}`
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1960
|
-
|
|
1961
|
-
const childInline = this.tryRenderInlineFull(element, tagName,
|
|
1962
|
-
filterNodes(element.open_tag?.children, HTMLAttributeNode),
|
|
1963
|
-
childrenToRender
|
|
1964
|
-
)
|
|
1965
|
-
|
|
1966
|
-
return childInline !== null ? childInline : ""
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
/**
|
|
1970
|
-
* Render an ERB node as a string
|
|
1971
|
-
*/
|
|
1972
|
-
private renderERBAsString(node: ERBContentNode): string {
|
|
1973
|
-
return this.capture(() => {
|
|
1974
|
-
this.inlineMode = true
|
|
1975
|
-
this.visit(node)
|
|
1976
|
-
}).join("")
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
/**
|
|
1980
|
-
* Visit remaining children after processing adjacent inline elements
|
|
1981
|
-
*/
|
|
1982
|
-
private visitRemainingChildren(children: Node[], processedIndices: Set<number>): void {
|
|
1983
|
-
for (let index = 0; index < children.length; index++) {
|
|
1984
|
-
const child = children[index]
|
|
1985
|
-
|
|
1986
|
-
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
1987
|
-
continue
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
if (processedIndices.has(index)) {
|
|
1991
|
-
continue
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
this.visit(child)
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
/**
|
|
1999
|
-
* Build words array from text/inline/ERB and wrap them
|
|
2000
|
-
*/
|
|
2001
|
-
private buildAndWrapTextFlow(children: Node[]): void {
|
|
2002
|
-
const unitsWithNodes: ContentUnitWithNode[] = this.buildContentUnitsWithNodes(children)
|
|
2003
|
-
const words: Array<{ word: string, isHerbDisable: boolean }> = []
|
|
2004
|
-
|
|
2005
|
-
for (const { unit, node } of unitsWithNodes) {
|
|
2006
|
-
if (unit.breaksFlow) {
|
|
2007
|
-
this.flushWords(words)
|
|
2008
|
-
|
|
2009
|
-
if (node) {
|
|
2010
|
-
this.visit(node)
|
|
2011
|
-
}
|
|
2012
|
-
} else if (unit.isAtomic) {
|
|
2013
|
-
words.push({ word: unit.content, isHerbDisable: unit.isHerbDisable || false })
|
|
2014
|
-
} else {
|
|
2015
|
-
const text = unit.content.replace(ASCII_WHITESPACE, ' ')
|
|
2016
|
-
const hasLeadingSpace = text.startsWith(' ')
|
|
2017
|
-
const hasTrailingSpace = text.endsWith(' ')
|
|
2018
|
-
const trimmedText = text.trim()
|
|
2019
|
-
|
|
2020
|
-
if (trimmedText) {
|
|
2021
|
-
if (hasLeadingSpace && words.length > 0) {
|
|
2022
|
-
const lastWord = words[words.length - 1]
|
|
2023
|
-
|
|
2024
|
-
if (!lastWord.word.endsWith(' ')) {
|
|
2025
|
-
lastWord.word += ' '
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
const textWords = trimmedText.split(' ').map(w => ({ word: w, isHerbDisable: false }))
|
|
2030
|
-
words.push(...textWords)
|
|
2031
|
-
|
|
2032
|
-
if (hasTrailingSpace && words.length > 0) {
|
|
2033
|
-
const lastWord = words[words.length - 1]
|
|
2034
|
-
|
|
2035
|
-
if (!isClosingPunctuation(lastWord.word)) {
|
|
2036
|
-
lastWord.word += ' '
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
} else if (text === ' ' && words.length > 0) {
|
|
2040
|
-
const lastWord = words[words.length - 1]
|
|
2041
|
-
|
|
2042
|
-
if (!lastWord.word.endsWith(' ')) {
|
|
2043
|
-
lastWord.word += ' '
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
this.flushWords(words)
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
/**
|
|
2053
|
-
* Try to merge text that follows an atomic unit (ERB/inline) with no whitespace
|
|
2054
|
-
* Returns true if merge was performed
|
|
2055
|
-
*/
|
|
2056
|
-
private tryMergeTextAfterAtomic(result: ContentUnitWithNode[], textNode: HTMLTextNode): boolean {
|
|
2057
|
-
if (result.length === 0) return false
|
|
2058
|
-
|
|
2059
|
-
const lastUnit = result[result.length - 1]
|
|
2060
|
-
|
|
2061
|
-
if (!lastUnit.unit.isAtomic || (lastUnit.unit.type !== 'erb' && lastUnit.unit.type !== 'inline')) {
|
|
2062
|
-
return false
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
const words = normalizeAndSplitWords(textNode.content)
|
|
2066
|
-
if (words.length === 0 || !words[0]) return false
|
|
2067
|
-
|
|
2068
|
-
const firstWord = words[0]
|
|
2069
|
-
const firstChar = firstWord[0]
|
|
2070
|
-
|
|
2071
|
-
if (' \t\n\r'.includes(firstChar)) {
|
|
2072
|
-
return false
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
lastUnit.unit.content += firstWord
|
|
2076
|
-
|
|
2077
|
-
if (words.length > 1) {
|
|
2078
|
-
let remainingText = words.slice(1).join(' ')
|
|
2079
|
-
|
|
2080
|
-
if (endsWithWhitespace(textNode.content)) {
|
|
2081
|
-
remainingText += ' '
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
result.push({
|
|
2085
|
-
unit: { content: remainingText, type: 'text', isAtomic: false, breaksFlow: false },
|
|
2086
|
-
node: textNode
|
|
2087
|
-
})
|
|
2088
|
-
} else if (endsWithWhitespace(textNode.content)) {
|
|
2089
|
-
result.push({
|
|
2090
|
-
unit: { content: ' ', type: 'text', isAtomic: false, breaksFlow: false },
|
|
2091
|
-
node: textNode
|
|
2092
|
-
})
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
return true
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
/**
|
|
2099
|
-
* Try to merge an atomic unit (ERB/inline) with preceding text that has no whitespace
|
|
2100
|
-
* Returns true if merge was performed
|
|
2101
|
-
*/
|
|
2102
|
-
private tryMergeAtomicAfterText(result: ContentUnitWithNode[], children: Node[], lastProcessedIndex: number, atomicContent: string, atomicType: 'erb' | 'inline', atomicNode: Node): boolean {
|
|
2103
|
-
if (result.length === 0) return false
|
|
2104
|
-
|
|
2105
|
-
const lastUnit = result[result.length - 1]
|
|
2106
|
-
|
|
2107
|
-
if (lastUnit.unit.type !== 'text' || lastUnit.unit.isAtomic) return false
|
|
2108
|
-
|
|
2109
|
-
const words = normalizeAndSplitWords(lastUnit.unit.content)
|
|
2110
|
-
const lastWord = words[words.length - 1]
|
|
2111
|
-
|
|
2112
|
-
if (!lastWord) return false
|
|
2113
|
-
|
|
2114
|
-
result.pop()
|
|
2115
|
-
|
|
2116
|
-
if (words.length > 1) {
|
|
2117
|
-
const remainingText = words.slice(0, -1).join(' ')
|
|
2118
|
-
|
|
2119
|
-
result.push({
|
|
2120
|
-
unit: { content: remainingText, type: 'text', isAtomic: false, breaksFlow: false },
|
|
2121
|
-
node: children[lastProcessedIndex]
|
|
2122
|
-
})
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
result.push({
|
|
2126
|
-
unit: { content: lastWord + atomicContent, type: atomicType, isAtomic: true, breaksFlow: false },
|
|
2127
|
-
node: atomicNode
|
|
2128
|
-
})
|
|
2129
|
-
|
|
2130
|
-
return true
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
/**
|
|
2134
|
-
* Check if there's whitespace between current node and last processed node
|
|
2135
|
-
*/
|
|
2136
|
-
private hasWhitespaceBeforeNode(children: Node[], lastProcessedIndex: number, currentIndex: number, currentNode: Node): boolean {
|
|
2137
|
-
if (hasWhitespaceBetween(children, lastProcessedIndex, currentIndex)) {
|
|
2138
|
-
return true
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
if (isNode(currentNode, HTMLTextNode) && /^[ \t\n\r]/.test(currentNode.content)) {
|
|
2142
|
-
return true
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
return false
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
/**
|
|
2149
|
-
* Check if last unit in result ends with whitespace
|
|
2150
|
-
*/
|
|
2151
|
-
private lastUnitEndsWithWhitespace(result: ContentUnitWithNode[]): boolean {
|
|
2152
|
-
if (result.length === 0) return false
|
|
2153
|
-
|
|
2154
|
-
const lastUnit = result[result.length - 1]
|
|
2155
|
-
|
|
2156
|
-
return lastUnit.unit.type === 'text' && endsWithWhitespace(lastUnit.unit.content)
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
/**
|
|
2160
|
-
* Process a text node and add it to results (with potential merging)
|
|
2161
|
-
*/
|
|
2162
|
-
private processTextNode(result: ContentUnitWithNode[], children: Node[], child: HTMLTextNode, index: number, lastProcessedIndex: number): void {
|
|
2163
|
-
const isAtomic = child.content === ' '
|
|
2164
|
-
|
|
2165
|
-
if (!isAtomic && lastProcessedIndex >= 0 && result.length > 0) {
|
|
2166
|
-
const hasWhitespace = this.hasWhitespaceBeforeNode(children, lastProcessedIndex, index, child)
|
|
2167
|
-
const lastUnit = result[result.length - 1]
|
|
2168
|
-
const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'erb' || lastUnit.unit.type === 'inline')
|
|
2169
|
-
|
|
2170
|
-
if (lastIsAtomic && !hasWhitespace && this.tryMergeTextAfterAtomic(result, child)) {
|
|
2171
|
-
return
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
result.push({
|
|
2176
|
-
unit: { content: child.content, type: 'text', isAtomic, breaksFlow: false },
|
|
2177
|
-
node: child
|
|
2178
|
-
})
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
/**
|
|
2182
|
-
* Process an inline element and add it to results (with potential merging)
|
|
2183
|
-
*/
|
|
2184
|
-
private processInlineElement(result: ContentUnitWithNode[], children: Node[], child: HTMLElementNode, index: number, lastProcessedIndex: number): boolean {
|
|
2185
|
-
const tagName = getTagName(child)
|
|
2186
|
-
const childrenToRender = this.getFilteredChildren(child.body)
|
|
2187
|
-
const inlineContent = this.tryRenderInlineFull(child, tagName, filterNodes(child.open_tag?.children, HTMLAttributeNode), childrenToRender)
|
|
2188
|
-
|
|
2189
|
-
if (inlineContent === null) {
|
|
2190
|
-
result.push({
|
|
2191
|
-
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
2192
|
-
node: child
|
|
2193
|
-
})
|
|
2194
|
-
|
|
2195
|
-
return false
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
if (lastProcessedIndex >= 0) {
|
|
2199
|
-
const hasWhitespace = hasWhitespaceBetween(children, lastProcessedIndex, index) || this.lastUnitEndsWithWhitespace(result)
|
|
2200
|
-
|
|
2201
|
-
if (!hasWhitespace && this.tryMergeAtomicAfterText(result, children, lastProcessedIndex, inlineContent, 'inline', child)) {
|
|
2202
|
-
return true
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
result.push({
|
|
2207
|
-
unit: { content: inlineContent, type: 'inline', isAtomic: true, breaksFlow: false },
|
|
2208
|
-
node: child
|
|
2209
|
-
})
|
|
2210
|
-
|
|
2211
|
-
return false
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
/**
|
|
2215
|
-
* Process an ERB content node and add it to results (with potential merging)
|
|
2216
|
-
*/
|
|
2217
|
-
private processERBContentNode(result: ContentUnitWithNode[], children: Node[], child: ERBContentNode, index: number, lastProcessedIndex: number): boolean {
|
|
2218
|
-
const erbContent = this.renderERBAsString(child)
|
|
2219
|
-
const isHerbDisable = isHerbDisableComment(child)
|
|
2220
|
-
|
|
2221
|
-
if (lastProcessedIndex >= 0) {
|
|
2222
|
-
const hasWhitespace = hasWhitespaceBetween(children, lastProcessedIndex, index) || this.lastUnitEndsWithWhitespace(result)
|
|
2223
|
-
|
|
2224
|
-
if (!hasWhitespace && this.tryMergeAtomicAfterText(result, children, lastProcessedIndex, erbContent, 'erb', child)) {
|
|
2225
|
-
return true
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
if (hasWhitespace && result.length > 0) {
|
|
2229
|
-
const lastUnit = result[result.length - 1]
|
|
2230
|
-
const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'inline' || lastUnit.unit.type === 'erb')
|
|
2231
|
-
|
|
2232
|
-
if (lastIsAtomic && !this.lastUnitEndsWithWhitespace(result)) {
|
|
2233
|
-
result.push({
|
|
2234
|
-
unit: { content: ' ', type: 'text', isAtomic: true, breaksFlow: false },
|
|
2235
|
-
node: null
|
|
2236
|
-
})
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
result.push({
|
|
2242
|
-
unit: { content: erbContent, type: 'erb', isAtomic: true, breaksFlow: false, isHerbDisable },
|
|
2243
|
-
node: child
|
|
2244
|
-
})
|
|
2245
|
-
|
|
2246
|
-
return false
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
/**
|
|
2250
|
-
* Convert AST nodes to content units with node references
|
|
2251
|
-
*/
|
|
2252
|
-
private buildContentUnitsWithNodes(children: Node[]): ContentUnitWithNode[] {
|
|
2253
|
-
const result: ContentUnitWithNode[] = []
|
|
2254
|
-
let lastProcessedIndex = -1
|
|
2255
|
-
|
|
2256
|
-
for (let i = 0; i < children.length; i++) {
|
|
2257
|
-
const child = children[i]
|
|
2258
|
-
|
|
2259
|
-
if (isNode(child, WhitespaceNode)) continue
|
|
2260
|
-
|
|
2261
|
-
if (isPureWhitespaceNode(child) && !(isNode(child, HTMLTextNode) && child.content === ' ')) {
|
|
2262
|
-
if (lastProcessedIndex >= 0) {
|
|
2263
|
-
const hasNonWhitespaceAfter = children.slice(i + 1).some(node =>
|
|
2264
|
-
!isNode(node, WhitespaceNode) && !isPureWhitespaceNode(node)
|
|
2265
|
-
)
|
|
2266
|
-
|
|
2267
|
-
if (hasNonWhitespaceAfter) {
|
|
2268
|
-
const previousNode = children[lastProcessedIndex]
|
|
2269
|
-
|
|
2270
|
-
if (!isLineBreakingElement(previousNode)) {
|
|
2271
|
-
result.push({
|
|
2272
|
-
unit: { content: ' ', type: 'text', isAtomic: true, breaksFlow: false },
|
|
2273
|
-
node: child
|
|
2274
|
-
})
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
continue
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
if (isNode(child, HTMLTextNode)) {
|
|
2283
|
-
this.processTextNode(result, children, child, i, lastProcessedIndex)
|
|
2284
|
-
|
|
2285
|
-
lastProcessedIndex = i
|
|
2286
|
-
} else if (isNode(child, HTMLElementNode)) {
|
|
2287
|
-
const tagName = getTagName(child)
|
|
2288
|
-
|
|
2289
|
-
if (isInlineElement(tagName)) {
|
|
2290
|
-
const merged = this.processInlineElement(result, children, child, i, lastProcessedIndex)
|
|
2291
|
-
|
|
2292
|
-
if (merged) {
|
|
2293
|
-
lastProcessedIndex = i
|
|
2294
|
-
|
|
2295
|
-
continue
|
|
2296
|
-
}
|
|
2297
|
-
} else {
|
|
2298
|
-
result.push({
|
|
2299
|
-
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
2300
|
-
node: child
|
|
2301
|
-
})
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
lastProcessedIndex = i
|
|
2305
|
-
} else if (isNode(child, ERBContentNode)) {
|
|
2306
|
-
const merged = this.processERBContentNode(result, children, child, i, lastProcessedIndex)
|
|
2307
|
-
|
|
2308
|
-
if (merged) {
|
|
2309
|
-
lastProcessedIndex = i
|
|
2310
|
-
|
|
2311
|
-
continue
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
lastProcessedIndex = i
|
|
2315
|
-
} else {
|
|
2316
|
-
result.push({
|
|
2317
|
-
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
2318
|
-
node: child
|
|
2319
|
-
})
|
|
2320
|
-
|
|
2321
|
-
lastProcessedIndex = i
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
return result
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
/**
|
|
2329
|
-
* Flush accumulated words to output with wrapping
|
|
2330
|
-
*/
|
|
2331
|
-
private flushWords(words: Array<{ word: string, isHerbDisable: boolean }>): void {
|
|
2332
|
-
if (words.length > 0) {
|
|
2333
|
-
this.wrapAndPushWords(words)
|
|
2334
|
-
words.length = 0
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
/**
|
|
2339
|
-
* Wrap words to fit within line length and push to output
|
|
2340
|
-
* Handles punctuation spacing intelligently
|
|
2341
|
-
* Excludes herb:disable comments from line length calculations
|
|
2342
|
-
*/
|
|
2343
|
-
private wrapAndPushWords(words: Array<{ word: string, isHerbDisable: boolean }>): void {
|
|
2344
|
-
const wrapWidth = this.maxLineLength - this.indent.length
|
|
2345
|
-
const lines: string[] = []
|
|
2346
|
-
let currentLine = ""
|
|
2347
|
-
let effectiveLength = 0
|
|
2348
|
-
|
|
2349
|
-
for (const { word, isHerbDisable } of words) {
|
|
2350
|
-
const nextLine = buildLineWithWord(currentLine, word)
|
|
2351
|
-
|
|
2352
|
-
let nextEffectiveLength = effectiveLength
|
|
2353
|
-
|
|
2354
|
-
if (!isHerbDisable) {
|
|
2355
|
-
const spaceBefore = currentLine && needsSpaceBetween(currentLine, word) ? 1 : 0
|
|
2356
|
-
nextEffectiveLength = effectiveLength + spaceBefore + word.length
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
if (currentLine && !isClosingPunctuation(word) && nextEffectiveLength >= wrapWidth) {
|
|
2360
|
-
lines.push(this.indent + currentLine.trimEnd())
|
|
2361
|
-
|
|
2362
|
-
currentLine = word
|
|
2363
|
-
effectiveLength = isHerbDisable ? 0 : word.length
|
|
2364
|
-
} else {
|
|
2365
|
-
currentLine = nextLine
|
|
2366
|
-
effectiveLength = nextEffectiveLength
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
if (currentLine) {
|
|
2371
|
-
lines.push(this.indent + currentLine.trimEnd())
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
lines.forEach(line => this.push(line))
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
private isInTextFlowContext(_parent: Node | null, children: Node[]): boolean {
|
|
2378
|
-
const hasTextContent = children.some(child => isNode(child, HTMLTextNode) &&child.content.trim() !== "")
|
|
2379
|
-
const nonTextChildren = children.filter(child => !isNode(child, HTMLTextNode))
|
|
2380
|
-
|
|
2381
|
-
if (!hasTextContent) return false
|
|
2382
|
-
if (nonTextChildren.length === 0) return false
|
|
2383
|
-
|
|
2384
|
-
const allInline = nonTextChildren.every(child => {
|
|
2385
|
-
if (isNode(child, ERBContentNode)) return true
|
|
2386
|
-
|
|
2387
|
-
if (isNode(child, HTMLElementNode)) {
|
|
2388
|
-
return isInlineElement(getTagName(child))
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
return false
|
|
2392
|
-
})
|
|
2393
|
-
|
|
2394
|
-
if (!allInline) return false
|
|
2395
|
-
|
|
2396
|
-
return true
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean, inlineNodes: Node[] = [], allChildren: Node[] = []): string {
|
|
2400
|
-
const parts = attributes.map(attribute => this.renderAttribute(attribute))
|
|
2401
|
-
|
|
2402
|
-
if (inlineNodes.length > 0) {
|
|
2403
|
-
let result = `<${name}`
|
|
1473
|
+
if (inlineNodes.length > 0) {
|
|
1474
|
+
let result = `<${name}`
|
|
2404
1475
|
|
|
2405
1476
|
if (allChildren.length > 0) {
|
|
2406
1477
|
const lines = this.capture(() => {
|
|
2407
1478
|
allChildren.forEach(child => {
|
|
2408
1479
|
if (isNode(child, HTMLAttributeNode)) {
|
|
2409
|
-
this.lines.push(" " + this.renderAttribute(child))
|
|
1480
|
+
this.lines.push(" " + this.attributeRenderer.renderAttribute(child, name))
|
|
2410
1481
|
} else if (!(isNode(child, WhitespaceNode))) {
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
this.lines.push(" ")
|
|
2416
|
-
this.visit(child)
|
|
2417
|
-
this.inlineMode = wasInlineMode
|
|
1482
|
+
this.withInlineMode(() => {
|
|
1483
|
+
this.lines.push(" ")
|
|
1484
|
+
this.visit(child)
|
|
1485
|
+
})
|
|
2418
1486
|
}
|
|
2419
1487
|
})
|
|
2420
1488
|
})
|
|
@@ -2427,15 +1495,11 @@ export class FormatPrinter extends Printer {
|
|
|
2427
1495
|
|
|
2428
1496
|
const lines = this.capture(() => {
|
|
2429
1497
|
inlineNodes.forEach(node => {
|
|
2430
|
-
const wasInlineMode = this.inlineMode
|
|
2431
|
-
|
|
2432
1498
|
if (!isERBControlFlowNode(node)) {
|
|
2433
|
-
this.
|
|
1499
|
+
this.withInlineMode(() => this.visit(node))
|
|
1500
|
+
} else {
|
|
1501
|
+
this.visit(node)
|
|
2434
1502
|
}
|
|
2435
|
-
|
|
2436
|
-
this.visit(node)
|
|
2437
|
-
|
|
2438
|
-
this.inlineMode = wasInlineMode
|
|
2439
1503
|
})
|
|
2440
1504
|
})
|
|
2441
1505
|
|
|
@@ -2450,70 +1514,14 @@ export class FormatPrinter extends Printer {
|
|
|
2450
1514
|
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " />" : ">"}`
|
|
2451
1515
|
}
|
|
2452
1516
|
|
|
2453
|
-
renderAttribute(attribute: HTMLAttributeNode): string {
|
|
2454
|
-
const name = attribute.name ? getCombinedAttributeName(attribute.name) : ""
|
|
2455
|
-
const equals = attribute.equals?.value ?? ""
|
|
2456
|
-
|
|
2457
|
-
this.currentAttributeName = name
|
|
2458
|
-
|
|
2459
|
-
let value = ""
|
|
2460
|
-
|
|
2461
|
-
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
2462
|
-
const attributeValue = attribute.value
|
|
2463
|
-
|
|
2464
|
-
let open_quote = attributeValue.open_quote?.value ?? ""
|
|
2465
|
-
let close_quote = attributeValue.close_quote?.value ?? ""
|
|
2466
|
-
let htmlTextContent = ""
|
|
2467
|
-
|
|
2468
|
-
const content = attributeValue.children.map((child: Node) => {
|
|
2469
|
-
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
2470
|
-
htmlTextContent += child.content
|
|
2471
|
-
|
|
2472
|
-
return child.content
|
|
2473
|
-
} else if (isNode(child, ERBContentNode)) {
|
|
2474
|
-
return this.reconstructERBNode(child, true)
|
|
2475
|
-
} else {
|
|
2476
|
-
const printed = IdentityPrinter.print(child)
|
|
2477
|
-
|
|
2478
|
-
if (this.isInTokenListAttribute) {
|
|
2479
|
-
return printed.replace(/%>([^<\s])/g, '%> $1').replace(/([^>\s])<%/g, '$1 <%')
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
|
-
return printed
|
|
2483
|
-
}
|
|
2484
|
-
}).join("")
|
|
2485
|
-
|
|
2486
|
-
if (open_quote === "" && close_quote === "") {
|
|
2487
|
-
open_quote = '"'
|
|
2488
|
-
close_quote = '"'
|
|
2489
|
-
} else if (open_quote === "'" && close_quote === "'" && !htmlTextContent.includes('"')) {
|
|
2490
|
-
open_quote = '"'
|
|
2491
|
-
close_quote = '"'
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
if (this.isFormattableAttribute(name, this.currentTagName)) {
|
|
2495
|
-
if (name === 'class') {
|
|
2496
|
-
value = this.formatClassAttribute(content, name, equals, open_quote, close_quote)
|
|
2497
|
-
} else {
|
|
2498
|
-
value = this.formatMultilineAttribute(content, name, open_quote, close_quote)
|
|
2499
|
-
}
|
|
2500
|
-
} else {
|
|
2501
|
-
value = open_quote + content + close_quote
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
this.currentAttributeName = null
|
|
2506
|
-
|
|
2507
|
-
return name + equals + value
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
1517
|
/**
|
|
2511
1518
|
* Try to render a complete element inline including opening tag, children, and closing tag
|
|
2512
1519
|
*/
|
|
2513
1520
|
private tryRenderInlineFull(_node: HTMLElementNode, tagName: string, attributes: HTMLAttributeNode[], children: Node[]): string | null {
|
|
2514
1521
|
let result = `<${tagName}`
|
|
2515
1522
|
|
|
2516
|
-
|
|
1523
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1524
|
+
result += this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
2517
1525
|
result += ">"
|
|
2518
1526
|
|
|
2519
1527
|
const childrenContent = this.tryRenderChildrenInline(children, tagName)
|
|
@@ -2526,21 +1534,6 @@ export class FormatPrinter extends Printer {
|
|
|
2526
1534
|
return result
|
|
2527
1535
|
}
|
|
2528
1536
|
|
|
2529
|
-
/**
|
|
2530
|
-
* Check if children contain a leading herb:disable comment (after optional whitespace)
|
|
2531
|
-
*/
|
|
2532
|
-
private hasLeadingHerbDisable(children: Node[]): boolean {
|
|
2533
|
-
for (const child of children) {
|
|
2534
|
-
if (isNode(child, WhitespaceNode) || (isNode(child, HTMLTextNode) && child.content.trim() === "")) {
|
|
2535
|
-
continue
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
return isNode(child, ERBContentNode) && isHerbDisableComment(child)
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
return false
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
1537
|
/**
|
|
2545
1538
|
* Try to render just the children inline (without tags)
|
|
2546
1539
|
*/
|
|
@@ -2549,7 +1542,7 @@ export class FormatPrinter extends Printer {
|
|
|
2549
1542
|
let hasInternalWhitespace = false
|
|
2550
1543
|
let addedLeadingSpace = false
|
|
2551
1544
|
|
|
2552
|
-
const hasHerbDisable =
|
|
1545
|
+
const hasHerbDisable = hasLeadingHerbDisable(children)
|
|
2553
1546
|
const hasOnlyTextContent = children.every(child => isNode(child, HTMLTextNode) || isNode(child, WhitespaceNode))
|
|
2554
1547
|
const shouldPreserveSpaces = hasOnlyTextContent && tagName && isInlineElement(tagName)
|
|
2555
1548
|
|
|
@@ -2575,9 +1568,7 @@ export class FormatPrinter extends Printer {
|
|
|
2575
1568
|
}
|
|
2576
1569
|
}
|
|
2577
1570
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
if (isWhitespace && !result.endsWith(' ')) {
|
|
1571
|
+
if (isPureWhitespaceNode(child) && !result.endsWith(' ')) {
|
|
2581
1572
|
if (!result && hasHerbDisable && !addedLeadingSpace) {
|
|
2582
1573
|
result += ' '
|
|
2583
1574
|
addedLeadingSpace = true
|
|
@@ -2594,7 +1585,7 @@ export class FormatPrinter extends Printer {
|
|
|
2594
1585
|
|
|
2595
1586
|
const childrenToRender = this.getFilteredChildren(child.body)
|
|
2596
1587
|
const childInline = this.tryRenderInlineFull(child, tagName,
|
|
2597
|
-
filterNodes(child
|
|
1588
|
+
filterNodes(getOpenTagChildren(child), HTMLAttributeNode),
|
|
2598
1589
|
childrenToRender
|
|
2599
1590
|
)
|
|
2600
1591
|
|
|
@@ -2603,11 +1594,8 @@ export class FormatPrinter extends Printer {
|
|
|
2603
1594
|
}
|
|
2604
1595
|
|
|
2605
1596
|
result += childInline
|
|
2606
|
-
} else if (!isNode(child, HTMLTextNode) && !
|
|
2607
|
-
const
|
|
2608
|
-
this.inlineMode = true
|
|
2609
|
-
const captured = this.capture(() => this.visit(child)).join("")
|
|
2610
|
-
this.inlineMode = wasInlineMode
|
|
1597
|
+
} else if (!isNode(child, HTMLTextNode) && !isPureWhitespaceNode(child)) {
|
|
1598
|
+
const captured = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
2611
1599
|
result += captured
|
|
2612
1600
|
}
|
|
2613
1601
|
}
|
|
@@ -2676,10 +1664,11 @@ export class FormatPrinter extends Printer {
|
|
|
2676
1664
|
for (const child of children) {
|
|
2677
1665
|
if (isNode(child, HTMLTextNode)) {
|
|
2678
1666
|
content += child.content
|
|
2679
|
-
} else if (isNode(child, HTMLElementNode)
|
|
1667
|
+
} else if (isNode(child, HTMLElementNode)) {
|
|
2680
1668
|
const tagName = getTagName(child)
|
|
2681
|
-
const attributes = filterNodes(child
|
|
2682
|
-
|
|
1669
|
+
const attributes = filterNodes(getOpenTagChildren(child), HTMLAttributeNode)
|
|
1670
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1671
|
+
const attributesString = this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
2683
1672
|
const childContent = this.renderElementInline(child)
|
|
2684
1673
|
|
|
2685
1674
|
content += `<${tagName}${attributesString}>${childContent}</${tagName}>`
|