fix: issue with extends layouts

This commit is contained in:
2026-01-22 23:08:53 +05:30
parent d53ff24931
commit 70ba7af27d

View File

@@ -4,6 +4,15 @@
//! - Shared helper functions (esc, truthy) //! - Shared helper functions (esc, truthy)
//! - All compiled template render functions //! - All compiled template render functions
//! //!
//! Supports full Pugz features:
//! - Template inheritance (extends/block)
//! - Mixins (definitions and calls)
//! - Includes
//! - Case/when statements
//! - Conditionals (if/else if/else/unless)
//! - Iteration (each)
//! - All element features (classes, ids, attributes, interpolation)
//!
//! ## Usage in build.zig: //! ## Usage in build.zig:
//! ```zig //! ```zig
//! const build_templates = @import("pugz").build_templates; //! const build_templates = @import("pugz").build_templates;
@@ -75,6 +84,7 @@ const CompileTemplatesStep = struct {
try generateSingleFile( try generateSingleFile(
allocator, allocator,
self.options.source_dir, self.options.source_dir,
self.options.extension,
out_path, out_path,
templates.items, templates.items,
); );
@@ -159,9 +169,16 @@ fn pathToIdent(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
return result; return result;
} }
/// Block definition for template inheritance
const BlockDef = struct {
mode: ast.Block.Mode,
children: []const ast.Node,
};
fn generateSingleFile( fn generateSingleFile(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
source_dir: []const u8, source_dir: []const u8,
extension: []const u8,
out_path: []const u8, out_path: []const u8,
templates: []const TemplateInfo, templates: []const TemplateInfo,
) !void { ) !void {
@@ -259,7 +276,10 @@ fn generateSingleFile(
}; };
defer allocator.free(source); defer allocator.free(source);
try compileTemplate(allocator, w, tpl.zig_name, source); compileTemplate(allocator, w, source_dir, extension, tpl.zig_name, source) catch |err| {
std.log.err("Failed to compile template {s}: {}", .{ tpl.rel_path, err });
return err;
};
} }
// Template names list // Template names list
@@ -277,6 +297,8 @@ fn generateSingleFile(
fn compileTemplate( fn compileTemplate(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
w: std.ArrayList(u8).Writer, w: std.ArrayList(u8).Writer,
source_dir: []const u8,
extension: []const u8,
name: []const u8, name: []const u8,
source: []const u8, source: []const u8,
) !void { ) !void {
@@ -293,9 +315,15 @@ fn compileTemplate(
return err; return err;
}; };
// Check if template has content // Create compiler with template resolution context
var compiler = Compiler.init(allocator, w, source_dir, extension);
// Handle template inheritance - resolve extends chain
const resolved_nodes = try compiler.resolveInheritance(doc);
// Check if template has content after resolution
var has_content = false; var has_content = false;
for (doc.nodes) |node| { for (resolved_nodes) |node| {
if (nodeHasOutput(node)) { if (nodeHasOutput(node)) {
has_content = true; has_content = true;
break; break;
@@ -304,7 +332,7 @@ fn compileTemplate(
// Check if template has any dynamic content // Check if template has any dynamic content
var has_dynamic = false; var has_dynamic = false;
for (doc.nodes) |node| { for (resolved_nodes) |node| {
if (nodeHasDynamic(node)) { if (nodeHasDynamic(node)) {
has_dynamic = true; has_dynamic = true;
break; break;
@@ -314,15 +342,14 @@ fn compileTemplate(
try w.print("pub fn {s}(a: Allocator, d: anytype) Allocator.Error![]u8 {{\n", .{name}); try w.print("pub fn {s}(a: Allocator, d: anytype) Allocator.Error![]u8 {{\n", .{name});
if (!has_content) { if (!has_content) {
// Empty template (extends-only, mixin definitions, etc.) // Empty template (mixin definitions only, etc.)
try w.writeAll(" _ = .{ a, d };\n"); try w.writeAll(" _ = .{ a, d };\n");
try w.writeAll(" return \"\";\n"); try w.writeAll(" return \"\";\n");
} else if (!has_dynamic) { } else if (!has_dynamic) {
// Static-only template - return literal string, no allocation // Static-only template - return literal string, no allocation
try w.writeAll(" _ = .{ a, d };\n"); try w.writeAll(" _ = .{ a, d };\n");
var compiler = Compiler.init(allocator, w);
try w.writeAll(" return "); try w.writeAll(" return ");
for (doc.nodes) |node| { for (resolved_nodes) |node| {
try compiler.emitNode(node); try compiler.emitNode(node);
} }
try compiler.flushAsReturn(); try compiler.flushAsReturn();
@@ -330,12 +357,16 @@ fn compileTemplate(
// Dynamic template - needs ArrayList // Dynamic template - needs ArrayList
try w.writeAll(" var o: ArrayList = .empty;\n"); try w.writeAll(" var o: ArrayList = .empty;\n");
var compiler = Compiler.init(allocator, w); for (resolved_nodes) |node| {
for (doc.nodes) |node| {
try compiler.emitNode(node); try compiler.emitNode(node);
} }
try compiler.flush(); try compiler.flush();
// If 'd' parameter wasn't used, discard it to avoid unused parameter error
if (!compiler.uses_data) {
try w.writeAll(" _ = d;\n");
}
try w.writeAll(" return o.items;\n"); try w.writeAll(" return o.items;\n");
} }
@@ -359,6 +390,25 @@ fn nodeHasOutput(node: ast.Node) bool {
} }
break :blk false; break :blk false;
}, },
.case => |c| blk: {
for (c.whens) |when| {
for (when.children) |child| {
if (nodeHasOutput(child)) break :blk true;
}
}
for (c.default_children) |child| {
if (nodeHasOutput(child)) break :blk true;
}
break :blk false;
},
.mixin_call => true, // Mixin calls may produce output
.block => |b| blk: {
for (b.children) |child| {
if (nodeHasOutput(child)) break :blk true;
}
break :blk false;
},
.include => true, // Includes may produce output
.document => |d| blk: { .document => |d| blk: {
for (d.nodes) |child| { for (d.nodes) |child| {
if (nodeHasOutput(child)) break :blk true; if (nodeHasOutput(child)) break :blk true;
@@ -389,7 +439,15 @@ fn nodeHasDynamic(node: ast.Node) bool {
} }
break :blk false; break :blk false;
}, },
.conditional, .each => true, .conditional, .each, .case => true,
.mixin_call => true, // Mixin calls are dynamic
.block => |b| blk: {
for (b.children) |child| {
if (nodeHasDynamic(child)) break :blk true;
}
break :blk false;
},
.include => true, // Includes may have dynamic content
.document => |d| blk: { .document => |d| blk: {
for (d.nodes) |child| { for (d.nodes) |child| {
if (nodeHasDynamic(child)) break :blk true; if (nodeHasDynamic(child)) break :blk true;
@@ -400,20 +458,213 @@ fn nodeHasDynamic(node: ast.Node) bool {
}; };
} }
/// Zig reserved keywords that need escaping with @"..."
const zig_keywords = std.StaticStringMap(void).initComptime(.{
.{ "addrspace", {} },
.{ "align", {} },
.{ "allowzero", {} },
.{ "and", {} },
.{ "anyframe", {} },
.{ "anytype", {} },
.{ "asm", {} },
.{ "async", {} },
.{ "await", {} },
.{ "break", {} },
.{ "callconv", {} },
.{ "catch", {} },
.{ "comptime", {} },
.{ "const", {} },
.{ "continue", {} },
.{ "defer", {} },
.{ "else", {} },
.{ "enum", {} },
.{ "errdefer", {} },
.{ "error", {} },
.{ "export", {} },
.{ "extern", {} },
.{ "false", {} },
.{ "fn", {} },
.{ "for", {} },
.{ "if", {} },
.{ "inline", {} },
.{ "linksection", {} },
.{ "noalias", {} },
.{ "noinline", {} },
.{ "nosuspend", {} },
.{ "null", {} },
.{ "opaque", {} },
.{ "or", {} },
.{ "orelse", {} },
.{ "packed", {} },
.{ "pub", {} },
.{ "resume", {} },
.{ "return", {} },
.{ "struct", {} },
.{ "suspend", {} },
.{ "switch", {} },
.{ "test", {} },
.{ "threadlocal", {} },
.{ "true", {} },
.{ "try", {} },
.{ "type", {} },
.{ "undefined", {} },
.{ "union", {} },
.{ "unreachable", {} },
.{ "usingnamespace", {} },
.{ "var", {} },
.{ "volatile", {} },
.{ "while", {} },
});
/// Returns the identifier escaped if it's a Zig keyword
fn escapeIdent(ident: []const u8, buf: []u8) []const u8 {
if (zig_keywords.has(ident)) {
return std.fmt.bufPrint(buf, "@\"{s}\"", .{ident}) catch ident;
}
return ident;
}
const Compiler = struct { const Compiler = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
writer: std.ArrayList(u8).Writer, writer: std.ArrayList(u8).Writer,
source_dir: []const u8,
extension: []const u8,
buf: std.ArrayList(u8), // Buffer for merging static strings buf: std.ArrayList(u8), // Buffer for merging static strings
depth: usize, depth: usize,
loop_vars: std.ArrayList([]const u8), // Track loop variable names loop_vars: std.ArrayList([]const u8), // Track loop variable names
mixin_params: std.ArrayList([]const u8), // Track current mixin parameter names
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
fn init(allocator: std.mem.Allocator, writer: std.ArrayList(u8).Writer) Compiler { fn init(
allocator: std.mem.Allocator,
writer: std.ArrayList(u8).Writer,
source_dir: []const u8,
extension: []const u8,
) Compiler {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.writer = writer, .writer = writer,
.source_dir = source_dir,
.extension = extension,
.buf = .{}, .buf = .{},
.depth = 1, .depth = 1,
.loop_vars = .{}, .loop_vars = .{},
.mixin_params = .{},
.mixins = std.StringHashMap(ast.MixinDef).init(allocator),
.blocks = std.StringHashMap(BlockDef).init(allocator),
.uses_data = false,
};
}
/// Resolves template inheritance by loading parent templates and merging blocks
fn resolveInheritance(self: *Compiler, doc: ast.Document) ![]const ast.Node {
// First, collect all mixin definitions from this template
try self.collectMixins(doc.nodes);
// Check if this template extends another
if (doc.extends_path) |extends_path| {
// Collect blocks from child template
try self.collectBlocks(doc.nodes);
// Load and parse parent template
const parent_doc = try self.loadTemplate(extends_path);
// Collect mixins from parent too
try self.collectMixins(parent_doc.nodes);
// Recursively resolve parent's inheritance
return try self.resolveInheritance(parent_doc);
}
// No extends - return nodes as-is (blocks will be resolved during emission)
return doc.nodes;
}
/// Collects mixin definitions from nodes
fn collectMixins(self: *Compiler, nodes: []const ast.Node) !void {
for (nodes) |node| {
switch (node) {
.mixin_def => |def| {
try self.mixins.put(def.name, def);
},
.element => |e| {
try self.collectMixins(e.children);
},
.conditional => |c| {
for (c.branches) |br| {
try self.collectMixins(br.children);
}
},
.each => |e| {
try self.collectMixins(e.children);
try self.collectMixins(e.else_children);
},
.block => |b| {
try self.collectMixins(b.children);
},
else => {},
}
}
}
/// Collects block definitions from child template
fn collectBlocks(self: *Compiler, nodes: []const ast.Node) !void {
for (nodes) |node| {
switch (node) {
.block => |blk| {
try self.blocks.put(blk.name, .{
.mode = blk.mode,
.children = blk.children,
});
},
.element => |e| {
try self.collectBlocks(e.children);
},
.conditional => |c| {
for (c.branches) |br| {
try self.collectBlocks(br.children);
}
},
.each => |e| {
try self.collectBlocks(e.children);
},
else => {},
}
}
}
/// Loads and parses a template file
fn loadTemplate(self: *Compiler, path: []const u8) !ast.Document {
// Build full path
const full_path = blk: {
// Check if path already has extension
if (std.mem.endsWith(u8, path, self.extension)) {
break :blk try std.fs.path.join(self.allocator, &.{ self.source_dir, path });
} else {
const with_ext = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ path, self.extension });
defer self.allocator.free(with_ext);
break :blk try std.fs.path.join(self.allocator, &.{ self.source_dir, with_ext });
}
};
defer self.allocator.free(full_path);
const source = std.fs.cwd().readFileAlloc(self.allocator, full_path, 5 * 1024 * 1024) catch |err| {
std.log.err("Failed to load template '{s}': {}", .{ full_path, err });
return err;
};
var lexer = Lexer.init(self.allocator, source);
const tokens = lexer.tokenize() catch |err| {
std.log.err("Tokenize error in included template '{s}': {}", .{ path, err });
return err;
};
var parser = Parser.init(self.allocator, tokens);
return parser.parse() catch |err| {
std.log.err("Parse error in included template '{s}': {}", .{ path, err });
return err;
}; };
} }
@@ -469,11 +720,18 @@ const Compiler = struct {
.raw_text => |r| try self.appendStatic(r.content), .raw_text => |r| try self.appendStatic(r.content),
.conditional => |c| try self.emitConditional(c), .conditional => |c| try self.emitConditional(c),
.each => |e| try self.emitEach(e), .each => |e| try self.emitEach(e),
.case => |c| try self.emitCase(c),
.comment => |c| if (c.rendered) { .comment => |c| if (c.rendered) {
try self.appendStatic("<!-- "); try self.appendStatic("<!-- ");
try self.appendStatic(c.content); try self.appendStatic(c.content);
try self.appendStatic(" -->"); try self.appendStatic(" -->");
}, },
.block => |b| try self.emitBlock(b),
.include => |inc| try self.emitInclude(inc),
.mixin_call => |call| try self.emitMixinCall(call),
.mixin_def => {}, // Mixin definitions are collected, not emitted directly
.mixin_block => {}, // Handled within mixin call context
.extends => {}, // Handled at document level
.document => |dc| for (dc.nodes) |child| try self.emitNode(child), .document => |dc| for (dc.nodes) |child| try self.emitNode(child),
else => {}, else => {},
} }
@@ -503,14 +761,9 @@ const Compiler = struct {
for (e.attributes) |attr| { for (e.attributes) |attr| {
if (attr.value) |v| { if (attr.value) |v| {
if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) { try self.emitAttribute(attr.name, v, attr.escaped);
try self.appendStatic(" ");
try self.appendStatic(attr.name);
try self.appendStatic("=\"");
try self.appendStatic(v[1 .. v.len - 1]);
try self.appendStatic("\"");
}
} else { } else {
// Boolean attribute
try self.appendStatic(" "); try self.appendStatic(" ");
try self.appendStatic(attr.name); try self.appendStatic(attr.name);
try self.appendStatic("=\""); try self.appendStatic("=\"");
@@ -570,6 +823,17 @@ const Compiler = struct {
} }
try self.appendStatic("\""); try self.appendStatic("\"");
} }
for (t.attributes) |attr| {
if (attr.value) |v| {
if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) {
try self.appendStatic(" ");
try self.appendStatic(attr.name);
try self.appendStatic("=\"");
try self.appendStatic(v[1 .. v.len - 1]);
try self.appendStatic("\"");
}
}
}
try self.appendStatic(">"); try self.appendStatic(">");
try self.emitText(t.text_segments); try self.emitText(t.text_segments);
try self.appendStatic("</"); try self.appendStatic("</");
@@ -593,6 +857,127 @@ const Compiler = struct {
} }
} }
/// Emits an attribute with its value, handling string concatenation expressions
fn emitAttribute(self: *Compiler, name: []const u8, value: []const u8, escaped: bool) !void {
_ = escaped;
// Check for string concatenation: "literal" + variable or variable + "literal"
if (findConcatOperator(value)) |concat_pos| {
// Parse concatenation expression
try self.flush();
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, \" {s}=\\\"\");\n", .{name});
try self.emitConcatExpr(value, concat_pos);
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n");
} else if (value.len >= 2 and (value[0] == '"' or value[0] == '\'')) {
// Simple string literal
try self.appendStatic(" ");
try self.appendStatic(name);
try self.appendStatic("=\"");
try self.appendStatic(value[1 .. value.len - 1]);
try self.appendStatic("\"");
} else {
// Dynamic value (variable reference)
try self.flush();
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, \" {s}=\\\"\");\n", .{name});
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(value, &accessor_buf);
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor});
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n");
}
}
/// Find the + operator for string concatenation, accounting for quoted strings
fn findConcatOperator(value: []const u8) ?usize {
var in_string = false;
var string_char: u8 = 0;
var i: usize = 0;
while (i < value.len) : (i += 1) {
const c = value[i];
if (in_string) {
if (c == string_char) {
in_string = false;
}
} else {
if (c == '"' or c == '\'') {
in_string = true;
string_char = c;
} else if (c == '+') {
// Check it's surrounded by spaces (typical concat)
if (i > 0 and i + 1 < value.len) {
return i;
}
}
}
}
return null;
}
/// Emit a concatenation expression like "btn btn-" + type
fn emitConcatExpr(self: *Compiler, value: []const u8, concat_pos: usize) !void {
// Split on the + operator
const left = std.mem.trim(u8, value[0..concat_pos], " ");
const right = std.mem.trim(u8, value[concat_pos + 1 ..], " ");
// Emit left part
if (left.len >= 2 and (left[0] == '"' or left[0] == '\'')) {
// String literal
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, {s});\n", .{left});
} else {
// Variable
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(left, &accessor_buf);
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor});
}
// Check if right part also has concatenation
if (findConcatOperator(right)) |next_concat| {
try self.emitConcatExpr(right, next_concat);
} else {
// Emit right part
if (right.len >= 2 and (right[0] == '"' or right[0] == '\'')) {
// String literal
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, {s});\n", .{right});
} else {
// Variable
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(right, &accessor_buf);
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor});
}
}
}
/// Emit expression inline (for attribute values) - doesn't flush or write indent
fn emitExprInline(self: *Compiler, expr: []const u8, escaped: bool) !void {
// For now, we need to flush and emit as separate statement
// This is a limitation - dynamic attribute values need special handling
try self.flush();
try self.writeIndent();
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(expr, &accessor_buf);
if (escaped) {
try self.writer.print("try esc(&o, a, strVal({s}));\n", .{accessor});
} else {
try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor});
}
}
fn isLoopVar(self: *Compiler, name: []const u8) bool { fn isLoopVar(self: *Compiler, name: []const u8) bool {
for (self.loop_vars.items) |v| { for (self.loop_vars.items) |v| {
if (std.mem.eql(u8, v, name)) return true; if (std.mem.eql(u8, v, name)) return true;
@@ -600,19 +985,40 @@ const Compiler = struct {
return false; return false;
} }
fn isMixinParam(self: *Compiler, name: []const u8) bool {
for (self.mixin_params.items) |p| {
if (std.mem.eql(u8, p, name)) return true;
}
return false;
}
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
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 ..];
// For loop variables like friend.name, access directly // For loop variables or mixin params like friend.name, access directly
return std.fmt.bufPrint(buf, "{s}.{s}", .{ base, rest }) catch expr; if (self.isLoopVar(base) or self.isMixinParam(base)) {
// Escape base if it's a keyword - use the output buffer
if (zig_keywords.has(base)) {
return std.fmt.bufPrint(buf, "@\"{s}\".{s}", .{ base, rest }) catch expr;
}
return std.fmt.bufPrint(buf, "{s}.{s}", .{ base, rest }) catch expr;
}
// For top-level data field access - mark that we use 'd'
self.uses_data = true;
return std.fmt.bufPrint(buf, "@field(d, \"{s}\").{s}", .{ base, rest }) catch expr;
} else { } else {
// Check if it's a loop variable (like color, item, tag) // Check if it's a loop variable or mixin param
if (self.isLoopVar(expr)) { if (self.isLoopVar(expr) or self.isMixinParam(expr)) {
// Escape if it's a keyword - use the output buffer
if (zig_keywords.has(expr)) {
return std.fmt.bufPrint(buf, "@\"{s}\"", .{expr}) catch expr;
}
return expr; return expr;
} }
// For top-level like "name", access from d // For top-level like "name", access from d - mark that we use 'd'
self.uses_data = true;
return std.fmt.bufPrint(buf, "@field(d, \"{s}\")", .{expr}) catch expr; return std.fmt.bufPrint(buf, "@field(d, \"{s}\")", .{expr}) catch expr;
} }
} }
@@ -652,7 +1058,9 @@ const Compiler = struct {
const rhs_start = eq_pos + 5; // skip ' == "' const rhs_start = eq_pos + 5; // skip ' == "'
if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| { if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| {
const rhs = cond[rhs_start .. rhs_start + rhs_end]; const rhs = cond[rhs_start .. rhs_start + rhs_end];
try self.writer.print("std.mem.eql(u8, {s}, \"{s}\")", .{ lhs, rhs }); var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(lhs, &accessor_buf);
try self.writer.print("std.mem.eql(u8, strVal({s}), \"{s}\")", .{ accessor, rhs });
return; return;
} }
} }
@@ -662,16 +1070,16 @@ const Compiler = struct {
const rhs_start = eq_pos + 5; const rhs_start = eq_pos + 5;
if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| { if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| {
const rhs = cond[rhs_start .. rhs_start + rhs_end]; const rhs = cond[rhs_start .. rhs_start + rhs_end];
try self.writer.print("!std.mem.eql(u8, {s}, \"{s}\")", .{ lhs, rhs }); var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(lhs, &accessor_buf);
try self.writer.print("!std.mem.eql(u8, strVal({s}), \"{s}\")", .{ accessor, rhs });
return; return;
} }
} }
// Regular field access // Regular field access - use buildAccessor for consistency
if (std.mem.indexOfScalar(u8, cond, '.')) |_| { var accessor_buf: [512]u8 = undefined;
try self.writer.print("truthy({s})", .{cond}); const accessor = self.buildAccessor(cond, &accessor_buf);
} else { try self.writer.print("truthy({s})", .{accessor});
try self.writer.print("truthy(@field(d, \"{s}\"))", .{cond});
}
} }
fn emitEach(self: *Compiler, e: ast.Each) anyerror!void { fn emitEach(self: *Compiler, e: ast.Each) anyerror!void {
@@ -681,14 +1089,24 @@ const Compiler = struct {
// Track this loop variable // Track this loop variable
try self.loop_vars.append(self.allocator, e.value_name); try self.loop_vars.append(self.allocator, e.value_name);
// Build accessor for collection
var accessor_buf: [512]u8 = undefined;
const collection_accessor = self.buildAccessor(e.collection, &accessor_buf);
// Check if we need else branch handling
if (e.else_children.len > 0) {
// Need to check length first for else branch
try self.writer.print("if ({s}.len > 0) {{\n", .{collection_accessor});
self.depth += 1;
try self.writeIndent();
}
// Generate the for loop - handle optional collections with orelse // Generate the for loop - handle optional collections with orelse
if (std.mem.indexOfScalar(u8, e.collection, '.')) |dot| { if (std.mem.indexOfScalar(u8, e.collection, '.')) |_| {
const base = e.collection[0..dot]; // Nested field - may be optional
const field = e.collection[dot + 1 ..]; try self.writer.print("for (if (@typeInfo(@TypeOf({s})) == .optional) ({s} orelse &.{{}}) else {s}) |{s}", .{ collection_accessor, collection_accessor, collection_accessor, e.value_name });
// Use orelse to handle optional slices
try self.writer.print("for (if (@typeInfo(@TypeOf({s}.{s})) == .optional) ({s}.{s} orelse &.{{}}) else {s}.{s}) |{s}", .{ base, field, base, field, base, field, e.value_name });
} else { } else {
try self.writer.print("for (@field(d, \"{s}\")) |{s}", .{ e.collection, e.value_name }); try self.writer.print("for ({s}) |{s}", .{ collection_accessor, e.value_name });
} }
if (e.index_name) |idx| { if (e.index_name) |idx| {
try self.writer.print(", {s}", .{idx}); try self.writer.print(", {s}", .{idx});
@@ -705,9 +1123,298 @@ const Compiler = struct {
try self.writeIndent(); try self.writeIndent();
try self.writer.writeAll("}\n"); try self.writer.writeAll("}\n");
// Handle else branch
if (e.else_children.len > 0) {
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("} else {\n");
self.depth += 1;
for (e.else_children) |child| {
try self.emitNode(child);
}
try self.flush();
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("}\n");
}
// Pop loop variable // Pop loop variable
_ = self.loop_vars.pop(); _ = self.loop_vars.pop();
} }
fn emitCase(self: *Compiler, c: ast.Case) anyerror!void {
try self.flush();
// Build accessor for the expression
var accessor_buf: [512]u8 = undefined;
const expr_accessor = self.buildAccessor(c.expression, &accessor_buf);
// Generate a series of if/else if statements to match case values
var first = true;
for (c.whens) |when| {
try self.writeIndent();
if (first) {
first = false;
} else {
try self.writer.writeAll("} else ");
}
// Check if value is a string literal
if (when.value.len >= 2 and when.value[0] == '"') {
const str_val = when.value[1 .. when.value.len - 1];
try self.writer.print("if (std.mem.eql(u8, strVal({s}), \"{s}\")) {{\n", .{ expr_accessor, str_val });
} else {
// Numeric or other comparison
try self.writer.print("if ({s} == {s}) {{\n", .{ expr_accessor, when.value });
}
self.depth += 1;
if (when.has_break) {
// Explicit break - do nothing
} else if (when.children.len == 0) {
// Fall-through - we'll handle this by continuing to next case
// For now, just skip (Zig doesn't have fall-through)
} else {
for (when.children) |child| {
try self.emitNode(child);
}
}
try self.flush();
self.depth -= 1;
}
// Default case
if (c.default_children.len > 0) {
try self.writeIndent();
if (!first) {
try self.writer.writeAll("} else {\n");
} else {
try self.writer.writeAll("{\n");
}
self.depth += 1;
for (c.default_children) |child| {
try self.emitNode(child);
}
try self.flush();
self.depth -= 1;
}
if (!first or c.default_children.len > 0) {
try self.writeIndent();
try self.writer.writeAll("}\n");
}
}
fn emitBlock(self: *Compiler, blk: ast.Block) anyerror!void {
// Check if child template overrides this block
if (self.blocks.get(blk.name)) |child_block| {
switch (child_block.mode) {
.replace => {
// Child completely replaces parent block
for (child_block.children) |child| {
try self.emitNode(child);
}
},
.append => {
// Parent content first, then child
for (blk.children) |child| {
try self.emitNode(child);
}
for (child_block.children) |child| {
try self.emitNode(child);
}
},
.prepend => {
// Child content first, then parent
for (child_block.children) |child| {
try self.emitNode(child);
}
for (blk.children) |child| {
try self.emitNode(child);
}
},
}
} else {
// No override - render default block content
for (blk.children) |child| {
try self.emitNode(child);
}
}
}
fn emitInclude(self: *Compiler, inc: ast.Include) anyerror!void {
// Load and parse the included template
const included_doc = self.loadTemplate(inc.path) catch |err| {
std.log.warn("Failed to load include '{s}': {}", .{ inc.path, err });
return;
};
// Collect mixins from included template
try self.collectMixins(included_doc.nodes);
// Emit included content inline
for (included_doc.nodes) |node| {
try self.emitNode(node);
}
}
fn emitMixinCall(self: *Compiler, call: ast.MixinCall) anyerror!void {
// Look up mixin definition
const mixin_def = self.mixins.get(call.name) orelse {
// Try to load from mixins directory
if (self.loadMixinFromDir(call.name)) |def| {
try self.mixins.put(def.name, def);
try self.emitMixinCallWithDef(call, def);
return;
}
std.log.warn("Mixin '{s}' not found", .{call.name});
return;
};
try self.emitMixinCallWithDef(call, mixin_def);
}
fn emitMixinCallWithDef(self: *Compiler, call: ast.MixinCall, mixin_def: ast.MixinDef) anyerror!void {
// For each mixin parameter, we need to create a local binding
// This is complex in compiled mode - we inline the mixin body
// Save current mixin params
const prev_params_len = self.mixin_params.items.len;
defer self.mixin_params.items.len = prev_params_len;
// Calculate regular params (excluding rest param)
const regular_params = if (mixin_def.has_rest and mixin_def.params.len > 0)
mixin_def.params.len - 1
else
mixin_def.params.len;
// Emit local variable declarations for mixin parameters
try self.flush();
for (mixin_def.params[0..regular_params], 0..) |param, i| {
try self.mixin_params.append(self.allocator, param);
// Escape param name if it's a Zig keyword
var ident_buf: [64]u8 = undefined;
const safe_param = escapeIdent(param, &ident_buf);
try self.writeIndent();
if (i < call.args.len) {
// Argument provided
const arg = call.args[i];
// Check if it's a string literal
if (arg.len >= 2 and (arg[0] == '"' or arg[0] == '\'')) {
try self.writer.print("const {s} = {s};\n", .{ safe_param, arg });
} else {
// It's a variable reference
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(arg, &accessor_buf);
try self.writer.print("const {s} = {s};\n", .{ safe_param, accessor });
}
} else if (i < mixin_def.defaults.len) {
// Use default value
if (mixin_def.defaults[i]) |default| {
try self.writer.print("const {s} = {s};\n", .{ safe_param, default });
} else {
try self.writer.print("const {s} = \"\";\n", .{safe_param});
}
} else {
// No value - use empty string
try self.writer.print("const {s} = \"\";\n", .{safe_param});
}
}
// Handle rest parameters
if (mixin_def.has_rest and mixin_def.params.len > 0) {
const rest_param = mixin_def.params[mixin_def.params.len - 1];
try self.mixin_params.append(self.allocator, rest_param);
// Rest args are remaining arguments as an array
try self.writeIndent();
try self.writer.print("const {s} = &[_][]const u8{{", .{rest_param});
for (call.args[regular_params..], 0..) |arg, i| {
if (i > 0) try self.writer.writeAll(", ");
try self.writer.print("{s}", .{arg});
}
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
for (mixin_def.children) |child| {
// Handle mixin_block specially - replace with call's block_children
if (child == .mixin_block) {
for (call.block_children) |block_child| {
try self.emitNode(block_child);
}
} else {
try self.emitNode(child);
}
}
}
/// Try to load a mixin from the mixins directory
fn loadMixinFromDir(self: *Compiler, name: []const u8) ?ast.MixinDef {
// Try specific file first: mixins/{name}.pug
const specific_path = std.fs.path.join(self.allocator, &.{ self.source_dir, "mixins", name }) catch return null;
defer self.allocator.free(specific_path);
const with_ext = std.fmt.allocPrint(self.allocator, "{s}{s}", .{ specific_path, self.extension }) catch return null;
defer self.allocator.free(with_ext);
if (std.fs.cwd().readFileAlloc(self.allocator, with_ext, 1024 * 1024)) |source| {
if (self.parseMixinFromSource(source, name)) |def| {
return def;
}
} else |_| {}
// Try scanning all files in mixins directory
const mixins_dir_path = std.fs.path.join(self.allocator, &.{ self.source_dir, "mixins" }) catch return null;
defer self.allocator.free(mixins_dir_path);
var dir = std.fs.cwd().openDir(mixins_dir_path, .{ .iterate = true }) catch return null;
defer dir.close();
var iter = dir.iterate();
while (iter.next() catch return null) |entry| {
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, self.extension)) {
const file_path = std.fs.path.join(self.allocator, &.{ mixins_dir_path, entry.name }) catch continue;
defer self.allocator.free(file_path);
if (std.fs.cwd().readFileAlloc(self.allocator, file_path, 1024 * 1024)) |source| {
if (self.parseMixinFromSource(source, name)) |def| {
return def;
}
} else |_| {}
}
}
return null;
}
/// Parse source and extract a specific mixin definition
fn parseMixinFromSource(self: *Compiler, source: []const u8, name: []const u8) ?ast.MixinDef {
var lexer = Lexer.init(self.allocator, source);
const tokens = lexer.tokenize() catch return null;
var parser = Parser.init(self.allocator, tokens);
const doc = parser.parse() catch return null;
// Find the mixin with matching name
for (doc.nodes) |node| {
if (node == .mixin_def) {
if (std.mem.eql(u8, node.mixin_def.name, name)) {
return node.mixin_def;
}
}
}
return null;
}
}; };
fn isVoidElement(tag: []const u8) bool { fn isVoidElement(tag: []const u8) bool {