From efaaa5565d359309ce40caa3dca2dd79739b41b8 Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Fri, 23 Jan 2026 12:02:04 +0530 Subject: [PATCH] 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 --- examples/demo/views/generated.zig | 37 +++++++++++- src/build_templates.zig | 96 ++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/examples/demo/views/generated.zig b/examples/demo/views/generated.zig index 8b8b6e9..1201f63 100644 --- a/examples/demo/views/generated.zig +++ b/examples/demo/views/generated.zig @@ -77,6 +77,12 @@ pub fn index(a: Allocator, d: anytype) Allocator.Error![]u8 { { const text = "click me "; const @"type" = "secondary"; + const mixin_attrs_1: struct { + class: []const u8 = "", + id: []const u8 = "", + style: []const u8 = "", + } = .{ + }; try o.appendSlice(a, ""); try esc(&o, a, strVal(text)); + _ = mixin_attrs_1; try o.appendSlice(a, ""); } try o.appendSlice(a, "
Google 1
Google 2
Google 3"); @@ -160,6 +167,12 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 { const name = "firstName"; const label = "First Name"; const placeholder = "first name"; + const mixin_attrs_1: struct { + class: []const u8 = "", + id: []const u8 = "", + style: []const u8 = "", + } = .{ + }; try o.appendSlice(a, "
"); try esc(&o, a, strVal(label)); try o.appendSlice(a, "
"); } try o.appendSlice(a, "
"); @@ -176,6 +190,12 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 { const name = "lastName"; const label = "Last Name"; const placeholder = "last name"; + const mixin_attrs_1: struct { + class: []const u8 = "", + id: []const u8 = "", + style: []const u8 = "", + } = .{ + }; try o.appendSlice(a, "
"); try esc(&o, a, strVal(label)); try o.appendSlice(a, "
"); } try o.appendSlice(a, "sumit"); if (truthy(@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, ""); try esc(&o, a, strVal(message)); try o.appendSlice(a, ""); } + _ = mixin_attrs_1; } } try o.appendSlice(a, "

some footer content

"); diff --git a/src/build_templates.zig b/src/build_templates.zig index 7bb8181..ef73301 100644 --- a/src/build_templates.zig +++ b/src/build_templates.zig @@ -536,6 +536,9 @@ const Compiler = struct { mixins: std.StringHashMap(ast.MixinDef), // Collected mixin definitions blocks: std.StringHashMap(BlockDef), // Collected block definitions for inheritance 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( allocator: std.mem.Allocator, @@ -555,6 +558,9 @@ const Compiler = struct { .mixins = std.StringHashMap(ast.MixinDef).init(allocator), .blocks = std.StringHashMap(BlockDef).init(allocator), .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 { - // 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| { const base = expr[0..dot]; 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 if (self.isLoopVar(base) or self.isMixinParam(base)) { // Escape base if it's a keyword - use the output buffer @@ -1161,6 +1176,14 @@ const Compiler = struct { self.uses_data = true; return std.fmt.bufPrint(buf, "@field(d, \"{s}\").{s}", .{ base, rest }) catch expr; } 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 if (self.isLoopVar(expr) or self.isMixinParam(expr)) { // Escape if it's a keyword - use the output buffer @@ -1505,6 +1528,66 @@ const Compiler = struct { 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 // Note: block content (call.block_children) is handled by mixin_block nodes // 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 try self.flush(); self.depth -= 1; try self.writeIndent(); 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