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:
@@ -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 = &.{
|
||||
|
||||
@@ -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 = .{},
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user