@herb-tools/formatter 0.8.10 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/herb-format.js +57938 -17957
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +23560 -3225
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +23560 -3225
- 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 +31 -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 +447 -1468
- 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,34 @@ import {
|
|
|
89
87
|
ERBUnlessNode,
|
|
90
88
|
ERBYieldNode,
|
|
91
89
|
ERBInNode,
|
|
90
|
+
ERBOpenTagNode,
|
|
91
|
+
HTMLVirtualCloseTagNode,
|
|
92
92
|
XMLDeclarationNode,
|
|
93
93
|
CDATANode,
|
|
94
94
|
Token
|
|
95
95
|
} from "@herb-tools/core"
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Gets the children of an open tag, narrowing from the union type.
|
|
99
|
+
* Returns empty array for conditional open tags.
|
|
100
|
+
*/
|
|
101
|
+
function getOpenTagChildren(element: HTMLElementNode): Node[] {
|
|
102
|
+
return isHTMLOpenTagNode(element.open_tag) ? element.open_tag.children : []
|
|
103
|
+
}
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
106
|
+
* Gets the tag_closing token of an open tag, narrowing from the union type.
|
|
107
|
+
* Returns null for conditional open tags.
|
|
103
108
|
*/
|
|
104
|
-
|
|
109
|
+
function getOpenTagClosing(element: HTMLElementNode): Token | null {
|
|
110
|
+
return isHTMLOpenTagNode(element.open_tag) ? element.open_tag.tag_closing : null
|
|
111
|
+
}
|
|
105
112
|
|
|
106
113
|
/**
|
|
107
114
|
* Printer traverses the Herb AST using the Visitor pattern
|
|
108
115
|
* and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
|
|
109
116
|
*/
|
|
110
|
-
export class FormatPrinter extends Printer {
|
|
117
|
+
export class FormatPrinter extends Printer implements TextFlowDelegate, AttributeRendererDelegate {
|
|
111
118
|
/**
|
|
112
119
|
* @deprecated integrate indentWidth into this.options and update FormatOptions to extend from @herb-tools/printer options
|
|
113
120
|
*/
|
|
@@ -116,7 +123,7 @@ export class FormatPrinter extends Printer {
|
|
|
116
123
|
/**
|
|
117
124
|
* @deprecated integrate maxLineLength into this.options and update FormatOptions to extend from @herb-tools/printer options
|
|
118
125
|
*/
|
|
119
|
-
|
|
126
|
+
maxLineLength: number
|
|
120
127
|
|
|
121
128
|
/**
|
|
122
129
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
@@ -124,14 +131,15 @@ export class FormatPrinter extends Printer {
|
|
|
124
131
|
private lines: string[] = []
|
|
125
132
|
private indentLevel: number = 0
|
|
126
133
|
private inlineMode: boolean = false
|
|
127
|
-
private
|
|
134
|
+
private inContentPreservingContext: boolean = false
|
|
135
|
+
private inConditionalOpenTagContext: boolean = false
|
|
128
136
|
private elementStack: HTMLElementNode[] = []
|
|
129
137
|
private elementFormattingAnalysis = new Map<HTMLElementNode, ElementFormattingAnalysis>()
|
|
130
138
|
private nodeIsMultiline = new Map<Node, boolean>()
|
|
131
139
|
private stringLineCount: number = 0
|
|
132
|
-
private
|
|
133
|
-
private
|
|
134
|
-
|
|
140
|
+
private textFlow: TextFlowEngine
|
|
141
|
+
private attributeRenderer: AttributeRenderer
|
|
142
|
+
private spacingAnalyzer: SpacingAnalyzer
|
|
135
143
|
|
|
136
144
|
public source: string
|
|
137
145
|
|
|
@@ -141,6 +149,9 @@ export class FormatPrinter extends Printer {
|
|
|
141
149
|
this.source = source
|
|
142
150
|
this.indentWidth = options.indentWidth
|
|
143
151
|
this.maxLineLength = options.maxLineLength
|
|
152
|
+
this.textFlow = new TextFlowEngine(this)
|
|
153
|
+
this.attributeRenderer = new AttributeRenderer(this, this.maxLineLength, this.indentWidth)
|
|
154
|
+
this.spacingAnalyzer = new SpacingAnalyzer(this.nodeIsMultiline)
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
print(input: Node | ParseResult | Token): string {
|
|
@@ -153,8 +164,7 @@ export class FormatPrinter extends Printer {
|
|
|
153
164
|
this.indentLevel = 0
|
|
154
165
|
this.stringLineCount = 0
|
|
155
166
|
this.nodeIsMultiline.clear()
|
|
156
|
-
this.
|
|
157
|
-
this.allSingleLineCache.clear()
|
|
167
|
+
this.spacingAnalyzer.clear()
|
|
158
168
|
|
|
159
169
|
this.visit(node)
|
|
160
170
|
|
|
@@ -172,7 +182,7 @@ export class FormatPrinter extends Printer {
|
|
|
172
182
|
* Get the current tag name from the current element context
|
|
173
183
|
*/
|
|
174
184
|
private get currentTagName(): string {
|
|
175
|
-
return this.currentElement?.
|
|
185
|
+
return this.currentElement?.tag_name?.value ?? ""
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/**
|
|
@@ -257,7 +267,7 @@ export class FormatPrinter extends Printer {
|
|
|
257
267
|
/**
|
|
258
268
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
259
269
|
*/
|
|
260
|
-
|
|
270
|
+
push(line: string) {
|
|
261
271
|
this.lines.push(line)
|
|
262
272
|
this.stringLineCount++
|
|
263
273
|
}
|
|
@@ -265,7 +275,7 @@ export class FormatPrinter extends Printer {
|
|
|
265
275
|
/**
|
|
266
276
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
267
277
|
*/
|
|
268
|
-
|
|
278
|
+
pushWithIndent(line: string) {
|
|
269
279
|
const indent = line.trim() === "" ? "" : this.indent
|
|
270
280
|
|
|
271
281
|
this.push(indent + line)
|
|
@@ -279,7 +289,25 @@ export class FormatPrinter extends Printer {
|
|
|
279
289
|
return result
|
|
280
290
|
}
|
|
281
291
|
|
|
282
|
-
private
|
|
292
|
+
private withInlineMode<T>(callback: () => T): T {
|
|
293
|
+
const was = this.inlineMode
|
|
294
|
+
this.inlineMode = true
|
|
295
|
+
const result = callback()
|
|
296
|
+
this.inlineMode = was
|
|
297
|
+
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private withContentPreserving<T>(callback: () => T): T {
|
|
302
|
+
const was = this.inContentPreservingContext
|
|
303
|
+
this.inContentPreservingContext = true
|
|
304
|
+
const result = callback()
|
|
305
|
+
this.inContentPreservingContext = was
|
|
306
|
+
|
|
307
|
+
return result
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
get indent(): string {
|
|
283
311
|
return " ".repeat(this.indentLevel * this.indentWidth)
|
|
284
312
|
}
|
|
285
313
|
|
|
@@ -289,11 +317,11 @@ export class FormatPrinter extends Printer {
|
|
|
289
317
|
* and a trailing space (or newline for heredoc content starting with "<<").
|
|
290
318
|
*/
|
|
291
319
|
private formatERBContent(content: string): string {
|
|
292
|
-
|
|
320
|
+
const trimmedContent = content.trim();
|
|
293
321
|
|
|
294
322
|
// See: https://github.com/marcoroth/herb/issues/476
|
|
295
323
|
// TODO: revisit once we have access to Prism nodes
|
|
296
|
-
|
|
324
|
+
const suffix = trimmedContent.startsWith("<<") ? "\n" : " "
|
|
297
325
|
|
|
298
326
|
return trimmedContent ? ` ${trimmedContent}${suffix}` : ""
|
|
299
327
|
}
|
|
@@ -323,401 +351,6 @@ export class FormatPrinter extends Printer {
|
|
|
323
351
|
return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode))
|
|
324
352
|
}
|
|
325
353
|
|
|
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
354
|
/**
|
|
722
355
|
* Render multiline attributes for a tag
|
|
723
356
|
*/
|
|
@@ -731,11 +364,10 @@ export class FormatPrinter extends Printer {
|
|
|
731
364
|
if (herbDisableComments.length > 0) {
|
|
732
365
|
const commentLines = this.capture(() => {
|
|
733
366
|
herbDisableComments.forEach(comment => {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.inlineMode = wasInlineMode
|
|
367
|
+
this.withInlineMode(() => {
|
|
368
|
+
this.lines.push(" ")
|
|
369
|
+
this.visit(comment)
|
|
370
|
+
})
|
|
739
371
|
})
|
|
740
372
|
})
|
|
741
373
|
|
|
@@ -745,9 +377,10 @@ export class FormatPrinter extends Printer {
|
|
|
745
377
|
this.pushWithIndent(openingLine)
|
|
746
378
|
|
|
747
379
|
this.withIndent(() => {
|
|
380
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
748
381
|
allChildren.forEach(child => {
|
|
749
382
|
if (isNode(child, HTMLAttributeNode)) {
|
|
750
|
-
this.pushWithIndent(this.renderAttribute(child))
|
|
383
|
+
this.pushWithIndent(this.attributeRenderer.renderAttribute(child, tagName))
|
|
751
384
|
} else if (!isNode(child, WhitespaceNode)) {
|
|
752
385
|
if (isNode(child, ERBContentNode) && isHerbDisableComment(child)) {
|
|
753
386
|
return
|
|
@@ -769,7 +402,7 @@ export class FormatPrinter extends Printer {
|
|
|
769
402
|
* Reconstruct the text representation of an ERB node
|
|
770
403
|
* @param withFormatting - if true, format the content; if false, preserve original
|
|
771
404
|
*/
|
|
772
|
-
|
|
405
|
+
reconstructERBNode(node: ERBNode, withFormatting: boolean = true): string {
|
|
773
406
|
const open = node.tag_opening?.value ?? ""
|
|
774
407
|
const close = node.tag_closing?.value ?? ""
|
|
775
408
|
const content = node.content?.value ?? ""
|
|
@@ -792,15 +425,12 @@ export class FormatPrinter extends Printer {
|
|
|
792
425
|
|
|
793
426
|
visitDocumentNode(node: DocumentNode) {
|
|
794
427
|
const children = this.formatFrontmatter(node)
|
|
795
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
428
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(children)
|
|
796
429
|
|
|
797
430
|
if (hasTextFlow) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
this.visitTextFlowChildren(children)
|
|
802
|
-
|
|
803
|
-
this.inlineMode = wasInlineMode
|
|
431
|
+
this.withInlineMode(() => {
|
|
432
|
+
this.textFlow.visitTextFlowChildren(children)
|
|
433
|
+
})
|
|
804
434
|
|
|
805
435
|
return
|
|
806
436
|
}
|
|
@@ -834,7 +464,7 @@ export class FormatPrinter extends Printer {
|
|
|
834
464
|
this.visit(child)
|
|
835
465
|
|
|
836
466
|
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
837
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
467
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
838
468
|
|
|
839
469
|
if (shouldAddSpacing) {
|
|
840
470
|
this.lines.splice(childStartLine, 0, "")
|
|
@@ -872,43 +502,104 @@ export class FormatPrinter extends Printer {
|
|
|
872
502
|
this.elementStack.pop()
|
|
873
503
|
}
|
|
874
504
|
|
|
875
|
-
|
|
876
|
-
|
|
505
|
+
visitHTMLConditionalElementNode(node: HTMLConditionalElementNode) {
|
|
506
|
+
this.trackBoundary(node, () => {
|
|
507
|
+
if (node.open_conditional) {
|
|
508
|
+
this.visit(node.open_conditional)
|
|
509
|
+
}
|
|
877
510
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (isNode(child, HTMLElementNode)) {
|
|
881
|
-
const wasInlineMode = this.inlineMode
|
|
882
|
-
this.inlineMode = true
|
|
511
|
+
if (node.body.length > 0) {
|
|
512
|
+
this.push("")
|
|
883
513
|
|
|
884
|
-
|
|
885
|
-
|
|
514
|
+
this.withIndent(() => {
|
|
515
|
+
for (const child of node.body) {
|
|
516
|
+
if (!isPureWhitespaceNode(child)) {
|
|
517
|
+
this.visit(child)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
})
|
|
886
521
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
522
|
+
this.push("")
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (node.close_conditional) {
|
|
526
|
+
this.visit(node.close_conditional)
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode) {
|
|
532
|
+
const wasInConditionalOpenTagContext = this.inConditionalOpenTagContext
|
|
533
|
+
this.inConditionalOpenTagContext = true
|
|
892
534
|
|
|
535
|
+
this.trackBoundary(node, () => {
|
|
536
|
+
if (node.conditional) {
|
|
537
|
+
this.visit(node.conditional)
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
this.inConditionalOpenTagContext = wasInConditionalOpenTagContext
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
visitHTMLElementBody(body: Node[], element: HTMLElementNode) {
|
|
545
|
+
if (isContentPreserving(element) || this.inContentPreservingContext) {
|
|
546
|
+
this.visitContentPreservingBody(element)
|
|
893
547
|
return
|
|
894
548
|
}
|
|
895
549
|
|
|
550
|
+
const tagName = getTagName(element)
|
|
896
551
|
const analysis = this.elementFormattingAnalysis.get(element)
|
|
897
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
552
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(body)
|
|
898
553
|
const children = filterSignificantChildren(body)
|
|
899
554
|
|
|
900
555
|
if (analysis?.elementContentInline) {
|
|
901
|
-
|
|
556
|
+
this.visitInlineElementBody(body, tagName, hasTextFlow, children)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (children.length === 0) return
|
|
561
|
+
|
|
562
|
+
const { comment, hasLeadingWhitespace, remainingChildren, remainingBody } = this.stripLeadingHerbDisable(children, body)
|
|
563
|
+
|
|
564
|
+
if (comment) {
|
|
565
|
+
const herbDisableString = this.captureHerbDisableInline(comment)
|
|
566
|
+
this.pushToLastLine((hasLeadingWhitespace ? ' ' : '') + herbDisableString)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (remainingChildren.length === 0) return
|
|
570
|
+
|
|
571
|
+
this.withIndent(() => {
|
|
572
|
+
if (hasTextFlow) {
|
|
573
|
+
this.textFlow.visitTextFlowChildren(remainingBody)
|
|
574
|
+
} else {
|
|
575
|
+
this.visitElementChildren(comment ? remainingChildren : body, element)
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private visitContentPreservingBody(element: HTMLElementNode) {
|
|
581
|
+
this.withContentPreserving(() => {
|
|
582
|
+
element.body.map(child => {
|
|
583
|
+
if (isNode(child, HTMLElementNode)) {
|
|
584
|
+
const formattedElement = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
585
|
+
this.pushToLastLine(formattedElement)
|
|
586
|
+
} else {
|
|
587
|
+
this.pushToLastLine(IdentityPrinter.print(child))
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
}
|
|
902
592
|
|
|
903
|
-
|
|
904
|
-
|
|
593
|
+
private visitInlineElementBody(body: Node[], tagName: string, hasTextFlow: boolean, children: Node[]) {
|
|
594
|
+
if (children.length === 0) return
|
|
905
595
|
|
|
906
|
-
|
|
907
|
-
const shouldPreserveSpaces = hasOnlyTextContent && isInlineElement(tagName)
|
|
596
|
+
const nodesToRender = hasTextFlow ? body : children
|
|
908
597
|
|
|
909
|
-
|
|
598
|
+
const hasOnlyTextContent = nodesToRender.every(child => isNode(child, HTMLTextNode) || isNode(child, WhitespaceNode))
|
|
599
|
+
const shouldPreserveSpaces = hasOnlyTextContent && isInlineElement(tagName)
|
|
910
600
|
|
|
911
|
-
|
|
601
|
+
const lines = this.withInlineMode(() => {
|
|
602
|
+
return this.capture(() => {
|
|
912
603
|
nodesToRender.forEach(child => {
|
|
913
604
|
if (isNode(child, HTMLTextNode)) {
|
|
914
605
|
if (hasTextFlow) {
|
|
@@ -941,29 +632,28 @@ export class FormatPrinter extends Printer {
|
|
|
941
632
|
}
|
|
942
633
|
})
|
|
943
634
|
})
|
|
635
|
+
})
|
|
944
636
|
|
|
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
|
-
}
|
|
637
|
+
const content = lines.join('')
|
|
954
638
|
|
|
955
|
-
|
|
639
|
+
const inlineContent = shouldPreserveSpaces
|
|
640
|
+
? (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ') : content)
|
|
641
|
+
: (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ').trim() : content.trim())
|
|
956
642
|
|
|
957
|
-
|
|
643
|
+
if (inlineContent) {
|
|
644
|
+
this.pushToLastLine(inlineContent)
|
|
958
645
|
}
|
|
646
|
+
}
|
|
959
647
|
|
|
960
|
-
|
|
961
|
-
|
|
648
|
+
private stripLeadingHerbDisable(children: Node[], body: Node[]): {
|
|
649
|
+
comment: Node | null
|
|
650
|
+
hasLeadingWhitespace: boolean
|
|
651
|
+
remainingChildren: Node[]
|
|
652
|
+
remainingBody: Node[]
|
|
653
|
+
} {
|
|
962
654
|
let leadingHerbDisableComment: Node | null = null
|
|
963
655
|
let leadingHerbDisableIndex = -1
|
|
964
656
|
let firstWhitespaceIndex = -1
|
|
965
|
-
let remainingChildren = children
|
|
966
|
-
let remainingBodyUnfiltered = body
|
|
967
657
|
|
|
968
658
|
for (let i = 0; i < children.length; i++) {
|
|
969
659
|
const child = children[i]
|
|
@@ -984,60 +674,30 @@ export class FormatPrinter extends Printer {
|
|
|
984
674
|
break
|
|
985
675
|
}
|
|
986
676
|
|
|
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
|
-
})
|
|
677
|
+
if (!leadingHerbDisableComment || leadingHerbDisableIndex < 0) {
|
|
678
|
+
return { comment: null, hasLeadingWhitespace: false, remainingChildren: children, remainingBody: body }
|
|
679
|
+
}
|
|
1001
680
|
|
|
1002
|
-
|
|
1003
|
-
|
|
681
|
+
const filterOut = (nodes: Node[]) => nodes.filter((_, index) => {
|
|
682
|
+
if (index === leadingHerbDisableIndex) return false
|
|
1004
683
|
|
|
1005
|
-
|
|
1006
|
-
|
|
684
|
+
if (firstWhitespaceIndex >= 0 && index === leadingHerbDisableIndex - 1) {
|
|
685
|
+
const child = nodes[index]
|
|
1007
686
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
687
|
+
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
688
|
+
return false
|
|
1011
689
|
}
|
|
690
|
+
}
|
|
1012
691
|
|
|
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
|
|
692
|
+
return true
|
|
693
|
+
})
|
|
1028
694
|
|
|
1029
|
-
|
|
695
|
+
return {
|
|
696
|
+
comment: leadingHerbDisableComment,
|
|
697
|
+
hasLeadingWhitespace: firstWhitespaceIndex >= 0 && firstWhitespaceIndex < leadingHerbDisableIndex,
|
|
698
|
+
remainingChildren: filterOut(children),
|
|
699
|
+
remainingBody: filterOut(body),
|
|
1030
700
|
}
|
|
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
701
|
}
|
|
1042
702
|
|
|
1043
703
|
/**
|
|
@@ -1053,9 +713,7 @@ export class FormatPrinter extends Printer {
|
|
|
1053
713
|
const child = body[index]
|
|
1054
714
|
|
|
1055
715
|
if (isNode(child, HTMLTextNode)) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (isWhitespaceOnly) {
|
|
716
|
+
if (isPureWhitespaceNode(child)) {
|
|
1059
717
|
const hasPreviousNonWhitespace = index > 0 && isNonWhitespaceNode(body[index - 1])
|
|
1060
718
|
const hasNextNonWhitespace = index < body.length - 1 && isNonWhitespaceNode(body[index + 1])
|
|
1061
719
|
const hasMultipleNewlines = child.content.includes('\n\n')
|
|
@@ -1071,59 +729,99 @@ export class FormatPrinter extends Printer {
|
|
|
1071
729
|
|
|
1072
730
|
if (!isNonWhitespaceNode(child)) continue
|
|
1073
731
|
|
|
1074
|
-
|
|
732
|
+
const textFlowResult = this.visitTextFlowRunInChildren(body, index, lastMeaningfulNode, hasHandledSpacing)
|
|
1075
733
|
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
734
|
+
if (textFlowResult) {
|
|
735
|
+
index = textFlowResult.newIndex
|
|
736
|
+
lastMeaningfulNode = textFlowResult.lastMeaningfulNode
|
|
737
|
+
hasHandledSpacing = textFlowResult.hasHandledSpacing
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
1079
740
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
741
|
+
const herbDisableResult: ChildVisitResult | null =
|
|
742
|
+
isNode(child, HTMLElementNode) && child.close_tag
|
|
743
|
+
? this.visitChildWithTrailingHerbDisable(child, body, index, parentElement, lastMeaningfulNode, hasHandledSpacing)
|
|
744
|
+
: null
|
|
1083
745
|
|
|
1084
|
-
|
|
1085
|
-
|
|
746
|
+
if (herbDisableResult) {
|
|
747
|
+
index = herbDisableResult.newIndex
|
|
748
|
+
lastMeaningfulNode = herbDisableResult.lastMeaningfulNode
|
|
749
|
+
hasHandledSpacing = herbDisableResult.hasHandledSpacing
|
|
750
|
+
continue
|
|
751
|
+
}
|
|
1086
752
|
|
|
1087
|
-
|
|
1088
|
-
|
|
753
|
+
if (shouldAppendToLastLine(child, body, index)) {
|
|
754
|
+
this.appendChildToLastLine(child, body, index)
|
|
755
|
+
lastMeaningfulNode = child
|
|
756
|
+
hasHandledSpacing = false
|
|
757
|
+
continue
|
|
758
|
+
}
|
|
1089
759
|
|
|
1090
|
-
|
|
1091
|
-
|
|
760
|
+
const childStartLine = this.stringLineCount
|
|
761
|
+
this.visit(child)
|
|
1092
762
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
763
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
764
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
765
|
+
|
|
766
|
+
if (shouldAddSpacing) {
|
|
767
|
+
this.lines.splice(childStartLine, 0, "")
|
|
768
|
+
this.stringLineCount++
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
lastMeaningfulNode = child
|
|
773
|
+
hasHandledSpacing = false
|
|
774
|
+
}
|
|
775
|
+
}
|
|
1098
776
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
this.indentLevel = 0
|
|
1102
|
-
this.inlineMode = true
|
|
1103
|
-
this.visit(nextChild)
|
|
1104
|
-
this.inlineMode = false
|
|
1105
|
-
this.indentLevel = savedIndentLevel
|
|
1106
|
-
}).join("")
|
|
777
|
+
private visitTextFlowRunInChildren(body: Node[], index: number, lastMeaningfulNode: Node | null, hasHandledSpacing: boolean): ChildVisitResult | null {
|
|
778
|
+
const child = body[index]
|
|
1107
779
|
|
|
1108
|
-
|
|
780
|
+
if (!isTextFlowNode(child)) return null
|
|
1109
781
|
|
|
1110
|
-
|
|
1111
|
-
lastMeaningfulNode = child
|
|
1112
|
-
hasHandledSpacing = false
|
|
782
|
+
const run = this.textFlow.collectTextFlowRun(body, index)
|
|
1113
783
|
|
|
1114
|
-
|
|
1115
|
-
}
|
|
784
|
+
if (!run) return null
|
|
1116
785
|
|
|
1117
|
-
|
|
1118
|
-
|
|
786
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
787
|
+
const hasBlankLineBefore = this.spacingAnalyzer.hasBlankLineBetween(body, index)
|
|
788
|
+
|
|
789
|
+
if (hasBlankLineBefore) {
|
|
790
|
+
this.push("")
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.textFlow.visitTextFlowChildren(run.nodes)
|
|
795
|
+
|
|
796
|
+
const lastRunNode = run.nodes[run.nodes.length - 1]
|
|
797
|
+
const hasBlankLineInTrailing = isNode(lastRunNode, HTMLTextNode) && lastRunNode.content.includes('\n\n')
|
|
798
|
+
const hasBlankLineAfter = hasBlankLineInTrailing || this.spacingAnalyzer.hasBlankLineBetween(body, run.endIndex)
|
|
799
|
+
|
|
800
|
+
if (hasBlankLineAfter) {
|
|
801
|
+
this.push("")
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
newIndex: run.endIndex - 1,
|
|
806
|
+
lastMeaningfulNode: run.nodes[run.nodes.length - 1],
|
|
807
|
+
hasHandledSpacing: hasBlankLineAfter,
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private visitChildWithTrailingHerbDisable(child: HTMLElementNode, body: Node[], index: number, parentElement: HTMLElementNode | null, lastMeaningfulNode: Node | null, hasHandledSpacing: boolean): ChildVisitResult | null {
|
|
812
|
+
for (let j = index + 1; j < body.length; j++) {
|
|
813
|
+
const nextChild = body[j]
|
|
814
|
+
|
|
815
|
+
if (isNode(nextChild, WhitespaceNode) || isPureWhitespaceNode(nextChild)) {
|
|
816
|
+
continue
|
|
1119
817
|
}
|
|
1120
818
|
|
|
1121
|
-
if (
|
|
819
|
+
if (isNode(nextChild, ERBContentNode) && isHerbDisableComment(nextChild)) {
|
|
1122
820
|
const childStartLine = this.stringLineCount
|
|
1123
821
|
this.visit(child)
|
|
1124
822
|
|
|
1125
823
|
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
1126
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
824
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
1127
825
|
|
|
1128
826
|
if (shouldAddSpacing) {
|
|
1129
827
|
this.lines.splice(childStartLine, 0, "")
|
|
@@ -1131,10 +829,21 @@ export class FormatPrinter extends Printer {
|
|
|
1131
829
|
}
|
|
1132
830
|
}
|
|
1133
831
|
|
|
1134
|
-
|
|
1135
|
-
|
|
832
|
+
const herbDisableString = this.captureHerbDisableInline(nextChild)
|
|
833
|
+
|
|
834
|
+
this.pushToLastLine(' ' + herbDisableString)
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
newIndex: j,
|
|
838
|
+
lastMeaningfulNode: child,
|
|
839
|
+
hasHandledSpacing: false,
|
|
840
|
+
}
|
|
1136
841
|
}
|
|
842
|
+
|
|
843
|
+
break
|
|
1137
844
|
}
|
|
845
|
+
|
|
846
|
+
return null
|
|
1138
847
|
}
|
|
1139
848
|
|
|
1140
849
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode) {
|
|
@@ -1142,6 +851,12 @@ export class FormatPrinter extends Printer {
|
|
|
1142
851
|
const inlineNodes = this.extractInlineNodes(node.children)
|
|
1143
852
|
const isSelfClosing = node.tag_closing?.value === "/>"
|
|
1144
853
|
|
|
854
|
+
if (this.inConditionalOpenTagContext) {
|
|
855
|
+
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children)
|
|
856
|
+
this.push(this.indent + inline)
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
|
|
1145
860
|
if (this.currentElement && this.elementFormattingAnalysis.has(this.currentElement)) {
|
|
1146
861
|
const analysis = this.elementFormattingAnalysis.get(this.currentElement)!
|
|
1147
862
|
|
|
@@ -1159,13 +874,14 @@ export class FormatPrinter extends Printer {
|
|
|
1159
874
|
|
|
1160
875
|
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children)
|
|
1161
876
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
1162
|
-
|
|
877
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
878
|
+
const shouldKeepInline = this.attributeRenderer.shouldRenderInline(
|
|
1163
879
|
totalAttributeCount,
|
|
1164
880
|
inline.length,
|
|
1165
881
|
this.indent.length,
|
|
1166
882
|
this.maxLineLength,
|
|
1167
883
|
false,
|
|
1168
|
-
this.hasMultilineAttributes(attributes),
|
|
884
|
+
this.attributeRenderer.hasMultilineAttributes(attributes),
|
|
1169
885
|
attributes
|
|
1170
886
|
)
|
|
1171
887
|
|
|
@@ -1199,7 +915,7 @@ export class FormatPrinter extends Printer {
|
|
|
1199
915
|
return
|
|
1200
916
|
}
|
|
1201
917
|
|
|
1202
|
-
|
|
918
|
+
const text = node.content.trim()
|
|
1203
919
|
|
|
1204
920
|
if (!text) return
|
|
1205
921
|
|
|
@@ -1224,7 +940,8 @@ export class FormatPrinter extends Printer {
|
|
|
1224
940
|
}
|
|
1225
941
|
|
|
1226
942
|
visitHTMLAttributeNode(node: HTMLAttributeNode) {
|
|
1227
|
-
this.
|
|
943
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
944
|
+
this.pushWithIndent(this.attributeRenderer.renderAttribute(node, this.currentTagName))
|
|
1228
945
|
}
|
|
1229
946
|
|
|
1230
947
|
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode) {
|
|
@@ -1235,119 +952,39 @@ export class FormatPrinter extends Printer {
|
|
|
1235
952
|
this.pushWithIndent(IdentityPrinter.print(node))
|
|
1236
953
|
}
|
|
1237
954
|
|
|
1238
|
-
// TODO: rework
|
|
1239
955
|
visitHTMLCommentNode(node: HTMLCommentNode) {
|
|
1240
956
|
const open = node.comment_start?.value ?? ""
|
|
1241
957
|
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
|
-
}
|
|
958
|
+
const rawInner = node.children && node.children.length > 0
|
|
959
|
+
? extractHTMLCommentContent(node.children)
|
|
960
|
+
: ""
|
|
961
|
+
const inner = rawInner ? formatHTMLCommentInner(rawInner, this.indentWidth) : ""
|
|
1306
962
|
|
|
1307
963
|
this.pushWithIndent(open + inner + close)
|
|
1308
964
|
}
|
|
1309
965
|
|
|
1310
966
|
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 ? "" : " "
|
|
967
|
+
const result = formatERBCommentLines(
|
|
968
|
+
node.tag_opening?.value || "<%#",
|
|
969
|
+
node?.content?.value || "",
|
|
970
|
+
node.tag_closing?.value || "%>"
|
|
971
|
+
)
|
|
1321
972
|
|
|
973
|
+
if (result.type === 'single-line') {
|
|
1322
974
|
if (this.inlineMode) {
|
|
1323
|
-
this.push(
|
|
975
|
+
this.push(result.text)
|
|
1324
976
|
} else {
|
|
1325
|
-
this.pushWithIndent(
|
|
977
|
+
this.pushWithIndent(result.text)
|
|
1326
978
|
}
|
|
979
|
+
} else {
|
|
980
|
+
this.pushWithIndent(result.header)
|
|
1327
981
|
|
|
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
|
-
}
|
|
982
|
+
this.withIndent(() => {
|
|
983
|
+
result.contentLines.forEach(line => this.pushWithIndent(line))
|
|
984
|
+
})
|
|
1337
985
|
|
|
1338
|
-
|
|
986
|
+
this.pushWithIndent(result.footer)
|
|
1339
987
|
}
|
|
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
988
|
}
|
|
1352
989
|
|
|
1353
990
|
visitHTMLDoctypeNode(node: HTMLDoctypeNode) {
|
|
@@ -1370,6 +1007,14 @@ export class FormatPrinter extends Printer {
|
|
|
1370
1007
|
}
|
|
1371
1008
|
}
|
|
1372
1009
|
|
|
1010
|
+
visitERBOpenTagNode(node: ERBOpenTagNode) {
|
|
1011
|
+
this.printERBNode(node)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
visitHTMLVirtualCloseTagNode(_node: HTMLVirtualCloseTagNode) {
|
|
1015
|
+
// Virtual closing tags don't print anything (they are synthetic)
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1373
1018
|
visitERBEndNode(node: ERBEndNode) {
|
|
1374
1019
|
this.printERBNode(node)
|
|
1375
1020
|
}
|
|
@@ -1404,10 +1049,10 @@ export class FormatPrinter extends Printer {
|
|
|
1404
1049
|
this.printERBNode(node)
|
|
1405
1050
|
|
|
1406
1051
|
this.withIndent(() => {
|
|
1407
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
1052
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(node.body)
|
|
1408
1053
|
|
|
1409
1054
|
if (hasTextFlow) {
|
|
1410
|
-
this.visitTextFlowChildren(node.body)
|
|
1055
|
+
this.textFlow.visitTextFlowChildren(node.body)
|
|
1411
1056
|
} else {
|
|
1412
1057
|
this.visitElementChildren(node.body, null)
|
|
1413
1058
|
}
|
|
@@ -1425,9 +1070,10 @@ export class FormatPrinter extends Printer {
|
|
|
1425
1070
|
node.statements.forEach(child => {
|
|
1426
1071
|
if (isNode(child, HTMLAttributeNode)) {
|
|
1427
1072
|
this.lines.push(" ")
|
|
1428
|
-
this.
|
|
1073
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1074
|
+
this.lines.push(this.attributeRenderer.renderAttribute(child, this.currentTagName))
|
|
1429
1075
|
} else {
|
|
1430
|
-
const shouldAddSpaces = this.isInTokenListAttribute
|
|
1076
|
+
const shouldAddSpaces = this.attributeRenderer.isInTokenListAttribute
|
|
1431
1077
|
|
|
1432
1078
|
if (shouldAddSpaces) {
|
|
1433
1079
|
this.lines.push(" ")
|
|
@@ -1442,7 +1088,7 @@ export class FormatPrinter extends Printer {
|
|
|
1442
1088
|
})
|
|
1443
1089
|
|
|
1444
1090
|
const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode))
|
|
1445
|
-
const isTokenList = this.isInTokenListAttribute
|
|
1091
|
+
const isTokenList = this.attributeRenderer.isInTokenListAttribute
|
|
1446
1092
|
|
|
1447
1093
|
if ((hasHTMLAttributes || isTokenList) && node.end_node) {
|
|
1448
1094
|
this.lines.push(" ")
|
|
@@ -1570,7 +1216,12 @@ export class FormatPrinter extends Printer {
|
|
|
1570
1216
|
* Determines if the open tag should be rendered inline
|
|
1571
1217
|
*/
|
|
1572
1218
|
private shouldRenderOpenTagInline(node: HTMLElementNode): boolean {
|
|
1573
|
-
|
|
1219
|
+
if (isNode(node.open_tag, HTMLConditionalOpenTagNode)) {
|
|
1220
|
+
return false
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const openTag = node.open_tag
|
|
1224
|
+
const children = openTag?.children || []
|
|
1574
1225
|
const attributes = filterNodes(children, HTMLAttributeNode)
|
|
1575
1226
|
const inlineNodes = this.extractInlineNodes(children)
|
|
1576
1227
|
const hasERBControlFlow = inlineNodes.some(node => isERBControlFlowNode(node)) || children.some(node => isERBControlFlowNode(node))
|
|
@@ -1579,19 +1230,20 @@ export class FormatPrinter extends Printer {
|
|
|
1579
1230
|
if (hasComplexERB) return false
|
|
1580
1231
|
|
|
1581
1232
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
1582
|
-
|
|
1233
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1234
|
+
const hasMultilineAttrs = this.attributeRenderer.hasMultilineAttributes(attributes)
|
|
1583
1235
|
|
|
1584
1236
|
if (hasMultilineAttrs) return false
|
|
1585
1237
|
|
|
1586
1238
|
const inline = this.renderInlineOpen(
|
|
1587
1239
|
getTagName(node),
|
|
1588
1240
|
attributes,
|
|
1589
|
-
|
|
1241
|
+
openTag?.tag_closing?.value === "/>",
|
|
1590
1242
|
inlineNodes,
|
|
1591
1243
|
children
|
|
1592
1244
|
)
|
|
1593
1245
|
|
|
1594
|
-
return this.shouldRenderInline(
|
|
1246
|
+
return this.attributeRenderer.shouldRenderInline(
|
|
1595
1247
|
totalAttributeCount,
|
|
1596
1248
|
inline.length,
|
|
1597
1249
|
this.indent.length,
|
|
@@ -1609,6 +1261,7 @@ export class FormatPrinter extends Printer {
|
|
|
1609
1261
|
const tagName = getTagName(node)
|
|
1610
1262
|
const children = filterSignificantChildren(node.body)
|
|
1611
1263
|
const openTagInline = this.shouldRenderOpenTagInline(node)
|
|
1264
|
+
const openTagClosing = getOpenTagClosing(node)
|
|
1612
1265
|
|
|
1613
1266
|
if (!openTagInline) return false
|
|
1614
1267
|
if (children.length === 0) return true
|
|
@@ -1623,46 +1276,46 @@ export class FormatPrinter extends Printer {
|
|
|
1623
1276
|
|
|
1624
1277
|
if (hasNonInlineChildElements) return false
|
|
1625
1278
|
|
|
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)) {
|
|
1279
|
+
if (hasLeadingHerbDisable(node.body) && !isInlineElement(tagName)) {
|
|
1639
1280
|
return false
|
|
1640
1281
|
}
|
|
1641
1282
|
|
|
1642
1283
|
if (isInlineElement(tagName)) {
|
|
1643
|
-
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node
|
|
1284
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(getOpenTagChildren(node), HTMLAttributeNode), node.body)
|
|
1644
1285
|
|
|
1645
1286
|
if (fullInlineResult) {
|
|
1646
|
-
|
|
1647
|
-
return totalLength <= this.maxLineLength
|
|
1287
|
+
return this.fitsOnCurrentLine(fullInlineResult)
|
|
1648
1288
|
}
|
|
1649
1289
|
|
|
1650
1290
|
return false
|
|
1651
1291
|
}
|
|
1652
1292
|
|
|
1293
|
+
if (SPACEABLE_CONTAINERS.has(tagName)) {
|
|
1294
|
+
const allChildrenAreERB = children.length > 1 && children.every(child => isERBNode(child))
|
|
1295
|
+
|
|
1296
|
+
if (allChildrenAreERB) return false
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (!isInlineElement(tagName) && openTagClosing) {
|
|
1300
|
+
const first = children[0]
|
|
1301
|
+
const startsOnNewLine = first.location.start.line > openTagClosing.location.end.line
|
|
1302
|
+
const hasLeadingNewline = isNode(first, HTMLTextNode) && /^\s*\n/.test(first.content)
|
|
1303
|
+
const contentStartsOnNewLine = startsOnNewLine || hasLeadingNewline
|
|
1304
|
+
|
|
1305
|
+
if (contentStartsOnNewLine) {
|
|
1306
|
+
return false
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1653
1310
|
const allNestedAreInline = areAllNestedElementsInline(children)
|
|
1654
1311
|
const hasMultilineText = hasMultilineTextContent(children)
|
|
1655
1312
|
const hasMixedContent = hasMixedTextAndInlineContent(children)
|
|
1656
1313
|
|
|
1657
1314
|
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
|
|
1315
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(getOpenTagChildren(node), HTMLAttributeNode), node.body)
|
|
1662
1316
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1317
|
+
if (fullInlineResult && this.fitsOnCurrentLine(fullInlineResult)) {
|
|
1318
|
+
return true
|
|
1666
1319
|
}
|
|
1667
1320
|
}
|
|
1668
1321
|
|
|
@@ -1671,17 +1324,16 @@ export class FormatPrinter extends Printer {
|
|
|
1671
1324
|
if (inlineResult) {
|
|
1672
1325
|
const openTagResult = this.renderInlineOpen(
|
|
1673
1326
|
tagName,
|
|
1674
|
-
filterNodes(node
|
|
1327
|
+
filterNodes(getOpenTagChildren(node), HTMLAttributeNode),
|
|
1675
1328
|
false,
|
|
1676
1329
|
[],
|
|
1677
|
-
node
|
|
1330
|
+
getOpenTagChildren(node)
|
|
1678
1331
|
)
|
|
1679
1332
|
|
|
1680
1333
|
const childrenContent = this.renderChildrenInline(children)
|
|
1681
1334
|
const fullLine = openTagResult + childrenContent + `</${tagName}>`
|
|
1682
|
-
const totalLength = this.indent.length + fullLine.length
|
|
1683
1335
|
|
|
1684
|
-
if (
|
|
1336
|
+
if (this.fitsOnCurrentLine(fullLine)) {
|
|
1685
1337
|
return true
|
|
1686
1338
|
}
|
|
1687
1339
|
}
|
|
@@ -1694,7 +1346,7 @@ export class FormatPrinter extends Printer {
|
|
|
1694
1346
|
*/
|
|
1695
1347
|
private shouldRenderCloseTagInline(node: HTMLElementNode, elementContentInline: boolean): boolean {
|
|
1696
1348
|
if (node.is_void) return true
|
|
1697
|
-
if (node
|
|
1349
|
+
if (getOpenTagClosing(node)?.value === "/>") return true
|
|
1698
1350
|
if (isContentPreserving(node)) return true
|
|
1699
1351
|
|
|
1700
1352
|
const children = filterSignificantChildren(node.body)
|
|
@@ -1707,6 +1359,19 @@ export class FormatPrinter extends Printer {
|
|
|
1707
1359
|
|
|
1708
1360
|
// --- Utility methods ---
|
|
1709
1361
|
|
|
1362
|
+
private captureHerbDisableInline(node: Node): string {
|
|
1363
|
+
return this.capture(() => {
|
|
1364
|
+
const savedIndentLevel = this.indentLevel
|
|
1365
|
+
this.indentLevel = 0
|
|
1366
|
+
this.withInlineMode(() => this.visit(node))
|
|
1367
|
+
this.indentLevel = savedIndentLevel
|
|
1368
|
+
}).join("")
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
private fitsOnCurrentLine(content: string): boolean {
|
|
1372
|
+
return this.indent.length + content.length <= this.maxLineLength
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1710
1375
|
private formatFrontmatter(node: DocumentNode): Node[] {
|
|
1711
1376
|
const firstChild = node.children[0]
|
|
1712
1377
|
const hasFrontmatter = firstChild && isFrontmatter(firstChild)
|
|
@@ -1739,687 +1404,80 @@ export class FormatPrinter extends Printer {
|
|
|
1739
1404
|
}
|
|
1740
1405
|
}
|
|
1741
1406
|
|
|
1742
|
-
const
|
|
1743
|
-
this.inlineMode = true
|
|
1744
|
-
const inlineContent = this.capture(() => this.visit(child)).join("")
|
|
1745
|
-
this.inlineMode = oldInlineMode
|
|
1407
|
+
const inlineContent = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
1746
1408
|
this.pushToLastLine((hasSpaceBefore ? " " : "") + inlineContent)
|
|
1747
1409
|
}
|
|
1748
1410
|
}
|
|
1749
1411
|
|
|
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
|
-
|
|
1761
|
-
return
|
|
1762
|
-
}
|
|
1763
1412
|
|
|
1764
|
-
|
|
1765
|
-
}
|
|
1413
|
+
// --- TextFlowDelegate implementation ---
|
|
1766
1414
|
|
|
1767
1415
|
/**
|
|
1768
|
-
*
|
|
1769
|
-
* Returns the wrapped lines with proper indentation
|
|
1416
|
+
* Render an inline element as a string
|
|
1770
1417
|
*/
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1418
|
+
renderInlineElementAsString(element: HTMLElementNode): string {
|
|
1419
|
+
const tagName = getTagName(element)
|
|
1420
|
+
const tagClosing = getOpenTagClosing(element)
|
|
1774
1421
|
|
|
1775
|
-
|
|
1776
|
-
const
|
|
1422
|
+
if (element.is_void || tagClosing?.value === "/>") {
|
|
1423
|
+
const attributes = filterNodes(getOpenTagChildren(element), HTMLAttributeNode)
|
|
1424
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1425
|
+
const attributesString = this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
1426
|
+
const isSelfClosing = tagClosing?.value === "/>"
|
|
1777
1427
|
|
|
1778
|
-
|
|
1779
|
-
lines.push(this.indent + line)
|
|
1780
|
-
line = word
|
|
1781
|
-
} else {
|
|
1782
|
-
line = testLine
|
|
1783
|
-
}
|
|
1428
|
+
return `<${tagName}${attributesString}${isSelfClosing ? " />" : ">"}`
|
|
1784
1429
|
}
|
|
1785
1430
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1431
|
+
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1432
|
+
|
|
1433
|
+
const childInline = this.tryRenderInlineFull(element, tagName,
|
|
1434
|
+
filterNodes(getOpenTagChildren(element), HTMLAttributeNode),
|
|
1435
|
+
childrenToRender
|
|
1436
|
+
)
|
|
1789
1437
|
|
|
1790
|
-
return
|
|
1438
|
+
return childInline !== null ? childInline : ""
|
|
1791
1439
|
}
|
|
1792
1440
|
|
|
1793
1441
|
/**
|
|
1794
|
-
*
|
|
1795
|
-
* Returns object with merged content and whether processing should stop
|
|
1442
|
+
* Render an ERB node as a string
|
|
1796
1443
|
*/
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
if (combined.length <= wrapWidth) {
|
|
1801
|
-
return {
|
|
1802
|
-
mergedContent: inlineContent + trimmedText,
|
|
1803
|
-
shouldStop: false,
|
|
1804
|
-
wrappedLines: []
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1444
|
+
renderERBAsString(node: ERBContentNode): string {
|
|
1445
|
+
return this.withInlineMode(() => this.capture(() => this.visit(node)).join(""))
|
|
1446
|
+
}
|
|
1807
1447
|
|
|
1808
|
-
|
|
1448
|
+
/**
|
|
1449
|
+
* Try to render an inline element, returning the full inline string or null if it can't be inlined.
|
|
1450
|
+
*/
|
|
1451
|
+
tryRenderInlineElement(element: HTMLElementNode): string | null {
|
|
1452
|
+
const tagName = getTagName(element)
|
|
1453
|
+
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1809
1454
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
mergedContent: inlineContent,
|
|
1813
|
-
shouldStop: false,
|
|
1814
|
-
wrappedLines: []
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1455
|
+
return this.tryRenderInlineFull(element, tagName, filterNodes(getOpenTagChildren(element), HTMLAttributeNode), childrenToRender)
|
|
1456
|
+
}
|
|
1817
1457
|
|
|
1818
|
-
const punctuation = match[0]
|
|
1819
|
-
const restText = trimmedText.substring(punctuation.length).trim()
|
|
1820
1458
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
shouldStop: false,
|
|
1825
|
-
wrappedLines: []
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1459
|
+
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean, inlineNodes: Node[] = [], allChildren: Node[] = []): string {
|
|
1460
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1461
|
+
const parts = attributes.map(attribute => this.attributeRenderer.renderAttribute(attribute, name))
|
|
1828
1462
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
let mergedWordCount = 0
|
|
1463
|
+
if (inlineNodes.length > 0) {
|
|
1464
|
+
let result = `<${name}`
|
|
1832
1465
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1466
|
+
if (allChildren.length > 0) {
|
|
1467
|
+
const lines = this.capture(() => {
|
|
1468
|
+
allChildren.forEach(child => {
|
|
1469
|
+
if (isNode(child, HTMLAttributeNode)) {
|
|
1470
|
+
this.lines.push(" " + this.attributeRenderer.renderAttribute(child, name))
|
|
1471
|
+
} else if (!(isNode(child, WhitespaceNode))) {
|
|
1472
|
+
this.withInlineMode(() => {
|
|
1473
|
+
this.lines.push(" ")
|
|
1474
|
+
this.visit(child)
|
|
1475
|
+
})
|
|
1476
|
+
}
|
|
1477
|
+
})
|
|
1478
|
+
})
|
|
1835
1479
|
|
|
1836
|
-
|
|
1837
|
-
toMerge = testMerge
|
|
1838
|
-
mergedWordCount++
|
|
1839
|
-
} else {
|
|
1840
|
-
break
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
const mergedContent = inlineContent + toMerge
|
|
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}`
|
|
2404
|
-
|
|
2405
|
-
if (allChildren.length > 0) {
|
|
2406
|
-
const lines = this.capture(() => {
|
|
2407
|
-
allChildren.forEach(child => {
|
|
2408
|
-
if (isNode(child, HTMLAttributeNode)) {
|
|
2409
|
-
this.lines.push(" " + this.renderAttribute(child))
|
|
2410
|
-
} else if (!(isNode(child, WhitespaceNode))) {
|
|
2411
|
-
const wasInlineMode = this.inlineMode
|
|
2412
|
-
|
|
2413
|
-
this.inlineMode = true
|
|
2414
|
-
|
|
2415
|
-
this.lines.push(" ")
|
|
2416
|
-
this.visit(child)
|
|
2417
|
-
this.inlineMode = wasInlineMode
|
|
2418
|
-
}
|
|
2419
|
-
})
|
|
2420
|
-
})
|
|
2421
|
-
|
|
2422
|
-
result += lines.join("")
|
|
1480
|
+
result += lines.join("")
|
|
2423
1481
|
} else {
|
|
2424
1482
|
if (parts.length > 0) {
|
|
2425
1483
|
result += ` ${parts.join(" ")}`
|
|
@@ -2427,15 +1485,11 @@ export class FormatPrinter extends Printer {
|
|
|
2427
1485
|
|
|
2428
1486
|
const lines = this.capture(() => {
|
|
2429
1487
|
inlineNodes.forEach(node => {
|
|
2430
|
-
const wasInlineMode = this.inlineMode
|
|
2431
|
-
|
|
2432
1488
|
if (!isERBControlFlowNode(node)) {
|
|
2433
|
-
this.
|
|
1489
|
+
this.withInlineMode(() => this.visit(node))
|
|
1490
|
+
} else {
|
|
1491
|
+
this.visit(node)
|
|
2434
1492
|
}
|
|
2435
|
-
|
|
2436
|
-
this.visit(node)
|
|
2437
|
-
|
|
2438
|
-
this.inlineMode = wasInlineMode
|
|
2439
1493
|
})
|
|
2440
1494
|
})
|
|
2441
1495
|
|
|
@@ -2450,70 +1504,14 @@ export class FormatPrinter extends Printer {
|
|
|
2450
1504
|
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " />" : ">"}`
|
|
2451
1505
|
}
|
|
2452
1506
|
|
|
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
1507
|
/**
|
|
2511
1508
|
* Try to render a complete element inline including opening tag, children, and closing tag
|
|
2512
1509
|
*/
|
|
2513
1510
|
private tryRenderInlineFull(_node: HTMLElementNode, tagName: string, attributes: HTMLAttributeNode[], children: Node[]): string | null {
|
|
2514
1511
|
let result = `<${tagName}`
|
|
2515
1512
|
|
|
2516
|
-
|
|
1513
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1514
|
+
result += this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
2517
1515
|
result += ">"
|
|
2518
1516
|
|
|
2519
1517
|
const childrenContent = this.tryRenderChildrenInline(children, tagName)
|
|
@@ -2526,21 +1524,6 @@ export class FormatPrinter extends Printer {
|
|
|
2526
1524
|
return result
|
|
2527
1525
|
}
|
|
2528
1526
|
|
|
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
1527
|
/**
|
|
2545
1528
|
* Try to render just the children inline (without tags)
|
|
2546
1529
|
*/
|
|
@@ -2549,7 +1532,7 @@ export class FormatPrinter extends Printer {
|
|
|
2549
1532
|
let hasInternalWhitespace = false
|
|
2550
1533
|
let addedLeadingSpace = false
|
|
2551
1534
|
|
|
2552
|
-
const hasHerbDisable =
|
|
1535
|
+
const hasHerbDisable = hasLeadingHerbDisable(children)
|
|
2553
1536
|
const hasOnlyTextContent = children.every(child => isNode(child, HTMLTextNode) || isNode(child, WhitespaceNode))
|
|
2554
1537
|
const shouldPreserveSpaces = hasOnlyTextContent && tagName && isInlineElement(tagName)
|
|
2555
1538
|
|
|
@@ -2575,9 +1558,7 @@ export class FormatPrinter extends Printer {
|
|
|
2575
1558
|
}
|
|
2576
1559
|
}
|
|
2577
1560
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
if (isWhitespace && !result.endsWith(' ')) {
|
|
1561
|
+
if (isPureWhitespaceNode(child) && !result.endsWith(' ')) {
|
|
2581
1562
|
if (!result && hasHerbDisable && !addedLeadingSpace) {
|
|
2582
1563
|
result += ' '
|
|
2583
1564
|
addedLeadingSpace = true
|
|
@@ -2594,7 +1575,7 @@ export class FormatPrinter extends Printer {
|
|
|
2594
1575
|
|
|
2595
1576
|
const childrenToRender = this.getFilteredChildren(child.body)
|
|
2596
1577
|
const childInline = this.tryRenderInlineFull(child, tagName,
|
|
2597
|
-
filterNodes(child
|
|
1578
|
+
filterNodes(getOpenTagChildren(child), HTMLAttributeNode),
|
|
2598
1579
|
childrenToRender
|
|
2599
1580
|
)
|
|
2600
1581
|
|
|
@@ -2603,11 +1584,8 @@ export class FormatPrinter extends Printer {
|
|
|
2603
1584
|
}
|
|
2604
1585
|
|
|
2605
1586
|
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
|
|
1587
|
+
} else if (!isNode(child, HTMLTextNode) && !isPureWhitespaceNode(child)) {
|
|
1588
|
+
const captured = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
2611
1589
|
result += captured
|
|
2612
1590
|
}
|
|
2613
1591
|
}
|
|
@@ -2676,10 +1654,11 @@ export class FormatPrinter extends Printer {
|
|
|
2676
1654
|
for (const child of children) {
|
|
2677
1655
|
if (isNode(child, HTMLTextNode)) {
|
|
2678
1656
|
content += child.content
|
|
2679
|
-
} else if (isNode(child, HTMLElementNode)
|
|
1657
|
+
} else if (isNode(child, HTMLElementNode)) {
|
|
2680
1658
|
const tagName = getTagName(child)
|
|
2681
|
-
const attributes = filterNodes(child
|
|
2682
|
-
|
|
1659
|
+
const attributes = filterNodes(getOpenTagChildren(child), HTMLAttributeNode)
|
|
1660
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1661
|
+
const attributesString = this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
2683
1662
|
const childContent = this.renderElementInline(child)
|
|
2684
1663
|
|
|
2685
1664
|
content += `<${tagName}${attributesString}>${childContent}</${tagName}>`
|