Skip to content

Commit fe15797

Browse files
committed
Parser: Detect turbo_frame_tag helper
1 parent dceed8f commit fe15797

File tree

19 files changed

+1143
-11
lines changed

19 files changed

+1143
-11
lines changed

javascript/packages/node/binding.gyp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@
2626
"./extension/libherb/analyze/action_view/content_tag.c",
2727
"./extension/libherb/analyze/action_view/link_to.c",
2828
"./extension/libherb/analyze/action_view/registry.c",
29-
"./extension/libherb/analyze/action_view/tag.c",
3029
"./extension/libherb/analyze/action_view/tag_helper_node_builders.c",
3130
"./extension/libherb/analyze/action_view/tag_helpers.c",
31+
"./extension/libherb/analyze/action_view/tag.c",
32+
"./extension/libherb/analyze/action_view/turbo_frame_tag.c",
3233
"./extension/libherb/ast_node.c",
3334
"./extension/libherb/ast_nodes.c",
3435
"./extension/libherb/ast_pretty_print.c",

javascript/packages/rewriter/src/built-ins/action-view-tag-helper-to-html.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class ActionViewTagHelperToHTMLVisitor extends Visitor {
5252
tag_closing: createSyntheticToken("%>"),
5353
parsed: false,
5454
valid: true,
55+
prism_node: null,
5556
}))
5657

5758
continue
@@ -126,6 +127,7 @@ class ActionViewTagHelperToHTMLVisitor extends Visitor {
126127
tag_closing: createSyntheticToken("%>"),
127128
parsed: false,
128129
valid: true,
130+
prism_node: null,
129131
})
130132
}
131133

@@ -149,7 +151,7 @@ export class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
149151
}
150152

151153
get description(): string {
152-
return "Converts ActionView tag helpers (tag.*, content_tag, link_to) to raw HTML elements"
154+
return "Converts ActionView tag helpers (tag.*, content_tag, link_to, turbo_frame_tag) to raw HTML elements"
153155
}
154156

155157
rewrite<T extends Node>(node: T, _context: RewriteContext): T {

javascript/packages/rewriter/src/built-ins/html-to-action-view-tag-helper.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ function dashToUnderscore(string: string): string {
3434
interface SerializedAttributes {
3535
attributes: string
3636
href: string | null
37+
id: string | null
3738
}
3839

39-
function serializeAttributes(children: Node[], extractHref = false): SerializedAttributes {
40+
function serializeAttributes(children: Node[], options: { extractHref?: boolean, extractId?: boolean } = {}): SerializedAttributes {
4041
const regular: string[] = []
4142
const prefixed: Map<string, string[]> = new Map()
4243

4344
let href: string | null = null
45+
let id: string | null = null
4446

4547
for (const child of children) {
4648
if (!isHTMLAttributeNode(child)) continue
@@ -50,11 +52,16 @@ function serializeAttributes(children: Node[], extractHref = false): SerializedA
5052

5153
const value = child.value ? serializeAttributeValue(child.value) : "true"
5254

53-
if (extractHref && name === "href") {
55+
if (options.extractHref && name === "href") {
5456
href = value
5557
continue
5658
}
5759

60+
if (options.extractId && name === "id") {
61+
id = value
62+
continue
63+
}
64+
5865
const dataMatch = name.match(/^(data|aria)-(.+)$/)
5966

6067
if (dataMatch) {
@@ -76,7 +83,7 @@ function serializeAttributes(children: Node[], extractHref = false): SerializedA
7683
parts.push(`${prefix}: { ${entries.join(", ")} }`)
7784
}
7885

79-
return { attributes: parts.join(", "), href }
86+
return { attributes: parts.join(", "), href, id }
8087
}
8188

8289
function isTextOnlyBody(body: Node[]): boolean {
@@ -108,8 +115,9 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
108115
}
109116

110117
const isAnchor = tagName.value === "a"
118+
const isTurboFrame = tagName.value === "turbo-frame"
111119
const attributes = openTag.children.filter(child => !isWhitespaceNode(child))
112-
const { attributes: attributesString, href } = serializeAttributes(attributes, isAnchor)
120+
const { attributes: attributesString, href, id } = serializeAttributes(attributes, { extractHref: isAnchor, extractId: isTurboFrame })
113121
const hasBody = node.body && node.body.length > 0 && !node.is_void
114122
const isInlineContent = hasBody && isTextOnlyBody(node.body)
115123

@@ -119,6 +127,9 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
119127
if (isAnchor) {
120128
content = this.buildLinkToContent(node, attributesString, href, isInlineContent)
121129
elementSource = "ActionView::Helpers::UrlHelper#link_to"
130+
} else if (isTurboFrame) {
131+
content = this.buildTurboFrameTagContent(node, attributesString, id, isInlineContent)
132+
elementSource = "Turbo::FramesHelper#turbo_frame_tag"
122133
} else {
123134
content = this.buildTagContent(tagName.value, node, attributesString, isInlineContent)
124135
elementSource = "ActionView::Helpers::TagHelper#tag"
@@ -131,16 +142,18 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
131142
tag_opening: createSyntheticToken("<%="),
132143
content: createSyntheticToken(content),
133144
tag_closing: createSyntheticToken("%>"),
134-
tag_name: createSyntheticToken(isAnchor ? "a" : tagName.value),
145+
tag_name: createSyntheticToken(tagName.value),
135146
children: [],
136147
})
137148

138149
asMutable(node).open_tag = erbOpenTag
139150
asMutable(node).element_source = elementSource
140151

152+
const isInlineForm = isInlineContent || (isTurboFrame && !hasBody)
153+
141154
if (node.is_void) {
142155
asMutable(node).close_tag = null
143-
} else if (isInlineContent) {
156+
} else if (isInlineForm) {
144157
asMutable(node).body = []
145158

146159
const virtualClose = new HTMLVirtualCloseTagNode({
@@ -185,6 +198,30 @@ class HTMLToActionViewTagHelperVisitor extends Visitor {
185198
: ` tag.${tag} do `
186199
}
187200

201+
private buildTurboFrameTagContent(node: HTMLElementNode, attributes: string, id: string | null, isInlineContent: boolean): string {
202+
const args: string[] = []
203+
204+
if (id) {
205+
args.push(id)
206+
}
207+
208+
if (isInlineContent && isHTMLTextNode(node.body[0])) {
209+
args.push(`"${node.body[0].content}"`)
210+
}
211+
212+
if (attributes) {
213+
args.push(attributes)
214+
}
215+
216+
const argString = args.join(", ")
217+
218+
if (isInlineContent || !node.body || node.body.length === 0) {
219+
return argString ? ` turbo_frame_tag ${argString} ` : ` turbo_frame_tag `
220+
}
221+
222+
return argString ? ` turbo_frame_tag ${argString} do ` : ` turbo_frame_tag do `
223+
}
224+
188225
private buildLinkToContent(node: HTMLElementNode, attribute: string, href: string | null, isInlineContent: boolean): string {
189226
const args: string[] = []
190227

@@ -216,7 +253,7 @@ export class HTMLToActionViewTagHelperRewriter extends ASTRewriter {
216253
}
217254

218255
get description(): string {
219-
return "Converts raw HTML elements to ActionView tag helpers (tag.*)"
256+
return "Converts raw HTML elements to ActionView tag helpers (tag.*, turbo_frame_tag)"
220257
}
221258

222259
rewrite<T extends Node>(node: T, _context: RewriteContext): T {

javascript/packages/rewriter/test/action-view-tag-helper-to-html.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,112 @@ describe("ActionViewTagHelperToHTMLRewriter", () => {
196196
})
197197
})
198198

199+
describe("turbo_frame_tag helpers", () => {
200+
test("turbo_frame_tag with block", () => {
201+
const input = dedent`
202+
<%= turbo_frame_tag "tray" do %>
203+
Content
204+
<% end %>
205+
`
206+
207+
const expected = dedent`
208+
<turbo-frame id="tray">
209+
Content
210+
</turbo-frame>
211+
`
212+
213+
expect(transform(input)).toBe(expected)
214+
})
215+
216+
test("turbo_frame_tag without block", () => {
217+
expect(transform('<%= turbo_frame_tag "tray" %>')).toBe(
218+
'<turbo-frame id="tray"></turbo-frame>'
219+
)
220+
})
221+
222+
test("turbo_frame_tag with src attribute", () => {
223+
expect(transform('<%= turbo_frame_tag "tray", src: tray_path(tray) %>')).toBe(
224+
'<turbo-frame id="tray" src="<%= tray_path(tray) %>"></turbo-frame>'
225+
)
226+
})
227+
228+
test("turbo_frame_tag with src and target attributes", () => {
229+
expect(transform('<%= turbo_frame_tag "tray", src: tray_path(tray), target: "_top" %>')).toBe(
230+
'<turbo-frame id="tray" src="<%= tray_path(tray) %>" target="_top"></turbo-frame>'
231+
)
232+
})
233+
234+
test("turbo_frame_tag with loading lazy", () => {
235+
expect(transform('<%= turbo_frame_tag "tray", src: tray_path(tray), loading: "lazy" %>')).toBe(
236+
'<turbo-frame id="tray" src="<%= tray_path(tray) %>" loading="lazy"></turbo-frame>'
237+
)
238+
})
239+
240+
test("turbo_frame_tag with class attribute and block", () => {
241+
const input = dedent`
242+
<%= turbo_frame_tag "tray", class: "frame" do %>
243+
Content
244+
<% end %>
245+
`
246+
247+
const expected = dedent`
248+
<turbo-frame id="tray" class="frame">
249+
Content
250+
</turbo-frame>
251+
`
252+
253+
expect(transform(input)).toBe(expected)
254+
})
255+
256+
test("turbo_frame_tag with variable id", () => {
257+
const input = dedent`
258+
<%= turbo_frame_tag dom_id(post) do %>
259+
Content
260+
<% end %>
261+
`
262+
263+
const expected = dedent`
264+
<turbo-frame id="<%= dom_id(post) %>">
265+
Content
266+
</turbo-frame>
267+
`
268+
269+
expect(transform(input)).toBe(expected)
270+
})
271+
272+
test("turbo_frame_tag with data attributes", () => {
273+
const input = dedent`
274+
<%= turbo_frame_tag "tray", data: { controller: "frame" } do %>
275+
Content
276+
<% end %>
277+
`
278+
279+
const expected = dedent`
280+
<turbo-frame id="tray" data-controller="frame">
281+
Content
282+
</turbo-frame>
283+
`
284+
285+
expect(transform(input)).toBe(expected)
286+
})
287+
288+
test("turbo_frame_tag with splat attributes", () => {
289+
const input = dedent`
290+
<%= turbo_frame_tag "tray", **attributes do %>
291+
Content
292+
<% end %>
293+
`
294+
295+
const expected = dedent`
296+
<turbo-frame id="tray" <%= **attributes %>>
297+
Content
298+
</turbo-frame>
299+
`
300+
301+
expect(transform(input)).toBe(expected)
302+
})
303+
})
304+
199305
describe("non-ActionView elements", () => {
200306
test("regular HTML elements are not modified", () => {
201307
expect(transform('<div class="content">Hello</div>')).toBe(

javascript/packages/rewriter/test/html-to-action-view-tag-helper.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,102 @@ describe("HTMLToActionViewTagHelperRewriter", () => {
255255
})
256256
})
257257

258+
describe("turbo_frame_tag for turbo-frame elements", () => {
259+
test("turbo-frame with id and body", () => {
260+
const input = dedent`
261+
<turbo-frame id="tray">
262+
Content
263+
</turbo-frame>
264+
`
265+
266+
const expected = dedent`
267+
<%= turbo_frame_tag "tray" do %>
268+
Content
269+
<% end %>
270+
`
271+
272+
expect(transform(input)).toBe(expected)
273+
})
274+
275+
test("turbo-frame with id only", () => {
276+
expect(transform('<turbo-frame id="tray"></turbo-frame>')).toBe(
277+
'<%= turbo_frame_tag "tray" %>'
278+
)
279+
})
280+
281+
test("turbo-frame with id and src", () => {
282+
expect(transform('<turbo-frame id="tray" src="/trays/1"></turbo-frame>')).toBe(
283+
'<%= turbo_frame_tag "tray", src: "/trays/1" %>'
284+
)
285+
})
286+
287+
test("turbo-frame with id, src and target", () => {
288+
expect(transform('<turbo-frame id="tray" src="/trays/1" target="_top"></turbo-frame>')).toBe(
289+
'<%= turbo_frame_tag "tray", src: "/trays/1", target: "_top" %>'
290+
)
291+
})
292+
293+
test("turbo-frame with id and class", () => {
294+
const input = dedent`
295+
<turbo-frame id="tray" class="frame">
296+
Content
297+
</turbo-frame>
298+
`
299+
300+
const expected = dedent`
301+
<%= turbo_frame_tag "tray", class: "frame" do %>
302+
Content
303+
<% end %>
304+
`
305+
306+
expect(transform(input)).toBe(expected)
307+
})
308+
309+
test("turbo-frame with id and data attributes", () => {
310+
expect(transform('<turbo-frame id="tray" data-controller="frame"></turbo-frame>')).toBe(
311+
'<%= turbo_frame_tag "tray", data: { controller: "frame" } %>'
312+
)
313+
})
314+
315+
test("turbo-frame with ERB id", () => {
316+
const input = dedent`
317+
<turbo-frame id="<%= dom_id(post) %>">
318+
Content
319+
</turbo-frame>
320+
`
321+
322+
const expected = dedent`
323+
<%= turbo_frame_tag dom_id(post) do %>
324+
Content
325+
<% end %>
326+
`
327+
328+
expect(transform(input)).toBe(expected)
329+
})
330+
331+
test("turbo-frame with loading lazy", () => {
332+
expect(transform('<turbo-frame id="tray" src="/trays/1" loading="lazy"></turbo-frame>')).toBe(
333+
'<%= turbo_frame_tag "tray", src: "/trays/1", loading: "lazy" %>'
334+
)
335+
})
336+
337+
test("turbo-frame without id", () => {
338+
const input = dedent`
339+
<turbo-frame>
340+
Content
341+
</turbo-frame>
342+
`
343+
344+
const expected = dedent`
345+
<%= turbo_frame_tag do %>
346+
Content
347+
<% end %>
348+
`
349+
350+
expect(transform(input)).toBe(expected)
351+
})
352+
})
353+
258354
describe("ERB in attribute values", () => {
259355
test("single ERB expression becomes Ruby variable", () => {
260356
expect(transform('<div class="<%= class_name %>">Content</div>')).toBe(

0 commit comments

Comments
 (0)