@herb-tools/formatter 0.8.10 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/herb-format.js +57938 -17957
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +23560 -3225
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +23560 -3225
- package/dist/index.esm.js.map +1 -1
- package/dist/types/attribute-renderer.d.ts +44 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/comment-helpers.d.ts +45 -0
- package/dist/types/format-helpers.d.ts +15 -11
- package/dist/types/format-printer.d.ts +31 -137
- package/dist/types/formatter.d.ts +3 -2
- package/dist/types/spacing-analyzer.d.ts +47 -0
- package/dist/types/text-flow-analyzer.d.ts +22 -0
- package/dist/types/text-flow-engine.d.ts +37 -0
- package/dist/types/text-flow-helpers.d.ts +58 -0
- package/package.json +5 -5
- package/src/attribute-renderer.ts +309 -0
- package/src/cli.ts +32 -11
- package/src/comment-helpers.ts +129 -0
- package/src/format-helpers.ts +73 -29
- package/src/format-printer.ts +447 -1468
- package/src/formatter.ts +10 -4
- package/src/spacing-analyzer.ts +244 -0
- package/src/text-flow-analyzer.ts +212 -0
- package/src/text-flow-engine.ts +311 -0
- package/src/text-flow-helpers.ts +319 -0
package/src/formatter.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { hasFormatterIgnoreDirective } from "./format-ignore.js"
|
|
|
6
6
|
|
|
7
7
|
import type { Config } from "@herb-tools/config"
|
|
8
8
|
import type { RewriteContext } from "@herb-tools/rewriter"
|
|
9
|
-
import type { HerbBackend, ParseResult } from "@herb-tools/core"
|
|
9
|
+
import type { HerbBackend, ParseResult, ParseOptions } from "@herb-tools/core"
|
|
10
10
|
import type { FormatOptions } from "./options.js"
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -16,6 +16,7 @@ import type { FormatOptions } from "./options.js"
|
|
|
16
16
|
export class Formatter {
|
|
17
17
|
private herb: HerbBackend
|
|
18
18
|
private options: Required<FormatOptions>
|
|
19
|
+
private parseOptions: ParseOptions
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Creates a Formatter instance from a Config object (recommended).
|
|
@@ -48,16 +49,21 @@ export class Formatter {
|
|
|
48
49
|
* @param herb - The Herb backend instance for parsing
|
|
49
50
|
* @param options - Format options (including rewriters)
|
|
50
51
|
*/
|
|
51
|
-
constructor(herb: HerbBackend, options: FormatOptions = {}) {
|
|
52
|
+
constructor(herb: HerbBackend, options: FormatOptions = {}, parseOptions: ParseOptions = {}) {
|
|
52
53
|
this.herb = herb
|
|
53
54
|
this.options = resolveFormatOptions(options)
|
|
55
|
+
this.parseOptions = parseOptions
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* Format a source string, optionally overriding format options per call.
|
|
58
60
|
*/
|
|
59
61
|
format(source: string, options: FormatOptions = {}, filePath?: string): string {
|
|
60
|
-
|
|
62
|
+
const result = this.parse(source)
|
|
63
|
+
|
|
64
|
+
if (result.options.action_view_helpers) {
|
|
65
|
+
console.warn("[Herb Formatter] Warning: Formatting a document parsed with `action_view_helpers: true`. The result may not be 100% accurate.")
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
if (result.failed) return source
|
|
63
69
|
if (isScaffoldTemplate(result)) return source
|
|
@@ -104,6 +110,6 @@ export class Formatter {
|
|
|
104
110
|
|
|
105
111
|
private parse(source: string): ParseResult {
|
|
106
112
|
this.herb.ensureBackend()
|
|
107
|
-
return this.herb.parse(source)
|
|
113
|
+
return this.herb.parse(source, this.parseOptions)
|
|
108
114
|
}
|
|
109
115
|
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Node, HTMLTextNode, HTMLElementNode, HTMLDoctypeNode, ERBContentNode, WhitespaceNode, XMLDeclarationNode } from "@herb-tools/core"
|
|
2
|
+
import { isNode, getTagName, isERBNode, isERBOutputNode, isERBCommentNode, isCommentNode, isERBControlFlowNode } from "@herb-tools/core"
|
|
3
|
+
import { findPreviousMeaningfulSibling, isBlockLevelNode, isContentPreserving, isNonWhitespaceNode } from "./format-helpers.js"
|
|
4
|
+
|
|
5
|
+
import { INLINE_ELEMENTS, SPACEABLE_CONTAINERS } from "./format-helpers.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SpacingAnalyzer determines when blank lines should be inserted between
|
|
9
|
+
* sibling elements. It implements the "rule of three" intelligent spacing
|
|
10
|
+
* system: adds spacing between 3+ meaningful siblings, respects semantic
|
|
11
|
+
* groupings, groups comments with following elements, and preserves
|
|
12
|
+
* user-added spacing.
|
|
13
|
+
*/
|
|
14
|
+
export class SpacingAnalyzer {
|
|
15
|
+
private nodeIsMultiline: Map<Node, boolean>
|
|
16
|
+
private tagGroupsCache = new Map<Node[], Map<number, { tagName: string; groupStart: number; groupEnd: number }>>()
|
|
17
|
+
private allSingleLineCache = new Map<Node[], boolean>()
|
|
18
|
+
|
|
19
|
+
constructor(nodeIsMultiline: Map<Node, boolean>) {
|
|
20
|
+
this.nodeIsMultiline = nodeIsMultiline
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clear(): void {
|
|
24
|
+
this.tagGroupsCache.clear()
|
|
25
|
+
this.allSingleLineCache.clear()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determine if spacing should be added between sibling elements
|
|
30
|
+
*
|
|
31
|
+
* This implements the "rule of three" intelligent spacing system:
|
|
32
|
+
* - Adds spacing between 3 or more meaningful siblings
|
|
33
|
+
* - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
|
|
34
|
+
* - Groups comments with following elements
|
|
35
|
+
* - Preserves user-added spacing
|
|
36
|
+
*
|
|
37
|
+
* @param parentElement - The parent element containing the siblings
|
|
38
|
+
* @param siblings - Array of all sibling nodes
|
|
39
|
+
* @param currentIndex - Index of the current node being evaluated
|
|
40
|
+
* @returns true if spacing should be added before the current element
|
|
41
|
+
*/
|
|
42
|
+
shouldAddSpacingBetweenSiblings(parentElement: HTMLElementNode | null, siblings: Node[], currentIndex: number): boolean {
|
|
43
|
+
const currentNode = siblings[currentIndex]
|
|
44
|
+
const previousMeaningfulIndex = findPreviousMeaningfulSibling(siblings, currentIndex)
|
|
45
|
+
const previousNode = previousMeaningfulIndex !== -1 ? siblings[previousMeaningfulIndex] : null
|
|
46
|
+
|
|
47
|
+
if (previousNode && (isNode(previousNode, XMLDeclarationNode) || isNode(previousNode, HTMLDoctypeNode))) {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hasMixedContent = siblings.some(child => isNode(child, HTMLTextNode) && child.content.trim() !== "")
|
|
52
|
+
|
|
53
|
+
if (hasMixedContent) return false
|
|
54
|
+
|
|
55
|
+
const isCurrentComment = isCommentNode(currentNode)
|
|
56
|
+
const isPreviousComment = previousNode ? isCommentNode(previousNode) : false
|
|
57
|
+
const isCurrentMultiline = this.isMultilineElement(currentNode)
|
|
58
|
+
const isPreviousMultiline = previousNode ? this.isMultilineElement(previousNode) : false
|
|
59
|
+
|
|
60
|
+
if (isPreviousComment && !isCurrentComment && (isNode(currentNode, HTMLElementNode) || isERBNode(currentNode))) {
|
|
61
|
+
return isPreviousMultiline && isCurrentMultiline
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isPreviousComment && isCurrentComment) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isCurrentMultiline || isPreviousMultiline) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const meaningfulSiblings = siblings.filter(child => isNonWhitespaceNode(child))
|
|
73
|
+
const parentTagName = parentElement ? getTagName(parentElement) : null
|
|
74
|
+
const isSpaceableContainer = !parentTagName || SPACEABLE_CONTAINERS.has(parentTagName)
|
|
75
|
+
const tagGroups = this.detectTagGroups(siblings)
|
|
76
|
+
|
|
77
|
+
const cached = this.allSingleLineCache.get(siblings)
|
|
78
|
+
let allSingleLineHTMLElements: boolean
|
|
79
|
+
if (cached !== undefined) {
|
|
80
|
+
allSingleLineHTMLElements = cached
|
|
81
|
+
} else {
|
|
82
|
+
allSingleLineHTMLElements = meaningfulSiblings.every(node => isNode(node, HTMLElementNode) && !this.isMultilineElement(node))
|
|
83
|
+
this.allSingleLineCache.set(siblings, allSingleLineHTMLElements)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isSpaceableContainer && meaningfulSiblings.length < 5) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const currentGroup = tagGroups.get(currentIndex)
|
|
91
|
+
const previousGroup = previousNode ? tagGroups.get(previousMeaningfulIndex) : undefined
|
|
92
|
+
|
|
93
|
+
if (currentGroup && previousGroup && currentGroup.groupStart === previousGroup.groupStart && currentGroup.groupEnd === previousGroup.groupEnd) {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (previousGroup && previousGroup.groupEnd === previousMeaningfulIndex) {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (allSingleLineHTMLElements && tagGroups.size === 0) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isNode(currentNode, HTMLElementNode)) {
|
|
106
|
+
const currentTagName = getTagName(currentNode)
|
|
107
|
+
|
|
108
|
+
if (currentTagName && INLINE_ELEMENTS.has(currentTagName)) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isBlockElement = isBlockLevelNode(currentNode)
|
|
114
|
+
const isERBBlock = isERBNode(currentNode) && isERBControlFlowNode(currentNode)
|
|
115
|
+
const isComment = isCommentNode(currentNode)
|
|
116
|
+
|
|
117
|
+
return isBlockElement || isERBBlock || isComment
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if there's a blank line (double newline) in the nodes at the given index
|
|
122
|
+
*/
|
|
123
|
+
hasBlankLineBetween(body: Node[], index: number): boolean {
|
|
124
|
+
for (let lookbackIndex = index - 1; lookbackIndex >= 0 && lookbackIndex >= index - 2; lookbackIndex--) {
|
|
125
|
+
const node = body[lookbackIndex]
|
|
126
|
+
|
|
127
|
+
if (isNode(node, HTMLTextNode) && node.content.includes('\n\n')) {
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isNode(node, WhitespaceNode)) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let lookaheadIndex = index; lookaheadIndex < body.length && lookaheadIndex <= index + 1; lookaheadIndex++) {
|
|
139
|
+
const node = body[lookaheadIndex]
|
|
140
|
+
|
|
141
|
+
if (isNode(node, HTMLTextNode) && node.content.includes('\n\n')) {
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isNode(node, WhitespaceNode)) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a node will render as multiple lines when formatted.
|
|
157
|
+
*/
|
|
158
|
+
private isMultilineElement(node: Node): boolean {
|
|
159
|
+
if (isNode(node, ERBContentNode)) {
|
|
160
|
+
return (node.content?.value || "").includes("\n")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isNode(node, HTMLElementNode) && isContentPreserving(node)) {
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tracked = this.nodeIsMultiline.get(node)
|
|
168
|
+
|
|
169
|
+
if (tracked !== undefined) {
|
|
170
|
+
return tracked
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get a grouping key for a node (tag name for HTML, ERB type for ERB)
|
|
178
|
+
*/
|
|
179
|
+
private getGroupingKey(node: Node): string | null {
|
|
180
|
+
if (isNode(node, HTMLElementNode)) {
|
|
181
|
+
return getTagName(node)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isERBOutputNode(node)) return "erb-output"
|
|
185
|
+
if (isERBCommentNode(node)) return "erb-comment"
|
|
186
|
+
if (isERBNode(node)) return "erb-code"
|
|
187
|
+
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Detect groups of consecutive same-tag/same-type single-line elements
|
|
193
|
+
* Returns a map of index -> group info for efficient lookup
|
|
194
|
+
*/
|
|
195
|
+
private detectTagGroups(siblings: Node[]): Map<number, { tagName: string; groupStart: number; groupEnd: number }> {
|
|
196
|
+
const cached = this.tagGroupsCache.get(siblings)
|
|
197
|
+
if (cached) return cached
|
|
198
|
+
|
|
199
|
+
const groupMap = new Map<number, { tagName: string; groupStart: number; groupEnd: number }>()
|
|
200
|
+
const meaningfulNodes: Array<{ index: number; groupKey: string }> = []
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
203
|
+
const node = siblings[i]
|
|
204
|
+
|
|
205
|
+
if (!this.isMultilineElement(node)) {
|
|
206
|
+
const groupKey = this.getGroupingKey(node)
|
|
207
|
+
|
|
208
|
+
if (groupKey) {
|
|
209
|
+
meaningfulNodes.push({ index: i, groupKey })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let groupStart = 0
|
|
215
|
+
|
|
216
|
+
while (groupStart < meaningfulNodes.length) {
|
|
217
|
+
const startGroupKey = meaningfulNodes[groupStart].groupKey
|
|
218
|
+
let groupEnd = groupStart
|
|
219
|
+
|
|
220
|
+
while (groupEnd + 1 < meaningfulNodes.length && meaningfulNodes[groupEnd + 1].groupKey === startGroupKey) {
|
|
221
|
+
groupEnd++
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (groupEnd > groupStart) {
|
|
225
|
+
const groupStartIndex = meaningfulNodes[groupStart].index
|
|
226
|
+
const groupEndIndex = meaningfulNodes[groupEnd].index
|
|
227
|
+
|
|
228
|
+
for (let i = groupStart; i <= groupEnd; i++) {
|
|
229
|
+
groupMap.set(meaningfulNodes[i].index, {
|
|
230
|
+
tagName: startGroupKey,
|
|
231
|
+
groupStart: groupStartIndex,
|
|
232
|
+
groupEnd: groupEndIndex
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
groupStart = groupEnd + 1
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.tagGroupsCache.set(siblings, groupMap)
|
|
241
|
+
|
|
242
|
+
return groupMap
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { isNode, getTagName, isERBCommentNode, isPureWhitespaceNode } from "@herb-tools/core"
|
|
2
|
+
import { Node, HTMLTextNode, HTMLElementNode, ERBContentNode, WhitespaceNode } from "@herb-tools/core"
|
|
3
|
+
|
|
4
|
+
import type { ContentUnitWithNode } from "./format-helpers.js"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
hasWhitespaceBetween,
|
|
8
|
+
isHerbDisableComment,
|
|
9
|
+
isInlineElement,
|
|
10
|
+
isLineBreakingElement,
|
|
11
|
+
} from "./format-helpers.js"
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
hasWhitespaceBeforeNode as hasWhitespaceBeforeNodeHelper,
|
|
15
|
+
lastUnitEndsWithWhitespace as lastUnitEndsWithWhitespaceHelper,
|
|
16
|
+
tryMergeAtomicAfterText as tryMergeAtomicAfterTextHelper,
|
|
17
|
+
tryMergeTextAfterAtomic as tryMergeTextAfterAtomicHelper,
|
|
18
|
+
} from "./text-flow-helpers.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Interface that the delegate must implement to provide
|
|
22
|
+
* rendering capabilities to the TextFlowAnalyzer.
|
|
23
|
+
*/
|
|
24
|
+
export interface TextFlowAnalyzerDelegate {
|
|
25
|
+
tryRenderInlineElement(element: HTMLElementNode): string | null
|
|
26
|
+
renderERBAsString(node: ERBContentNode): string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* TextFlowAnalyzer converts AST nodes into the ContentUnitWithNode[]
|
|
31
|
+
* intermediate representation used by the TextFlowEngine for rendering.
|
|
32
|
+
*/
|
|
33
|
+
export class TextFlowAnalyzer {
|
|
34
|
+
private delegate: TextFlowAnalyzerDelegate
|
|
35
|
+
|
|
36
|
+
constructor(delegate: TextFlowAnalyzerDelegate) {
|
|
37
|
+
this.delegate = delegate
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
buildContentUnits(children: Node[]): ContentUnitWithNode[] {
|
|
41
|
+
const result: ContentUnitWithNode[] = []
|
|
42
|
+
let lastProcessedIndex = -1
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < children.length; i++) {
|
|
45
|
+
const child = children[i]
|
|
46
|
+
|
|
47
|
+
if (isNode(child, WhitespaceNode)) continue
|
|
48
|
+
|
|
49
|
+
if (isPureWhitespaceNode(child) && !(isNode(child, HTMLTextNode) && child.content === ' ')) {
|
|
50
|
+
if (lastProcessedIndex >= 0) {
|
|
51
|
+
const hasNonWhitespaceAfter = children.slice(i + 1).some(node =>
|
|
52
|
+
!isNode(node, WhitespaceNode) && !isPureWhitespaceNode(node)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (hasNonWhitespaceAfter) {
|
|
56
|
+
const previousNode = children[lastProcessedIndex]
|
|
57
|
+
|
|
58
|
+
if (!isLineBreakingElement(previousNode)) {
|
|
59
|
+
result.push({
|
|
60
|
+
unit: { content: ' ', type: 'text', isAtomic: true, breaksFlow: false },
|
|
61
|
+
node: child
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isNode(child, HTMLTextNode)) {
|
|
71
|
+
this.processTextNode(result, children, child, i, lastProcessedIndex)
|
|
72
|
+
|
|
73
|
+
lastProcessedIndex = i
|
|
74
|
+
} else if (isNode(child, HTMLElementNode)) {
|
|
75
|
+
const tagName = getTagName(child)
|
|
76
|
+
|
|
77
|
+
if (isInlineElement(tagName)) {
|
|
78
|
+
const merged = this.processInlineElement(result, children, child, i, lastProcessedIndex)
|
|
79
|
+
|
|
80
|
+
if (merged) {
|
|
81
|
+
lastProcessedIndex = i
|
|
82
|
+
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
result.push({
|
|
87
|
+
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
88
|
+
node: child
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lastProcessedIndex = i
|
|
93
|
+
} else if (isNode(child, ERBContentNode)) {
|
|
94
|
+
const merged = this.processERBContentNode(result, children, child, i, lastProcessedIndex)
|
|
95
|
+
|
|
96
|
+
if (merged) {
|
|
97
|
+
lastProcessedIndex = i
|
|
98
|
+
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lastProcessedIndex = i
|
|
103
|
+
} else {
|
|
104
|
+
result.push({
|
|
105
|
+
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
106
|
+
node: child
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
lastProcessedIndex = i
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private processTextNode(result: ContentUnitWithNode[], children: Node[], child: HTMLTextNode, index: number, lastProcessedIndex: number): void {
|
|
117
|
+
const isAtomic = child.content === ' '
|
|
118
|
+
|
|
119
|
+
if (!isAtomic && lastProcessedIndex >= 0 && result.length > 0) {
|
|
120
|
+
const hasWhitespace = hasWhitespaceBeforeNodeHelper(children, lastProcessedIndex, index, child)
|
|
121
|
+
const lastUnit = result[result.length - 1]
|
|
122
|
+
const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'erb' || lastUnit.unit.type === 'inline')
|
|
123
|
+
|
|
124
|
+
if (lastIsAtomic && !hasWhitespace && tryMergeTextAfterAtomicHelper(result, child)) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
result.push({
|
|
130
|
+
unit: { content: child.content, type: 'text', isAtomic, breaksFlow: false },
|
|
131
|
+
node: child
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private processInlineElement(result: ContentUnitWithNode[], children: Node[], child: HTMLElementNode, index: number, lastProcessedIndex: number): boolean {
|
|
136
|
+
const inlineContent = this.delegate.tryRenderInlineElement(child)
|
|
137
|
+
|
|
138
|
+
if (inlineContent === null) {
|
|
139
|
+
result.push({
|
|
140
|
+
unit: { content: '', type: 'block', isAtomic: false, breaksFlow: true },
|
|
141
|
+
node: child
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (lastProcessedIndex >= 0) {
|
|
148
|
+
const hasWhitespace = hasWhitespaceBetween(children, lastProcessedIndex, index) || lastUnitEndsWithWhitespaceHelper(result)
|
|
149
|
+
|
|
150
|
+
if (!hasWhitespace && tryMergeAtomicAfterTextHelper(result, children, lastProcessedIndex, inlineContent, 'inline', child)) {
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
result.push({
|
|
156
|
+
unit: { content: inlineContent, type: 'inline', isAtomic: true, breaksFlow: false },
|
|
157
|
+
node: child
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private processERBContentNode(result: ContentUnitWithNode[], children: Node[], child: ERBContentNode, index: number, lastProcessedIndex: number): boolean {
|
|
164
|
+
const erbContent = this.delegate.renderERBAsString(child)
|
|
165
|
+
const herbDisable = isHerbDisableComment(child)
|
|
166
|
+
|
|
167
|
+
if (lastProcessedIndex >= 0) {
|
|
168
|
+
const hasWhitespace = hasWhitespaceBetween(children, lastProcessedIndex, index) || lastUnitEndsWithWhitespaceHelper(result)
|
|
169
|
+
|
|
170
|
+
if (!hasWhitespace && tryMergeAtomicAfterTextHelper(result, children, lastProcessedIndex, erbContent, 'erb', child)) {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (hasWhitespace && result.length > 0) {
|
|
175
|
+
const lastUnit = result[result.length - 1]
|
|
176
|
+
const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'inline' || lastUnit.unit.type === 'erb')
|
|
177
|
+
|
|
178
|
+
if (lastIsAtomic && !lastUnitEndsWithWhitespaceHelper(result)) {
|
|
179
|
+
result.push({
|
|
180
|
+
unit: { content: ' ', type: 'text', isAtomic: true, breaksFlow: false },
|
|
181
|
+
node: null
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
result.push({
|
|
188
|
+
unit: { content: erbContent, type: 'erb', isAtomic: true, breaksFlow: false, isHerbDisable: herbDisable },
|
|
189
|
+
node: child
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (isERBCommentNode(child) && !herbDisable) {
|
|
193
|
+
for (let j = index + 1; j < children.length; j++) {
|
|
194
|
+
const nextChild = children[j]
|
|
195
|
+
if (isNode(nextChild, WhitespaceNode)) continue
|
|
196
|
+
if (isPureWhitespaceNode(nextChild)) continue
|
|
197
|
+
|
|
198
|
+
const hasNewlineBefore = isNode(nextChild, HTMLTextNode) && /\n/.test(nextChild.content.split(/\S/)[0] || '')
|
|
199
|
+
if (nextChild.location.start.line > child.location.end.line || hasNewlineBefore) {
|
|
200
|
+
result.push({
|
|
201
|
+
unit: { content: '', type: 'text', isAtomic: false, breaksFlow: true },
|
|
202
|
+
node: null
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|