Fix mixin expansion in compiled templates and adjust include resolution

This commit is contained in:
2026-01-30 22:05:00 +05:30
parent 2c98dab144
commit e337a28202
4 changed files with 16 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .pugz, .name = .pugz,
.version = "0.3.8", .version = "0.3.9",
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications. .fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{}, .dependencies = .{},

View File

@@ -207,14 +207,17 @@ fn compileSingleFile(
// Parse template with full resolution (handles includes, extends, mixins) // Parse template with full resolution (handles includes, extends, mixins)
const final_ast = try engine.parseTemplate(arena_allocator, template_name, registry); const final_ast = try engine.parseTemplate(arena_allocator, template_name, registry);
// Expand mixin calls into concrete AST nodes for codegen
const expanded_ast = try mixin.expandMixins(arena_allocator, final_ast, registry);
// Extract field names // Extract field names
const fields = try zig_codegen.extractFieldNames(arena_allocator, final_ast); const fields = try zig_codegen.extractFieldNames(arena_allocator, expanded_ast);
// Generate Zig code // Generate Zig code
var codegen = zig_codegen.Codegen.init(arena_allocator); var codegen = zig_codegen.Codegen.init(arena_allocator);
defer codegen.deinit(); defer codegen.deinit();
const zig_code = try codegen.generate(final_ast, "render", fields); const zig_code = try codegen.generate(expanded_ast, "render", fields);
// Create flat filename from views-relative path to avoid collisions // Create flat filename from views-relative path to avoid collisions
// e.g., "pages/404.pug" → "pages_404.zig" // e.g., "pages/404.pug" → "pages_404.zig"

View File

@@ -114,12 +114,15 @@ fn compileSingleFile(allocator: mem.Allocator, input_path: []const u8, output_pa
// Parse template with full resolution (handles includes, extends, mixins) // Parse template with full resolution (handles includes, extends, mixins)
const final_ast = try engine.parseTemplate(allocator, template_name, &registry); const final_ast = try engine.parseTemplate(allocator, template_name, &registry);
// Expand mixin calls into concrete AST nodes for codegen
const expanded_ast = try mixin.expandMixins(allocator, final_ast, &registry);
// Note: Don't free final_ast as it's managed by the ViewEngine // Note: Don't free final_ast as it's managed by the ViewEngine
// The normalized_source is intentionally leaked as AST strings point into it // The normalized_source is intentionally leaked as AST strings point into it
// Both will be cleaned up by the allocator when the CLI exits // Both will be cleaned up by the allocator when the CLI exits
// Extract field names from final resolved AST // Extract field names from final resolved AST
const fields = try zig_codegen.extractFieldNames(allocator, final_ast); const fields = try zig_codegen.extractFieldNames(allocator, expanded_ast);
defer { defer {
for (fields) |field| allocator.free(field); for (fields) |field| allocator.free(field);
allocator.free(fields); allocator.free(fields);
@@ -139,7 +142,7 @@ fn compileSingleFile(allocator: mem.Allocator, input_path: []const u8, output_pa
var codegen = Codegen.init(allocator); var codegen = Codegen.init(allocator);
defer codegen.deinit(); defer codegen.deinit();
const zig_code = try codegen.generate(final_ast, function_name, fields); const zig_code = try codegen.generate(expanded_ast, function_name, fields);
defer allocator.free(zig_code); defer allocator.free(zig_code);
// Write output file // Write output file

View File

@@ -280,8 +280,7 @@ pub const ViewEngine = struct {
/// Resolves a path relative to the current file's directory. /// Resolves a path relative to the current file's directory.
/// - Paths starting with "/" are absolute from views_dir root /// - Paths starting with "/" are absolute from views_dir root
/// - Paths starting with "./" or "../" are relative to current file's directory /// - Other paths are relative to current file's directory (if provided)
/// - Other paths are relative to views_dir root (Pug convention)
/// Returns a path relative to views_dir. /// Returns a path relative to views_dir.
fn resolveRelativePath(self: *const ViewEngine, allocator: std.mem.Allocator, path: []const u8, current_dir: ?[]const u8) ![]const u8 { fn resolveRelativePath(self: *const ViewEngine, allocator: std.mem.Allocator, path: []const u8, current_dir: ?[]const u8) ![]const u8 {
_ = self; _ = self;
@@ -291,14 +290,6 @@ pub const ViewEngine = struct {
return allocator.dupe(u8, path[1..]); return allocator.dupe(u8, path[1..]);
} }
// Check if path is explicitly relative (starts with "./" or "../")
const is_explicit_relative = std.mem.startsWith(u8, path, "./") or std.mem.startsWith(u8, path, "../");
// If not explicitly relative, treat as relative to views_dir root (Pug convention)
if (!is_explicit_relative) {
return allocator.dupe(u8, path);
}
// If no current directory (top-level call), path is already relative to views_dir // If no current directory (top-level call), path is already relative to views_dir
const dir = current_dir orelse { const dir = current_dir orelse {
return allocator.dupe(u8, path); return allocator.dupe(u8, path);
@@ -455,15 +446,15 @@ test "resolveRelativePath - relative paths from subdirectory" {
defer allocator.free(result3); defer allocator.free(result3);
try std.testing.expectEqualStrings("pages/utils", result3); try std.testing.expectEqualStrings("pages/utils", result3);
// From pages/, include header (no ./) -> header (relative to views root, Pug convention) // From pages/, include header (no ./) -> pages/header (relative to current dir)
const result4 = try engine.resolveRelativePath(allocator, "header", "pages"); const result4 = try engine.resolveRelativePath(allocator, "header", "pages");
defer allocator.free(result4); defer allocator.free(result4);
try std.testing.expectEqualStrings("header", result4); try std.testing.expectEqualStrings("pages/header", result4);
// From pages/, include includes/partial -> includes/partial (relative to views root) // From pages/, include includes/partial -> pages/includes/partial (relative to current dir)
const result5 = try engine.resolveRelativePath(allocator, "includes/partial", "pages"); const result5 = try engine.resolveRelativePath(allocator, "includes/partial", "pages");
defer allocator.free(result5); defer allocator.free(result5);
try std.testing.expectEqualStrings("includes/partial", result5); try std.testing.expectEqualStrings("pages/includes/partial", result5);
} }
test "resolveRelativePath - absolute paths from views root" { test "resolveRelativePath - absolute paths from views root" {