@herb-tools/rewriter 0.9.2 → 0.9.4

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
  }
@@ -0,0 +1,21 @@
1
+ import { ASTRewriter } from "../ast-rewriter.js";
2
+ import type { RewriteContext } from "../context.js";
3
+ import type { Node, PrismNode } from "@herb-tools/core";
4
+ export interface TextPart {
5
+ type: "text";
6
+ content: string;
7
+ }
8
+ export interface ExpressionPart {
9
+ type: "expression";
10
+ expression: string;
11
+ }
12
+ export type ReplacementPart = TextPart | ExpressionPart;
13
+ export declare class ERBStringToDirectOutputRewriter extends ASTRewriter {
14
+ get name(): string;
15
+ get description(): string;
16
+ rewrite<T extends Node>(node: T, _context: RewriteContext): T;
17
+ static isStringOutputNode(prismNode: PrismNode): boolean;
18
+ static extractStringContent(stringNode: PrismNode, source: string): string;
19
+ static extractExpressionSource(embeddedNode: PrismNode, source: string): string | null;
20
+ static extractReplacementParts(prismNode: PrismNode, source: string): ReplacementPart[] | null;
21
+ }
@@ -12,5 +12,7 @@ export declare function getBuiltinRewriter(name: string): RewriterClass | undefi
12
12
  */
13
13
  export declare function getBuiltinRewriterNames(): string[];
14
14
  export { ActionViewTagHelperToHTMLRewriter } from "./action-view-tag-helper-to-html.js";
15
+ export { ERBStringToDirectOutputRewriter } from "./erb-string-to-direct-output.js";
15
16
  export { HTMLToActionViewTagHelperRewriter } from "./html-to-action-view-tag-helper.js";
16
17
  export { TailwindClassSorterRewriter } from "./tailwind-class-sorter.js";
18
+ export type { TextPart, ExpressionPart, ReplacementPart } from "./erb-string-to-direct-output.js";
@@ -1,5 +1,6 @@
1
1
  export { ASTRewriter } from "./ast-rewriter.js";
2
2
  export { ActionViewTagHelperToHTMLRewriter } from "./built-ins/action-view-tag-helper-to-html.js";
3
+ export { ERBStringToDirectOutputRewriter } from "./built-ins/erb-string-to-direct-output.js";
3
4
  export { HTMLToActionViewTagHelperRewriter } from "./built-ins/html-to-action-view-tag-helper.js";
4
5
  export { StringRewriter } from "./string-rewriter.js";
5
6
  export { asMutable } from "./mutable.js";
@@ -10,3 +11,4 @@ export type { Mutable } from "./mutable.js";
10
11
  export type { RewriterClass } from "./type-guards.js";
11
12
  export type { Rewriter, RewriteOptions, RewriteResult } from "./rewrite.js";
12
13
  export type { TailwindClassSorterOptions } from "./rewriter-factories.js";
14
+ export type { TextPart, ExpressionPart, ReplacementPart } from "./built-ins/erb-string-to-direct-output.js";
@@ -1,5 +1,6 @@
1
1
  import { TailwindClassSorterRewriter } from "./built-ins/tailwind-class-sorter.js";
2
2
  import { ActionViewTagHelperToHTMLRewriter } from "./built-ins/action-view-tag-helper-to-html.js";
3
+ import { ERBStringToDirectOutputRewriter } from "./built-ins/erb-string-to-direct-output.js";
3
4
  import { HTMLToActionViewTagHelperRewriter } from "./built-ins/html-to-action-view-tag-helper.js";
4
5
  export interface TailwindClassSorterOptions {
5
6
  /**
@@ -29,4 +30,5 @@ export interface TailwindClassSorterOptions {
29
30
  */
30
31
  export declare function tailwindClassSorter(options?: TailwindClassSorterOptions): Promise<TailwindClassSorterRewriter>;
31
32
  export declare function actionViewTagHelperToHTML(): ActionViewTagHelperToHTMLRewriter;
33
+ export declare function erbStringToDirectOutput(): ERBStringToDirectOutputRewriter;
32
34
  export declare function htmlToActionViewTagHelper(): HTMLToActionViewTagHelperRewriter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/rewriter",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
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.2",
42
- "@herb-tools/tailwind-class-sorter": "0.9.2",
41
+ "@herb-tools/core": "0.9.4",
42
+ "@herb-tools/tailwind-class-sorter": "0.9.4",
43
43
  "tinyglobby": "^0.2.15"
44
44
  },
45
45
  "devDependencies": {
46
- "@herb-tools/printer": "0.9.2"
46
+ "@herb-tools/printer": "0.9.4"
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
  }
@@ -169,8 +213,8 @@ export class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
169
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
 
@@ -0,0 +1,176 @@
1
+ import { Visitor, isERBOutputNode, createLiteral, createERBOutputNode, findParentArray } from "@herb-tools/core"
2
+
3
+ import { ASTRewriter } from "../ast-rewriter.js"
4
+
5
+ import type { RewriteContext } from "../context.js"
6
+ import type { Node, ERBContentNode, PrismNode } from "@herb-tools/core"
7
+
8
+ const STRING_NODE_TYPE = "StringNode"
9
+ const INTERPOLATED_STRING_NODE_TYPE = "InterpolatedStringNode"
10
+ const EMBEDDED_STATEMENTS_NODE_TYPE = "EmbeddedStatementsNode"
11
+
12
+ export interface TextPart {
13
+ type: "text"
14
+ content: string
15
+ }
16
+
17
+ export interface ExpressionPart {
18
+ type: "expression"
19
+ expression: string
20
+ }
21
+
22
+ export type ReplacementPart = TextPart | ExpressionPart
23
+
24
+ class ERBStringToDirectOutputVisitor extends Visitor {
25
+ private root: Node
26
+
27
+ constructor(root: Node) {
28
+ super()
29
+ this.root = root
30
+ }
31
+
32
+ visitERBContentNode(node: ERBContentNode): void {
33
+ if (!isERBOutputNode(node)) {
34
+ this.visitChildNodes(node)
35
+ return
36
+ }
37
+
38
+ const prismNode = node.prismNode
39
+ if (!prismNode) {
40
+ this.visitChildNodes(node)
41
+ return
42
+ }
43
+
44
+ const source = node.source
45
+ if (!source) {
46
+ this.visitChildNodes(node)
47
+ return
48
+ }
49
+
50
+ if (!ERBStringToDirectOutputRewriter.isStringOutputNode(prismNode)) {
51
+ this.visitChildNodes(node)
52
+ return
53
+ }
54
+
55
+ const replacementParts = ERBStringToDirectOutputRewriter.extractReplacementParts(prismNode, source)
56
+
57
+ if (!replacementParts) {
58
+ this.visitChildNodes(node)
59
+ return
60
+ }
61
+
62
+ const tagOpening = node.tag_opening?.value ?? "<%="
63
+ const tagClosing = node.tag_closing?.value ?? "%>"
64
+
65
+ const parentInfo = findParentArray(this.root, node)
66
+
67
+ if (!parentInfo) {
68
+ this.visitChildNodes(node)
69
+ return
70
+ }
71
+
72
+ const { array: parentArray, index: nodeIndex } = parentInfo
73
+ const replacementNodes: Node[] = []
74
+
75
+ for (const part of replacementParts) {
76
+ if (part.type === "text") {
77
+ replacementNodes.push(createLiteral(part.content))
78
+ } else {
79
+ replacementNodes.push(createERBOutputNode(` ${part.expression.trim()} `, tagOpening, tagClosing))
80
+ }
81
+ }
82
+
83
+ parentArray.splice(nodeIndex, 1, ...replacementNodes)
84
+ }
85
+ }
86
+
87
+ export class ERBStringToDirectOutputRewriter extends ASTRewriter {
88
+ get name(): string {
89
+ return "erb-string-to-direct-output"
90
+ }
91
+
92
+ get description(): string {
93
+ return "Replaces ERB string output with direct text and expression tags"
94
+ }
95
+
96
+ rewrite<T extends Node>(node: T, _context: RewriteContext): T {
97
+ const visitor = new ERBStringToDirectOutputVisitor(node)
98
+
99
+ visitor.visit(node)
100
+
101
+ return node
102
+ }
103
+
104
+ static isStringOutputNode(prismNode: PrismNode): boolean {
105
+ const nodeType = prismNode.constructor.name
106
+
107
+ return nodeType === STRING_NODE_TYPE || nodeType === INTERPOLATED_STRING_NODE_TYPE
108
+ }
109
+
110
+ static extractStringContent(stringNode: PrismNode, source: string): string {
111
+ const unescapedValue = stringNode.unescaped?.value
112
+
113
+ if (typeof unescapedValue === "string") {
114
+ return unescapedValue
115
+ }
116
+
117
+ const location = stringNode.contentLoc
118
+
119
+ if (location) {
120
+ return source.substring(location.startOffset, location.startOffset + location.length)
121
+ }
122
+
123
+ return ""
124
+ }
125
+
126
+ static extractExpressionSource(embeddedNode: PrismNode, source: string): string | null {
127
+ const openingLocation = embeddedNode.openingLoc
128
+ const closingLocation = embeddedNode.closingLoc
129
+
130
+ if (!openingLocation || !closingLocation) return null
131
+
132
+ const expressionStart = openingLocation.startOffset + openingLocation.length
133
+ const expressionEnd = closingLocation.startOffset
134
+
135
+ return source.substring(expressionStart, expressionEnd)
136
+ }
137
+
138
+ static extractReplacementParts(prismNode: PrismNode, source: string): ReplacementPart[] | null {
139
+ const nodeType = prismNode.constructor.name
140
+
141
+ if (nodeType === STRING_NODE_TYPE) {
142
+ const textContent = this.extractStringContent(prismNode, source)
143
+ return [{ type: "text", content: textContent }]
144
+ }
145
+
146
+ if (nodeType === INTERPOLATED_STRING_NODE_TYPE) {
147
+ const parts = prismNode.parts
148
+
149
+ if (!parts || parts.length === 0) return null
150
+
151
+ const replacementParts: ReplacementPart[] = []
152
+
153
+ for (const part of parts) {
154
+ const partType = part.constructor.name
155
+
156
+ if (partType === STRING_NODE_TYPE) {
157
+ const textContent = this.extractStringContent(part, source)
158
+
159
+ if (textContent) {
160
+ replacementParts.push({ type: "text", content: textContent })
161
+ }
162
+ } else if (partType === EMBEDDED_STATEMENTS_NODE_TYPE) {
163
+ const expression = this.extractExpressionSource(part, source)
164
+
165
+ if (expression) {
166
+ replacementParts.push({ type: "expression", expression })
167
+ }
168
+ }
169
+ }
170
+
171
+ return replacementParts.length > 0 ? replacementParts : null
172
+ }
173
+
174
+ return null
175
+ }
176
+ }
@@ -124,9 +124,10 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
124
124
  const isAnchor = tagName.value === "a"
125
125
  const isTurboFrame = tagName.value === "turbo-frame"
126
126
  const isScript = tagName.value === "script"
127
+ const isImg = tagName.value === "img"
127
128
  const attributes = openTag.children.filter(child => !isWhitespaceNode(child))
128
- const hasSrcAttribute = isScript && attributes.some(child => isHTMLAttributeNode(child) && getStaticAttributeName(child.name!) === "src")
129
- const { attributes: attributesString, href, id, src } = serializeAttributes(attributes, { extractHref: isAnchor, extractId: isTurboFrame, extractSrc: isScript })
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 })
130
131
  const hasBody = node.body && node.body.length > 0 && !node.is_void
131
132
  const isInlineContent = hasBody && isTextOnlyBody(node.body)
132
133
 
@@ -145,6 +146,9 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
145
146
  } else if (isScript) {
146
147
  content = this.buildJavascriptTagContent(node, attributesString, isInlineContent)
147
148
  elementSource = "ActionView::Helpers::JavaScriptHelper#javascript_tag"
149
+ } else if (isImg) {
150
+ content = this.buildImageTagContent(attributesString, src)
151
+ elementSource = "ActionView::Helpers::AssetTagHelper#image_tag"
148
152
  } else {
149
153
  content = this.buildTagContent(tagName.value, node, attributesString, isInlineContent)
150
154
  elementSource = "ActionView::Helpers::TagHelper#tag"
@@ -165,7 +169,7 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
165
169
  asMutable(node).element_source = elementSource
166
170
 
167
171
  const isInlineLiteralContent = isScript && hasBody && node.body.length === 1 && isLiteralNode(node.body[0]) && !node.body[0].content.includes("\n")
168
- const isInlineForm = isInlineContent || isInlineLiteralContent || (isTurboFrame && !hasBody) || (isScript && hasSrcAttribute)
172
+ const isInlineForm = isInlineContent || isInlineLiteralContent || (isTurboFrame && !hasBody) || (isScript && hasSrcAttribute) || isImg
169
173
 
170
174
  if (node.is_void) {
171
175
  asMutable(node).close_tag = null
@@ -270,6 +274,17 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
270
274
  return argString ? ` javascript_include_tag ${argString} ` : ` javascript_include_tag `
271
275
  }
272
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
+
273
288
  private buildLinkToContent(node: HTMLElementNode, attribute: string, href: string | null, isInlineContent: boolean): string {
274
289
  const args: string[] = []
275
290
 
@@ -301,7 +316,7 @@ export class HTMLToActionViewTagHelperRewriter extends ASTRewriter {
301
316
  }
302
317
 
303
318
  get description(): string {
304
- return "Converts raw HTML elements to ActionView tag helpers (tag.*, turbo_frame_tag, javascript_tag, javascript_include_tag)"
319
+ return "Converts raw HTML elements to ActionView tag helpers (tag.*, turbo_frame_tag, javascript_tag, javascript_include_tag, image_tag)"
305
320
  }
306
321
 
307
322
  rewrite<T extends Node>(node: T, _context: RewriteContext): T {
@@ -1,4 +1,5 @@
1
1
  import { ActionViewTagHelperToHTMLRewriter } from "./action-view-tag-helper-to-html.js"
2
+ import { ERBStringToDirectOutputRewriter } from "./erb-string-to-direct-output.js"
2
3
  import { HTMLToActionViewTagHelperRewriter } from "./html-to-action-view-tag-helper.js"
3
4
  import { TailwindClassSorterRewriter } from "./tailwind-class-sorter.js"
4
5
 
@@ -9,6 +10,7 @@ import type { RewriterClass } from "../type-guards.js"
9
10
  */
10
11
  export const builtinRewriters: RewriterClass[] = [
11
12
  ActionViewTagHelperToHTMLRewriter,
13
+ ERBStringToDirectOutputRewriter,
12
14
  HTMLToActionViewTagHelperRewriter,
13
15
  TailwindClassSorterRewriter
14
16
  ]
@@ -36,5 +38,8 @@ export function getBuiltinRewriterNames(): string[] {
36
38
  }
37
39
 
38
40
  export { ActionViewTagHelperToHTMLRewriter } from "./action-view-tag-helper-to-html.js"
41
+ export { ERBStringToDirectOutputRewriter } from "./erb-string-to-direct-output.js"
39
42
  export { HTMLToActionViewTagHelperRewriter } from "./html-to-action-view-tag-helper.js"
40
43
  export { TailwindClassSorterRewriter } from "./tailwind-class-sorter.js"
44
+
45
+ export type { TextPart, ExpressionPart, ReplacementPart } from "./erb-string-to-direct-output.js"
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ASTRewriter } from "./ast-rewriter.js"
2
2
  export { ActionViewTagHelperToHTMLRewriter } from "./built-ins/action-view-tag-helper-to-html.js"
3
+ export { ERBStringToDirectOutputRewriter } from "./built-ins/erb-string-to-direct-output.js"
3
4
  export { HTMLToActionViewTagHelperRewriter } from "./built-ins/html-to-action-view-tag-helper.js"
4
5
  export { StringRewriter } from "./string-rewriter.js"
5
6
 
@@ -13,3 +14,4 @@ export type { Mutable } from "./mutable.js"
13
14
  export type { RewriterClass } from "./type-guards.js"
14
15
  export type { Rewriter, RewriteOptions, RewriteResult } from "./rewrite.js"
15
16
  export type { TailwindClassSorterOptions } from "./rewriter-factories.js"
17
+ export type { TextPart, ExpressionPart, ReplacementPart } from "./built-ins/erb-string-to-direct-output.js"
@@ -1,5 +1,6 @@
1
1
  import { TailwindClassSorterRewriter } from "./built-ins/tailwind-class-sorter.js"
2
2
  import { ActionViewTagHelperToHTMLRewriter } from "./built-ins/action-view-tag-helper-to-html.js"
3
+ import { ERBStringToDirectOutputRewriter } from "./built-ins/erb-string-to-direct-output.js"
3
4
  import { HTMLToActionViewTagHelperRewriter } from "./built-ins/html-to-action-view-tag-helper.js"
4
5
 
5
6
  export interface TailwindClassSorterOptions {
@@ -42,6 +43,10 @@ export function actionViewTagHelperToHTML(): ActionViewTagHelperToHTMLRewriter {
42
43
  return new ActionViewTagHelperToHTMLRewriter()
43
44
  }
44
45
 
46
+ export function erbStringToDirectOutput(): ERBStringToDirectOutputRewriter {
47
+ return new ERBStringToDirectOutputRewriter()
48
+ }
49
+
45
50
  export function htmlToActionViewTagHelper(): HTMLToActionViewTagHelperRewriter {
46
51
  return new HTMLToActionViewTagHelperRewriter()
47
52
  }