@herb-tools/formatter 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/herb-format.js +447 -227
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +372 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +372 -176
- package/dist/index.esm.js.map +1 -1
- package/dist/types/format-helpers.d.ts +0 -3
- package/dist/types/format-ignore.d.ts +9 -0
- package/dist/types/format-printer.d.ts +23 -0
- package/package.json +5 -5
- package/src/format-helpers.ts +0 -11
- package/src/format-ignore.ts +45 -0
- package/src/format-printer.ts +339 -170
- package/src/formatter.ts +2 -1
package/src/format-printer.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
isERBNode,
|
|
14
14
|
isCommentNode,
|
|
15
15
|
isERBControlFlowNode,
|
|
16
|
+
isERBCommentNode,
|
|
17
|
+
isERBOutputNode,
|
|
16
18
|
filterNodes,
|
|
17
19
|
} from "@herb-tools/core"
|
|
18
20
|
|
|
@@ -47,9 +49,6 @@ import {
|
|
|
47
49
|
FORMATTABLE_ATTRIBUTES,
|
|
48
50
|
INLINE_ELEMENTS,
|
|
49
51
|
SPACEABLE_CONTAINERS,
|
|
50
|
-
SPACING_THRESHOLD,
|
|
51
|
-
TIGHT_GROUP_CHILDREN,
|
|
52
|
-
TIGHT_GROUP_PARENTS,
|
|
53
52
|
TOKEN_LIST_ATTRIBUTES,
|
|
54
53
|
} from "./format-helpers.js"
|
|
55
54
|
|
|
@@ -122,6 +121,11 @@ export class FormatPrinter extends Printer {
|
|
|
122
121
|
private currentAttributeName: string | null = null
|
|
123
122
|
private elementStack: HTMLElementNode[] = []
|
|
124
123
|
private elementFormattingAnalysis = new Map<HTMLElementNode, ElementFormattingAnalysis>()
|
|
124
|
+
private nodeIsMultiline = new Map<Node, boolean>()
|
|
125
|
+
private stringLineCount: number = 0
|
|
126
|
+
private tagGroupsCache = new Map<Node[], Map<number, { tagName: string; groupStart: number; groupEnd: number }>>()
|
|
127
|
+
private allSingleLineCache = new Map<Node[], boolean>()
|
|
128
|
+
|
|
125
129
|
|
|
126
130
|
public source: string
|
|
127
131
|
|
|
@@ -141,6 +145,10 @@ export class FormatPrinter extends Printer {
|
|
|
141
145
|
// TODO: refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
142
146
|
this.lines = []
|
|
143
147
|
this.indentLevel = 0
|
|
148
|
+
this.stringLineCount = 0
|
|
149
|
+
this.nodeIsMultiline.clear()
|
|
150
|
+
this.tagGroupsCache.clear()
|
|
151
|
+
this.allSingleLineCache.clear()
|
|
144
152
|
|
|
145
153
|
this.visit(node)
|
|
146
154
|
|
|
@@ -179,19 +187,32 @@ export class FormatPrinter extends Printer {
|
|
|
179
187
|
private capture(callback: () => void): string[] {
|
|
180
188
|
const previousLines = this.lines
|
|
181
189
|
const previousInlineMode = this.inlineMode
|
|
190
|
+
const previousStringLineCount = this.stringLineCount
|
|
182
191
|
|
|
183
192
|
this.lines = []
|
|
193
|
+
this.stringLineCount = 0
|
|
184
194
|
|
|
185
195
|
try {
|
|
186
196
|
callback()
|
|
187
|
-
|
|
188
197
|
return this.lines
|
|
189
198
|
} finally {
|
|
190
199
|
this.lines = previousLines
|
|
191
200
|
this.inlineMode = previousInlineMode
|
|
201
|
+
this.stringLineCount = previousStringLineCount
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Track a boundary node's multiline status by comparing line count before/after rendering.
|
|
207
|
+
*/
|
|
208
|
+
private trackBoundary(node: Node, callback: () => void): void {
|
|
209
|
+
const startLineCount = this.stringLineCount
|
|
210
|
+
callback()
|
|
211
|
+
const endLineCount = this.stringLineCount
|
|
212
|
+
|
|
213
|
+
this.nodeIsMultiline.set(node, (endLineCount - startLineCount) > 1)
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/**
|
|
196
217
|
* Capture all nodes that would be visited during a callback
|
|
197
218
|
* Returns a flat list of all nodes without generating any output
|
|
@@ -232,6 +253,7 @@ export class FormatPrinter extends Printer {
|
|
|
232
253
|
*/
|
|
233
254
|
private push(line: string) {
|
|
234
255
|
this.lines.push(line)
|
|
256
|
+
this.stringLineCount++
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
/**
|
|
@@ -288,6 +310,96 @@ export class FormatPrinter extends Printer {
|
|
|
288
310
|
return nodes.filter(child => isNoneOf(child, HTMLAttributeNode, WhitespaceNode))
|
|
289
311
|
}
|
|
290
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Check if a node will render as multiple lines when formatted.
|
|
315
|
+
*/
|
|
316
|
+
private isMultilineElement(node: Node): boolean {
|
|
317
|
+
if (isNode(node, ERBContentNode)) {
|
|
318
|
+
return (node.content?.value || "").includes("\n")
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (isNode(node, HTMLElementNode) && isContentPreserving(node)) {
|
|
322
|
+
return true
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const tracked = this.nodeIsMultiline.get(node)
|
|
326
|
+
|
|
327
|
+
if (tracked !== undefined) {
|
|
328
|
+
return tracked
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return false
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get a grouping key for a node (tag name for HTML, ERB type for ERB)
|
|
336
|
+
*/
|
|
337
|
+
private getGroupingKey(node: Node): string | null {
|
|
338
|
+
if (isNode(node, HTMLElementNode)) {
|
|
339
|
+
return getTagName(node)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (isERBOutputNode(node)) return "erb-output"
|
|
343
|
+
if (isERBCommentNode(node)) return "erb-comment"
|
|
344
|
+
if (isERBNode(node)) return "erb-code"
|
|
345
|
+
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Detect groups of consecutive same-tag/same-type single-line elements
|
|
351
|
+
* Returns a map of index -> group info for efficient lookup
|
|
352
|
+
*/
|
|
353
|
+
private detectTagGroups(siblings: Node[]): Map<number, { tagName: string; groupStart: number; groupEnd: number }> {
|
|
354
|
+
const cached = this.tagGroupsCache.get(siblings)
|
|
355
|
+
if (cached) return cached
|
|
356
|
+
|
|
357
|
+
const groupMap = new Map<number, { tagName: string; groupStart: number; groupEnd: number }>()
|
|
358
|
+
const meaningfulNodes: Array<{ index: number; groupKey: string }> = []
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
361
|
+
const node = siblings[i]
|
|
362
|
+
|
|
363
|
+
if (!this.isMultilineElement(node)) {
|
|
364
|
+
const groupKey = this.getGroupingKey(node)
|
|
365
|
+
|
|
366
|
+
if (groupKey) {
|
|
367
|
+
meaningfulNodes.push({ index: i, groupKey })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let groupStart = 0
|
|
373
|
+
|
|
374
|
+
while (groupStart < meaningfulNodes.length) {
|
|
375
|
+
const startGroupKey = meaningfulNodes[groupStart].groupKey
|
|
376
|
+
let groupEnd = groupStart
|
|
377
|
+
|
|
378
|
+
while (groupEnd + 1 < meaningfulNodes.length && meaningfulNodes[groupEnd + 1].groupKey === startGroupKey) {
|
|
379
|
+
groupEnd++
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (groupEnd > groupStart) {
|
|
383
|
+
const groupStartIndex = meaningfulNodes[groupStart].index
|
|
384
|
+
const groupEndIndex = meaningfulNodes[groupEnd].index
|
|
385
|
+
|
|
386
|
+
for (let i = groupStart; i <= groupEnd; i++) {
|
|
387
|
+
groupMap.set(meaningfulNodes[i].index, {
|
|
388
|
+
tagName: startGroupKey,
|
|
389
|
+
groupStart: groupStartIndex,
|
|
390
|
+
groupEnd: groupEndIndex
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
groupStart = groupEnd + 1
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
this.tagGroupsCache.set(siblings, groupMap)
|
|
399
|
+
|
|
400
|
+
return groupMap
|
|
401
|
+
}
|
|
402
|
+
|
|
291
403
|
/**
|
|
292
404
|
* Determine if spacing should be added between sibling elements
|
|
293
405
|
*
|
|
@@ -303,69 +415,73 @@ export class FormatPrinter extends Printer {
|
|
|
303
415
|
* @param hasExistingSpacing - Whether user-added spacing already exists
|
|
304
416
|
* @returns true if spacing should be added before the current element
|
|
305
417
|
*/
|
|
306
|
-
private shouldAddSpacingBetweenSiblings(
|
|
307
|
-
|
|
308
|
-
siblings
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (hasExistingSpacing) {
|
|
418
|
+
private shouldAddSpacingBetweenSiblings(parentElement: HTMLElementNode | null, siblings: Node[], currentIndex: number): boolean {
|
|
419
|
+
const currentNode = siblings[currentIndex]
|
|
420
|
+
const previousMeaningfulIndex = findPreviousMeaningfulSibling(siblings, currentIndex)
|
|
421
|
+
const previousNode = previousMeaningfulIndex !== -1 ? siblings[previousMeaningfulIndex] : null
|
|
422
|
+
|
|
423
|
+
if (previousNode && (isNode(previousNode, XMLDeclarationNode) || isNode(previousNode, HTMLDoctypeNode))) {
|
|
313
424
|
return true
|
|
314
425
|
}
|
|
315
426
|
|
|
316
427
|
const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "")
|
|
317
428
|
|
|
318
|
-
if (hasMixedContent)
|
|
319
|
-
return false
|
|
320
|
-
}
|
|
429
|
+
if (hasMixedContent) return false
|
|
321
430
|
|
|
322
|
-
const
|
|
431
|
+
const isCurrentComment = isCommentNode(currentNode)
|
|
432
|
+
const isPreviousComment = previousNode ? isCommentNode(previousNode) : false
|
|
433
|
+
const isCurrentMultiline = this.isMultilineElement(currentNode)
|
|
434
|
+
const isPreviousMultiline = previousNode ? this.isMultilineElement(previousNode) : false
|
|
323
435
|
|
|
324
|
-
if (
|
|
436
|
+
if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
|
|
437
|
+
return isPreviousMultiline && isCurrentMultiline
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (isPreviousComment && isCurrentComment) {
|
|
325
441
|
return false
|
|
326
442
|
}
|
|
327
443
|
|
|
444
|
+
if (isCurrentMultiline || isPreviousMultiline) {
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const meaningfulSiblings = siblings.filter(child => isNonWhitespaceNode(child))
|
|
328
449
|
const parentTagName = parentElement ? getTagName(parentElement) : null
|
|
450
|
+
const isSpaceableContainer = !parentTagName || SPACEABLE_CONTAINERS.has(parentTagName)
|
|
451
|
+
const tagGroups = this.detectTagGroups(siblings)
|
|
329
452
|
|
|
330
|
-
|
|
331
|
-
|
|
453
|
+
const cached = this.allSingleLineCache.get(siblings)
|
|
454
|
+
let allSingleLineHTMLElements: boolean
|
|
455
|
+
if (cached !== undefined) {
|
|
456
|
+
allSingleLineHTMLElements = cached
|
|
457
|
+
} else {
|
|
458
|
+
allSingleLineHTMLElements = meaningfulSiblings.every(node => isNode(node, HTMLElementNode) && !this.isMultilineElement(node))
|
|
459
|
+
this.allSingleLineCache.set(siblings, allSingleLineHTMLElements)
|
|
332
460
|
}
|
|
333
461
|
|
|
334
|
-
const isSpaceableContainer = !parentTagName || (parentTagName && SPACEABLE_CONTAINERS.has(parentTagName))
|
|
335
|
-
|
|
336
462
|
if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
|
|
337
463
|
return false
|
|
338
464
|
}
|
|
339
465
|
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
const isCurrentComment = isCommentNode(currentNode)
|
|
466
|
+
const currentGroup = tagGroups.get(currentIndex)
|
|
467
|
+
const previousGroup = previousNode ? tagGroups.get(previousMeaningfulIndex) : undefined
|
|
343
468
|
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
469
|
+
if (currentGroup && previousGroup && currentGroup.groupStart === previousGroup.groupStart && currentGroup.groupEnd === previousGroup.groupEnd) {
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
347
472
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
473
|
+
if (previousGroup && previousGroup.groupEnd === previousMeaningfulIndex) {
|
|
474
|
+
return true
|
|
475
|
+
}
|
|
351
476
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
477
|
+
if (allSingleLineHTMLElements && tagGroups.size === 0) {
|
|
478
|
+
return false
|
|
355
479
|
}
|
|
356
480
|
|
|
357
481
|
if (isNode(currentNode, HTMLElementNode)) {
|
|
358
482
|
const currentTagName = getTagName(currentNode)
|
|
359
483
|
|
|
360
|
-
if (INLINE_ELEMENTS.has(currentTagName)) {
|
|
361
|
-
return false
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (TIGHT_GROUP_CHILDREN.has(currentTagName)) {
|
|
365
|
-
return false
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (currentTagName === 'a' && parentTagName === 'nav') {
|
|
484
|
+
if (currentTagName && INLINE_ELEMENTS.has(currentTagName)) {
|
|
369
485
|
return false
|
|
370
486
|
}
|
|
371
487
|
}
|
|
@@ -652,7 +768,7 @@ export class FormatPrinter extends Printer {
|
|
|
652
768
|
return
|
|
653
769
|
}
|
|
654
770
|
|
|
655
|
-
let
|
|
771
|
+
let lastMeaningfulNode: Node | null = null
|
|
656
772
|
let hasHandledSpacing = false
|
|
657
773
|
|
|
658
774
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -670,21 +786,27 @@ export class FormatPrinter extends Printer {
|
|
|
670
786
|
|
|
671
787
|
if (shouldAppendToLastLine(child, children, i)) {
|
|
672
788
|
this.appendChildToLastLine(child, children, i)
|
|
673
|
-
|
|
789
|
+
lastMeaningfulNode = child
|
|
674
790
|
hasHandledSpacing = false
|
|
675
791
|
continue
|
|
676
792
|
}
|
|
677
793
|
|
|
678
|
-
if (isNonWhitespaceNode(child)
|
|
679
|
-
this.push("")
|
|
680
|
-
}
|
|
794
|
+
if (!isNonWhitespaceNode(child)) continue
|
|
681
795
|
|
|
796
|
+
const childStartLine = this.stringLineCount
|
|
682
797
|
this.visit(child)
|
|
683
798
|
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
799
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
800
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings( null, children, i)
|
|
801
|
+
|
|
802
|
+
if (shouldAddSpacing) {
|
|
803
|
+
this.lines.splice(childStartLine, 0, "")
|
|
804
|
+
this.stringLineCount++
|
|
805
|
+
}
|
|
687
806
|
}
|
|
807
|
+
|
|
808
|
+
lastMeaningfulNode = child
|
|
809
|
+
hasHandledSpacing = false
|
|
688
810
|
}
|
|
689
811
|
}
|
|
690
812
|
|
|
@@ -692,23 +814,23 @@ export class FormatPrinter extends Printer {
|
|
|
692
814
|
this.elementStack.push(node)
|
|
693
815
|
this.elementFormattingAnalysis.set(node, this.analyzeElementFormatting(node))
|
|
694
816
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
817
|
+
this.trackBoundary(node, () => {
|
|
818
|
+
if (this.inlineMode && node.is_void && this.indentLevel === 0) {
|
|
819
|
+
const openTag = this.capture(() => this.visit(node.open_tag)).join('')
|
|
820
|
+
this.pushToLastLine(openTag)
|
|
821
|
+
return
|
|
822
|
+
}
|
|
702
823
|
|
|
703
|
-
|
|
824
|
+
this.visit(node.open_tag)
|
|
704
825
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
826
|
+
if (node.body.length > 0) {
|
|
827
|
+
this.visitHTMLElementBody(node.body, node)
|
|
828
|
+
}
|
|
708
829
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
830
|
+
if (node.close_tag) {
|
|
831
|
+
this.visit(node.close_tag)
|
|
832
|
+
}
|
|
833
|
+
})
|
|
712
834
|
|
|
713
835
|
this.elementStack.pop()
|
|
714
836
|
}
|
|
@@ -883,21 +1005,22 @@ export class FormatPrinter extends Printer {
|
|
|
883
1005
|
|
|
884
1006
|
/**
|
|
885
1007
|
* Visit element children with intelligent spacing logic
|
|
1008
|
+
*
|
|
1009
|
+
* Tracks line positions and immediately splices blank lines after rendering each child.
|
|
886
1010
|
*/
|
|
887
1011
|
private visitElementChildren(body: Node[], parentElement: HTMLElementNode | null) {
|
|
888
|
-
let
|
|
1012
|
+
let lastMeaningfulNode: Node | null = null
|
|
889
1013
|
let hasHandledSpacing = false
|
|
890
1014
|
|
|
891
|
-
for (let
|
|
892
|
-
const child = body[
|
|
1015
|
+
for (let index = 0; index < body.length; index++) {
|
|
1016
|
+
const child = body[index]
|
|
893
1017
|
|
|
894
1018
|
if (isNode(child, HTMLTextNode)) {
|
|
895
1019
|
const isWhitespaceOnly = child.content.trim() === ""
|
|
896
1020
|
|
|
897
1021
|
if (isWhitespaceOnly) {
|
|
898
|
-
const hasPreviousNonWhitespace =
|
|
899
|
-
const hasNextNonWhitespace =
|
|
900
|
-
|
|
1022
|
+
const hasPreviousNonWhitespace = index > 0 && isNonWhitespaceNode(body[index - 1])
|
|
1023
|
+
const hasNextNonWhitespace = index < body.length - 1 && isNonWhitespaceNode(body[index + 1])
|
|
901
1024
|
const hasMultipleNewlines = child.content.includes('\n\n')
|
|
902
1025
|
|
|
903
1026
|
if (hasPreviousNonWhitespace && hasNextNonWhitespace && hasMultipleNewlines) {
|
|
@@ -909,26 +1032,12 @@ export class FormatPrinter extends Printer {
|
|
|
909
1032
|
}
|
|
910
1033
|
}
|
|
911
1034
|
|
|
912
|
-
if (isNonWhitespaceNode(child)
|
|
913
|
-
const element = body[i - 1]
|
|
914
|
-
const hasExistingSpacing = i > 0 && isNode(element, HTMLTextNode) && element.content.trim() === "" && (element.content.includes('\n\n') || element.content.split('\n').length > 2)
|
|
915
|
-
|
|
916
|
-
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(
|
|
917
|
-
parentElement,
|
|
918
|
-
body,
|
|
919
|
-
i,
|
|
920
|
-
hasExistingSpacing
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
if (shouldAddSpacing) {
|
|
924
|
-
this.push("")
|
|
925
|
-
}
|
|
926
|
-
}
|
|
1035
|
+
if (!isNonWhitespaceNode(child)) continue
|
|
927
1036
|
|
|
928
1037
|
let hasTrailingHerbDisable = false
|
|
929
1038
|
|
|
930
1039
|
if (isNode(child, HTMLElementNode) && child.close_tag) {
|
|
931
|
-
for (let j =
|
|
1040
|
+
for (let j = index + 1; j < body.length; j++) {
|
|
932
1041
|
const nextChild = body[j]
|
|
933
1042
|
|
|
934
1043
|
if (isNode(nextChild, WhitespaceNode) || isPureWhitespaceNode(nextChild)) {
|
|
@@ -938,8 +1047,18 @@ export class FormatPrinter extends Printer {
|
|
|
938
1047
|
if (isNode(nextChild, ERBContentNode) && isHerbDisableComment(nextChild)) {
|
|
939
1048
|
hasTrailingHerbDisable = true
|
|
940
1049
|
|
|
1050
|
+
const childStartLine = this.stringLineCount
|
|
941
1051
|
this.visit(child)
|
|
942
1052
|
|
|
1053
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
1054
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
1055
|
+
|
|
1056
|
+
if (shouldAddSpacing) {
|
|
1057
|
+
this.lines.splice(childStartLine, 0, "")
|
|
1058
|
+
this.stringLineCount++
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
943
1062
|
const herbDisableString = this.capture(() => {
|
|
944
1063
|
const savedIndentLevel = this.indentLevel
|
|
945
1064
|
this.indentLevel = 0
|
|
@@ -951,7 +1070,9 @@ export class FormatPrinter extends Printer {
|
|
|
951
1070
|
|
|
952
1071
|
this.pushToLastLine(' ' + herbDisableString)
|
|
953
1072
|
|
|
954
|
-
|
|
1073
|
+
index = j
|
|
1074
|
+
lastMeaningfulNode = child
|
|
1075
|
+
hasHandledSpacing = false
|
|
955
1076
|
|
|
956
1077
|
break
|
|
957
1078
|
}
|
|
@@ -961,11 +1082,19 @@ export class FormatPrinter extends Printer {
|
|
|
961
1082
|
}
|
|
962
1083
|
|
|
963
1084
|
if (!hasTrailingHerbDisable) {
|
|
1085
|
+
const childStartLine = this.stringLineCount
|
|
964
1086
|
this.visit(child)
|
|
965
|
-
}
|
|
966
1087
|
|
|
967
|
-
|
|
968
|
-
|
|
1088
|
+
if (lastMeaningfulNode && !hasHandledSpacing) {
|
|
1089
|
+
const shouldAddSpacing = this.shouldAddSpacingBetweenSiblings(parentElement, body, index)
|
|
1090
|
+
|
|
1091
|
+
if (shouldAddSpacing) {
|
|
1092
|
+
this.lines.splice(childStartLine, 0, "")
|
|
1093
|
+
this.stringLineCount++
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
lastMeaningfulNode = child
|
|
969
1098
|
hasHandledSpacing = false
|
|
970
1099
|
}
|
|
971
1100
|
}
|
|
@@ -1087,6 +1216,14 @@ export class FormatPrinter extends Printer {
|
|
|
1087
1216
|
}
|
|
1088
1217
|
}).join("")
|
|
1089
1218
|
|
|
1219
|
+
const trimmedInner = inner.trim()
|
|
1220
|
+
|
|
1221
|
+
if (trimmedInner.startsWith('[if ') && trimmedInner.endsWith('<![endif]')) {
|
|
1222
|
+
this.pushWithIndent(open + inner + close)
|
|
1223
|
+
|
|
1224
|
+
return
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1090
1227
|
const hasNewlines = inner.includes('\n')
|
|
1091
1228
|
|
|
1092
1229
|
if (hasNewlines) {
|
|
@@ -1189,8 +1326,7 @@ export class FormatPrinter extends Printer {
|
|
|
1189
1326
|
}
|
|
1190
1327
|
|
|
1191
1328
|
visitERBContentNode(node: ERBContentNode) {
|
|
1192
|
-
|
|
1193
|
-
if (node.tag_opening?.value === "<%#") {
|
|
1329
|
+
if (isERBCommentNode(node)) {
|
|
1194
1330
|
this.visitERBCommentNode(node)
|
|
1195
1331
|
} else {
|
|
1196
1332
|
this.printERBNode(node)
|
|
@@ -1202,91 +1338,101 @@ export class FormatPrinter extends Printer {
|
|
|
1202
1338
|
}
|
|
1203
1339
|
|
|
1204
1340
|
visitERBYieldNode(node: ERBYieldNode) {
|
|
1205
|
-
this.
|
|
1341
|
+
this.trackBoundary(node, () => {
|
|
1342
|
+
this.printERBNode(node)
|
|
1343
|
+
})
|
|
1206
1344
|
}
|
|
1207
1345
|
|
|
1208
1346
|
visitERBInNode(node: ERBInNode) {
|
|
1209
|
-
this.
|
|
1210
|
-
|
|
1347
|
+
this.trackBoundary(node, () => {
|
|
1348
|
+
this.printERBNode(node)
|
|
1349
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1350
|
+
})
|
|
1211
1351
|
}
|
|
1212
1352
|
|
|
1213
1353
|
visitERBCaseMatchNode(node: ERBCaseMatchNode) {
|
|
1214
|
-
this.
|
|
1354
|
+
this.trackBoundary(node, () => {
|
|
1355
|
+
this.printERBNode(node)
|
|
1215
1356
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1357
|
+
this.withIndent(() => this.visitAll(node.children))
|
|
1358
|
+
this.visitAll(node.conditions)
|
|
1218
1359
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1360
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
1361
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1362
|
+
})
|
|
1221
1363
|
}
|
|
1222
1364
|
|
|
1223
1365
|
visitERBBlockNode(node: ERBBlockNode) {
|
|
1224
|
-
this.
|
|
1366
|
+
this.trackBoundary(node, () => {
|
|
1367
|
+
this.printERBNode(node)
|
|
1225
1368
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1369
|
+
this.withIndent(() => {
|
|
1370
|
+
const hasTextFlow = this.isInTextFlowContext(null, node.body)
|
|
1228
1371
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1372
|
+
if (hasTextFlow) {
|
|
1373
|
+
this.visitTextFlowChildren(node.body)
|
|
1374
|
+
} else {
|
|
1375
|
+
this.visitElementChildren(node.body, null)
|
|
1376
|
+
}
|
|
1377
|
+
})
|
|
1235
1378
|
|
|
1236
|
-
|
|
1379
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1380
|
+
})
|
|
1237
1381
|
}
|
|
1238
1382
|
|
|
1239
1383
|
visitERBIfNode(node: ERBIfNode) {
|
|
1240
|
-
|
|
1241
|
-
this.
|
|
1242
|
-
|
|
1243
|
-
node.statements.forEach(child => {
|
|
1244
|
-
if (isNode(child, HTMLAttributeNode)) {
|
|
1245
|
-
this.lines.push(" ")
|
|
1246
|
-
this.lines.push(this.renderAttribute(child))
|
|
1247
|
-
} else {
|
|
1248
|
-
const shouldAddSpaces = this.isInTokenListAttribute
|
|
1384
|
+
this.trackBoundary(node, () => {
|
|
1385
|
+
if (this.inlineMode) {
|
|
1386
|
+
this.printERBNode(node)
|
|
1249
1387
|
|
|
1250
|
-
|
|
1388
|
+
node.statements.forEach(child => {
|
|
1389
|
+
if (isNode(child, HTMLAttributeNode)) {
|
|
1251
1390
|
this.lines.push(" ")
|
|
1252
|
-
|
|
1391
|
+
this.lines.push(this.renderAttribute(child))
|
|
1392
|
+
} else {
|
|
1393
|
+
const shouldAddSpaces = this.isInTokenListAttribute
|
|
1253
1394
|
|
|
1254
|
-
|
|
1395
|
+
if (shouldAddSpaces) {
|
|
1396
|
+
this.lines.push(" ")
|
|
1397
|
+
}
|
|
1255
1398
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1399
|
+
this.visit(child)
|
|
1400
|
+
|
|
1401
|
+
if (shouldAddSpaces) {
|
|
1402
|
+
this.lines.push(" ")
|
|
1403
|
+
}
|
|
1258
1404
|
}
|
|
1259
|
-
}
|
|
1260
|
-
})
|
|
1405
|
+
})
|
|
1261
1406
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1407
|
+
const hasHTMLAttributes = node.statements.some(child => isNode(child, HTMLAttributeNode))
|
|
1408
|
+
const isTokenList = this.isInTokenListAttribute
|
|
1264
1409
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1410
|
+
if ((hasHTMLAttributes || isTokenList) && node.end_node) {
|
|
1411
|
+
this.lines.push(" ")
|
|
1412
|
+
}
|
|
1268
1413
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1414
|
+
if (node.subsequent) this.visit(node.subsequent)
|
|
1415
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1416
|
+
} else {
|
|
1417
|
+
this.printERBNode(node)
|
|
1273
1418
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1419
|
+
this.withIndent(() => {
|
|
1420
|
+
node.statements.forEach(child => this.visit(child))
|
|
1421
|
+
})
|
|
1277
1422
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1423
|
+
if (node.subsequent) this.visit(node.subsequent)
|
|
1424
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1425
|
+
}
|
|
1426
|
+
})
|
|
1281
1427
|
}
|
|
1282
1428
|
|
|
1283
1429
|
visitERBElseNode(node: ERBElseNode) {
|
|
1430
|
+
this.printERBNode(node)
|
|
1431
|
+
|
|
1284
1432
|
if (this.inlineMode) {
|
|
1285
|
-
this.
|
|
1286
|
-
node.statements.forEach(statement => this.visit(statement))
|
|
1433
|
+
this.visitAll(node.statements)
|
|
1287
1434
|
} else {
|
|
1288
|
-
this.
|
|
1289
|
-
this.withIndent(() => node.statements.forEach(statement => this.visit(statement)))
|
|
1435
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1290
1436
|
}
|
|
1291
1437
|
}
|
|
1292
1438
|
|
|
@@ -1296,44 +1442,54 @@ export class FormatPrinter extends Printer {
|
|
|
1296
1442
|
}
|
|
1297
1443
|
|
|
1298
1444
|
visitERBCaseNode(node: ERBCaseNode) {
|
|
1299
|
-
this.
|
|
1445
|
+
this.trackBoundary(node, () => {
|
|
1446
|
+
this.printERBNode(node)
|
|
1300
1447
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1448
|
+
this.withIndent(() => this.visitAll(node.children))
|
|
1449
|
+
this.visitAll(node.conditions)
|
|
1303
1450
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1451
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
1452
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1453
|
+
})
|
|
1306
1454
|
}
|
|
1307
1455
|
|
|
1308
1456
|
visitERBBeginNode(node: ERBBeginNode) {
|
|
1309
|
-
this.
|
|
1310
|
-
|
|
1457
|
+
this.trackBoundary(node, () => {
|
|
1458
|
+
this.printERBNode(node)
|
|
1459
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1311
1460
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1461
|
+
if (node.rescue_clause) this.visit(node.rescue_clause)
|
|
1462
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
1463
|
+
if (node.ensure_clause) this.visit(node.ensure_clause)
|
|
1464
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1465
|
+
})
|
|
1316
1466
|
}
|
|
1317
1467
|
|
|
1318
1468
|
visitERBWhileNode(node: ERBWhileNode) {
|
|
1319
|
-
this.
|
|
1320
|
-
|
|
1469
|
+
this.trackBoundary(node, () => {
|
|
1470
|
+
this.printERBNode(node)
|
|
1471
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1321
1472
|
|
|
1322
|
-
|
|
1473
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1474
|
+
})
|
|
1323
1475
|
}
|
|
1324
1476
|
|
|
1325
1477
|
visitERBUntilNode(node: ERBUntilNode) {
|
|
1326
|
-
this.
|
|
1327
|
-
|
|
1478
|
+
this.trackBoundary(node, () => {
|
|
1479
|
+
this.printERBNode(node)
|
|
1480
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1328
1481
|
|
|
1329
|
-
|
|
1482
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1483
|
+
})
|
|
1330
1484
|
}
|
|
1331
1485
|
|
|
1332
1486
|
visitERBForNode(node: ERBForNode) {
|
|
1333
|
-
this.
|
|
1334
|
-
|
|
1487
|
+
this.trackBoundary(node, () => {
|
|
1488
|
+
this.printERBNode(node)
|
|
1489
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1335
1490
|
|
|
1336
|
-
|
|
1491
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1492
|
+
})
|
|
1337
1493
|
}
|
|
1338
1494
|
|
|
1339
1495
|
visitERBRescueNode(node: ERBRescueNode) {
|
|
@@ -1347,11 +1503,13 @@ export class FormatPrinter extends Printer {
|
|
|
1347
1503
|
}
|
|
1348
1504
|
|
|
1349
1505
|
visitERBUnlessNode(node: ERBUnlessNode) {
|
|
1350
|
-
this.
|
|
1351
|
-
|
|
1506
|
+
this.trackBoundary(node, () => {
|
|
1507
|
+
this.printERBNode(node)
|
|
1508
|
+
this.withIndent(() => this.visitAll(node.statements))
|
|
1352
1509
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1510
|
+
if (node.else_clause) this.visit(node.else_clause)
|
|
1511
|
+
if (node.end_node) this.visit(node.end_node)
|
|
1512
|
+
})
|
|
1355
1513
|
}
|
|
1356
1514
|
|
|
1357
1515
|
// --- Element Formatting Analysis Helpers ---
|
|
@@ -1418,6 +1576,16 @@ export class FormatPrinter extends Printer {
|
|
|
1418
1576
|
if (!openTagInline) return false
|
|
1419
1577
|
if (children.length === 0) return true
|
|
1420
1578
|
|
|
1579
|
+
const hasNonInlineChildElements = children.some(child => {
|
|
1580
|
+
if (isNode(child, HTMLElementNode)) {
|
|
1581
|
+
return !this.shouldRenderElementContentInline(child)
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
return false
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
if (hasNonInlineChildElements) return false
|
|
1588
|
+
|
|
1421
1589
|
let hasLeadingHerbDisable = false
|
|
1422
1590
|
|
|
1423
1591
|
for (const child of node.body) {
|
|
@@ -1439,7 +1607,7 @@ export class FormatPrinter extends Printer {
|
|
|
1439
1607
|
|
|
1440
1608
|
if (fullInlineResult) {
|
|
1441
1609
|
const totalLength = this.indent.length + fullInlineResult.length
|
|
1442
|
-
return totalLength <= this.maxLineLength
|
|
1610
|
+
return totalLength <= this.maxLineLength
|
|
1443
1611
|
}
|
|
1444
1612
|
|
|
1445
1613
|
return false
|
|
@@ -1474,8 +1642,9 @@ export class FormatPrinter extends Printer {
|
|
|
1474
1642
|
|
|
1475
1643
|
const childrenContent = this.renderChildrenInline(children)
|
|
1476
1644
|
const fullLine = openTagResult + childrenContent + `</${tagName}>`
|
|
1645
|
+
const totalLength = this.indent.length + fullLine.length
|
|
1477
1646
|
|
|
1478
|
-
if (
|
|
1647
|
+
if (totalLength <= this.maxLineLength) {
|
|
1479
1648
|
return true
|
|
1480
1649
|
}
|
|
1481
1650
|
}
|