fix: flush static buffer in conditionals and correct helpers import path

- Flush static buffer at end of each conditional branch (if/else/else-if)
  to ensure content is rendered inside the correct blocks
- Add helpers_path parameter to zig_codegen.generate() for correct
  relative imports in nested directories (e.g., '../helpers.zig')
- Fix build.zig to use correct CLI path (src/tpl_compiler/main.zig)
- Export zig_codegen from root.zig for CLI module usage

Bump version to 0.3.11
This commit is contained in:
2026-01-30 22:53:20 +05:30
parent 5ce319b335
commit dd2191829d
9 changed files with 122 additions and 21 deletions

View File

@@ -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 = &.{

View File

@@ -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 = .{},

View File

@@ -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")

View File

@@ -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)

View File

@@ -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"

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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| {