Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Visitor, Location, HTMLOpenTagNode, HTMLCloseTagNode, HTMLElementNode, HTMLAttributeValueNode, WhitespaceNode, ERBContentNode } from "@herb-tools/core"
import { isHTMLAttributeNode, isERBOpenTagNode, isRubyLiteralNode, isRubyHTMLAttributesSplatNode, createSyntheticToken } from "@herb-tools/core"
import { isHTMLAttributeNode, isERBOpenTagNode, isRubyLiteralNode, isRubyHTMLAttributesSplatNode, isWhitespaceNode, createSyntheticToken } from "@herb-tools/core"

import { ASTRewriter } from "../ast-rewriter.js"
import { asMutable } from "../mutable.js"
Expand All @@ -26,6 +26,36 @@ class ActionViewTagHelperToHTMLVisitor extends Visitor {
this.includeBody = options.includeBody ?? true
}

visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
const newChildren: Node[] = []

for (let index = 0; index < node.children.length; index++) {
const child = node.children[index]

if (isHTMLAttributeNode(child)) {
if (child.equals && child.equals.value !== "=") {
asMutable(child).equals = createSyntheticToken("=")
}

if (child.value) {
this.transformAttributeValue(child.value)
}

const previous = index > 0 ? node.children[index - 1] : null

if (!previous || !isWhitespaceNode(previous)) {
newChildren.push(createWhitespaceNode())
}
}

newChildren.push(child)
}

asMutable(node).children = newChildren

this.visitChildNodes(node)
}

visitHTMLElementNode(node: HTMLElementNode): void {
if (!node.element_source) {
this.visitChildNodes(node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,32 @@ describe("ActionViewTagHelperToHTMLRewriter", () => {
})
})

describe("tag.attributes", () => {
test("tag.attributes extracts attributes into parent element", () => {
expect(transform('<input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>')).toBe(
'<input type="text" aria-label="Search">'
)
})

test("tag.attributes with attributes after", () => {
expect(transform('<button <%= tag.attributes(id: "cta", aria: { expanded: false }) %> class="primary">Click</button>')).toBe(
'<button id="cta" aria-expanded="false" class="primary">Click</button>'
)
})

test("tag.attributes with attributes before", () => {
expect(transform('<button class="primary" <%= tag.attributes(id: "cta") %>>Click</button>')).toBe(
'<button class="primary" id="cta">Click</button>'
)
})

test("tag.attributes with data hash", () => {
expect(transform('<div <%= tag.attributes(data: { controller: "hello" }) %>></div>')).toBe(
'<div data-controller="hello"></div>'
)
})
})

describe("non-ActionView elements", () => {
test("regular HTML elements are not modified", () => {
expect(transform('<div class="content">Hello</div>')).toBe(
Expand Down
102 changes: 102 additions & 0 deletions src/analyze/action_view/tag_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,41 @@ void transform_tag_helper_array(hb_array_T* array, analyze_ruby_context_T* conte
}
} else if (strcmp(parse_context->matched_handler->name, "link_to") == 0) {
replacement = transform_link_to_helper(erb_node, context, parse_context);
} else if (string_equals(parse_context->matched_handler->name, "tag") && parse_context->info->tag_name
&& string_equals(parse_context->info->tag_name, "attributes")) {
hb_array_T* attributes = NULL;

if (parse_context->info->call_node) {
attributes = extract_html_attributes_from_call_node(
parse_context->info->call_node,
parse_context->prism_source,
parse_context->original_source,
parse_context->erb_content_offset,
context->allocator
);
}

if (attributes && hb_array_size(attributes) > 0) {
size_t old_size = hb_array_size(array);
size_t attributes_size = hb_array_size(attributes);
hb_array_T* new_array = hb_array_init(old_size - 1 + attributes_size, context->allocator);

for (size_t j = 0; j < old_size; j++) {
if (j == i) {
for (size_t k = 0; k < attributes_size; k++) {
hb_array_append(new_array, hb_array_get(attributes, k));
}
} else {
hb_array_append(new_array, hb_array_get(array, j));
}
}

array->items = new_array->items;
array->size = new_array->size;
array->capacity = new_array->capacity;

i += attributes_size - 1;
}
} else {
replacement = transform_tag_helper_with_attributes(erb_node, context, parse_context);
}
Expand All @@ -1210,6 +1245,73 @@ void transform_tag_helper_array(hb_array_T* array, analyze_ruby_context_T* conte

free(erb_string);
}
} else if (child->type == AST_HTML_ATTRIBUTE_NODE) {
AST_HTML_ATTRIBUTE_NODE_T* attribute_node = (AST_HTML_ATTRIBUTE_NODE_T*) child;

if (attribute_node->name && !attribute_node->equals && !attribute_node->value && attribute_node->name->children
&& hb_array_size(attribute_node->name->children) == 1) {
AST_NODE_T* name_child = hb_array_get(attribute_node->name->children, 0);

if (name_child && name_child->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* erb_node = (AST_ERB_CONTENT_NODE_T*) name_child;
token_T* erb_content = erb_node->content;

if (erb_content && !hb_string_is_empty(erb_content->value)) {
char* erb_string = hb_string_to_c_string_using_malloc(erb_content->value);
size_t erb_content_offset = 0;

if (context->source) {
erb_content_offset = calculate_byte_offset_from_position(context->source, erb_content->location.start);
}

tag_helper_parse_context_T* parse_context =
parse_tag_helper_content(erb_string, context->source, erb_content_offset, context->allocator);

if (parse_context && string_equals(parse_context->matched_handler->name, "tag")
&& parse_context->info->tag_name && string_equals(parse_context->info->tag_name, "attributes")) {
hb_array_T* attributes = NULL;

if (parse_context->info->call_node) {
attributes = extract_html_attributes_from_call_node(
parse_context->info->call_node,
parse_context->prism_source,
parse_context->original_source,
parse_context->erb_content_offset,
context->allocator
);
}

if (attributes && hb_array_size(attributes) > 0) {
size_t old_size = hb_array_size(array);
size_t attributes_size = hb_array_size(attributes);
hb_array_T* new_array = hb_array_init(old_size - 1 + attributes_size, context->allocator);

for (size_t j = 0; j < old_size; j++) {
if (j == i) {
for (size_t k = 0; k < attributes_size; k++) {
hb_array_append(new_array, hb_array_get(attributes, k));
}
} else {
hb_array_append(new_array, hb_array_get(array, j));
}
}

array->items = new_array->items;
array->size = new_array->size;
array->capacity = new_array->capacity;

i += attributes_size - 1;
}

free_tag_helper_parse_context(parse_context);
} else if (parse_context) {
free_tag_helper_parse_context(parse_context);
}

free(erb_string);
}
}
}
}

if (replacement) {
Expand Down
26 changes: 26 additions & 0 deletions test/analyze/action_view/tag_helper/tag_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,31 @@ class TagTest < Minitest::Spec
<%= tag.img "/image.png", data: { controller: "image" } %>
HTML
end

test "tag.attributes inside HTML open tag extracts attributes" do
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
<input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
HTML
end

test "tag.attributes with mixed HTML attributes and disabled false" do
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
<button <%= tag.attributes(id: "call-to-action", disabled: false, aria: { expanded: false }) %> class="primary">Get Started!</button>
HTML
end

test "tag.attributes with attribute before" do
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
<button class="primary" <%= tag.attributes(id: "call-to-action", disabled: false, aria: { expanded: false }) %>>Get Started!</button>
HTML
end

test "tag.attributes with attribute before and after" do
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
<button class="primary" <%= tag.attributes(id: "call-to-action", disabled: false, aria: { expanded: false }) %> data-controller="hello">
Get Started!
</button>
HTML
end
end
end

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading