diff --git a/build.zig b/build.zig index 4efc003..630fb34 100644 --- a/build.zig +++ b/build.zig @@ -18,7 +18,7 @@ pub fn build(b: *std.Build) void { const cli_exe = b.addExecutable(.{ .name = "pug-compile", .root_module = b.createModule(.{ - .root_source_file = b.path("src/cli/main.zig"), + .root_source_file = b.path("src/tpl_compiler/main.zig"), .target = target, .optimize = optimize, .imports = &.{ diff --git a/build.zig.zon b/build.zig.zon index 2c79cfc..60b5e78 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .pugz, - .version = "0.3.10", + .version = "0.3.11", .fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, diff --git a/examples/demo/views/mixins/alerts.pug b/examples/demo/views/mixins/alerts.pug index 34b4e4b..8937e35 100644 --- a/examples/demo/views/mixins/alerts.pug +++ b/examples/demo/views/mixins/alerts.pug @@ -1,12 +1,9 @@ -//- Alert/notification mixins +mixin alert(alert_messgae) + div.alert(role="alert" class!=attributes.class) + svg(xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" 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") + span= alert_messgae -mixin alert(message, type) - - var alertClass = type ? "alert alert-" + type : "alert alert-info" - .alert(class=alertClass) - p= message -mixin alert-dismissible(message, type) - - var alertClass = type ? "alert alert-" + type : "alert alert-info" - .alert.alert-dismissible(class=alertClass) - p= message - button.alert-close(type="button" aria-label="Close") x +mixin alert_error(alert_messgae) + +alert(alert_messgae)(class="alert-error") diff --git a/examples/demo/views/pages/home.pug b/examples/demo/views/pages/home.pug index 8906e42..b451f9c 100644 --- a/examples/demo/views/pages/home.pug +++ b/examples/demo/views/pages/home.pug @@ -1,5 +1,5 @@ extends ../layouts/base.pug - +include ../mixins/alerts.pug block title title #{title} | Pugz Store @@ -54,3 +54,6 @@ block content .category-icon H h3 Home Office span 12 products + + if alert_message + +alert_error(alert_message) diff --git a/src/compile_tpls.zig b/src/compile_tpls.zig index 6d6b3d9..7e0df92 100644 --- a/src/compile_tpls.zig +++ b/src/compile_tpls.zig @@ -217,7 +217,7 @@ fn compileSingleFile( var codegen = zig_codegen.Codegen.init(arena_allocator); defer codegen.deinit(); - const zig_code = try codegen.generate(expanded_ast, "render", fields); + const zig_code = try codegen.generate(expanded_ast, "render", fields, null); // Create flat filename from views-relative path to avoid collisions // e.g., "pages/404.pug" → "pages_404.zig" diff --git a/src/mixin.zig b/src/mixin.zig index f296dfa..1af412f 100644 --- a/src/mixin.zig +++ b/src/mixin.zig @@ -255,6 +255,25 @@ fn expandNodeWithArgs( // Substitute argument references in text/val if (node.val) |val| { new_node.val = try substituteArgs(allocator, val, arg_bindings); + + // If a Code node's val was completely substituted with a literal string, + // convert it to a Text node so it's not treated as a data field reference. + // This handles cases like `= label` where label is a mixin parameter that + // gets substituted with a literal string like "First Name". + if (node.type == .Code and node.buffer) { + const trimmed_val = mem.trim(u8, val, " \t"); + // Check if the original val was a simple parameter reference (single identifier) + if (isSimpleIdentifier(trimmed_val)) { + // And it was substituted (val changed) + if (new_node.val) |new_val| { + if (!mem.eql(u8, new_val, val)) { + // Convert to Text node - it's now a literal value + new_node.type = .Text; + new_node.buffer = false; + } + } + } + } } // Clone attributes with argument substitution @@ -262,6 +281,19 @@ fn expandNodeWithArgs( var new_attr = attr; if (attr.val) |val| { new_attr.val = try substituteArgs(allocator, val, arg_bindings); + + // If attribute value was a simple parameter that got substituted, + // mark it as quoted so it's treated as a static string value + if (!attr.quoted) { + const trimmed_val = mem.trim(u8, val, " \t"); + if (isSimpleIdentifier(trimmed_val)) { + if (new_attr.val) |new_val| { + if (!mem.eql(u8, new_val, val)) { + new_attr.quoted = true; + } + } + } + } } new_node.attrs.append(allocator, new_attr) catch return error.OutOfMemory; } @@ -398,6 +430,23 @@ fn isIdentChar(c: u8) bool { c == '_' or c == '-'; } +/// Check if a string is a simple identifier (valid mixin parameter name) +fn isSimpleIdentifier(s: []const u8) bool { + if (s.len == 0) return false; + // First char must be letter or underscore + const first = s[0]; + if (!((first >= 'a' and first <= 'z') or (first >= 'A' and first <= 'Z') or first == '_')) { + return false; + } + // Rest must be alphanumeric or underscore + for (s[1..]) |c| { + if (!((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_')) { + return false; + } + } + return true; +} + /// Bind call arguments to mixin parameters fn bindArguments( allocator: Allocator, diff --git a/src/root.zig b/src/root.zig index d30bff0..033c9d4 100644 --- a/src/root.zig +++ b/src/root.zig @@ -13,6 +13,7 @@ pub const mixin = @import("mixin.zig"); pub const runtime = @import("runtime.zig"); pub const codegen = @import("codegen.zig"); pub const compile_tpls = @import("compile_tpls.zig"); +pub const zig_codegen = @import("tpl_compiler/zig_codegen.zig"); // Re-export main types pub const ViewEngine = view_engine.ViewEngine; diff --git a/src/tpl_compiler/main.zig b/src/tpl_compiler/main.zig index 161d8a6..f747777 100644 --- a/src/tpl_compiler/main.zig +++ b/src/tpl_compiler/main.zig @@ -6,13 +6,13 @@ const std = @import("std"); const pugz = @import("pugz"); -const zig_codegen = @import("zig_codegen.zig"); const fs = std.fs; const mem = std.mem; const pug = pugz.pug; const template = pugz.template; const view_engine = pugz.view_engine; const mixin = pugz.mixin; +const zig_codegen = pugz.zig_codegen; const Codegen = zig_codegen.Codegen; pub fn main() !void { @@ -60,7 +60,7 @@ pub fn main() !void { const input_file = args[1]; const output_file = args[2]; - try compileSingleFile(allocator, input_file, output_file, null); + try compileSingleFile(allocator, input_file, output_file, null, null); } std.debug.print("Compilation complete!\n", .{}); @@ -83,7 +83,7 @@ fn printUsage() !void { , .{}); } -fn compileSingleFile(allocator: mem.Allocator, input_path: []const u8, output_path: []const u8, views_dir: ?[]const u8) !void { +fn compileSingleFile(allocator: mem.Allocator, input_path: []const u8, output_path: []const u8, views_dir: ?[]const u8, output_base_dir: ?[]const u8) !void { std.debug.print("Compiling {s} -> {s}\n", .{ input_path, output_path }); // Use ViewEngine to properly resolve extends, includes, and mixins at build time @@ -138,11 +138,43 @@ fn compileSingleFile(allocator: mem.Allocator, input_path: []const u8, output_pa // Generate function name from file path (always "render") const function_name = "render"; // Always use "render", no allocation needed + // Calculate relative path to helpers.zig from output file + var helpers_path: ?[]const u8 = null; + defer if (helpers_path) |hp| allocator.free(hp); + + if (output_base_dir) |base_dir| { + // Get the relative path from output file to the base output directory + const output_dir = fs.path.dirname(output_path) orelse "."; + + // Count directory depth relative to base_dir + if (mem.startsWith(u8, output_dir, base_dir)) { + const rel_output = output_dir[base_dir.len..]; + const trimmed = if (rel_output.len > 0 and rel_output[0] == '/') rel_output[1..] else rel_output; + + if (trimmed.len > 0) { + // Count the number of directories deep + var depth: usize = 1; + for (trimmed) |c| { + if (c == '/') depth += 1; + } + + // Build "../" prefix for each level + var path_buf: std.ArrayList(u8) = .{}; + defer path_buf.deinit(allocator); + for (0..depth) |_| { + try path_buf.appendSlice(allocator, "../"); + } + try path_buf.appendSlice(allocator, "helpers.zig"); + helpers_path = try path_buf.toOwnedSlice(allocator); + } + } + } + // Generate Zig code from final resolved AST var codegen = Codegen.init(allocator); defer codegen.deinit(); - const zig_code = try codegen.generate(expanded_ast, function_name, fields); + const zig_code = try codegen.generate(expanded_ast, function_name, fields, helpers_path); defer allocator.free(zig_code); // Write output file @@ -209,7 +241,7 @@ fn compileDirectory(allocator: mem.Allocator, input_dir: []const u8, output_dir: } // Compile the file (pass input_dir as views_dir for includes/extends resolution) - compileSingleFile(allocator, pug_file, output_path, input_dir) catch |err| { + compileSingleFile(allocator, pug_file, output_path, input_dir, output_dir) catch |err| { std.debug.print(" ERROR: Failed to compile {s}: {}\n", .{ pug_file, err }); continue; }; diff --git a/src/tpl_compiler/zig_codegen.zig b/src/tpl_compiler/zig_codegen.zig index fe4ce0b..4e78116 100644 --- a/src/tpl_compiler/zig_codegen.zig +++ b/src/tpl_compiler/zig_codegen.zig @@ -81,7 +81,8 @@ pub const Codegen = struct { } /// Generate Zig code for a template - pub fn generate(self: *Codegen, ast: *Node, function_name: []const u8, fields: []const []const u8) ![]const u8 { + /// helpers_path: relative path to helpers.zig from the output file (e.g., "../helpers.zig" for nested dirs) + pub fn generate(self: *Codegen, ast: *Node, function_name: []const u8, fields: []const []const u8, helpers_path: ?[]const u8) ![]const u8 { // Reset state self.output.clearRetainingCapacity(); self.static_buffer.clearRetainingCapacity(); @@ -106,7 +107,9 @@ pub const Codegen = struct { // Generate imports try self.writeLine("const std = @import(\"std\");"); - try self.writeLine("const helpers = @import(\"helpers.zig\");"); + try self.write("const helpers = @import(\""); + try self.write(helpers_path orelse "helpers.zig"); + try self.writeLine("\");"); try self.writeLine(""); // Generate Data struct with typed fields @@ -528,6 +531,9 @@ pub const Codegen = struct { try self.generateNode(cons); } + // Flush static buffer before closing the if block + try self.flushStaticBuffer(); + self.indent_level -= 1; // Generate alternate (else/else if) @@ -546,6 +552,9 @@ pub const Codegen = struct { try self.generateNode(alt_cons); } + // Flush static buffer before closing the else-if block + try self.flushStaticBuffer(); + self.indent_level -= 1; // Handle nested alternates @@ -554,6 +563,8 @@ pub const Codegen = struct { try self.writeLine("} else {"); self.indent_level += 1; try self.generateNode(nested_alt); + // Flush static buffer before closing the else block + try self.flushStaticBuffer(); self.indent_level -= 1; } @@ -564,6 +575,8 @@ pub const Codegen = struct { try self.writeLine("} else {"); self.indent_level += 1; try self.generateNode(alt); + // Flush static buffer before closing the else block + try self.flushStaticBuffer(); self.indent_level -= 1; try self.writeIndent(); try self.writeLine("}"); @@ -828,6 +841,12 @@ pub fn extractFieldNames(allocator: Allocator, ast: *Node) ![][]const u8 { } fn extractFieldNamesRecursive(node: *Node, fields: *std.StringHashMap(void), loop_vars: *std.StringHashMap(void)) !void { + // Skip mixin DEFINITIONS - they contain parameter references that shouldn't + // be extracted as data fields. Only expanded mixin CALLS should be processed. + if (node.type == .Mixin and !node.call) { + return; + } + // Handle TypeHint nodes - just add the field name, type info is handled separately if (node.type == .TypeHint) { if (node.type_hint_field) |field| {