@herb-tools/rewriter 0.9.1 → 0.9.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.
@@ -4,5 +4,5 @@ import type { Node } from "@herb-tools/core";
4
4
  export declare class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
5
5
  get name(): string;
6
6
  get description(): string;
7
- rewrite<T extends Node>(node: T, _context: RewriteContext): T;
7
+ rewrite<T extends Node>(node: T, context: RewriteContext): T;
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/rewriter",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Rewriter system for transforming HTML+ERB AST nodes and formatted strings",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -38,12 +38,12 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@herb-tools/core": "0.9.1",
42
- "@herb-tools/tailwind-class-sorter": "0.9.1",
41
+ "@herb-tools/core": "0.9.3",
42
+ "@herb-tools/tailwind-class-sorter": "0.9.3",
43
43
  "tinyglobby": "^0.2.15"
44
44
  },
45
45
  "devDependencies": {
46
- "@herb-tools/printer": "0.9.1"
46
+ "@herb-tools/printer": "0.9.3"
47
47
  },
48
48
  "files": [
49
49
  "package.json",
@@ -1,5 +1,5 @@
1
1
  import { Visitor, Location, HTMLOpenTagNode, HTMLCloseTagNode, HTMLElementNode, HTMLAttributeValueNode, WhitespaceNode, ERBContentNode } from "@herb-tools/core"
2
- import { isHTMLAttributeNode, isERBOpenTagNode, isRubyLiteralNode, isRubyHTMLAttributesSplatNode, createSyntheticToken } from "@herb-tools/core"
2
+ import { isHTMLAttributeNode, isERBOpenTagNode, isRubyLiteralNode, isRubyHTMLAttributesSplatNode, isWhitespaceNode, createSyntheticToken } from "@herb-tools/core"
3
3
 
4
4
  import { ASTRewriter } from "../ast-rewriter.js"
5
5
  import { asMutable } from "../mutable.js"
@@ -17,6 +17,45 @@ function createWhitespaceNode(): WhitespaceNode {
17
17
  }
18
18
 
19
19
  class ActionViewTagHelperToHTMLVisitor extends Visitor {
20
+ private shallow: boolean
21
+ private includeBody: boolean
22
+
23
+ constructor(options: { shallow?: boolean; includeBody?: boolean } = {}) {
24
+ super()
25
+ this.shallow = options.shallow ?? false
26
+ this.includeBody = options.includeBody ?? true
27
+ }
28
+
29
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
30
+ const newChildren: Node[] = []
31
+
32
+ for (let index = 0; index < node.children.length; index++) {
33
+ const child = node.children[index]
34
+
35
+ if (isHTMLAttributeNode(child)) {
36
+ if (child.equals && child.equals.value !== "=") {
37
+ asMutable(child).equals = createSyntheticToken("=")
38
+ }
39
+
40
+ if (child.value) {
41
+ this.transformAttributeValue(child.value)
42
+ }
43
+
44
+ const previous = index > 0 ? node.children[index - 1] : null
45
+
46
+ if (!previous || !isWhitespaceNode(previous)) {
47
+ newChildren.push(createWhitespaceNode())
48
+ }
49
+ }
50
+
51
+ newChildren.push(child)
52
+ }
53
+
54
+ asMutable(node).children = newChildren
55
+
56
+ this.visitChildNodes(node)
57
+ }
58
+
20
59
  visitHTMLElementNode(node: HTMLElementNode): void {
21
60
  if (!node.element_source) {
22
61
  this.visitChildNodes(node)
@@ -104,7 +143,9 @@ class ActionViewTagHelperToHTMLVisitor extends Visitor {
104
143
 
105
144
  asMutable(node).element_source = "HTML"
106
145
 
107
- if (node.body) {
146
+ if (!this.includeBody) {
147
+ asMutable(node).body = []
148
+ } else if (node.body) {
108
149
  asMutable(node).body = node.body.map(child => {
109
150
  if (isRubyLiteralNode(child)) {
110
151
  return new ERBContentNode({
@@ -120,7 +161,10 @@ class ActionViewTagHelperToHTMLVisitor extends Visitor {
120
161
  })
121
162
  }
122
163
 
123
- this.visit(child)
164
+ if (!this.shallow) {
165
+ this.visit(child)
166
+ }
167
+
124
168
  return child
125
169
  })
126
170
  }
@@ -150,12 +194,12 @@ class ActionViewTagHelperToHTMLVisitor extends Visitor {
150
194
  })
151
195
 
152
196
  mutableValue.children = newChildren
197
+ }
153
198
 
154
- if (!value.quoted) {
155
- mutableValue.quoted = true
156
- mutableValue.open_quote = createSyntheticToken('"')
157
- mutableValue.close_quote = createSyntheticToken('"')
158
- }
199
+ if (!value.quoted) {
200
+ mutableValue.quoted = true
201
+ mutableValue.open_quote = createSyntheticToken('"')
202
+ mutableValue.close_quote = createSyntheticToken('"')
159
203
  }
160
204
  }
161
205
  }
@@ -166,11 +210,11 @@ export class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
166
210
  }
167
211
 
168
212
  get description(): string {
169
- return "Converts ActionView tag helpers (tag.*, content_tag, link_to, turbo_frame_tag) to raw HTML elements"
213
+ return "Converts ActionView tag helpers to raw HTML elements"
170
214
  }
171
215
 
172
- rewrite<T extends Node>(node: T, _context: RewriteContext): T {
173
- const visitor = new ActionViewTagHelperToHTMLVisitor()
216
+ rewrite<T extends Node>(node: T, context: RewriteContext): T {
217
+ const visitor = new ActionViewTagHelperToHTMLVisitor({ shallow: context.shallow, includeBody: context.includeBody })
174
218
 
175
219
  visitor.visit(node)
176
220
 
@@ -35,14 +35,16 @@ interface SerializedAttributes {
35
35
  attributes: string
36
36
  href: string | null
37
37
  id: string | null
38
+ src: string | null
38
39
  }
39
40
 
40
- function serializeAttributes(children: Node[], options: { extractHref?: boolean, extractId?: boolean } = {}): SerializedAttributes {
41
+ function serializeAttributes(children: Node[], options: { extractHref?: boolean, extractId?: boolean, extractSrc?: boolean } = {}): SerializedAttributes {
41
42
  const regular: string[] = []
42
43
  const prefixed: Map<string, string[]> = new Map()
43
44
 
44
45
  let href: string | null = null
45
46
  let id: string | null = null
47
+ let src: string | null = null
46
48
 
47
49
  for (const child of children) {
48
50
  if (!isHTMLAttributeNode(child)) continue
@@ -62,6 +64,11 @@ function serializeAttributes(children: Node[], options: { extractHref?: boolean,
62
64
  continue
63
65
  }
64
66
 
67
+ if (options.extractSrc && name === "src") {
68
+ src = value
69
+ continue
70
+ }
71
+
65
72
  const dataMatch = name.match(/^(data|aria)-(.+)$/)
66
73
 
67
74
  if (dataMatch) {
@@ -83,7 +90,7 @@ function serializeAttributes(children: Node[], options: { extractHref?: boolean,
83
90
  parts.push(`${prefix}: { ${entries.join(", ")} }`)
84
91
  }
85
92
 
86
- return { attributes: parts.join(", "), href, id }
93
+ return { attributes: parts.join(", "), href, id, src }
87
94
  }
88
95
 
89
96
  function isTextOnlyBody(body: Node[]): boolean {
@@ -116,8 +123,11 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
116
123
 
117
124
  const isAnchor = tagName.value === "a"
118
125
  const isTurboFrame = tagName.value === "turbo-frame"
126
+ const isScript = tagName.value === "script"
127
+ const isImg = tagName.value === "img"
119
128
  const attributes = openTag.children.filter(child => !isWhitespaceNode(child))
120
- const { attributes: attributesString, href, id } = serializeAttributes(attributes, { extractHref: isAnchor, extractId: isTurboFrame })
129
+ const hasSrcAttribute = (isScript || isImg) && attributes.some(child => isHTMLAttributeNode(child) && getStaticAttributeName(child.name!) === "src")
130
+ const { attributes: attributesString, href, id, src } = serializeAttributes(attributes, { extractHref: isAnchor, extractId: isTurboFrame, extractSrc: isScript || isImg })
121
131
  const hasBody = node.body && node.body.length > 0 && !node.is_void
122
132
  const isInlineContent = hasBody && isTextOnlyBody(node.body)
123
133
 
@@ -130,6 +140,15 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
130
140
  } else if (isTurboFrame) {
131
141
  content = this.buildTurboFrameTagContent(node, attributesString, id, isInlineContent)
132
142
  elementSource = "Turbo::FramesHelper#turbo_frame_tag"
143
+ } else if (isScript && hasSrcAttribute) {
144
+ content = this.buildJavascriptIncludeTagContent(attributesString, src)
145
+ elementSource = "ActionView::Helpers::AssetTagHelper#javascript_include_tag"
146
+ } else if (isScript) {
147
+ content = this.buildJavascriptTagContent(node, attributesString, isInlineContent)
148
+ elementSource = "ActionView::Helpers::JavaScriptHelper#javascript_tag"
149
+ } else if (isImg) {
150
+ content = this.buildImageTagContent(attributesString, src)
151
+ elementSource = "ActionView::Helpers::AssetTagHelper#image_tag"
133
152
  } else {
134
153
  content = this.buildTagContent(tagName.value, node, attributesString, isInlineContent)
135
154
  elementSource = "ActionView::Helpers::TagHelper#tag"
@@ -149,7 +168,8 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
149
168
  asMutable(node).open_tag = erbOpenTag
150
169
  asMutable(node).element_source = elementSource
151
170
 
152
- const isInlineForm = isInlineContent || (isTurboFrame && !hasBody)
171
+ const isInlineLiteralContent = isScript && hasBody && node.body.length === 1 && isLiteralNode(node.body[0]) && !node.body[0].content.includes("\n")
172
+ const isInlineForm = isInlineContent || isInlineLiteralContent || (isTurboFrame && !hasBody) || (isScript && hasSrcAttribute) || isImg
153
173
 
154
174
  if (node.is_void) {
155
175
  asMutable(node).close_tag = null
@@ -224,6 +244,47 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
224
244
  return argString ? ` turbo_frame_tag ${argString} do ` : ` turbo_frame_tag do `
225
245
  }
226
246
 
247
+ private buildJavascriptTagContent(node: HTMLElementNode, attributes: string, isInlineContent: boolean): string {
248
+ const bodyNode = node.body?.[0]
249
+ const isInlineLiteral = bodyNode && isLiteralNode(bodyNode) && !bodyNode.content.includes("\n")
250
+ const isInlineText = isInlineContent && isHTMLTextNode(bodyNode)
251
+
252
+ if (isInlineText || isInlineLiteral) {
253
+ const textContent = isHTMLTextNode(bodyNode) ? bodyNode.content : bodyNode.content
254
+ const args = [`"${textContent}"`]
255
+
256
+ if (attributes) args.push(attributes)
257
+
258
+ return ` javascript_tag ${args.join(", ")} `
259
+ }
260
+
261
+ return attributes
262
+ ? ` javascript_tag ${attributes} do `
263
+ : ` javascript_tag do `
264
+ }
265
+
266
+ private buildJavascriptIncludeTagContent(attributes: string, source: string | null): string {
267
+ const args: string[] = []
268
+
269
+ if (source) args.push(source)
270
+ if (attributes) args.push(attributes)
271
+
272
+ const argString = args.join(", ")
273
+
274
+ return argString ? ` javascript_include_tag ${argString} ` : ` javascript_include_tag `
275
+ }
276
+
277
+ private buildImageTagContent(attributes: string, source: string | null): string {
278
+ const args: string[] = []
279
+
280
+ if (source) args.push(source)
281
+ if (attributes) args.push(attributes)
282
+
283
+ const argString = args.join(", ")
284
+
285
+ return argString ? ` image_tag ${argString} ` : ` image_tag `
286
+ }
287
+
227
288
  private buildLinkToContent(node: HTMLElementNode, attribute: string, href: string | null, isInlineContent: boolean): string {
228
289
  const args: string[] = []
229
290
 
@@ -255,7 +316,7 @@ export class HTMLToActionViewTagHelperRewriter extends ASTRewriter {
255
316
  }
256
317
 
257
318
  get description(): string {
258
- return "Converts raw HTML elements to ActionView tag helpers (tag.*, turbo_frame_tag)"
319
+ return "Converts raw HTML elements to ActionView tag helpers (tag.*, turbo_frame_tag, javascript_tag, javascript_include_tag, image_tag)"
259
320
  }
260
321
 
261
322
  rewrite<T extends Node>(node: T, _context: RewriteContext): T {