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:
@@ -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>");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user