Skip to content

Commit d626fef

Browse files
committed
Parser: Detect image_tag Action View helper
1 parent 4856642 commit d626fef

File tree

7 files changed

+184
-7
lines changed

7 files changed

+184
-7
lines changed

javascript/packages/language-server/src/action_view_helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ export const ACTION_VIEW_HELPERS: Record<string, ActionViewHelperInfo> = {
2828
signature: "javascript_include_tag(*sources)",
2929
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-javascript_include_tag",
3030
},
31+
"ActionView::Helpers::AssetTagHelper#image_tag": {
32+
signature: "image_tag(source, options = {})",
33+
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-image_tag",
34+
},
3135
}

javascript/packages/language-server/src/rewrite_code_action_service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,17 @@ export class RewriteCodeActionService {
137137
const tagName = element.node.tag_name?.value
138138
const isAnchor = tagName === "a"
139139
const isTurboFrame = tagName === "turbo-frame"
140+
const isImg = tagName === "img"
140141
const methodName = tagName?.replace(/-/g, "_")
141142
const title = isAnchor
142143
? "Herb: Convert to `link_to`"
143144
: isTurboFrame
144145
? "Herb: Convert to `turbo_frame_tag`"
145-
: methodName
146-
? `Herb: Convert to \`tag.${methodName}\``
147-
: "Herb: Convert to tag helper"
146+
: isImg
147+
? "Herb: Convert to `image_tag`"
148+
: methodName
149+
? `Herb: Convert to \`tag.${methodName}\``
150+
: "Herb: Convert to tag helper"
148151

149152
return {
150153
title,

javascript/packages/node/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"./extension/libherb/analyze/transform.c",
2727
"./extension/libherb/analyze/action_view/attribute_extraction_helpers.c",
2828
"./extension/libherb/analyze/action_view/content_tag.c",
29+
"./extension/libherb/analyze/action_view/image_tag.c",
2930
"./extension/libherb/analyze/action_view/javascript_include_tag.c",
3031
"./extension/libherb/analyze/action_view/javascript_tag.c",
3132
"./extension/libherb/analyze/action_view/link_to.c",

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe("HTMLToActionViewTagHelperRewriter", () => {
184184

185185
test("img with attributes", () => {
186186
expect(transform('<img src="image.png" alt="Photo">')).toBe(
187-
'<%= tag.img src: "image.png", alt: "Photo" %>'
187+
'<%= image_tag "image.png", alt: "Photo" %>'
188188
)
189189
})
190190
})
@@ -425,6 +425,44 @@ describe("HTMLToActionViewTagHelperRewriter", () => {
425425
})
426426
})
427427

428+
describe("image_tag for img elements", () => {
429+
test("img with src attribute", () => {
430+
expect(transform('<img src="icon.png">')).toBe(
431+
'<%= image_tag "icon.png" %>'
432+
)
433+
})
434+
435+
test("img with src and alt", () => {
436+
expect(transform('<img src="icon.png" alt="Icon">')).toBe(
437+
'<%= image_tag "icon.png", alt: "Icon" %>'
438+
)
439+
})
440+
441+
test("img with src, alt and class", () => {
442+
expect(transform('<img src="photo.jpg" alt="Photo" class="avatar">')).toBe(
443+
'<%= image_tag "photo.jpg", alt: "Photo", class: "avatar" %>'
444+
)
445+
})
446+
447+
test("img with src and data attributes", () => {
448+
expect(transform('<img src="icon.png" data-controller="image">')).toBe(
449+
'<%= image_tag "icon.png", data: { controller: "image" } %>'
450+
)
451+
})
452+
453+
test("img self-closing", () => {
454+
expect(transform('<img src="icon.png" />')).toBe(
455+
'<%= image_tag "icon.png" %>'
456+
)
457+
})
458+
459+
test("img without src", () => {
460+
expect(transform('<img alt="Photo">')).toBe(
461+
'<%= image_tag alt: "Photo" %>'
462+
)
463+
})
464+
})
465+
428466
describe("ERB in attribute values", () => {
429467
test("single ERB expression becomes Ruby variable", () => {
430468
expect(transform('<div class="<%= class_name %>">Content</div>')).toBe(
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <prism.h>
2+
#include <stdbool.h>
3+
#include <stdlib.h>
4+
#include <string.h>
5+
6+
#include "../../include/analyze/action_view/tag_helper_handler.h"
7+
8+
bool detect_image_tag(pm_call_node_t* call_node, pm_parser_t* parser) {
9+
if (!call_node || !call_node->name) { return false; }
10+
11+
pm_constant_t* constant = pm_constant_pool_id_to_constant(&parser->constant_pool, call_node->name);
12+
return constant && constant->length == 9 && strncmp((const char*) constant->start, "image_tag", 9) == 0;
13+
}
14+
15+
char* extract_image_tag_name(pm_call_node_t* _call_node, pm_parser_t* _parser, hb_allocator_T* allocator) {
16+
return hb_allocator_strdup(allocator, "img");
17+
}
18+
19+
char* extract_image_tag_content(pm_call_node_t* _call_node, pm_parser_t* _parser, hb_allocator_T* _allocator) {
20+
return NULL;
21+
}
22+
23+
char* extract_image_tag_src(pm_call_node_t* call_node, pm_parser_t* _parser, hb_allocator_T* allocator) {
24+
if (!call_node || !call_node->arguments) { return NULL; }
25+
26+
pm_arguments_node_t* arguments = call_node->arguments;
27+
if (!arguments->arguments.size) { return NULL; }
28+
29+
pm_node_t* first_argument = arguments->arguments.nodes[0];
30+
31+
if (first_argument->type == PM_STRING_NODE) {
32+
pm_string_node_t* string_node = (pm_string_node_t*) first_argument;
33+
size_t length = pm_string_length(&string_node->unescaped);
34+
35+
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
36+
}
37+
38+
size_t source_length = first_argument->location.end - first_argument->location.start;
39+
return hb_allocator_strndup(allocator, (const char*) first_argument->location.start, source_length);
40+
}
41+
42+
bool image_tag_source_is_url(const char* source, size_t length) {
43+
if (!source || length == 0) { return false; }
44+
45+
if (length >= 2 && source[0] == '/' && source[1] == '/') { return true; }
46+
if (strstr(source, "://") != NULL) { return true; }
47+
48+
return false;
49+
}
50+
51+
char* wrap_in_image_path(const char* source, size_t source_length, hb_allocator_T* allocator) {
52+
const char* prefix = "image_path(";
53+
const char* suffix = ")";
54+
55+
size_t prefix_length = strlen(prefix);
56+
size_t suffix_length = strlen(suffix);
57+
size_t total_length = prefix_length + source_length + suffix_length;
58+
char* result = hb_allocator_alloc(allocator, total_length + 1);
59+
60+
memcpy(result, prefix, prefix_length);
61+
memcpy(result + prefix_length, source, source_length);
62+
memcpy(result + prefix_length + source_length, suffix, suffix_length);
63+
64+
result[total_length] = '\0';
65+
66+
return result;
67+
}
68+
69+
bool image_tag_supports_block(void) {
70+
return false;
71+
}
72+
73+
const tag_helper_handler_T image_tag_handler = { .name = "image_tag",
74+
.source =
75+
HB_STRING_LITERAL("ActionView::Helpers::AssetTagHelper#image_tag"),
76+
.detect = detect_image_tag,
77+
.extract_tag_name = extract_image_tag_name,
78+
.extract_content = extract_image_tag_content,
79+
.supports_block = image_tag_supports_block };

src/analyze/action_view/registry.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ extern const tag_helper_handler_T link_to_handler;
1010
extern const tag_helper_handler_T turbo_frame_tag_handler;
1111
extern const tag_helper_handler_T javascript_tag_handler;
1212
extern const tag_helper_handler_T javascript_include_tag_handler;
13+
extern const tag_helper_handler_T image_tag_handler;
1314

14-
static size_t handlers_count = 6;
15+
static size_t handlers_count = 7;
1516

1617
tag_helper_info_T* tag_helper_info_init(hb_allocator_T* allocator) {
1718
tag_helper_info_T* info = hb_allocator_alloc(allocator, sizeof(tag_helper_info_T));
@@ -43,7 +44,7 @@ void tag_helper_info_free(tag_helper_info_T** info) {
4344
}
4445

4546
tag_helper_handler_T* get_tag_helper_handlers(void) {
46-
static tag_helper_handler_T static_handlers[6];
47+
static tag_helper_handler_T static_handlers[7];
4748
static bool initialized = false;
4849

4950
if (!initialized) {
@@ -53,6 +54,8 @@ tag_helper_handler_T* get_tag_helper_handlers(void) {
5354
static_handlers[3] = turbo_frame_tag_handler;
5455
static_handlers[4] = javascript_tag_handler;
5556
static_handlers[5] = javascript_include_tag_handler;
57+
static_handlers[6] = image_tag_handler;
58+
5659
initialized = true;
5760
}
5861

src/analyze/action_view/tag_helpers.c

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ extern bool detect_javascript_include_tag(pm_call_node_t*, pm_parser_t*);
2929
extern char* extract_javascript_include_tag_src(pm_call_node_t*, pm_parser_t*, hb_allocator_T*);
3030
extern char* wrap_in_javascript_path(const char*, size_t, hb_allocator_T*);
3131
extern bool javascript_include_tag_source_is_url(const char*, size_t);
32+
extern bool detect_image_tag(pm_call_node_t*, pm_parser_t*);
33+
extern char* extract_image_tag_src(pm_call_node_t*, pm_parser_t*, hb_allocator_T*);
34+
extern char* wrap_in_image_path(const char*, size_t, hb_allocator_T*);
35+
extern bool image_tag_source_is_url(const char*, size_t);
3236

3337
typedef struct {
3438
pm_parser_t parser;
@@ -360,6 +364,50 @@ static AST_NODE_T* transform_tag_helper_with_attributes(
360364
}
361365
}
362366

367+
if (detect_image_tag(parse_context->info->call_node, &parse_context->parser)
368+
&& parse_context->info->call_node->arguments && parse_context->info->call_node->arguments->arguments.size >= 1) {
369+
char* source_value = extract_image_tag_src(parse_context->info->call_node, &parse_context->parser, allocator);
370+
371+
if (source_value) {
372+
if (!attributes) { attributes = hb_array_init(4, allocator); }
373+
374+
pm_node_t* first_argument = parse_context->info->call_node->arguments->arguments.nodes[0];
375+
position_T source_start, source_end;
376+
prism_node_location_to_positions(&first_argument->location, parse_context, &source_start, &source_end);
377+
bool source_is_string = (first_argument->type == PM_STRING_NODE);
378+
bool source_is_path_helper = is_route_helper_node(first_argument, &parse_context->parser);
379+
380+
size_t source_length = strlen(source_value);
381+
bool is_url = image_tag_source_is_url(source_value, source_length);
382+
383+
char* source_attribute_value = source_value;
384+
385+
if (source_is_string && !is_url) {
386+
size_t quoted_length = source_length + 2;
387+
char* quoted_source = hb_allocator_alloc(allocator, quoted_length + 1);
388+
quoted_source[0] = '"';
389+
memcpy(quoted_source + 1, source_value, source_length);
390+
quoted_source[quoted_length - 1] = '"';
391+
quoted_source[quoted_length] = '\0';
392+
393+
source_attribute_value = wrap_in_image_path(quoted_source, quoted_length, allocator);
394+
hb_allocator_dealloc(allocator, quoted_source);
395+
} else if (!source_is_string && !is_url && !source_is_path_helper) {
396+
source_attribute_value = wrap_in_image_path(source_value, source_length, allocator);
397+
}
398+
399+
AST_HTML_ATTRIBUTE_NODE_T* source_attribute =
400+
is_url
401+
? create_html_attribute_node("src", source_attribute_value, source_start, source_end, allocator)
402+
: create_html_attribute_with_ruby_literal("src", source_attribute_value, source_start, source_end, allocator);
403+
404+
if (source_attribute) { attributes = prepend_attribute(attributes, (AST_NODE_T*) source_attribute, allocator); }
405+
if (source_attribute_value != source_value) { hb_allocator_dealloc(allocator, source_attribute_value); }
406+
407+
hb_allocator_dealloc(allocator, source_value);
408+
}
409+
}
410+
363411
token_T* tag_name_token =
364412
tag_name ? create_synthetic_token(allocator, tag_name, TOKEN_IDENTIFIER, tag_name_start, tag_name_end) : NULL;
365413

@@ -378,7 +426,8 @@ static AST_NODE_T* transform_tag_helper_with_attributes(
378426
);
379427

380428
hb_array_T* body = hb_array_init(1, allocator);
381-
bool is_void = tag_name && (strcmp(handler->name, "tag") == 0) && is_void_element(hb_string_from_c_string(tag_name));
429+
bool is_void = tag_name && ((strcmp(handler->name, "tag") == 0) || (strcmp(handler->name, "image_tag") == 0))
430+
&& is_void_element(hb_string_from_c_string(tag_name));
382431

383432
if (helper_content) {
384433
append_body_content_node(

0 commit comments

Comments
 (0)