@herb-tools/formatter 0.8.9 → 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 +57408 -17354
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +23348 -2973
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +23348 -2973
- package/dist/index.esm.js.map +1 -1
- package/dist/types/attribute-renderer.d.ts +44 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/comment-helpers.d.ts +45 -0
- package/dist/types/format-helpers.d.ts +15 -11
- package/dist/types/format-printer.d.ts +33 -138
- 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 +450 -1464
- 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,16 +289,41 @@ 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
|
|
|
286
314
|
/**
|
|
287
315
|
* Format ERB content with proper spacing around the inner content.
|
|
288
|
-
* Returns empty string if content is empty, otherwise
|
|
316
|
+
* Returns empty string if content is empty, otherwise adds a leading space
|
|
317
|
+
* and a trailing space (or newline for heredoc content starting with "<<").
|
|
289
318
|
*/
|
|
290
319
|
private formatERBContent(content: string): string {
|
|
291
|
-
|
|
320
|
+
const trimmedContent = content.trim();
|
|
321
|
+
|
|
322
|
+
// See: https://github.com/marcoroth/herb/issues/476
|
|
323
|
+
// TODO: revisit once we have access to Prism nodes
|
|
324
|
+
const suffix = trimmedContent.startsWith("<<") ? "\n" : " "
|
|
325
|
+
|
|
326
|
+
return trimmedContent ? ` ${trimmedContent}${suffix}` : ""
|
|
292
327
|
}
|
|
293
328
|
|
|
294
329
|
/**
|
|
@@ -316,401 +351,6 @@ export class FormatPrinter extends Printer {
|
|
|
316
351
|
return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode))
|
|
317
352
|
}
|
|
318
353
|
|
|
319
|
-
/**
|
|
320
|
-
* Check if a node will render as multiple lines when formatted.
|
|
321
|
-
*/
|
|
322
|
-
private isMultilineElement(node: Node): boolean {
|
|
323
|
-
if (isNode(node, ERBContentNode)) {
|
|
324
|
-
return (node.content?.value || "").includes("\n")
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (isNode(node, HTMLElementNode) && isContentPreserving(node)) {
|
|
328
|
-
return true
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const tracked = this.nodeIsMultiline.get(node)
|
|
332
|
-
|
|
333
|
-
if (tracked !== undefined) {
|
|
334
|
-
return tracked
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return false
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Get a grouping key for a node (tag name for HTML, ERB type for ERB)
|
|
342
|
-
*/
|
|
343
|
-
private getGroupingKey(node: Node): string | null {
|
|
344
|
-
if (isNode(node, HTMLElementNode)) {
|
|
345
|
-
return getTagName(node)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (isERBOutputNode(node)) return "erb-output"
|
|
349
|
-
if (isERBCommentNode(node)) return "erb-comment"
|
|
350
|
-
if (isERBNode(node)) return "erb-code"
|
|
351
|
-
|
|
352
|
-
return null
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Detect groups of consecutive same-tag/same-type single-line elements
|
|
357
|
-
* Returns a map of index -> group info for efficient lookup
|
|
358
|
-
*/
|
|
359
|
-
private detectTagGroups(siblings: Node[]): Map<number, { tagName: string; groupStart: number; groupEnd: number }> {
|
|
360
|
-
const cached = this.tagGroupsCache.get(siblings)
|
|
361
|
-
if (cached) return cached
|
|
362
|
-
|
|
363
|
-
const groupMap = new Map<number, { tagName: string; groupStart: number; groupEnd: number }>()
|
|
364
|
-
const meaningfulNodes: Array<{ index: number; groupKey: string }> = []
|
|
365
|
-
|
|
366
|
-
for (let i = 0; i < siblings.length; i++) {
|
|
367
|
-
const node = siblings[i]
|
|
368
|
-
|
|
369
|
-
if (!this.isMultilineElement(node)) {
|
|
370
|
-
const groupKey = this.getGroupingKey(node)
|
|
371
|
-
|
|
372
|
-
if (groupKey) {
|
|
373
|
-
meaningfulNodes.push({ index: i, groupKey })
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
let groupStart = 0
|
|
379
|
-
|
|
380
|
-
while (groupStart < meaningfulNodes.length) {
|
|
381
|
-
const startGroupKey = meaningfulNodes[groupStart].groupKey
|
|
382
|
-
let groupEnd = groupStart
|
|
383
|
-
|
|
384
|
-
while (groupEnd + 1 < meaningfulNodes.length && meaningfulNodes[groupEnd + 1].groupKey === startGroupKey) {
|
|
385
|
-
groupEnd++
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (groupEnd > groupStart) {
|
|
389
|
-
const groupStartIndex = meaningfulNodes[groupStart].index
|
|
390
|
-
const groupEndIndex = meaningfulNodes[groupEnd].index
|
|
391
|
-
|
|
392
|
-
for (let i = groupStart; i <= groupEnd; i++) {
|
|
393
|
-
groupMap.set(meaningfulNodes[i].index, {
|
|
394
|
-
tagName: startGroupKey,
|
|
395
|
-
groupStart: groupStartIndex,
|
|
396
|
-
groupEnd: groupEndIndex
|
|
397
|
-
})
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
groupStart = groupEnd + 1
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
this.tagGroupsCache.set(siblings, groupMap)
|
|
405
|
-
|
|
406
|
-
return groupMap
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Determine if spacing should be added between sibling elements
|
|
411
|
-
*
|
|
412
|
-
* This implements the "rule of three" intelligent spacing system:
|
|
413
|
-
* - Adds spacing between 3 or more meaningful siblings
|
|
414
|
-
* - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
|
|
415
|
-
* - Groups comments with following elements
|
|
416
|
-
* - Preserves user-added spacing
|
|
417
|
-
*
|
|
418
|
-
* @param parentElement - The parent element containing the siblings
|
|
419
|
-
* @param siblings - Array of all sibling nodes
|
|
420
|
-
* @param currentIndex - Index of the current node being evaluated
|
|
421
|
-
* @param hasExistingSpacing - Whether user-added spacing already exists
|
|
422
|
-
* @returns true if spacing should be added before the current element
|
|
423
|
-
*/
|
|
424
|
-
private shouldAddSpacingBetweenSiblings(parentElement: HTMLElementNode | null, siblings: Node[], currentIndex: number): boolean {
|
|
425
|
-
const currentNode = siblings[currentIndex]
|
|
426
|
-
const previousMeaningfulIndex = findPreviousMeaningfulSibling(siblings, currentIndex)
|
|
427
|
-
const previousNode = previousMeaningfulIndex !== -1 ? siblings[previousMeaningfulIndex] : null
|
|
428
|
-
|
|
429
|
-
if (previousNode && (isNode(previousNode, XMLDeclarationNode) || isNode(previousNode, HTMLDoctypeNode))) {
|
|
430
|
-
return true
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "")
|
|
434
|
-
|
|
435
|
-
if (hasMixedContent) return false
|
|
436
|
-
|
|
437
|
-
const isCurrentComment = isCommentNode(currentNode)
|
|
438
|
-
const isPreviousComment = previousNode ? isCommentNode(previousNode) : false
|
|
439
|
-
const isCurrentMultiline = this.isMultilineElement(currentNode)
|
|
440
|
-
const isPreviousMultiline = previousNode ? this.isMultilineElement(previousNode) : false
|
|
441
|
-
|
|
442
|
-
if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
|
|
443
|
-
return isPreviousMultiline && isCurrentMultiline
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (isPreviousComment && isCurrentComment) {
|
|
447
|
-
return false
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (isCurrentMultiline || isPreviousMultiline) {
|
|
451
|
-
return true
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const meaningfulSiblings = siblings.filter(child => isNonWhitespaceNode(child))
|
|
455
|
-
const parentTagName = parentElement ? getTagName(parentElement) : null
|
|
456
|
-
const isSpaceableContainer = !parentTagName || SPACEABLE_CONTAINERS.has(parentTagName)
|
|
457
|
-
const tagGroups = this.detectTagGroups(siblings)
|
|
458
|
-
|
|
459
|
-
const cached = this.allSingleLineCache.get(siblings)
|
|
460
|
-
let allSingleLineHTMLElements: boolean
|
|
461
|
-
if (cached !== undefined) {
|
|
462
|
-
allSingleLineHTMLElements = cached
|
|
463
|
-
} else {
|
|
464
|
-
allSingleLineHTMLElements = meaningfulSiblings.every(node => isNode(node, HTMLElementNode) && !this.isMultilineElement(node))
|
|
465
|
-
this.allSingleLineCache.set(siblings, allSingleLineHTMLElements)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
|
|
469
|
-
return false
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const currentGroup = tagGroups.get(currentIndex)
|
|
473
|
-
const previousGroup = previousNode ? tagGroups.get(previousMeaningfulIndex) : undefined
|
|
474
|
-
|
|
475
|
-
if (currentGroup && previousGroup && currentGroup.groupStart === previousGroup.groupStart && currentGroup.groupEnd === previousGroup.groupEnd) {
|
|
476
|
-
return false
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (previousGroup && previousGroup.groupEnd === previousMeaningfulIndex) {
|
|
480
|
-
return true
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (allSingleLineHTMLElements && tagGroups.size === 0) {
|
|
484
|
-
return false
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (isNode(currentNode, HTMLElementNode)) {
|
|
488
|
-
const currentTagName = getTagName(currentNode)
|
|
489
|
-
|
|
490
|
-
if (currentTagName && INLINE_ELEMENTS.has(currentTagName)) {
|
|
491
|
-
return false
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const isBlockElement = isBlockLevelNode(currentNode)
|
|
496
|
-
const isERBBlock = isERBNode(currentNode) && isERBControlFlowNode(currentNode)
|
|
497
|
-
const isComment = isCommentNode(currentNode)
|
|
498
|
-
|
|
499
|
-
return isBlockElement || isERBBlock || isComment
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Check if we're currently processing a token list attribute that needs spacing
|
|
504
|
-
*/
|
|
505
|
-
private get isInTokenListAttribute(): boolean {
|
|
506
|
-
return this.currentAttributeName !== null && TOKEN_LIST_ATTRIBUTES.has(this.currentAttributeName)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Render attributes as a space-separated string
|
|
511
|
-
*/
|
|
512
|
-
private renderAttributesString(attributes: HTMLAttributeNode[]): string {
|
|
513
|
-
if (attributes.length === 0) return ""
|
|
514
|
-
|
|
515
|
-
return ` ${attributes.map(attribute => this.renderAttribute(attribute)).join(" ")}`
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Determine if a tag should be rendered inline based on attribute count and other factors
|
|
520
|
-
*/
|
|
521
|
-
private shouldRenderInline(
|
|
522
|
-
totalAttributeCount: number,
|
|
523
|
-
inlineLength: number,
|
|
524
|
-
indentLength: number,
|
|
525
|
-
maxLineLength: number = this.maxLineLength,
|
|
526
|
-
hasComplexERB: boolean = false,
|
|
527
|
-
hasMultilineAttributes: boolean = false,
|
|
528
|
-
attributes: HTMLAttributeNode[] = []
|
|
529
|
-
): boolean {
|
|
530
|
-
if (hasComplexERB || hasMultilineAttributes) return false
|
|
531
|
-
|
|
532
|
-
if (totalAttributeCount === 0) {
|
|
533
|
-
return inlineLength + indentLength <= maxLineLength
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (totalAttributeCount === 1 && attributes.length === 1) {
|
|
537
|
-
const attribute = attributes[0]
|
|
538
|
-
const attributeName = this.getAttributeName(attribute)
|
|
539
|
-
|
|
540
|
-
if (attributeName === 'class') {
|
|
541
|
-
const attributeValue = this.getAttributeValue(attribute)
|
|
542
|
-
const wouldBeMultiline = this.wouldClassAttributeBeMultiline(attributeValue, indentLength)
|
|
543
|
-
|
|
544
|
-
if (!wouldBeMultiline) {
|
|
545
|
-
return true
|
|
546
|
-
} else {
|
|
547
|
-
return false
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (totalAttributeCount > 3 || inlineLength + indentLength > maxLineLength) {
|
|
553
|
-
return false
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return true
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
private wouldClassAttributeBeMultiline(content: string, indentLength: number): boolean {
|
|
560
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
561
|
-
const hasActualNewlines = /\r?\n/.test(content)
|
|
562
|
-
|
|
563
|
-
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
564
|
-
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line)
|
|
565
|
-
|
|
566
|
-
if (lines.length > 1) {
|
|
567
|
-
return true
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const attributeLine = `class="${normalizedContent}"`
|
|
572
|
-
const currentIndent = indentLength
|
|
573
|
-
|
|
574
|
-
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
575
|
-
if (/<%[^%]*%>/.test(normalizedContent)) {
|
|
576
|
-
return false
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const classes = normalizedContent.split(' ')
|
|
580
|
-
const lines = this.breakTokensIntoLines(classes, currentIndent)
|
|
581
|
-
return lines.length > 1
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return false
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// TOOD: extract to core or reuse function from core
|
|
588
|
-
private getAttributeName(attribute: HTMLAttributeNode): string {
|
|
589
|
-
return attribute.name ? getCombinedAttributeName(attribute.name) : ""
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// TOOD: extract to core or reuse function from core
|
|
593
|
-
private getAttributeValue(attribute: HTMLAttributeNode): string {
|
|
594
|
-
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
595
|
-
return attribute.value.children.map(child => isNode(child, HTMLTextNode) ? child.content : IdentityPrinter.print(child)).join('')
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return ''
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
private hasMultilineAttributes(attributes: HTMLAttributeNode[]): boolean {
|
|
602
|
-
return attributes.some(attribute => {
|
|
603
|
-
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
604
|
-
const content = getCombinedStringFromNodes(attribute.value.children)
|
|
605
|
-
|
|
606
|
-
if (/\r?\n/.test(content)) {
|
|
607
|
-
const name = attribute.name ? getCombinedAttributeName(attribute.name) : ""
|
|
608
|
-
|
|
609
|
-
if (name === "class") {
|
|
610
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
611
|
-
|
|
612
|
-
return normalizedContent.length > 80
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const lines = content.split(/\r?\n/)
|
|
616
|
-
|
|
617
|
-
if (lines.length > 1) {
|
|
618
|
-
return lines.slice(1).some(line => /^[ \t\n\r]+/.test(line))
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return false
|
|
624
|
-
})
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
private formatClassAttribute(content: string, name: string, equals: string, open_quote: string, close_quote: string): string {
|
|
628
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
629
|
-
const hasActualNewlines = /\r?\n/.test(content)
|
|
630
|
-
|
|
631
|
-
if (hasActualNewlines && normalizedContent.length > 80) {
|
|
632
|
-
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line)
|
|
633
|
-
|
|
634
|
-
if (lines.length > 1) {
|
|
635
|
-
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const currentIndent = this.indentLevel * this.indentWidth
|
|
640
|
-
const attributeLine = `${name}${equals}${open_quote}${normalizedContent}${close_quote}`
|
|
641
|
-
|
|
642
|
-
if (currentIndent + attributeLine.length > this.maxLineLength && normalizedContent.length > 60) {
|
|
643
|
-
if (/<%[^%]*%>/.test(normalizedContent)) {
|
|
644
|
-
return open_quote + normalizedContent + close_quote
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const classes = normalizedContent.split(' ')
|
|
648
|
-
const lines = this.breakTokensIntoLines(classes, currentIndent)
|
|
649
|
-
|
|
650
|
-
if (lines.length > 1) {
|
|
651
|
-
return open_quote + this.formatMultilineAttributeValue(lines) + close_quote
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return open_quote + normalizedContent + close_quote
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private isFormattableAttribute(attributeName: string, tagName: string): boolean {
|
|
659
|
-
const globalFormattable = FORMATTABLE_ATTRIBUTES['*'] || []
|
|
660
|
-
const tagSpecificFormattable = FORMATTABLE_ATTRIBUTES[tagName.toLowerCase()] || []
|
|
661
|
-
|
|
662
|
-
return globalFormattable.includes(attributeName) || tagSpecificFormattable.includes(attributeName)
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
private formatMultilineAttribute(content: string, name: string, open_quote: string, close_quote: string): string {
|
|
666
|
-
if (name === 'srcset' || name === 'sizes') {
|
|
667
|
-
const normalizedContent = content.replace(ASCII_WHITESPACE, ' ').trim()
|
|
668
|
-
|
|
669
|
-
return open_quote + normalizedContent + close_quote
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const lines = content.split('\n')
|
|
673
|
-
|
|
674
|
-
if (lines.length <= 1) {
|
|
675
|
-
return open_quote + content + close_quote
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const formattedContent = this.formatMultilineAttributeValue(lines)
|
|
679
|
-
|
|
680
|
-
return open_quote + formattedContent + close_quote
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
private formatMultilineAttributeValue(lines: string[]): string {
|
|
684
|
-
const indent = " ".repeat((this.indentLevel + 1) * this.indentWidth)
|
|
685
|
-
const closeIndent = " ".repeat(this.indentLevel * this.indentWidth)
|
|
686
|
-
|
|
687
|
-
return "\n" + lines.map(line => indent + line).join("\n") + "\n" + closeIndent
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
private breakTokensIntoLines(tokens: string[], currentIndent: number, separator: string = ' '): string[] {
|
|
691
|
-
const lines: string[] = []
|
|
692
|
-
let currentLine = ''
|
|
693
|
-
|
|
694
|
-
for (const token of tokens) {
|
|
695
|
-
const testLine = currentLine ? currentLine + separator + token : token
|
|
696
|
-
|
|
697
|
-
if (testLine.length > (this.maxLineLength - currentIndent - 6)) {
|
|
698
|
-
if (currentLine) {
|
|
699
|
-
lines.push(currentLine)
|
|
700
|
-
currentLine = token
|
|
701
|
-
} else {
|
|
702
|
-
lines.push(token)
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
currentLine = testLine
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (currentLine) lines.push(currentLine)
|
|
710
|
-
|
|
711
|
-
return lines
|
|
712
|
-
}
|
|
713
|
-
|
|
714
354
|
/**
|
|
715
355
|
* Render multiline attributes for a tag
|
|
716
356
|
*/
|
|
@@ -724,11 +364,10 @@ export class FormatPrinter extends Printer {
|
|
|
724
364
|
if (herbDisableComments.length > 0) {
|
|
725
365
|
const commentLines = this.capture(() => {
|
|
726
366
|
herbDisableComments.forEach(comment => {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
this.inlineMode = wasInlineMode
|
|
367
|
+
this.withInlineMode(() => {
|
|
368
|
+
this.lines.push(" ")
|
|
369
|
+
this.visit(comment)
|
|
370
|
+
})
|
|
732
371
|
})
|
|
733
372
|
})
|
|
734
373
|
|
|
@@ -738,9 +377,10 @@ export class FormatPrinter extends Printer {
|
|
|
738
377
|
this.pushWithIndent(openingLine)
|
|
739
378
|
|
|
740
379
|
this.withIndent(() => {
|
|
380
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
741
381
|
allChildren.forEach(child => {
|
|
742
382
|
if (isNode(child, HTMLAttributeNode)) {
|
|
743
|
-
this.pushWithIndent(this.renderAttribute(child))
|
|
383
|
+
this.pushWithIndent(this.attributeRenderer.renderAttribute(child, tagName))
|
|
744
384
|
} else if (!isNode(child, WhitespaceNode)) {
|
|
745
385
|
if (isNode(child, ERBContentNode) && isHerbDisableComment(child)) {
|
|
746
386
|
return
|
|
@@ -762,7 +402,7 @@ export class FormatPrinter extends Printer {
|
|
|
762
402
|
* Reconstruct the text representation of an ERB node
|
|
763
403
|
* @param withFormatting - if true, format the content; if false, preserve original
|
|
764
404
|
*/
|
|
765
|
-
|
|
405
|
+
reconstructERBNode(node: ERBNode, withFormatting: boolean = true): string {
|
|
766
406
|
const open = node.tag_opening?.value ?? ""
|
|
767
407
|
const close = node.tag_closing?.value ?? ""
|
|
768
408
|
const content = node.content?.value ?? ""
|
|
@@ -785,15 +425,12 @@ export class FormatPrinter extends Printer {
|
|
|
785
425
|
|
|
786
426
|
visitDocumentNode(node: DocumentNode) {
|
|
787
427
|
const children = this.formatFrontmatter(node)
|
|
788
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
428
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(children)
|
|
789
429
|
|
|
790
430
|
if (hasTextFlow) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
this.visitTextFlowChildren(children)
|
|
795
|
-
|
|
796
|
-
this.inlineMode = wasInlineMode
|
|
431
|
+
this.withInlineMode(() => {
|
|
432
|
+
this.textFlow.visitTextFlowChildren(children)
|
|
433
|
+
})
|
|
797
434
|
|
|
798
435
|
return
|
|
799
436
|
}
|
|
@@ -827,7 +464,7 @@ export class FormatPrinter extends Printer {
|
|
|
827
464
|
this.visit(child)
|
|
828
465
|
|
|
829
466
|
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
830
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
467
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
831
468
|
|
|
832
469
|
if (shouldAddSpacing) {
|
|
833
470
|
this.lines.splice(childStartLine, 0, "")
|
|
@@ -865,43 +502,104 @@ export class FormatPrinter extends Printer {
|
|
|
865
502
|
this.elementStack.pop()
|
|
866
503
|
}
|
|
867
504
|
|
|
868
|
-
|
|
869
|
-
|
|
505
|
+
visitHTMLConditionalElementNode(node: HTMLConditionalElementNode) {
|
|
506
|
+
this.trackBoundary(node, () => {
|
|
507
|
+
if (node.open_conditional) {
|
|
508
|
+
this.visit(node.open_conditional)
|
|
509
|
+
}
|
|
870
510
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (isNode(child, HTMLElementNode)) {
|
|
874
|
-
const wasInlineMode = this.inlineMode
|
|
875
|
-
this.inlineMode = true
|
|
511
|
+
if (node.body.length > 0) {
|
|
512
|
+
this.push("")
|
|
876
513
|
|
|
877
|
-
|
|
878
|
-
|
|
514
|
+
this.withIndent(() => {
|
|
515
|
+
for (const child of node.body) {
|
|
516
|
+
if (!isPureWhitespaceNode(child)) {
|
|
517
|
+
this.visit(child)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
})
|
|
879
521
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
this.pushToLastLine(IdentityPrinter.print(child))
|
|
883
|
-
}
|
|
884
|
-
})
|
|
522
|
+
this.push("")
|
|
523
|
+
}
|
|
885
524
|
|
|
886
|
-
|
|
887
|
-
|
|
525
|
+
if (node.close_conditional) {
|
|
526
|
+
this.visit(node.close_conditional)
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
}
|
|
888
530
|
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
|
|
531
|
+
visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode) {
|
|
532
|
+
const wasInConditionalOpenTagContext = this.inConditionalOpenTagContext
|
|
533
|
+
this.inConditionalOpenTagContext = true
|
|
892
534
|
|
|
893
|
-
|
|
894
|
-
if (
|
|
535
|
+
this.trackBoundary(node, () => {
|
|
536
|
+
if (node.conditional) {
|
|
537
|
+
this.visit(node.conditional)
|
|
538
|
+
}
|
|
539
|
+
})
|
|
895
540
|
|
|
896
|
-
|
|
897
|
-
|
|
541
|
+
this.inConditionalOpenTagContext = wasInConditionalOpenTagContext
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
visitHTMLElementBody(body: Node[], element: HTMLElementNode) {
|
|
545
|
+
if (isContentPreserving(element) || this.inContentPreservingContext) {
|
|
546
|
+
this.visitContentPreservingBody(element)
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const tagName = getTagName(element)
|
|
551
|
+
const analysis = this.elementFormattingAnalysis.get(element)
|
|
552
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(body)
|
|
553
|
+
const children = filterSignificantChildren(body)
|
|
554
|
+
|
|
555
|
+
if (analysis?.elementContentInline) {
|
|
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
|
+
}
|
|
592
|
+
|
|
593
|
+
private visitInlineElementBody(body: Node[], tagName: string, hasTextFlow: boolean, children: Node[]) {
|
|
594
|
+
if (children.length === 0) return
|
|
898
595
|
|
|
899
|
-
|
|
900
|
-
const shouldPreserveSpaces = hasOnlyTextContent && isInlineElement(tagName)
|
|
596
|
+
const nodesToRender = hasTextFlow ? body : children
|
|
901
597
|
|
|
902
|
-
|
|
598
|
+
const hasOnlyTextContent = nodesToRender.every(child => isNode(child, HTMLTextNode) || isNode(child, WhitespaceNode))
|
|
599
|
+
const shouldPreserveSpaces = hasOnlyTextContent && isInlineElement(tagName)
|
|
903
600
|
|
|
904
|
-
|
|
601
|
+
const lines = this.withInlineMode(() => {
|
|
602
|
+
return this.capture(() => {
|
|
905
603
|
nodesToRender.forEach(child => {
|
|
906
604
|
if (isNode(child, HTMLTextNode)) {
|
|
907
605
|
if (hasTextFlow) {
|
|
@@ -934,29 +632,28 @@ export class FormatPrinter extends Printer {
|
|
|
934
632
|
}
|
|
935
633
|
})
|
|
936
634
|
})
|
|
635
|
+
})
|
|
937
636
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const inlineContent = shouldPreserveSpaces
|
|
941
|
-
? (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ') : content)
|
|
942
|
-
: (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ').trim() : content.trim())
|
|
943
|
-
|
|
944
|
-
if (inlineContent) {
|
|
945
|
-
this.pushToLastLine(inlineContent)
|
|
946
|
-
}
|
|
637
|
+
const content = lines.join('')
|
|
947
638
|
|
|
948
|
-
|
|
639
|
+
const inlineContent = shouldPreserveSpaces
|
|
640
|
+
? (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ') : content)
|
|
641
|
+
: (hasTextFlow ? content.replace(ASCII_WHITESPACE, ' ').trim() : content.trim())
|
|
949
642
|
|
|
950
|
-
|
|
643
|
+
if (inlineContent) {
|
|
644
|
+
this.pushToLastLine(inlineContent)
|
|
951
645
|
}
|
|
646
|
+
}
|
|
952
647
|
|
|
953
|
-
|
|
954
|
-
|
|
648
|
+
private stripLeadingHerbDisable(children: Node[], body: Node[]): {
|
|
649
|
+
comment: Node | null
|
|
650
|
+
hasLeadingWhitespace: boolean
|
|
651
|
+
remainingChildren: Node[]
|
|
652
|
+
remainingBody: Node[]
|
|
653
|
+
} {
|
|
955
654
|
let leadingHerbDisableComment: Node | null = null
|
|
956
655
|
let leadingHerbDisableIndex = -1
|
|
957
656
|
let firstWhitespaceIndex = -1
|
|
958
|
-
let remainingChildren = children
|
|
959
|
-
let remainingBodyUnfiltered = body
|
|
960
657
|
|
|
961
658
|
for (let i = 0; i < children.length; i++) {
|
|
962
659
|
const child = children[i]
|
|
@@ -977,60 +674,30 @@ export class FormatPrinter extends Printer {
|
|
|
977
674
|
break
|
|
978
675
|
}
|
|
979
676
|
|
|
980
|
-
if (leadingHerbDisableComment
|
|
981
|
-
remainingChildren
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (firstWhitespaceIndex >= 0 && index === leadingHerbDisableIndex - 1) {
|
|
985
|
-
const child = children[index]
|
|
986
|
-
|
|
987
|
-
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
988
|
-
return false
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
return true
|
|
993
|
-
})
|
|
677
|
+
if (!leadingHerbDisableComment || leadingHerbDisableIndex < 0) {
|
|
678
|
+
return { comment: null, hasLeadingWhitespace: false, remainingChildren: children, remainingBody: body }
|
|
679
|
+
}
|
|
994
680
|
|
|
995
|
-
|
|
996
|
-
|
|
681
|
+
const filterOut = (nodes: Node[]) => nodes.filter((_, index) => {
|
|
682
|
+
if (index === leadingHerbDisableIndex) return false
|
|
997
683
|
|
|
998
|
-
|
|
999
|
-
|
|
684
|
+
if (firstWhitespaceIndex >= 0 && index === leadingHerbDisableIndex - 1) {
|
|
685
|
+
const child = nodes[index]
|
|
1000
686
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
}
|
|
687
|
+
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
688
|
+
return false
|
|
1004
689
|
}
|
|
690
|
+
}
|
|
1005
691
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
if (leadingHerbDisableComment) {
|
|
1011
|
-
const herbDisableString = this.capture(() => {
|
|
1012
|
-
const savedIndentLevel = this.indentLevel
|
|
1013
|
-
this.indentLevel = 0
|
|
1014
|
-
this.inlineMode = true
|
|
1015
|
-
this.visit(leadingHerbDisableComment)
|
|
1016
|
-
this.inlineMode = false
|
|
1017
|
-
this.indentLevel = savedIndentLevel
|
|
1018
|
-
}).join("")
|
|
1019
|
-
|
|
1020
|
-
const hasLeadingWhitespace = firstWhitespaceIndex >= 0 && firstWhitespaceIndex < leadingHerbDisableIndex
|
|
692
|
+
return true
|
|
693
|
+
})
|
|
1021
694
|
|
|
1022
|
-
|
|
695
|
+
return {
|
|
696
|
+
comment: leadingHerbDisableComment,
|
|
697
|
+
hasLeadingWhitespace: firstWhitespaceIndex >= 0 && firstWhitespaceIndex < leadingHerbDisableIndex,
|
|
698
|
+
remainingChildren: filterOut(children),
|
|
699
|
+
remainingBody: filterOut(body),
|
|
1023
700
|
}
|
|
1024
|
-
|
|
1025
|
-
if (remainingChildren.length === 0) return
|
|
1026
|
-
|
|
1027
|
-
this.withIndent(() => {
|
|
1028
|
-
if (hasTextFlow) {
|
|
1029
|
-
this.visitTextFlowChildren(remainingBodyUnfiltered)
|
|
1030
|
-
} else {
|
|
1031
|
-
this.visitElementChildren(leadingHerbDisableComment ? remainingChildren : body, element)
|
|
1032
|
-
}
|
|
1033
|
-
})
|
|
1034
701
|
}
|
|
1035
702
|
|
|
1036
703
|
/**
|
|
@@ -1046,9 +713,7 @@ export class FormatPrinter extends Printer {
|
|
|
1046
713
|
const child = body[index]
|
|
1047
714
|
|
|
1048
715
|
if (isNode(child, HTMLTextNode)) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (isWhitespaceOnly) {
|
|
716
|
+
if (isPureWhitespaceNode(child)) {
|
|
1052
717
|
const hasPreviousNonWhitespace = index > 0 && isNonWhitespaceNode(body[index - 1])
|
|
1053
718
|
const hasNextNonWhitespace = index < body.length - 1 && isNonWhitespaceNode(body[index + 1])
|
|
1054
719
|
const hasMultipleNewlines = child.content.includes('\n\n')
|
|
@@ -1064,59 +729,99 @@ export class FormatPrinter extends Printer {
|
|
|
1064
729
|
|
|
1065
730
|
if (!isNonWhitespaceNode(child)) continue
|
|
1066
731
|
|
|
1067
|
-
|
|
732
|
+
const textFlowResult = this.visitTextFlowRunInChildren(body, index, lastMeaningfulNode, hasHandledSpacing)
|
|
733
|
+
|
|
734
|
+
if (textFlowResult) {
|
|
735
|
+
index = textFlowResult.newIndex
|
|
736
|
+
lastMeaningfulNode = textFlowResult.lastMeaningfulNode
|
|
737
|
+
hasHandledSpacing = textFlowResult.hasHandledSpacing
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
1068
740
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
741
|
+
const herbDisableResult: ChildVisitResult | null =
|
|
742
|
+
isNode(child, HTMLElementNode) && child.close_tag
|
|
743
|
+
? this.visitChildWithTrailingHerbDisable(child, body, index, parentElement, lastMeaningfulNode, hasHandledSpacing)
|
|
744
|
+
: null
|
|
1072
745
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
746
|
+
if (herbDisableResult) {
|
|
747
|
+
index = herbDisableResult.newIndex
|
|
748
|
+
lastMeaningfulNode = herbDisableResult.lastMeaningfulNode
|
|
749
|
+
hasHandledSpacing = herbDisableResult.hasHandledSpacing
|
|
750
|
+
continue
|
|
751
|
+
}
|
|
1076
752
|
|
|
1077
|
-
|
|
1078
|
-
|
|
753
|
+
if (shouldAppendToLastLine(child, body, index)) {
|
|
754
|
+
this.appendChildToLastLine(child, body, index)
|
|
755
|
+
lastMeaningfulNode = child
|
|
756
|
+
hasHandledSpacing = false
|
|
757
|
+
continue
|
|
758
|
+
}
|
|
1079
759
|
|
|
1080
|
-
|
|
1081
|
-
|
|
760
|
+
const childStartLine = this.stringLineCount
|
|
761
|
+
this.visit(child)
|
|
1082
762
|
|
|
1083
|
-
|
|
1084
|
-
|
|
763
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
764
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
1085
765
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
766
|
+
if (shouldAddSpacing) {
|
|
767
|
+
this.lines.splice(childStartLine, 0, "")
|
|
768
|
+
this.stringLineCount++
|
|
769
|
+
}
|
|
770
|
+
}
|
|
1091
771
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
this.visit(nextChild)
|
|
1097
|
-
this.inlineMode = false
|
|
1098
|
-
this.indentLevel = savedIndentLevel
|
|
1099
|
-
}).join("")
|
|
772
|
+
lastMeaningfulNode = child
|
|
773
|
+
hasHandledSpacing = false
|
|
774
|
+
}
|
|
775
|
+
}
|
|
1100
776
|
|
|
1101
|
-
|
|
777
|
+
private visitTextFlowRunInChildren(body: Node[], index: number, lastMeaningfulNode: Node | null, hasHandledSpacing: boolean): ChildVisitResult | null {
|
|
778
|
+
const child = body[index]
|
|
1102
779
|
|
|
1103
|
-
|
|
1104
|
-
lastMeaningfulNode = child
|
|
1105
|
-
hasHandledSpacing = false
|
|
780
|
+
if (!isTextFlowNode(child)) return null
|
|
1106
781
|
|
|
1107
|
-
|
|
1108
|
-
}
|
|
782
|
+
const run = this.textFlow.collectTextFlowRun(body, index)
|
|
1109
783
|
|
|
1110
|
-
|
|
1111
|
-
|
|
784
|
+
if (!run) return null
|
|
785
|
+
|
|
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
|
|
1112
817
|
}
|
|
1113
818
|
|
|
1114
|
-
if (
|
|
819
|
+
if (isNode(nextChild, ERBContentNode) && isHerbDisableComment(nextChild)) {
|
|
1115
820
|
const childStartLine = this.stringLineCount
|
|
1116
821
|
this.visit(child)
|
|
1117
822
|
|
|
1118
823
|
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
1119
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
824
|
+
const shouldAddSpacing = this.spacingAnalyzer.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
1120
825
|
|
|
1121
826
|
if (shouldAddSpacing) {
|
|
1122
827
|
this.lines.splice(childStartLine, 0, "")
|
|
@@ -1124,10 +829,21 @@ export class FormatPrinter extends Printer {
|
|
|
1124
829
|
}
|
|
1125
830
|
}
|
|
1126
831
|
|
|
1127
|
-
|
|
1128
|
-
|
|
832
|
+
const herbDisableString = this.captureHerbDisableInline(nextChild)
|
|
833
|
+
|
|
834
|
+
this.pushToLastLine(' ' + herbDisableString)
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
newIndex: j,
|
|
838
|
+
lastMeaningfulNode: child,
|
|
839
|
+
hasHandledSpacing: false,
|
|
840
|
+
}
|
|
1129
841
|
}
|
|
842
|
+
|
|
843
|
+
break
|
|
1130
844
|
}
|
|
845
|
+
|
|
846
|
+
return null
|
|
1131
847
|
}
|
|
1132
848
|
|
|
1133
849
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode) {
|
|
@@ -1135,6 +851,12 @@ export class FormatPrinter extends Printer {
|
|
|
1135
851
|
const inlineNodes = this.extractInlineNodes(node.children)
|
|
1136
852
|
const isSelfClosing = node.tag_closing?.value === "/>"
|
|
1137
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
|
+
|
|
1138
860
|
if (this.currentElement && this.elementFormattingAnalysis.has(this.currentElement)) {
|
|
1139
861
|
const analysis = this.elementFormattingAnalysis.get(this.currentElement)!
|
|
1140
862
|
|
|
@@ -1152,13 +874,14 @@ export class FormatPrinter extends Printer {
|
|
|
1152
874
|
|
|
1153
875
|
const inline = this.renderInlineOpen(getTagName(node), attributes, isSelfClosing, inlineNodes, node.children)
|
|
1154
876
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
1155
|
-
|
|
877
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
878
|
+
const shouldKeepInline = this.attributeRenderer.shouldRenderInline(
|
|
1156
879
|
totalAttributeCount,
|
|
1157
880
|
inline.length,
|
|
1158
881
|
this.indent.length,
|
|
1159
882
|
this.maxLineLength,
|
|
1160
883
|
false,
|
|
1161
|
-
this.hasMultilineAttributes(attributes),
|
|
884
|
+
this.attributeRenderer.hasMultilineAttributes(attributes),
|
|
1162
885
|
attributes
|
|
1163
886
|
)
|
|
1164
887
|
|
|
@@ -1192,7 +915,7 @@ export class FormatPrinter extends Printer {
|
|
|
1192
915
|
return
|
|
1193
916
|
}
|
|
1194
917
|
|
|
1195
|
-
|
|
918
|
+
const text = node.content.trim()
|
|
1196
919
|
|
|
1197
920
|
if (!text) return
|
|
1198
921
|
|
|
@@ -1217,7 +940,8 @@ export class FormatPrinter extends Printer {
|
|
|
1217
940
|
}
|
|
1218
941
|
|
|
1219
942
|
visitHTMLAttributeNode(node: HTMLAttributeNode) {
|
|
1220
|
-
this.
|
|
943
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
944
|
+
this.pushWithIndent(this.attributeRenderer.renderAttribute(node, this.currentTagName))
|
|
1221
945
|
}
|
|
1222
946
|
|
|
1223
947
|
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode) {
|
|
@@ -1228,119 +952,39 @@ export class FormatPrinter extends Printer {
|
|
|
1228
952
|
this.pushWithIndent(IdentityPrinter.print(node))
|
|
1229
953
|
}
|
|
1230
954
|
|
|
1231
|
-
// TODO: rework
|
|
1232
955
|
visitHTMLCommentNode(node: HTMLCommentNode) {
|
|
1233
956
|
const open = node.comment_start?.value ?? ""
|
|
1234
957
|
const close = node.comment_end?.value ?? ""
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
inner = node.children.map(child => {
|
|
1240
|
-
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
1241
|
-
return child.content
|
|
1242
|
-
} else if (isERBNode(child)) {
|
|
1243
|
-
return IdentityPrinter.print(child)
|
|
1244
|
-
} else {
|
|
1245
|
-
return ""
|
|
1246
|
-
}
|
|
1247
|
-
}).join("")
|
|
1248
|
-
|
|
1249
|
-
const trimmedInner = inner.trim()
|
|
1250
|
-
|
|
1251
|
-
if (trimmedInner.startsWith('[if ') && trimmedInner.endsWith('<![endif]')) {
|
|
1252
|
-
this.pushWithIndent(open + inner + close)
|
|
1253
|
-
|
|
1254
|
-
return
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
const hasNewlines = inner.includes('\n')
|
|
1258
|
-
|
|
1259
|
-
if (hasNewlines) {
|
|
1260
|
-
const lines = inner.split('\n')
|
|
1261
|
-
const childIndent = " ".repeat(this.indentWidth)
|
|
1262
|
-
const firstLineHasContent = lines[0].trim() !== ''
|
|
1263
|
-
|
|
1264
|
-
if (firstLineHasContent && lines.length > 1) {
|
|
1265
|
-
const contentLines = lines.map(line => line.trim()).filter(line => line !== '')
|
|
1266
|
-
inner = '\n' + contentLines.map(line => childIndent + line).join('\n') + '\n'
|
|
1267
|
-
} else {
|
|
1268
|
-
const contentLines = lines.filter((line, index) => {
|
|
1269
|
-
return line.trim() !== '' && !(index === 0 || index === lines.length - 1)
|
|
1270
|
-
})
|
|
1271
|
-
|
|
1272
|
-
const minIndent = contentLines.length > 0 ? Math.min(...contentLines.map(line => line.length - line.trimStart().length)) : 0
|
|
1273
|
-
|
|
1274
|
-
const processedLines = lines.map((line, index) => {
|
|
1275
|
-
const trimmedLine = line.trim()
|
|
1276
|
-
|
|
1277
|
-
if ((index === 0 || index === lines.length - 1) && trimmedLine === '') {
|
|
1278
|
-
return line
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
if (trimmedLine !== '') {
|
|
1282
|
-
const currentIndent = line.length - line.trimStart().length
|
|
1283
|
-
const relativeIndent = Math.max(0, currentIndent - minIndent)
|
|
1284
|
-
|
|
1285
|
-
return childIndent + " ".repeat(relativeIndent) + trimmedLine
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
return line
|
|
1289
|
-
})
|
|
1290
|
-
|
|
1291
|
-
inner = processedLines.join('\n')
|
|
1292
|
-
}
|
|
1293
|
-
} else {
|
|
1294
|
-
inner = ` ${inner.trim()} `
|
|
1295
|
-
}
|
|
1296
|
-
} else {
|
|
1297
|
-
inner = ""
|
|
1298
|
-
}
|
|
958
|
+
const rawInner = node.children && node.children.length > 0
|
|
959
|
+
? extractHTMLCommentContent(node.children)
|
|
960
|
+
: ""
|
|
961
|
+
const inner = rawInner ? formatHTMLCommentInner(rawInner, this.indentWidth) : ""
|
|
1299
962
|
|
|
1300
963
|
this.pushWithIndent(open + inner + close)
|
|
1301
964
|
}
|
|
1302
965
|
|
|
1303
966
|
visitERBCommentNode(node: ERBContentNode) {
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const contentTrimmedLines = content.trim().split("\n")
|
|
1310
|
-
|
|
1311
|
-
if (contentLines.length === 1 && contentTrimmedLines.length === 1) {
|
|
1312
|
-
const startsWithSpace = content[0] === " "
|
|
1313
|
-
const before = startsWithSpace ? "" : " "
|
|
967
|
+
const result = formatERBCommentLines(
|
|
968
|
+
node.tag_opening?.value || "<%#",
|
|
969
|
+
node?.content?.value || "",
|
|
970
|
+
node.tag_closing?.value || "%>"
|
|
971
|
+
)
|
|
1314
972
|
|
|
973
|
+
if (result.type === 'single-line') {
|
|
1315
974
|
if (this.inlineMode) {
|
|
1316
|
-
this.push(
|
|
975
|
+
this.push(result.text)
|
|
1317
976
|
} else {
|
|
1318
|
-
this.pushWithIndent(
|
|
977
|
+
this.pushWithIndent(result.text)
|
|
1319
978
|
}
|
|
979
|
+
} else {
|
|
980
|
+
this.pushWithIndent(result.header)
|
|
1320
981
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
if (contentTrimmedLines.length === 1) {
|
|
1325
|
-
if (this.inlineMode) {
|
|
1326
|
-
this.push(open + ' ' + content.trim() + ' ' + close)
|
|
1327
|
-
} else {
|
|
1328
|
-
this.pushWithIndent(open + ' ' + content.trim() + ' ' + close)
|
|
1329
|
-
}
|
|
982
|
+
this.withIndent(() => {
|
|
983
|
+
result.contentLines.forEach(line => this.pushWithIndent(line))
|
|
984
|
+
})
|
|
1330
985
|
|
|
1331
|
-
|
|
986
|
+
this.pushWithIndent(result.footer)
|
|
1332
987
|
}
|
|
1333
|
-
|
|
1334
|
-
const firstLineEmpty = contentLines[0].trim() === ""
|
|
1335
|
-
const dedentedContent = dedent(firstLineEmpty ? content : content.trimStart())
|
|
1336
|
-
|
|
1337
|
-
this.pushWithIndent(open)
|
|
1338
|
-
|
|
1339
|
-
this.withIndent(() => {
|
|
1340
|
-
dedentedContent.split("\n").forEach(line => this.pushWithIndent(line))
|
|
1341
|
-
})
|
|
1342
|
-
|
|
1343
|
-
this.pushWithIndent(close)
|
|
1344
988
|
}
|
|
1345
989
|
|
|
1346
990
|
visitHTMLDoctypeNode(node: HTMLDoctypeNode) {
|
|
@@ -1363,6 +1007,14 @@ export class FormatPrinter extends Printer {
|
|
|
1363
1007
|
}
|
|
1364
1008
|
}
|
|
1365
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
|
+
|
|
1366
1018
|
visitERBEndNode(node: ERBEndNode) {
|
|
1367
1019
|
this.printERBNode(node)
|
|
1368
1020
|
}
|
|
@@ -1397,10 +1049,10 @@ export class FormatPrinter extends Printer {
|
|
|
1397
1049
|
this.printERBNode(node)
|
|
1398
1050
|
|
|
1399
1051
|
this.withIndent(() => {
|
|
1400
|
-
const hasTextFlow = this.isInTextFlowContext(
|
|
1052
|
+
const hasTextFlow = this.textFlow.isInTextFlowContext(node.body)
|
|
1401
1053
|
|
|
1402
1054
|
if (hasTextFlow) {
|
|
1403
|
-
this.visitTextFlowChildren(node.body)
|
|
1055
|
+
this.textFlow.visitTextFlowChildren(node.body)
|
|
1404
1056
|
} else {
|
|
1405
1057
|
this.visitElementChildren(node.body, null)
|
|
1406
1058
|
}
|
|
@@ -1418,9 +1070,10 @@ export class FormatPrinter extends Printer {
|
|
|
1418
1070
|
node.statements.forEach(child => {
|
|
1419
1071
|
if (isNode(child, HTMLAttributeNode)) {
|
|
1420
1072
|
this.lines.push(" ")
|
|
1421
|
-
this.
|
|
1073
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1074
|
+
this.lines.push(this.attributeRenderer.renderAttribute(child, this.currentTagName))
|
|
1422
1075
|
} else {
|
|
1423
|
-
const shouldAddSpaces = this.isInTokenListAttribute
|
|
1076
|
+
const shouldAddSpaces = this.attributeRenderer.isInTokenListAttribute
|
|
1424
1077
|
|
|
1425
1078
|
if (shouldAddSpaces) {
|
|
1426
1079
|
this.lines.push(" ")
|
|
@@ -1435,7 +1088,7 @@ export class FormatPrinter extends Printer {
|
|
|
1435
1088
|
})
|
|
1436
1089
|
|
|
1437
1090
|
const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode))
|
|
1438
|
-
const isTokenList = this.isInTokenListAttribute
|
|
1091
|
+
const isTokenList = this.attributeRenderer.isInTokenListAttribute
|
|
1439
1092
|
|
|
1440
1093
|
if ((hasHTMLAttributes || isTokenList) && node.end_node) {
|
|
1441
1094
|
this.lines.push(" ")
|
|
@@ -1563,7 +1216,12 @@ export class FormatPrinter extends Printer {
|
|
|
1563
1216
|
* Determines if the open tag should be rendered inline
|
|
1564
1217
|
*/
|
|
1565
1218
|
private shouldRenderOpenTagInline(node: HTMLElementNode): boolean {
|
|
1566
|
-
|
|
1219
|
+
if (isNode(node.open_tag, HTMLConditionalOpenTagNode)) {
|
|
1220
|
+
return false
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const openTag = node.open_tag
|
|
1224
|
+
const children = openTag?.children || []
|
|
1567
1225
|
const attributes = filterNodes(children, HTMLAttributeNode)
|
|
1568
1226
|
const inlineNodes = this.extractInlineNodes(children)
|
|
1569
1227
|
const hasERBControlFlow = inlineNodes.some(node => isERBControlFlowNode(node)) || children.some(node => isERBControlFlowNode(node))
|
|
@@ -1572,19 +1230,20 @@ export class FormatPrinter extends Printer {
|
|
|
1572
1230
|
if (hasComplexERB) return false
|
|
1573
1231
|
|
|
1574
1232
|
const totalAttributeCount = this.getTotalAttributeCount(attributes, inlineNodes)
|
|
1575
|
-
|
|
1233
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1234
|
+
const hasMultilineAttrs = this.attributeRenderer.hasMultilineAttributes(attributes)
|
|
1576
1235
|
|
|
1577
1236
|
if (hasMultilineAttrs) return false
|
|
1578
1237
|
|
|
1579
1238
|
const inline = this.renderInlineOpen(
|
|
1580
1239
|
getTagName(node),
|
|
1581
1240
|
attributes,
|
|
1582
|
-
|
|
1241
|
+
openTag?.tag_closing?.value === "/>",
|
|
1583
1242
|
inlineNodes,
|
|
1584
1243
|
children
|
|
1585
1244
|
)
|
|
1586
1245
|
|
|
1587
|
-
return this.shouldRenderInline(
|
|
1246
|
+
return this.attributeRenderer.shouldRenderInline(
|
|
1588
1247
|
totalAttributeCount,
|
|
1589
1248
|
inline.length,
|
|
1590
1249
|
this.indent.length,
|
|
@@ -1602,6 +1261,7 @@ export class FormatPrinter extends Printer {
|
|
|
1602
1261
|
const tagName = getTagName(node)
|
|
1603
1262
|
const children = filterSignificantChildren(node.body)
|
|
1604
1263
|
const openTagInline = this.shouldRenderOpenTagInline(node)
|
|
1264
|
+
const openTagClosing = getOpenTagClosing(node)
|
|
1605
1265
|
|
|
1606
1266
|
if (!openTagInline) return false
|
|
1607
1267
|
if (children.length === 0) return true
|
|
@@ -1616,46 +1276,46 @@ export class FormatPrinter extends Printer {
|
|
|
1616
1276
|
|
|
1617
1277
|
if (hasNonInlineChildElements) return false
|
|
1618
1278
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
for (const child of node.body) {
|
|
1622
|
-
if (isNode(child, WhitespaceNode) || isPureWhitespaceNode(child)) {
|
|
1623
|
-
continue
|
|
1624
|
-
}
|
|
1625
|
-
if (isNode(child, ERBContentNode) && isHerbDisableComment(child)) {
|
|
1626
|
-
hasLeadingHerbDisable = true
|
|
1627
|
-
}
|
|
1628
|
-
break
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
if (hasLeadingHerbDisable && !isInlineElement(tagName)) {
|
|
1279
|
+
if (hasLeadingHerbDisable(node.body) && !isInlineElement(tagName)) {
|
|
1632
1280
|
return false
|
|
1633
1281
|
}
|
|
1634
1282
|
|
|
1635
1283
|
if (isInlineElement(tagName)) {
|
|
1636
|
-
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node
|
|
1284
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(getOpenTagChildren(node), HTMLAttributeNode), node.body)
|
|
1637
1285
|
|
|
1638
1286
|
if (fullInlineResult) {
|
|
1639
|
-
|
|
1640
|
-
return totalLength <= this.maxLineLength
|
|
1287
|
+
return this.fitsOnCurrentLine(fullInlineResult)
|
|
1641
1288
|
}
|
|
1642
1289
|
|
|
1643
1290
|
return false
|
|
1644
1291
|
}
|
|
1645
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
|
+
|
|
1646
1310
|
const allNestedAreInline = areAllNestedElementsInline(children)
|
|
1647
1311
|
const hasMultilineText = hasMultilineTextContent(children)
|
|
1648
1312
|
const hasMixedContent = hasMixedTextAndInlineContent(children)
|
|
1649
1313
|
|
|
1650
1314
|
if (allNestedAreInline && (!hasMultilineText || hasMixedContent)) {
|
|
1651
|
-
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(node
|
|
1652
|
-
|
|
1653
|
-
if (fullInlineResult) {
|
|
1654
|
-
const totalLength = this.indent.length + fullInlineResult.length
|
|
1315
|
+
const fullInlineResult = this.tryRenderInlineFull(node, tagName, filterNodes(getOpenTagChildren(node), HTMLAttributeNode), node.body)
|
|
1655
1316
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1317
|
+
if (fullInlineResult && this.fitsOnCurrentLine(fullInlineResult)) {
|
|
1318
|
+
return true
|
|
1659
1319
|
}
|
|
1660
1320
|
}
|
|
1661
1321
|
|
|
@@ -1664,17 +1324,16 @@ export class FormatPrinter extends Printer {
|
|
|
1664
1324
|
if (inlineResult) {
|
|
1665
1325
|
const openTagResult = this.renderInlineOpen(
|
|
1666
1326
|
tagName,
|
|
1667
|
-
filterNodes(node
|
|
1327
|
+
filterNodes(getOpenTagChildren(node), HTMLAttributeNode),
|
|
1668
1328
|
false,
|
|
1669
1329
|
[],
|
|
1670
|
-
node
|
|
1330
|
+
getOpenTagChildren(node)
|
|
1671
1331
|
)
|
|
1672
1332
|
|
|
1673
1333
|
const childrenContent = this.renderChildrenInline(children)
|
|
1674
1334
|
const fullLine = openTagResult + childrenContent + `</${tagName}>`
|
|
1675
|
-
const totalLength = this.indent.length + fullLine.length
|
|
1676
1335
|
|
|
1677
|
-
if (
|
|
1336
|
+
if (this.fitsOnCurrentLine(fullLine)) {
|
|
1678
1337
|
return true
|
|
1679
1338
|
}
|
|
1680
1339
|
}
|
|
@@ -1687,7 +1346,7 @@ export class FormatPrinter extends Printer {
|
|
|
1687
1346
|
*/
|
|
1688
1347
|
private shouldRenderCloseTagInline(node: HTMLElementNode, elementContentInline: boolean): boolean {
|
|
1689
1348
|
if (node.is_void) return true
|
|
1690
|
-
if (node
|
|
1349
|
+
if (getOpenTagClosing(node)?.value === "/>") return true
|
|
1691
1350
|
if (isContentPreserving(node)) return true
|
|
1692
1351
|
|
|
1693
1352
|
const children = filterSignificantChildren(node.body)
|
|
@@ -1700,6 +1359,19 @@ export class FormatPrinter extends Printer {
|
|
|
1700
1359
|
|
|
1701
1360
|
// --- Utility methods ---
|
|
1702
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
|
+
|
|
1703
1375
|
private formatFrontmatter(node: DocumentNode): Node[] {
|
|
1704
1376
|
const firstChild = node.children[0]
|
|
1705
1377
|
const hasFrontmatter = firstChild && isFrontmatter(firstChild)
|
|
@@ -1732,682 +1404,75 @@ export class FormatPrinter extends Printer {
|
|
|
1732
1404
|
}
|
|
1733
1405
|
}
|
|
1734
1406
|
|
|
1735
|
-
const
|
|
1736
|
-
this.inlineMode = true
|
|
1737
|
-
const inlineContent = this.capture(() => this.visit(child)).join("")
|
|
1738
|
-
this.inlineMode = oldInlineMode
|
|
1407
|
+
const inlineContent = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
1739
1408
|
this.pushToLastLine((hasSpaceBefore ? " " : "") + inlineContent)
|
|
1740
1409
|
}
|
|
1741
1410
|
}
|
|
1742
1411
|
|
|
1743
|
-
/**
|
|
1744
|
-
* Visit children in a text flow context (mixed text and inline elements)
|
|
1745
|
-
* Handles word wrapping and keeps adjacent inline elements together
|
|
1746
|
-
*/
|
|
1747
|
-
private visitTextFlowChildren(children: Node[]) {
|
|
1748
|
-
const adjacentInlineCount = countAdjacentInlineElements(children)
|
|
1749
|
-
|
|
1750
|
-
if (adjacentInlineCount >= 2) {
|
|
1751
|
-
const { processedIndices } = this.renderAdjacentInlineElements(children, adjacentInlineCount)
|
|
1752
|
-
this.visitRemainingChildren(children, processedIndices)
|
|
1753
|
-
|
|
1754
|
-
return
|
|
1755
|
-
}
|
|
1756
1412
|
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1413
|
+
// --- TextFlowDelegate implementation ---
|
|
1759
1414
|
|
|
1760
1415
|
/**
|
|
1761
|
-
*
|
|
1762
|
-
* Returns the wrapped lines with proper indentation
|
|
1416
|
+
* Render an inline element as a string
|
|
1763
1417
|
*/
|
|
1764
|
-
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1418
|
+
renderInlineElementAsString(element: HTMLElementNode): string {
|
|
1419
|
+
const tagName = getTagName(element)
|
|
1420
|
+
const tagClosing = getOpenTagClosing(element)
|
|
1767
1421
|
|
|
1768
|
-
|
|
1769
|
-
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 === "/>"
|
|
1770
1427
|
|
|
1771
|
-
|
|
1772
|
-
lines.push(this.indent + line)
|
|
1773
|
-
line = word
|
|
1774
|
-
} else {
|
|
1775
|
-
line = testLine
|
|
1776
|
-
}
|
|
1428
|
+
return `<${tagName}${attributesString}${isSelfClosing ? " />" : ">"}`
|
|
1777
1429
|
}
|
|
1778
1430
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1431
|
+
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1432
|
+
|
|
1433
|
+
const childInline = this.tryRenderInlineFull(element, tagName,
|
|
1434
|
+
filterNodes(getOpenTagChildren(element), HTMLAttributeNode),
|
|
1435
|
+
childrenToRender
|
|
1436
|
+
)
|
|
1782
1437
|
|
|
1783
|
-
return
|
|
1438
|
+
return childInline !== null ? childInline : ""
|
|
1784
1439
|
}
|
|
1785
1440
|
|
|
1786
1441
|
/**
|
|
1787
|
-
*
|
|
1788
|
-
* Returns object with merged content and whether processing should stop
|
|
1442
|
+
* Render an ERB node as a string
|
|
1789
1443
|
*/
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (combined.length <= wrapWidth) {
|
|
1794
|
-
return {
|
|
1795
|
-
mergedContent: inlineContent + trimmedText,
|
|
1796
|
-
shouldStop: false,
|
|
1797
|
-
wrappedLines: []
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1444
|
+
renderERBAsString(node: ERBContentNode): string {
|
|
1445
|
+
return this.withInlineMode(() => this.capture(() => this.visit(node)).join(""))
|
|
1446
|
+
}
|
|
1800
1447
|
|
|
1801
|
-
|
|
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)
|
|
1802
1454
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
mergedContent: inlineContent,
|
|
1806
|
-
shouldStop: false,
|
|
1807
|
-
wrappedLines: []
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1455
|
+
return this.tryRenderInlineFull(element, tagName, filterNodes(getOpenTagChildren(element), HTMLAttributeNode), childrenToRender)
|
|
1456
|
+
}
|
|
1810
1457
|
|
|
1811
|
-
const punctuation = match[0]
|
|
1812
|
-
const restText = trimmedText.substring(punctuation.length).trim()
|
|
1813
1458
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
shouldStop: false,
|
|
1818
|
-
wrappedLines: []
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
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))
|
|
1821
1462
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
let mergedWordCount = 0
|
|
1825
|
-
|
|
1826
|
-
for (const word of words) {
|
|
1827
|
-
const testMerge = toMerge + ' ' + word
|
|
1828
|
-
|
|
1829
|
-
if ((inlineContent + testMerge).length <= wrapWidth) {
|
|
1830
|
-
toMerge = testMerge
|
|
1831
|
-
mergedWordCount++
|
|
1832
|
-
} else {
|
|
1833
|
-
break
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
const mergedContent = inlineContent + toMerge
|
|
1838
|
-
|
|
1839
|
-
if (mergedWordCount >= words.length) {
|
|
1840
|
-
return {
|
|
1841
|
-
mergedContent,
|
|
1842
|
-
shouldStop: false,
|
|
1843
|
-
wrappedLines: []
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
const remainingWords = words.slice(mergedWordCount)
|
|
1848
|
-
const wrappedLines = this.wrapRemainingWords(remainingWords, wrapWidth)
|
|
1849
|
-
|
|
1850
|
-
return {
|
|
1851
|
-
mergedContent,
|
|
1852
|
-
shouldStop: true,
|
|
1853
|
-
wrappedLines
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
/**
|
|
1858
|
-
* Render adjacent inline elements together on one line
|
|
1859
|
-
*/
|
|
1860
|
-
private renderAdjacentInlineElements(children: Node[], count: number): { processedIndices: Set<number> } {
|
|
1861
|
-
let inlineContent = ""
|
|
1862
|
-
let processedCount = 0
|
|
1863
|
-
let lastProcessedIndex = -1
|
|
1864
|
-
const processedIndices = new Set<number>()
|
|
1865
|
-
|
|
1866
|
-
for (let index = 0; index < children.length && processedCount < count; index++) {
|
|
1867
|
-
const child = children[index]
|
|
1868
|
-
|
|
1869
|
-
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
1870
|
-
continue
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
if (isNode(child, HTMLElementNode) && isInlineElement(getTagName(child))) {
|
|
1874
|
-
inlineContent += this.renderInlineElementAsString(child)
|
|
1875
|
-
processedCount++
|
|
1876
|
-
lastProcessedIndex = index
|
|
1877
|
-
processedIndices.add(index)
|
|
1878
|
-
|
|
1879
|
-
if (inlineContent && isLineBreakingElement(child)) {
|
|
1880
|
-
this.pushWithIndent(inlineContent)
|
|
1881
|
-
inlineContent = ""
|
|
1882
|
-
}
|
|
1883
|
-
} else if (isNode(child, ERBContentNode)) {
|
|
1884
|
-
inlineContent += this.renderERBAsString(child)
|
|
1885
|
-
processedCount++
|
|
1886
|
-
lastProcessedIndex = index
|
|
1887
|
-
processedIndices.add(index)
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
if (lastProcessedIndex >= 0) {
|
|
1892
|
-
for (let index = lastProcessedIndex + 1; index < children.length; index++) {
|
|
1893
|
-
const child = children[index]
|
|
1894
|
-
|
|
1895
|
-
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
1896
|
-
continue
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
if (isNode(child, ERBContentNode)) {
|
|
1900
|
-
inlineContent += this.renderERBAsString(child)
|
|
1901
|
-
processedIndices.add(index)
|
|
1902
|
-
continue
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
if (isNode(child, HTMLTextNode)) {
|
|
1906
|
-
const trimmed = child.content.trim()
|
|
1907
|
-
|
|
1908
|
-
if (trimmed && /^[.!?:;]/.test(trimmed)) {
|
|
1909
|
-
const wrapWidth = this.maxLineLength - this.indent.length
|
|
1910
|
-
const result = this.tryMergePunctuationText(inlineContent, trimmed, wrapWidth)
|
|
1911
|
-
|
|
1912
|
-
inlineContent = result.mergedContent
|
|
1913
|
-
processedIndices.add(index)
|
|
1914
|
-
|
|
1915
|
-
if (result.shouldStop) {
|
|
1916
|
-
if (inlineContent) {
|
|
1917
|
-
this.pushWithIndent(inlineContent)
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
result.wrappedLines.forEach(line => this.push(line))
|
|
1921
|
-
|
|
1922
|
-
return { processedIndices }
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
break
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
if (inlineContent) {
|
|
1932
|
-
this.pushWithIndent(inlineContent)
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
return { processedIndices }
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
/**
|
|
1939
|
-
* Render an inline element as a string
|
|
1940
|
-
*/
|
|
1941
|
-
private renderInlineElementAsString(element: HTMLElementNode): string {
|
|
1942
|
-
const tagName = getTagName(element)
|
|
1943
|
-
|
|
1944
|
-
if (element.is_void || element.open_tag?.tag_closing?.value === "/>") {
|
|
1945
|
-
const attributes = filterNodes(element.open_tag?.children, HTMLAttributeNode)
|
|
1946
|
-
const attributesString = this.renderAttributesString(attributes)
|
|
1947
|
-
const isSelfClosing = element.open_tag?.tag_closing?.value === "/>"
|
|
1948
|
-
|
|
1949
|
-
return `<${tagName}${attributesString}${isSelfClosing ? " />" : ">"}`
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
const childrenToRender = this.getFilteredChildren(element.body)
|
|
1953
|
-
|
|
1954
|
-
const childInline = this.tryRenderInlineFull(element, tagName,
|
|
1955
|
-
filterNodes(element.open_tag?.children, HTMLAttributeNode),
|
|
1956
|
-
childrenToRender
|
|
1957
|
-
)
|
|
1958
|
-
|
|
1959
|
-
return childInline !== null ? childInline : ""
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
/**
|
|
1963
|
-
* Render an ERB node as a string
|
|
1964
|
-
*/
|
|
1965
|
-
private renderERBAsString(node: ERBContentNode): string {
|
|
1966
|
-
return this.capture(() => {
|
|
1967
|
-
this.inlineMode = true
|
|
1968
|
-
this.visit(node)
|
|
1969
|
-
}).join("")
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
/**
|
|
1973
|
-
* Visit remaining children after processing adjacent inline elements
|
|
1974
|
-
*/
|
|
1975
|
-
private visitRemainingChildren(children: Node[], processedIndices: Set<number>): void {
|
|
1976
|
-
for (let index = 0; index < children.length; index++) {
|
|
1977
|
-
const child = children[index]
|
|
1978
|
-
|
|
1979
|
-
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
1980
|
-
continue
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
if (processedIndices.has(index)) {
|
|
1984
|
-
continue
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
this.visit(child)
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
/**
|
|
1992
|
-
* Build words array from text/inline/ERB and wrap them
|
|
1993
|
-
*/
|
|
1994
|
-
private buildAndWrapTextFlow(children: Node[]): void {
|
|
1995
|
-
const unitsWithNodes: ContentUnitWithNode[] = this.buildContentUnitsWithNodes(children)
|
|
1996
|
-
const words: Array<{ word: string, isHerbDisable: boolean }> = []
|
|
1997
|
-
|
|
1998
|
-
for (const { unit, node } of unitsWithNodes) {
|
|
1999
|
-
if (unit.breaksFlow) {
|
|
2000
|
-
this.flushWords(words)
|
|
2001
|
-
|
|
2002
|
-
if (node) {
|
|
2003
|
-
this.visit(node)
|
|
2004
|
-
}
|
|
2005
|
-
} else if (unit.isAtomic) {
|
|
2006
|
-
words.push({ word: unit.content, isHerbDisable: unit.isHerbDisable || false })
|
|
2007
|
-
} else {
|
|
2008
|
-
const text = unit.content.replace(ASCII_WHITESPACE, ' ')
|
|
2009
|
-
const hasLeadingSpace = text.startsWith(' ')
|
|
2010
|
-
const hasTrailingSpace = text.endsWith(' ')
|
|
2011
|
-
const trimmedText = text.trim()
|
|
2012
|
-
|
|
2013
|
-
if (trimmedText) {
|
|
2014
|
-
if (hasLeadingSpace && words.length > 0) {
|
|
2015
|
-
const lastWord = words[words.length - 1]
|
|
2016
|
-
|
|
2017
|
-
if (!lastWord.word.endsWith(' ')) {
|
|
2018
|
-
lastWord.word += ' '
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
const textWords = trimmedText.split(' ').map(w => ({ word: w, isHerbDisable: false }))
|
|
2023
|
-
words.push(...textWords)
|
|
2024
|
-
|
|
2025
|
-
if (hasTrailingSpace && words.length > 0) {
|
|
2026
|
-
const lastWord = words[words.length - 1]
|
|
2027
|
-
|
|
2028
|
-
if (!isClosingPunctuation(lastWord.word)) {
|
|
2029
|
-
lastWord.word += ' '
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
} else if (text === ' ' && words.length > 0) {
|
|
2033
|
-
const lastWord = words[words.length - 1]
|
|
2034
|
-
|
|
2035
|
-
if (!lastWord.word.endsWith(' ')) {
|
|
2036
|
-
lastWord.word += ' '
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
this.flushWords(words)
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
/**
|
|
2046
|
-
* Try to merge text that follows an atomic unit (ERB/inline) with no whitespace
|
|
2047
|
-
* Returns true if merge was performed
|
|
2048
|
-
*/
|
|
2049
|
-
private tryMergeTextAfterAtomic(result: ContentUnitWithNode[], textNode: HTMLTextNode): boolean {
|
|
2050
|
-
if (result.length === 0) return false
|
|
2051
|
-
|
|
2052
|
-
const lastUnit = result[result.length - 1]
|
|
2053
|
-
|
|
2054
|
-
if (!lastUnit.unit.isAtomic || (lastUnit.unit.type !== 'erb' && lastUnit.unit.type !== 'inline')) {
|
|
2055
|
-
return false
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
const words = normalizeAndSplitWords(textNode.content)
|
|
2059
|
-
if (words.length === 0 || !words[0]) return false
|
|
2060
|
-
|
|
2061
|
-
const firstWord = words[0]
|
|
2062
|
-
const firstChar = firstWord[0]
|
|
2063
|
-
|
|
2064
|
-
if (' \t\n\r'.includes(firstChar)) {
|
|
2065
|
-
return false
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
lastUnit.unit.content += firstWord
|
|
2069
|
-
|
|
2070
|
-
if (words.length > 1) {
|
|
2071
|
-
let remainingText = words.slice(1).join(' ')
|
|
2072
|
-
|
|
2073
|
-
if (endsWithWhitespace(textNode.content)) {
|
|
2074
|
-
remainingText += ' '
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
result.push({
|
|
2078
|
-
unit: { content: remainingText, type: 'text', isAtomic: false, breaksFlow: false },
|
|
2079
|
-
node: textNode
|
|
2080
|
-
})
|
|
2081
|
-
} else if (endsWithWhitespace(textNode.content)) {
|
|
2082
|
-
result.push({
|
|
2083
|
-
unit: { content: ' ', type: 'text', isAtomic: false, breaksFlow: false },
|
|
2084
|
-
node: textNode
|
|
2085
|
-
})
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
return true
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
/**
|
|
2092
|
-
* Try to merge an atomic unit (ERB/inline) with preceding text that has no whitespace
|
|
2093
|
-
* Returns true if merge was performed
|
|
2094
|
-
*/
|
|
2095
|
-
private tryMergeAtomicAfterText(result: ContentUnitWithNode[], children: Node[], lastProcessedIndex: number, atomicContent: string, atomicType: 'erb' | 'inline', atomicNode: Node): boolean {
|
|
2096
|
-
if (result.length === 0) return false
|
|
2097
|
-
|
|
2098
|
-
const lastUnit = result[result.length - 1]
|
|
2099
|
-
|
|
2100
|
-
if (lastUnit.unit.type !== 'text' || lastUnit.unit.isAtomic) return false
|
|
2101
|
-
|
|
2102
|
-
const words = normalizeAndSplitWords(lastUnit.unit.content)
|
|
2103
|
-
const lastWord = words[words.length - 1]
|
|
2104
|
-
|
|
2105
|
-
if (!lastWord) return false
|
|
2106
|
-
|
|
2107
|
-
result.pop()
|
|
2108
|
-
|
|
2109
|
-
if (words.length > 1) {
|
|
2110
|
-
const remainingText = words.slice(0, -1).join(' ')
|
|
2111
|
-
|
|
2112
|
-
result.push({
|
|
2113
|
-
unit: { content: remainingText, type: 'text', isAtomic: false, breaksFlow: false },
|
|
2114
|
-
node: children[lastProcessedIndex]
|
|
2115
|
-
})
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
result.push({
|
|
2119
|
-
unit: { content: lastWord + atomicContent, type: atomicType, isAtomic: true, breaksFlow: false },
|
|
2120
|
-
node: atomicNode
|
|
2121
|
-
})
|
|
2122
|
-
|
|
2123
|
-
return true
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
/**
|
|
2127
|
-
* Check if there's whitespace between current node and last processed node
|
|
2128
|
-
*/
|
|
2129
|
-
private hasWhitespaceBeforeNode(children: Node[], lastProcessedIndex: number, currentIndex: number, currentNode: Node): boolean {
|
|
2130
|
-
if (hasWhitespaceBetween(children, lastProcessedIndex, currentIndex)) {
|
|
2131
|
-
return true
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
if (isNode(currentNode, HTMLTextNode) && /^[ \t\n\r]/.test(currentNode.content)) {
|
|
2135
|
-
return true
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
return false
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
/**
|
|
2142
|
-
* Check if last unit in result ends with whitespace
|
|
2143
|
-
*/
|
|
2144
|
-
private lastUnitEndsWithWhitespace(result: ContentUnitWithNode[]): boolean {
|
|
2145
|
-
if (result.length === 0) return false
|
|
2146
|
-
|
|
2147
|
-
const lastUnit = result[result.length - 1]
|
|
2148
|
-
|
|
2149
|
-
return lastUnit.unit.type === 'text' && endsWithWhitespace(lastUnit.unit.content)
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
/**
|
|
2153
|
-
* Process a text node and add it to results (with potential merging)
|
|
2154
|
-
*/
|
|
2155
|
-
private processTextNode(result: ContentUnitWithNode[], children: Node[], child: HTMLTextNode, index: number, lastProcessedIndex: number): void {
|
|
2156
|
-
const isAtomic = child.content === ' '
|
|
2157
|
-
|
|
2158
|
-
if (!isAtomic && lastProcessedIndex >= 0 && result.length > 0) {
|
|
2159
|
-
const hasWhitespace = this.hasWhitespaceBeforeNode(children, lastProcessedIndex, index, child)
|
|
2160
|
-
const lastUnit = result[result.length - 1]
|
|
2161
|
-
const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'erb' || lastUnit.unit.type === 'inline')
|
|
2162
|
-
|
|
2163
|
-
if (lastIsAtomic && !hasWhitespace && this.tryMergeTextAfterAtomic(result, child)) {
|
|
2164
|
-
return
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
result.push({
|
|
2169
|
-
unit: { content: child.content, type: 'text', isAtomic, breaksFlow: false },
|
|
2170
|
-
node: child
|
|
2171
|
-
})
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
/**
|
|
2175
|
-
* Process an inline element and add it to results (with potential merging)
|
|
2176
|
-
*/
|
|
2177
|
-
private processInlineElement(result: ContentUnitWithNode[], children: Node[], child: HTMLElementNode, index: number, lastProcessedIndex: number): boolean {
|
|
2178
|
-
const tagName = getTagName(child)
|
|
2179
|
-
const childrenToRender = this.getFilteredChildren(child.body)
|
|
2180
|
-
const inlineContent = this.tryRenderInlineFull(child, tagName, filterNodes(child.open_tag?.children, HTMLAttributeNode), childrenToRender)
|
|
2181
|
-
|
|
2182
|
-
if (inlineContent === null) {
|
|
2183
|
-
result.push({
|
|
2184
|
-
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
2185
|
-
node: child
|
|
2186
|
-
})
|
|
2187
|
-
|
|
2188
|
-
return false
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
if (lastProcessedIndex >= 0) {
|
|
2192
|
-
const hasWhitespace = hasWhitespaceBetween(children, lastProcessedIndex, index) || this.lastUnitEndsWithWhitespace(result)
|
|
2193
|
-
|
|
2194
|
-
if (!hasWhitespace && this.tryMergeAtomicAfterText(result, children, lastProcessedIndex, inlineContent, 'inline', child)) {
|
|
2195
|
-
return true
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
result.push({
|
|
2200
|
-
unit: { content: inlineContent, type: 'inline', isAtomic: true, breaksFlow: false },
|
|
2201
|
-
node: child
|
|
2202
|
-
})
|
|
2203
|
-
|
|
2204
|
-
return false
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
/**
|
|
2208
|
-
* Process an ERB content node and add it to results (with potential merging)
|
|
2209
|
-
*/
|
|
2210
|
-
private processERBContentNode(result: ContentUnitWithNode[], children: Node[], child: ERBContentNode, index: number, lastProcessedIndex: number): boolean {
|
|
2211
|
-
const erbContent = this.renderERBAsString(child)
|
|
2212
|
-
const isHerbDisable = isHerbDisableComment(child)
|
|
2213
|
-
|
|
2214
|
-
if (lastProcessedIndex >= 0) {
|
|
2215
|
-
const hasWhitespace = hasWhitespaceBetween(children, lastProcessedIndex, index) || this.lastUnitEndsWithWhitespace(result)
|
|
2216
|
-
|
|
2217
|
-
if (!hasWhitespace && this.tryMergeAtomicAfterText(result, children, lastProcessedIndex, erbContent, 'erb', child)) {
|
|
2218
|
-
return true
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
if (hasWhitespace && result.length > 0) {
|
|
2222
|
-
const lastUnit = result[result.length - 1]
|
|
2223
|
-
const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'inline' || lastUnit.unit.type === 'erb')
|
|
2224
|
-
|
|
2225
|
-
if (lastIsAtomic && !this.lastUnitEndsWithWhitespace(result)) {
|
|
2226
|
-
result.push({
|
|
2227
|
-
unit: { content: ' ', type: 'text', isAtomic: true, breaksFlow: false },
|
|
2228
|
-
node: null
|
|
2229
|
-
})
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
result.push({
|
|
2235
|
-
unit: { content: erbContent, type: 'erb', isAtomic: true, breaksFlow: false, isHerbDisable },
|
|
2236
|
-
node: child
|
|
2237
|
-
})
|
|
2238
|
-
|
|
2239
|
-
return false
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* Convert AST nodes to content units with node references
|
|
2244
|
-
*/
|
|
2245
|
-
private buildContentUnitsWithNodes(children: Node[]): ContentUnitWithNode[] {
|
|
2246
|
-
const result: ContentUnitWithNode[] = []
|
|
2247
|
-
let lastProcessedIndex = -1
|
|
2248
|
-
|
|
2249
|
-
for (let i = 0; i < children.length; i++) {
|
|
2250
|
-
const child = children[i]
|
|
2251
|
-
|
|
2252
|
-
if (isNode(child, WhitespaceNode)) continue
|
|
2253
|
-
|
|
2254
|
-
if (isPureWhitespaceNode(child) && !(isNode(child, HTMLTextNode) && child.content === ' ')) {
|
|
2255
|
-
if (lastProcessedIndex >= 0) {
|
|
2256
|
-
const hasNonWhitespaceAfter = children.slice(i + 1).some(node =>
|
|
2257
|
-
!isNode(node, WhitespaceNode) && !isPureWhitespaceNode(node)
|
|
2258
|
-
)
|
|
2259
|
-
|
|
2260
|
-
if (hasNonWhitespaceAfter) {
|
|
2261
|
-
const previousNode = children[lastProcessedIndex]
|
|
2262
|
-
|
|
2263
|
-
if (!isLineBreakingElement(previousNode)) {
|
|
2264
|
-
result.push({
|
|
2265
|
-
unit: { content: ' ', type: 'text', isAtomic: true, breaksFlow: false },
|
|
2266
|
-
node: child
|
|
2267
|
-
})
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
continue
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
if (isNode(child, HTMLTextNode)) {
|
|
2276
|
-
this.processTextNode(result, children, child, i, lastProcessedIndex)
|
|
2277
|
-
|
|
2278
|
-
lastProcessedIndex = i
|
|
2279
|
-
} else if (isNode(child, HTMLElementNode)) {
|
|
2280
|
-
const tagName = getTagName(child)
|
|
2281
|
-
|
|
2282
|
-
if (isInlineElement(tagName)) {
|
|
2283
|
-
const merged = this.processInlineElement(result, children, child, i, lastProcessedIndex)
|
|
2284
|
-
|
|
2285
|
-
if (merged) {
|
|
2286
|
-
lastProcessedIndex = i
|
|
2287
|
-
|
|
2288
|
-
continue
|
|
2289
|
-
}
|
|
2290
|
-
} else {
|
|
2291
|
-
result.push({
|
|
2292
|
-
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
2293
|
-
node: child
|
|
2294
|
-
})
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
lastProcessedIndex = i
|
|
2298
|
-
} else if (isNode(child, ERBContentNode)) {
|
|
2299
|
-
const merged = this.processERBContentNode(result, children, child, i, lastProcessedIndex)
|
|
2300
|
-
|
|
2301
|
-
if (merged) {
|
|
2302
|
-
lastProcessedIndex = i
|
|
2303
|
-
|
|
2304
|
-
continue
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
lastProcessedIndex = i
|
|
2308
|
-
} else {
|
|
2309
|
-
result.push({
|
|
2310
|
-
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
2311
|
-
node: child
|
|
2312
|
-
})
|
|
2313
|
-
|
|
2314
|
-
lastProcessedIndex = i
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
return result
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
/**
|
|
2322
|
-
* Flush accumulated words to output with wrapping
|
|
2323
|
-
*/
|
|
2324
|
-
private flushWords(words: Array<{ word: string, isHerbDisable: boolean }>): void {
|
|
2325
|
-
if (words.length > 0) {
|
|
2326
|
-
this.wrapAndPushWords(words)
|
|
2327
|
-
words.length = 0
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
/**
|
|
2332
|
-
* Wrap words to fit within line length and push to output
|
|
2333
|
-
* Handles punctuation spacing intelligently
|
|
2334
|
-
* Excludes herb:disable comments from line length calculations
|
|
2335
|
-
*/
|
|
2336
|
-
private wrapAndPushWords(words: Array<{ word: string, isHerbDisable: boolean }>): void {
|
|
2337
|
-
const wrapWidth = this.maxLineLength - this.indent.length
|
|
2338
|
-
const lines: string[] = []
|
|
2339
|
-
let currentLine = ""
|
|
2340
|
-
let effectiveLength = 0
|
|
2341
|
-
|
|
2342
|
-
for (const { word, isHerbDisable } of words) {
|
|
2343
|
-
const nextLine = buildLineWithWord(currentLine, word)
|
|
2344
|
-
|
|
2345
|
-
let nextEffectiveLength = effectiveLength
|
|
2346
|
-
|
|
2347
|
-
if (!isHerbDisable) {
|
|
2348
|
-
const spaceBefore = currentLine && needsSpaceBetween(currentLine, word) ? 1 : 0
|
|
2349
|
-
nextEffectiveLength = effectiveLength + spaceBefore + word.length
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
if (currentLine && !isClosingPunctuation(word) && nextEffectiveLength >= wrapWidth) {
|
|
2353
|
-
lines.push(this.indent + currentLine.trimEnd())
|
|
2354
|
-
|
|
2355
|
-
currentLine = word
|
|
2356
|
-
effectiveLength = isHerbDisable ? 0 : word.length
|
|
2357
|
-
} else {
|
|
2358
|
-
currentLine = nextLine
|
|
2359
|
-
effectiveLength = nextEffectiveLength
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
if (currentLine) {
|
|
2364
|
-
lines.push(this.indent + currentLine.trimEnd())
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
lines.forEach(line => this.push(line))
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
private isInTextFlowContext(_parent: Node | null, children: Node[]): boolean {
|
|
2371
|
-
const hasTextContent = children.some(child => isNode(child, HTMLTextNode) &&child.content.trim() !== "")
|
|
2372
|
-
const nonTextChildren = children.filter(child => !isNode(child, HTMLTextNode))
|
|
2373
|
-
|
|
2374
|
-
if (!hasTextContent) return false
|
|
2375
|
-
if (nonTextChildren.length === 0) return false
|
|
2376
|
-
|
|
2377
|
-
const allInline = nonTextChildren.every(child => {
|
|
2378
|
-
if (isNode(child, ERBContentNode)) return true
|
|
2379
|
-
|
|
2380
|
-
if (isNode(child, HTMLElementNode)) {
|
|
2381
|
-
return isInlineElement(getTagName(child))
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
return false
|
|
2385
|
-
})
|
|
2386
|
-
|
|
2387
|
-
if (!allInline) return false
|
|
2388
|
-
|
|
2389
|
-
return true
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
private renderInlineOpen(name: string, attributes: HTMLAttributeNode[], selfClose: boolean, inlineNodes: Node[] = [], allChildren: Node[] = []): string {
|
|
2393
|
-
const parts = attributes.map(attribute => this.renderAttribute(attribute))
|
|
2394
|
-
|
|
2395
|
-
if (inlineNodes.length > 0) {
|
|
2396
|
-
let result = `<${name}`
|
|
1463
|
+
if (inlineNodes.length > 0) {
|
|
1464
|
+
let result = `<${name}`
|
|
2397
1465
|
|
|
2398
1466
|
if (allChildren.length > 0) {
|
|
2399
1467
|
const lines = this.capture(() => {
|
|
2400
1468
|
allChildren.forEach(child => {
|
|
2401
1469
|
if (isNode(child, HTMLAttributeNode)) {
|
|
2402
|
-
this.lines.push(" " + this.renderAttribute(child))
|
|
1470
|
+
this.lines.push(" " + this.attributeRenderer.renderAttribute(child, name))
|
|
2403
1471
|
} else if (!(isNode(child, WhitespaceNode))) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
this.lines.push(" ")
|
|
2409
|
-
this.visit(child)
|
|
2410
|
-
this.inlineMode = wasInlineMode
|
|
1472
|
+
this.withInlineMode(() => {
|
|
1473
|
+
this.lines.push(" ")
|
|
1474
|
+
this.visit(child)
|
|
1475
|
+
})
|
|
2411
1476
|
}
|
|
2412
1477
|
})
|
|
2413
1478
|
})
|
|
@@ -2420,15 +1485,11 @@ export class FormatPrinter extends Printer {
|
|
|
2420
1485
|
|
|
2421
1486
|
const lines = this.capture(() => {
|
|
2422
1487
|
inlineNodes.forEach(node => {
|
|
2423
|
-
const wasInlineMode = this.inlineMode
|
|
2424
|
-
|
|
2425
1488
|
if (!isERBControlFlowNode(node)) {
|
|
2426
|
-
this.
|
|
1489
|
+
this.withInlineMode(() => this.visit(node))
|
|
1490
|
+
} else {
|
|
1491
|
+
this.visit(node)
|
|
2427
1492
|
}
|
|
2428
|
-
|
|
2429
|
-
this.visit(node)
|
|
2430
|
-
|
|
2431
|
-
this.inlineMode = wasInlineMode
|
|
2432
1493
|
})
|
|
2433
1494
|
})
|
|
2434
1495
|
|
|
@@ -2443,70 +1504,14 @@ export class FormatPrinter extends Printer {
|
|
|
2443
1504
|
return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " />" : ">"}`
|
|
2444
1505
|
}
|
|
2445
1506
|
|
|
2446
|
-
renderAttribute(attribute: HTMLAttributeNode): string {
|
|
2447
|
-
const name = attribute.name ? getCombinedAttributeName(attribute.name) : ""
|
|
2448
|
-
const equals = attribute.equals?.value ?? ""
|
|
2449
|
-
|
|
2450
|
-
this.currentAttributeName = name
|
|
2451
|
-
|
|
2452
|
-
let value = ""
|
|
2453
|
-
|
|
2454
|
-
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
2455
|
-
const attributeValue = attribute.value
|
|
2456
|
-
|
|
2457
|
-
let open_quote = attributeValue.open_quote?.value ?? ""
|
|
2458
|
-
let close_quote = attributeValue.close_quote?.value ?? ""
|
|
2459
|
-
let htmlTextContent = ""
|
|
2460
|
-
|
|
2461
|
-
const content = attributeValue.children.map((child: Node) => {
|
|
2462
|
-
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
2463
|
-
htmlTextContent += child.content
|
|
2464
|
-
|
|
2465
|
-
return child.content
|
|
2466
|
-
} else if (isNode(child, ERBContentNode)) {
|
|
2467
|
-
return this.reconstructERBNode(child, true)
|
|
2468
|
-
} else {
|
|
2469
|
-
const printed = IdentityPrinter.print(child)
|
|
2470
|
-
|
|
2471
|
-
if (this.isInTokenListAttribute) {
|
|
2472
|
-
return printed.replace(/%>([^<\s])/g, '%> $1').replace(/([^>\s])<%/g, '$1 <%')
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
return printed
|
|
2476
|
-
}
|
|
2477
|
-
}).join("")
|
|
2478
|
-
|
|
2479
|
-
if (open_quote === "" && close_quote === "") {
|
|
2480
|
-
open_quote = '"'
|
|
2481
|
-
close_quote = '"'
|
|
2482
|
-
} else if (open_quote === "'" && close_quote === "'" && !htmlTextContent.includes('"')) {
|
|
2483
|
-
open_quote = '"'
|
|
2484
|
-
close_quote = '"'
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
if (this.isFormattableAttribute(name, this.currentTagName)) {
|
|
2488
|
-
if (name === 'class') {
|
|
2489
|
-
value = this.formatClassAttribute(content, name, equals, open_quote, close_quote)
|
|
2490
|
-
} else {
|
|
2491
|
-
value = this.formatMultilineAttribute(content, name, open_quote, close_quote)
|
|
2492
|
-
}
|
|
2493
|
-
} else {
|
|
2494
|
-
value = open_quote + content + close_quote
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
this.currentAttributeName = null
|
|
2499
|
-
|
|
2500
|
-
return name + equals + value
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
1507
|
/**
|
|
2504
1508
|
* Try to render a complete element inline including opening tag, children, and closing tag
|
|
2505
1509
|
*/
|
|
2506
1510
|
private tryRenderInlineFull(_node: HTMLElementNode, tagName: string, attributes: HTMLAttributeNode[], children: Node[]): string | null {
|
|
2507
1511
|
let result = `<${tagName}`
|
|
2508
1512
|
|
|
2509
|
-
|
|
1513
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1514
|
+
result += this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
2510
1515
|
result += ">"
|
|
2511
1516
|
|
|
2512
1517
|
const childrenContent = this.tryRenderChildrenInline(children, tagName)
|
|
@@ -2519,21 +1524,6 @@ export class FormatPrinter extends Printer {
|
|
|
2519
1524
|
return result
|
|
2520
1525
|
}
|
|
2521
1526
|
|
|
2522
|
-
/**
|
|
2523
|
-
* Check if children contain a leading herb:disable comment (after optional whitespace)
|
|
2524
|
-
*/
|
|
2525
|
-
private hasLeadingHerbDisable(children: Node[]): boolean {
|
|
2526
|
-
for (const child of children) {
|
|
2527
|
-
if (isNode(child, WhitespaceNode) || (isNode(child, HTMLTextNode) && child.content.trim() === "")) {
|
|
2528
|
-
continue
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
return isNode(child, ERBContentNode) && isHerbDisableComment(child)
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
return false
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
1527
|
/**
|
|
2538
1528
|
* Try to render just the children inline (without tags)
|
|
2539
1529
|
*/
|
|
@@ -2542,7 +1532,7 @@ export class FormatPrinter extends Printer {
|
|
|
2542
1532
|
let hasInternalWhitespace = false
|
|
2543
1533
|
let addedLeadingSpace = false
|
|
2544
1534
|
|
|
2545
|
-
const hasHerbDisable =
|
|
1535
|
+
const hasHerbDisable = hasLeadingHerbDisable(children)
|
|
2546
1536
|
const hasOnlyTextContent = children.every(child => isNode(child, HTMLTextNode) || isNode(child, WhitespaceNode))
|
|
2547
1537
|
const shouldPreserveSpaces = hasOnlyTextContent && tagName && isInlineElement(tagName)
|
|
2548
1538
|
|
|
@@ -2568,9 +1558,7 @@ export class FormatPrinter extends Printer {
|
|
|
2568
1558
|
}
|
|
2569
1559
|
}
|
|
2570
1560
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
if (isWhitespace && !result.endsWith(' ')) {
|
|
1561
|
+
if (isPureWhitespaceNode(child) && !result.endsWith(' ')) {
|
|
2574
1562
|
if (!result && hasHerbDisable && !addedLeadingSpace) {
|
|
2575
1563
|
result += ' '
|
|
2576
1564
|
addedLeadingSpace = true
|
|
@@ -2587,7 +1575,7 @@ export class FormatPrinter extends Printer {
|
|
|
2587
1575
|
|
|
2588
1576
|
const childrenToRender = this.getFilteredChildren(child.body)
|
|
2589
1577
|
const childInline = this.tryRenderInlineFull(child, tagName,
|
|
2590
|
-
filterNodes(child
|
|
1578
|
+
filterNodes(getOpenTagChildren(child), HTMLAttributeNode),
|
|
2591
1579
|
childrenToRender
|
|
2592
1580
|
)
|
|
2593
1581
|
|
|
@@ -2596,11 +1584,8 @@ export class FormatPrinter extends Printer {
|
|
|
2596
1584
|
}
|
|
2597
1585
|
|
|
2598
1586
|
result += childInline
|
|
2599
|
-
} else if (!isNode(child, HTMLTextNode) && !
|
|
2600
|
-
const
|
|
2601
|
-
this.inlineMode = true
|
|
2602
|
-
const captured = this.capture(() => this.visit(child)).join("")
|
|
2603
|
-
this.inlineMode = wasInlineMode
|
|
1587
|
+
} else if (!isNode(child, HTMLTextNode) && !isPureWhitespaceNode(child)) {
|
|
1588
|
+
const captured = this.withInlineMode(() => this.capture(() => this.visit(child)).join(""))
|
|
2604
1589
|
result += captured
|
|
2605
1590
|
}
|
|
2606
1591
|
}
|
|
@@ -2669,10 +1654,11 @@ export class FormatPrinter extends Printer {
|
|
|
2669
1654
|
for (const child of children) {
|
|
2670
1655
|
if (isNode(child, HTMLTextNode)) {
|
|
2671
1656
|
content += child.content
|
|
2672
|
-
} else if (isNode(child, HTMLElementNode)
|
|
1657
|
+
} else if (isNode(child, HTMLElementNode)) {
|
|
2673
1658
|
const tagName = getTagName(child)
|
|
2674
|
-
const attributes = filterNodes(child
|
|
2675
|
-
|
|
1659
|
+
const attributes = filterNodes(getOpenTagChildren(child), HTMLAttributeNode)
|
|
1660
|
+
this.attributeRenderer.indentLevel = this.indentLevel
|
|
1661
|
+
const attributesString = this.attributeRenderer.renderAttributesString(attributes, tagName)
|
|
2676
1662
|
const childContent = this.renderElementInline(child)
|
|
2677
1663
|
|
|
2678
1664
|
content += `<${tagName}${attributesString}>${childContent}</${tagName}>`
|