@herb-tools/rewriter 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/index.cjs +20936 -380
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +20935 -381
- package/dist/index.esm.js.map +1 -1
- package/dist/loader.cjs +20843 -448
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.esm.js +20842 -449
- package/dist/loader.esm.js.map +1 -1
- package/dist/types/built-ins/action-view-tag-helper-to-html.d.ts +8 -0
- package/dist/types/built-ins/html-to-action-view-tag-helper.d.ts +8 -0
- package/dist/types/built-ins/index.d.ts +3 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/rewriter-factories.d.ts +4 -0
- package/package.json +4 -4
- package/src/built-ins/action-view-tag-helper-to-html.ts +179 -0
- package/src/built-ins/html-to-action-view-tag-helper.ts +268 -0
- package/src/built-ins/index.ts +10 -3
- package/src/built-ins/tailwind-class-sorter.ts +157 -64
- package/src/index.ts +2 -0
- package/src/rewriter-factories.ts +10 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getStaticAttributeName, isLiteralNode } from "@herb-tools/core"
|
|
1
|
+
import { getStaticAttributeName, isLiteralNode, isPureWhitespaceNode, splitLiteralsAtWhitespace, groupNodesByClass } from "@herb-tools/core"
|
|
2
2
|
import { LiteralNode, Location, Visitor } from "@herb-tools/core"
|
|
3
3
|
|
|
4
4
|
import { TailwindClassSorter } from "@herb-tools/tailwind-class-sorter"
|
|
@@ -45,7 +45,23 @@ class TailwindClassSorterVisitor extends Visitor {
|
|
|
45
45
|
const attributeName = getStaticAttributeName(node.name)
|
|
46
46
|
if (attributeName !== "class") return
|
|
47
47
|
|
|
48
|
-
this.
|
|
48
|
+
const classAttributeSorter = new ClassAttributeSorter(this.sorter)
|
|
49
|
+
|
|
50
|
+
classAttributeSorter.visit(node.value)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Visitor that sorts classes within a single class attribute value.
|
|
56
|
+
* Only operates on the content of a class attribute, not the full document.
|
|
57
|
+
*/
|
|
58
|
+
class ClassAttributeSorter extends Visitor {
|
|
59
|
+
private sorter: TailwindClassSorter
|
|
60
|
+
|
|
61
|
+
constructor(sorter: TailwindClassSorter) {
|
|
62
|
+
super()
|
|
63
|
+
|
|
64
|
+
this.sorter = sorter
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void {
|
|
@@ -127,104 +143,181 @@ class TailwindClassSorterVisitor extends Visitor {
|
|
|
127
143
|
})
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
private
|
|
131
|
-
return
|
|
146
|
+
private isInterpolatedGroup(group: Node[]): boolean {
|
|
147
|
+
return group.some(node => !isLiteralNode(node))
|
|
132
148
|
}
|
|
133
149
|
|
|
134
|
-
private
|
|
135
|
-
return
|
|
150
|
+
private isWhitespaceGroup(group: Node[]): boolean {
|
|
151
|
+
return group.every(node => isPureWhitespaceNode(node))
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
private
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
private getStaticClassContent(group: Node[]): string {
|
|
155
|
+
return group
|
|
156
|
+
.filter(node => isLiteralNode(node))
|
|
157
|
+
.map(node => (node as LiteralNode).content)
|
|
158
|
+
.join("")
|
|
143
159
|
}
|
|
144
160
|
|
|
145
|
-
private
|
|
146
|
-
const
|
|
147
|
-
const
|
|
161
|
+
private categorizeGroups(groups: Node[][]): { staticClasses: string[], interpolationGroups: Node[][], standaloneERBNodes: Node[] } {
|
|
162
|
+
const staticClasses: string[] = []
|
|
163
|
+
const interpolationGroups: Node[][] = []
|
|
164
|
+
const standaloneERBNodes: Node[] = []
|
|
165
|
+
|
|
166
|
+
for (const group of groups) {
|
|
167
|
+
if (this.isWhitespaceGroup(group)) {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.isInterpolatedGroup(group)) {
|
|
172
|
+
const hasAttachedLiteral = group.some(node => isLiteralNode(node) && node.content.trim())
|
|
173
|
+
|
|
174
|
+
if (hasAttachedLiteral) {
|
|
175
|
+
for (const node of group) {
|
|
176
|
+
if (!isLiteralNode(node)) {
|
|
177
|
+
this.visit(node)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
148
180
|
|
|
149
|
-
|
|
150
|
-
if (isLiteralNode(node)) {
|
|
151
|
-
if (node.content.trim()) {
|
|
152
|
-
classLiterals.push(node)
|
|
181
|
+
interpolationGroups.push(group)
|
|
153
182
|
} else {
|
|
154
|
-
|
|
183
|
+
for (const node of group) {
|
|
184
|
+
if (!isLiteralNode(node)) {
|
|
185
|
+
this.visit(node)
|
|
186
|
+
standaloneERBNodes.push(node)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
155
189
|
}
|
|
156
190
|
} else {
|
|
157
|
-
this.
|
|
158
|
-
|
|
191
|
+
const content = this.getStaticClassContent(group).trim()
|
|
192
|
+
|
|
193
|
+
if (content) {
|
|
194
|
+
staticClasses.push(content)
|
|
195
|
+
}
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
198
|
|
|
162
|
-
return {
|
|
199
|
+
return { staticClasses, interpolationGroups, standaloneERBNodes }
|
|
163
200
|
}
|
|
164
201
|
|
|
165
|
-
private
|
|
166
|
-
if (
|
|
167
|
-
if (
|
|
202
|
+
private formatNodes(nodes: Node[], isNested: boolean): Node[] {
|
|
203
|
+
if (nodes.length === 0) return nodes
|
|
204
|
+
if (nodes.every(child => isPureWhitespaceNode(child))) return nodes
|
|
205
|
+
|
|
206
|
+
const splitNodes = splitLiteralsAtWhitespace(nodes)
|
|
207
|
+
const groups = groupNodesByClass(splitNodes)
|
|
208
|
+
const groupPrecedingWhitespace = new Map<Node[], Node[]>()
|
|
209
|
+
const nodePrecedingWhitespace = new Map<Node, Node[]>()
|
|
210
|
+
|
|
211
|
+
for (let i = 1; i < groups.length; i++) {
|
|
212
|
+
if (!this.isWhitespaceGroup(groups[i]) && this.isWhitespaceGroup(groups[i - 1])) {
|
|
213
|
+
groupPrecedingWhitespace.set(groups[i], groups[i - 1])
|
|
214
|
+
|
|
215
|
+
for (const node of groups[i]) {
|
|
216
|
+
if (!isLiteralNode(node)) {
|
|
217
|
+
nodePrecedingWhitespace.set(node, groups[i - 1])
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let leadingWhitespace: Node[] | null = null
|
|
224
|
+
let trailingWhitespace: Node[] | null = null
|
|
168
225
|
|
|
169
|
-
|
|
170
|
-
|
|
226
|
+
if (isNested && groups.length > 0) {
|
|
227
|
+
if (this.isWhitespaceGroup(groups[0])) {
|
|
228
|
+
leadingWhitespace = groups[0]
|
|
229
|
+
}
|
|
230
|
+
if (groups.length > 1 && this.isWhitespaceGroup(groups[groups.length - 1])) {
|
|
231
|
+
trailingWhitespace = groups[groups.length - 1]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
171
234
|
|
|
172
|
-
|
|
235
|
+
const { staticClasses, interpolationGroups, standaloneERBNodes } = this.categorizeGroups(groups)
|
|
173
236
|
|
|
174
|
-
|
|
175
|
-
|
|
237
|
+
const allStaticContent = staticClasses.join(" ")
|
|
238
|
+
let sortedContent = allStaticContent
|
|
176
239
|
|
|
177
|
-
|
|
178
|
-
|
|
240
|
+
if (allStaticContent) {
|
|
241
|
+
try {
|
|
242
|
+
sortedContent = this.sorter.sortClasses(allStaticContent)
|
|
243
|
+
} catch {
|
|
244
|
+
// Keep original on error
|
|
179
245
|
}
|
|
246
|
+
}
|
|
180
247
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
248
|
+
const parts: Node[] = []
|
|
249
|
+
|
|
250
|
+
if (sortedContent) {
|
|
251
|
+
parts.push(new LiteralNode({
|
|
252
|
+
type: "AST_LITERAL_NODE",
|
|
253
|
+
content: sortedContent,
|
|
254
|
+
errors: [],
|
|
255
|
+
location: Location.zero
|
|
256
|
+
}))
|
|
184
257
|
}
|
|
185
|
-
}
|
|
186
258
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
259
|
+
for (const group of interpolationGroups) {
|
|
260
|
+
if (parts.length > 0) {
|
|
261
|
+
const whitespace = groupPrecedingWhitespace.get(group)
|
|
262
|
+
parts.push(...(whitespace ?? [this.spaceLiteral]))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
parts.push(...this.trimGroupWhitespace(group))
|
|
266
|
+
}
|
|
191
267
|
|
|
192
|
-
|
|
268
|
+
for (const node of standaloneERBNodes) {
|
|
269
|
+
if (parts.length > 0) {
|
|
270
|
+
const whitespace = nodePrecedingWhitespace.get(node)
|
|
271
|
+
parts.push(...(whitespace ?? [this.spaceLiteral]))
|
|
272
|
+
}
|
|
273
|
+
parts.push(node)
|
|
274
|
+
}
|
|
193
275
|
|
|
194
|
-
|
|
276
|
+
if (isNested && parts.length > 0) {
|
|
277
|
+
const leading = leadingWhitespace ?? [this.spaceLiteral]
|
|
278
|
+
const trailing = trailingWhitespace ?? [this.spaceLiteral]
|
|
279
|
+
return [...leading, ...parts, ...trailing]
|
|
280
|
+
}
|
|
195
281
|
|
|
196
|
-
return
|
|
282
|
+
return parts
|
|
197
283
|
}
|
|
198
284
|
|
|
199
|
-
private
|
|
200
|
-
|
|
201
|
-
const trailingSpace = fullContent.match(/\s*$/)?.[0] || ""
|
|
285
|
+
private trimGroupWhitespace(group: Node[]): Node[] {
|
|
286
|
+
if (group.length === 0) return group
|
|
202
287
|
|
|
203
|
-
const
|
|
204
|
-
const firstIsWhitespace = this.isWhitespaceLiteral(others[0])
|
|
205
|
-
const spaceBetween = firstIsWhitespace ? "" : " "
|
|
288
|
+
const result = [...group]
|
|
206
289
|
|
|
207
|
-
|
|
290
|
+
if (isLiteralNode(result[0])) {
|
|
291
|
+
const first = result[0] as LiteralNode
|
|
292
|
+
const trimmed = first.content.trimStart()
|
|
208
293
|
|
|
209
|
-
|
|
294
|
+
if (trimmed !== first.content) {
|
|
295
|
+
result[0] = new LiteralNode({
|
|
296
|
+
type: "AST_LITERAL_NODE",
|
|
297
|
+
content: trimmed,
|
|
298
|
+
errors: [],
|
|
299
|
+
location: first.location
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
}
|
|
210
303
|
|
|
211
|
-
|
|
212
|
-
}
|
|
304
|
+
const lastIndex = result.length - 1
|
|
213
305
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
306
|
+
if (isLiteralNode(result[lastIndex])) {
|
|
307
|
+
const last = result[lastIndex] as LiteralNode
|
|
308
|
+
const trimmed = last.content.trimEnd()
|
|
217
309
|
|
|
218
|
-
if (
|
|
219
|
-
|
|
310
|
+
if (trimmed !== last.content) {
|
|
311
|
+
result[lastIndex] = new LiteralNode({
|
|
312
|
+
type: "AST_LITERAL_NODE",
|
|
313
|
+
content: trimmed,
|
|
314
|
+
errors: [],
|
|
315
|
+
location: last.location
|
|
316
|
+
})
|
|
220
317
|
}
|
|
318
|
+
}
|
|
221
319
|
|
|
222
|
-
|
|
223
|
-
const nextIsWhitespace = this.isWhitespaceLiteral(nodes[index + 1])
|
|
224
|
-
const needsSpace = !currentIsWhitespace && !nextIsWhitespace
|
|
225
|
-
|
|
226
|
-
return needsSpace ? [node, this.spaceLiteral] : [node]
|
|
227
|
-
})
|
|
320
|
+
return result
|
|
228
321
|
}
|
|
229
322
|
}
|
|
230
323
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { ASTRewriter } from "./ast-rewriter.js"
|
|
2
|
+
export { ActionViewTagHelperToHTMLRewriter } from "./built-ins/action-view-tag-helper-to-html.js"
|
|
3
|
+
export { HTMLToActionViewTagHelperRewriter } from "./built-ins/html-to-action-view-tag-helper.js"
|
|
2
4
|
export { StringRewriter } from "./string-rewriter.js"
|
|
3
5
|
|
|
4
6
|
export { asMutable } from "./mutable.js"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { TailwindClassSorterRewriter } from "./built-ins/tailwind-class-sorter.js"
|
|
2
|
+
import { ActionViewTagHelperToHTMLRewriter } from "./built-ins/action-view-tag-helper-to-html.js"
|
|
3
|
+
import { HTMLToActionViewTagHelperRewriter } from "./built-ins/html-to-action-view-tag-helper.js"
|
|
2
4
|
|
|
3
5
|
export interface TailwindClassSorterOptions {
|
|
4
6
|
/**
|
|
@@ -35,3 +37,11 @@ export async function tailwindClassSorter(options: TailwindClassSorterOptions =
|
|
|
35
37
|
|
|
36
38
|
return rewriter
|
|
37
39
|
}
|
|
40
|
+
|
|
41
|
+
export function actionViewTagHelperToHTML(): ActionViewTagHelperToHTMLRewriter {
|
|
42
|
+
return new ActionViewTagHelperToHTMLRewriter()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function htmlToActionViewTagHelper(): HTMLToActionViewTagHelperRewriter {
|
|
46
|
+
return new HTMLToActionViewTagHelperRewriter()
|
|
47
|
+
}
|