fix: properly handle mixin call attributes in compiled templates

- Create typed attributes struct for each mixin call with optional fields (class, id, style)
- Use unique variable names (mixin_attrs_N) to avoid shadowing in nested mixin calls
- Track current attributes variable for buildAccessor to resolve attributes.class correctly
- Only suppress unused variable warning when attributes aren't actually accessed
This commit is contained in:
2026-01-23 12:02:04 +05:30
parent a5192e9323
commit efaaa5565d
2 changed files with 131 additions and 2 deletions

View File

@@ -77,6 +77,12 @@ pub fn index(a: Allocator, d: anytype) Allocator.Error![]u8 {
{ {
const text = "click me "; const text = "click me ";
const @"type" = "secondary"; const @"type" = "secondary";
const mixin_attrs_1: struct {
class: []const u8 = "",
id: []const u8 = "",
style: []const u8 = "",
} = .{
};
try o.appendSlice(a, "<button"); try o.appendSlice(a, "<button");
try o.appendSlice(a, " class=\""); try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "btn btn-"); try o.appendSlice(a, "btn btn-");
@@ -84,6 +90,7 @@ pub fn index(a: Allocator, d: anytype) Allocator.Error![]u8 {
try o.appendSlice(a, "\""); try o.appendSlice(a, "\"");
try o.appendSlice(a, ">"); try o.appendSlice(a, ">");
try esc(&o, a, strVal(text)); try esc(&o, a, strVal(text));
_ = mixin_attrs_1;
try o.appendSlice(a, "</button>"); try o.appendSlice(a, "</button>");
} }
try o.appendSlice(a, "</body><br /><a href=\"//google.com\" target=\"_blank\">Google 1</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 2</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 3</a></html>"); try o.appendSlice(a, "</body><br /><a href=\"//google.com\" target=\"_blank\">Google 1</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 2</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 3</a></html>");
@@ -160,6 +167,12 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
const name = "firstName"; const name = "firstName";
const label = "First Name"; const label = "First Name";
const placeholder = "first name"; const placeholder = "first name";
const mixin_attrs_1: struct {
class: []const u8 = "",
id: []const u8 = "",
style: []const u8 = "",
} = .{
};
try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">"); try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">");
try esc(&o, a, strVal(label)); try esc(&o, a, strVal(label));
try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\""); try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\"");
@@ -169,6 +182,7 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
try o.appendSlice(a, " placeholder=\""); try o.appendSlice(a, " placeholder=\"");
try o.appendSlice(a, strVal(placeholder)); try o.appendSlice(a, strVal(placeholder));
try o.appendSlice(a, "\""); try o.appendSlice(a, "\"");
_ = mixin_attrs_1;
try o.appendSlice(a, " /></fieldset>"); try o.appendSlice(a, " /></fieldset>");
} }
try o.appendSlice(a, "<br />"); try o.appendSlice(a, "<br />");
@@ -176,6 +190,12 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
const name = "lastName"; const name = "lastName";
const label = "Last Name"; const label = "Last Name";
const placeholder = "last name"; const placeholder = "last name";
const mixin_attrs_1: struct {
class: []const u8 = "",
id: []const u8 = "",
style: []const u8 = "",
} = .{
};
try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">"); try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">");
try esc(&o, a, strVal(label)); try esc(&o, a, strVal(label));
try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\""); try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\"");
@@ -185,22 +205,37 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
try o.appendSlice(a, " placeholder=\""); try o.appendSlice(a, " placeholder=\"");
try o.appendSlice(a, strVal(placeholder)); try o.appendSlice(a, strVal(placeholder));
try o.appendSlice(a, "\""); try o.appendSlice(a, "\"");
_ = mixin_attrs_1;
try o.appendSlice(a, " /></fieldset>"); try o.appendSlice(a, " /></fieldset>");
} }
try o.appendSlice(a, "<submit>sumit</submit>"); try o.appendSlice(a, "<submit>sumit</submit>");
if (truthy(@field(d, "error"))) { if (truthy(@field(d, "error"))) {
{ {
const message = @field(d, "error"); const message = @field(d, "error");
const mixin_attrs_1: struct {
class: []const u8 = "",
id: []const u8 = "",
style: []const u8 = "",
} = .{
};
{ {
const mixin_attrs_2: struct {
class: []const u8 = "",
id: []const u8 = "",
style: []const u8 = "",
} = .{
.class = "alert-error",
};
try o.appendSlice(a, "<div"); try o.appendSlice(a, "<div");
try o.appendSlice(a, " class=\""); try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "alert "); try o.appendSlice(a, "alert ");
try o.appendSlice(a, strVal(@field(d, "attributes").class)); try o.appendSlice(a, strVal(mixin_attrs_2.class));
try o.appendSlice(a, "\""); try o.appendSlice(a, "\"");
try o.appendSlice(a, " role=\"alert\"><svg class=\"h-6 w-6 shrink-0 stroke-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z\"></path></svg><span>"); try o.appendSlice(a, " role=\"alert\"><svg class=\"h-6 w-6 shrink-0 stroke-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z\"></path></svg><span>");
try esc(&o, a, strVal(message)); try esc(&o, a, strVal(message));
try o.appendSlice(a, "</span></div>"); try o.appendSlice(a, "</span></div>");
} }
_ = mixin_attrs_1;
} }
} }
try o.appendSlice(a, "</form><div id=\"footer\"><p>some footer content</p></div></body></html>"); try o.appendSlice(a, "</form><div id=\"footer\"><p>some footer content</p></div></body></html>");

View File

@@ -536,6 +536,9 @@ const Compiler = struct {
mixins: std.StringHashMap(ast.MixinDef), // Collected mixin definitions mixins: std.StringHashMap(ast.MixinDef), // Collected mixin definitions
blocks: std.StringHashMap(BlockDef), // Collected block definitions for inheritance blocks: std.StringHashMap(BlockDef), // Collected block definitions for inheritance
uses_data: bool, // Track whether the data parameter 'd' is actually used uses_data: bool, // Track whether the data parameter 'd' is actually used
mixin_depth: usize, // Track nesting depth for unique variable names
current_attrs_var: ?[]const u8, // Current mixin's attributes variable name
used_attrs_var: bool, // Track if current mixin's attributes were accessed
fn init( fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
@@ -555,6 +558,9 @@ const Compiler = struct {
.mixins = std.StringHashMap(ast.MixinDef).init(allocator), .mixins = std.StringHashMap(ast.MixinDef).init(allocator),
.blocks = std.StringHashMap(BlockDef).init(allocator), .blocks = std.StringHashMap(BlockDef).init(allocator),
.uses_data = false, .uses_data = false,
.mixin_depth = 0,
.current_attrs_var = null,
.used_attrs_var = false,
}; };
} }
@@ -1145,10 +1151,19 @@ const Compiler = struct {
} }
fn buildAccessor(self: *Compiler, expr: []const u8, buf: []u8) []const u8 { fn buildAccessor(self: *Compiler, expr: []const u8, buf: []u8) []const u8 {
// Handle nested field access like friend.name, subFriend.id // Handle nested field access like friend.name, subFriend.id, attributes.class
if (std.mem.indexOfScalar(u8, expr, '.')) |dot| { if (std.mem.indexOfScalar(u8, expr, '.')) |dot| {
const base = expr[0..dot]; const base = expr[0..dot];
const rest = expr[dot + 1 ..]; const rest = expr[dot + 1 ..];
// Special case: attributes.X should use current mixin's attributes variable
if (std.mem.eql(u8, base, "attributes")) {
if (self.current_attrs_var) |attrs_var| {
self.used_attrs_var = true;
return std.fmt.bufPrint(buf, "{s}.{s}", .{ attrs_var, rest }) catch expr;
}
}
// For loop variables or mixin params like friend.name, access directly // For loop variables or mixin params like friend.name, access directly
if (self.isLoopVar(base) or self.isMixinParam(base)) { if (self.isLoopVar(base) or self.isMixinParam(base)) {
// Escape base if it's a keyword - use the output buffer // Escape base if it's a keyword - use the output buffer
@@ -1161,6 +1176,14 @@ const Compiler = struct {
self.uses_data = true; self.uses_data = true;
return std.fmt.bufPrint(buf, "@field(d, \"{s}\").{s}", .{ base, rest }) catch expr; return std.fmt.bufPrint(buf, "@field(d, \"{s}\").{s}", .{ base, rest }) catch expr;
} else { } else {
// Special case: 'attributes' alone should use current mixin's attributes variable
if (std.mem.eql(u8, expr, "attributes")) {
if (self.current_attrs_var) |attrs_var| {
self.used_attrs_var = true;
return attrs_var;
}
}
// Check if it's a loop variable or mixin param // Check if it's a loop variable or mixin param
if (self.isLoopVar(expr) or self.isMixinParam(expr)) { if (self.isLoopVar(expr) or self.isMixinParam(expr)) {
// Escape if it's a keyword - use the output buffer // Escape if it's a keyword - use the output buffer
@@ -1505,6 +1528,66 @@ const Compiler = struct {
try self.writer.writeAll("};\n"); try self.writer.writeAll("};\n");
} }
// Handle mixin call attributes: +mixin(args)(class="foo", data-id="bar")
// Create an 'attributes' struct with optional fields that the mixin body can access
// Use unique name based on mixin depth to avoid shadowing in nested mixin calls
self.mixin_depth += 1;
const current_depth = self.mixin_depth;
// Save previous attrs var and restore after mixin body
const prev_attrs_var = self.current_attrs_var;
const prev_used_attrs = self.used_attrs_var;
self.used_attrs_var = false;
// Generate unique attribute variable name for this mixin depth
var attr_var_buf: [32]u8 = undefined;
const attr_var_name = std.fmt.bufPrint(&attr_var_buf, "mixin_attrs_{d}", .{current_depth}) catch "mixin_attrs";
// Set current attrs var for buildAccessor to use
self.current_attrs_var = attr_var_name;
try self.mixin_params.append(self.allocator, attr_var_name);
try self.writeIndent();
try self.writer.print("const {s}: struct {{\n", .{attr_var_name});
self.depth += 1;
// Define fields as optional with defaults
try self.writeIndent();
try self.writer.writeAll("class: []const u8 = \"\",\n");
try self.writeIndent();
try self.writer.writeAll("id: []const u8 = \"\",\n");
try self.writeIndent();
try self.writer.writeAll("style: []const u8 = \"\",\n");
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("} = .{\n");
self.depth += 1;
for (call.attributes) |attr| {
// Only emit known attributes (class, id, style for now)
if (std.mem.eql(u8, attr.name, "class") or
std.mem.eql(u8, attr.name, "id") or
std.mem.eql(u8, attr.name, "style"))
{
try self.writeIndent();
try self.writer.print(".{s} = ", .{attr.name});
if (attr.value) |val| {
// Check if it's a string literal
if (val.len >= 2 and (val[0] == '"' or val[0] == '\'')) {
try self.writer.print("{s},\n", .{val});
} else {
// It's a variable reference
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(val, &accessor_buf);
try self.writer.print("{s},\n", .{accessor});
}
} else {
try self.writer.writeAll("\"\",\n");
}
}
}
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("};\n");
// Emit mixin body // Emit mixin body
// Note: block content (call.block_children) is handled by mixin_block nodes // Note: block content (call.block_children) is handled by mixin_block nodes
// For now, we'll inline the mixin body directly // For now, we'll inline the mixin body directly
@@ -1519,11 +1602,22 @@ const Compiler = struct {
} }
} }
// Suppress unused variable warning if attributes wasn't used
if (!self.used_attrs_var) {
try self.writeIndent();
try self.writer.print("_ = {s};\n", .{attr_var_name});
}
// Close scope block // Close scope block
try self.flush(); try self.flush();
self.depth -= 1; self.depth -= 1;
try self.writeIndent(); try self.writeIndent();
try self.writer.writeAll("}\n"); try self.writer.writeAll("}\n");
// Restore previous state
self.current_attrs_var = prev_attrs_var;
self.used_attrs_var = prev_used_attrs;
self.mixin_depth -= 1;
} }
/// Try to load a mixin from the mixins directory /// Try to load a mixin from the mixins directory