diff --git a/.gitignore b/.gitignore
index 4d574c5..b5c98e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ zig-cache/
node_modules
# compiled template file
-generated.zig
+#generated.zig
# IDE
.vscode/
diff --git a/examples/demo/views/generated.zig b/examples/demo/views/generated.zig
new file mode 100644
index 0000000..8b8b6e9
--- /dev/null
+++ b/examples/demo/views/generated.zig
@@ -0,0 +1,284 @@
+//! Auto-generated by pugz.compileTemplates()
+//! Do not edit manually - regenerate by running: zig build
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArrayList = std.ArrayList(u8);
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Helpers
+// ─────────────────────────────────────────────────────────────────────────────
+
+const esc_lut: [256]?[]const u8 = blk: {
+ var t: [256]?[]const u8 = .{null} ** 256;
+ t['&'] = "&";
+ t['<'] = "<";
+ t['>'] = ">";
+ t['"'] = """;
+ t['\''] = "'";
+ break :blk t;
+};
+
+fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void {
+ var i: usize = 0;
+ for (s, 0..) |c, j| {
+ if (esc_lut[c]) |e| {
+ if (j > i) try o.appendSlice(a, s[i..j]);
+ try o.appendSlice(a, e);
+ i = j + 1;
+ }
+ }
+ if (i < s.len) try o.appendSlice(a, s[i..]);
+}
+
+fn truthy(v: anytype) bool {
+ return switch (@typeInfo(@TypeOf(v))) {
+ .bool => v,
+ .optional => v != null,
+ .pointer => |p| if (p.size == .slice) v.len > 0 else true,
+ .int, .comptime_int => v != 0,
+ else => true,
+ };
+}
+
+var int_buf: [32]u8 = undefined;
+
+fn strVal(v: anytype) []const u8 {
+ const T = @TypeOf(v);
+ switch (@typeInfo(T)) {
+ .pointer => |p| switch (p.size) {
+ .slice => return v,
+ .one => {
+ // For pointer-to-array, slice it
+ const child_info = @typeInfo(p.child);
+ if (child_info == .array) {
+ const arr_info = child_info.array;
+ const ptr: [*]const arr_info.child = @ptrCast(v);
+ return ptr[0..arr_info.len];
+ }
+ return strVal(v.*);
+ },
+ else => @compileError("unsupported pointer type"),
+ },
+ .array => @compileError("arrays must be passed by pointer"),
+ .int, .comptime_int => return std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0",
+ .optional => return if (v) |val| strVal(val) else "",
+ else => @compileError("strVal: unsupported type " ++ @typeName(T)),
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Templates
+// ─────────────────────────────────────────────────────────────────────────────
+
+pub fn index(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "
hellosome thing
ballahballah");
+ {
+ const text = "click me ";
+ const @"type" = "secondary";
+ try o.appendSlice(a, "");
+ }
+ try o.appendSlice(a, "
Google 1
Google 2
Google 3");
+ _ = d;
+ return o.items;
+}
+
+pub fn sub_layout(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "My Site - ");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ return o.items;
+}
+
+pub fn _404(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "Route no found
";
+}
+
+pub fn mixins_alert(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "";
+}
+
+pub fn mixins_buttons(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "";
+}
+
+pub fn mixins_cards(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "";
+}
+
+pub fn mixins_alert_error(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "";
+}
+
+pub fn mixins_input_text(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "";
+}
+
+pub fn home(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "
");
+ if (truthy(@field(d, "authenticated"))) {
+ try o.appendSlice(a, "Welcome back!");
+ }
+ try o.appendSlice(a, "This page is rendered using a compiled template.
Compiled templates are 3x faster than Pug.js!
");
+ return o.items;
+}
+
+pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "My Site - ");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "
Welcome to the pets page!
");
+ for (@field(d, "items")) |val| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(val));
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
one
two
sdfsdfsbtn
");
+ return o.items;
+}
+
+pub fn page_b(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "My Site - ");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ return o.items;
+}
+
+pub fn layout_2(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "";
+}
+
+pub fn layout(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "My Site - ");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ return o.items;
+}
+
+pub fn page_append(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ _ = .{ a, d };
+ return "cheks manually the head section
hello there
";
+}
+
+pub fn users(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "UsersUser List
");
+ for (@field(d, "users")) |user| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(user.name));
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(user.email));
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub fn page_appen_optional_blk(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "My Site - ");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ return o.items;
+}
+
+pub fn pet(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(@field(d, "petName")));
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub const template_names = [_][]const u8{
+ "index",
+ "sub_layout",
+ "_404",
+ "mixins_alert",
+ "mixins_buttons",
+ "mixins_cards",
+ "mixins_alert_error",
+ "mixins_input_text",
+ "home",
+ "page_a",
+ "page_b",
+ "layout_2",
+ "layout",
+ "page_append",
+ "users",
+ "page_appen_optional_blk",
+ "pet",
+};
diff --git a/src/benchmarks/templates/generated.zig b/src/benchmarks/templates/generated.zig
new file mode 100644
index 0000000..cbaa362
--- /dev/null
+++ b/src/benchmarks/templates/generated.zig
@@ -0,0 +1,279 @@
+//! Auto-generated by pugz.compileTemplates()
+//! Do not edit manually - regenerate by running: zig build
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArrayList = std.ArrayList(u8);
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Helpers
+// ─────────────────────────────────────────────────────────────────────────────
+
+const esc_lut: [256]?[]const u8 = blk: {
+ var t: [256]?[]const u8 = .{null} ** 256;
+ t['&'] = "&";
+ t['<'] = "<";
+ t['>'] = ">";
+ t['"'] = """;
+ t['\''] = "'";
+ break :blk t;
+};
+
+fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void {
+ var i: usize = 0;
+ for (s, 0..) |c, j| {
+ if (esc_lut[c]) |e| {
+ if (j > i) try o.appendSlice(a, s[i..j]);
+ try o.appendSlice(a, e);
+ i = j + 1;
+ }
+ }
+ if (i < s.len) try o.appendSlice(a, s[i..]);
+}
+
+fn truthy(v: anytype) bool {
+ return switch (@typeInfo(@TypeOf(v))) {
+ .bool => v,
+ .optional => v != null,
+ .pointer => |p| if (p.size == .slice) v.len > 0 else true,
+ .int, .comptime_int => v != 0,
+ else => true,
+ };
+}
+
+var int_buf: [32]u8 = undefined;
+
+fn strVal(v: anytype) []const u8 {
+ const T = @TypeOf(v);
+ switch (@typeInfo(T)) {
+ .pointer => |p| switch (p.size) {
+ .slice => return v,
+ .one => {
+ // For pointer-to-array, slice it
+ const child_info = @typeInfo(p.child);
+ if (child_info == .array) {
+ const arr_info = child_info.array;
+ const ptr: [*]const arr_info.child = @ptrCast(v);
+ return ptr[0..arr_info.len];
+ }
+ return strVal(v.*);
+ },
+ else => @compileError("unsupported pointer type"),
+ },
+ .array => @compileError("arrays must be passed by pointer"),
+ .int, .comptime_int => return std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0",
+ .optional => return if (v) |val| strVal(val) else "",
+ else => @compileError("strVal: unsupported type " ++ @typeName(T)),
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Templates
+// ─────────────────────────────────────────────────────────────────────────────
+
+pub fn simple_2(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "");
+ for (@field(d, "list")) |item| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(item));
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub fn simple_1(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "Hello ");
+ try esc(&o, a, strVal(@field(d, "name")));
+ try o.appendSlice(a, "!You have ");
+ try esc(&o, a, strVal(@field(d, "messageCount")));
+ try o.appendSlice(a, " messages!");
+ if (truthy(@field(d, "colors"))) {
+ try o.appendSlice(a, "
");
+ for (@field(d, "colors")) |color| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(color));
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ } else {
+ try o.appendSlice(a, "
No colors!
");
+ }
+ try o.appendSlice(a, "
");
+ if (truthy(@field(d, "primary"))) {
+ try o.appendSlice(a, "
");
+ } else {
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub fn simple_0(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "Hello, ");
+ try esc(&o, a, strVal(@field(d, "name")));
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub fn projects_escaped(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(@field(d, "title")));
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(@field(d, "text")));
+ try o.appendSlice(a, "
");
+ if (@field(d, "projects").len > 0) {
+ for (@field(d, "projects")) |project| {
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(project.description));
+ try o.appendSlice(a, "
");
+ }
+ } else {
+ try o.appendSlice(a, "No projects
");
+ }
+ try o.appendSlice(a, "");
+ return o.items;
+}
+
+pub fn friends(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "Friends");
+ for (@field(d, "friends")) |friend| {
+ try o.appendSlice(a, "
- Name: ");
+ try esc(&o, a, strVal(friend.name));
+ try o.appendSlice(a, "
- Balance: ");
+ try esc(&o, a, strVal(friend.balance));
+ try o.appendSlice(a, "
- Age: ");
+ try esc(&o, a, strVal(friend.age));
+ try o.appendSlice(a, "
- Address: ");
+ try esc(&o, a, strVal(friend.address));
+ try o.appendSlice(a, "
- Image:
;)
- Company: ");
+ try esc(&o, a, strVal(friend.company));
+ try o.appendSlice(a, "
- Email:
- About: ");
+ try esc(&o, a, strVal(friend.about));
+ try o.appendSlice(a, "
");
+ if (truthy(friend.tags)) {
+ try o.appendSlice(a, "- Tags:
");
+ for (if (@typeInfo(@TypeOf(friend.tags)) == .optional) (friend.tags orelse &.{}) else friend.tags) |tag| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(tag));
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ }
+ if (truthy(friend.friends)) {
+ try o.appendSlice(a, "- Friends:
");
+ for (if (@typeInfo(@TypeOf(friend.friends)) == .optional) (friend.friends orelse &.{}) else friend.friends) |subFriend| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(subFriend.name));
+ try o.appendSlice(a, " (");
+ try esc(&o, a, strVal(subFriend.id));
+ try o.appendSlice(a, ")
");
+ }
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub fn search_results(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ try o.appendSlice(a, "");
+ for (@field(d, "searchRecords")) |searchRecord| {
+ try o.appendSlice(a, "
");
+ try esc(&o, a, strVal(searchRecord.description));
+ if (truthy(searchRecord.featured)) {
+ try o.appendSlice(a, "
Featured!
");
+ }
+ if (truthy(searchRecord.sizes)) {
+ try o.appendSlice(a, "
Sizes available:
");
+ for (if (@typeInfo(@TypeOf(searchRecord.sizes)) == .optional) (searchRecord.sizes orelse &.{}) else searchRecord.sizes) |size| {
+ try o.appendSlice(a, "- ");
+ try esc(&o, a, strVal(size));
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ return o.items;
+}
+
+pub fn if_expression(a: Allocator, d: anytype) Allocator.Error![]u8 {
+ var o: ArrayList = .empty;
+ for (@field(d, "accounts")) |account| {
+ try o.appendSlice(a, "");
+ if (std.mem.eql(u8, strVal(account.status), "closed")) {
+ try o.appendSlice(a, "
Your account has been closed!
");
+ }
+ if (std.mem.eql(u8, strVal(account.status), "suspended")) {
+ try o.appendSlice(a, "
Your account has been temporarily suspended
");
+ }
+ if (std.mem.eql(u8, strVal(account.status), "open")) {
+ try o.appendSlice(a, "
Bank balance:");
+ if (truthy(account.negative)) {
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(account.balanceFormatted));
+ try o.appendSlice(a, "");
+ } else {
+ try o.appendSlice(a, "");
+ try esc(&o, a, strVal(account.balanceFormatted));
+ try o.appendSlice(a, "");
+ }
+ try o.appendSlice(a, "
");
+ }
+ try o.appendSlice(a, "
");
+ }
+ return o.items;
+}
+
+pub const template_names = [_][]const u8{
+ "simple_2",
+ "simple_1",
+ "simple_0",
+ "projects_escaped",
+ "friends",
+ "search_results",
+ "if_expression",
+};
diff --git a/src/build_templates.zig b/src/build_templates.zig
index c182f61..7bb8181 100644
--- a/src/build_templates.zig
+++ b/src/build_templates.zig
@@ -702,6 +702,7 @@ const Compiler = struct {
/// Appends string content with normalized whitespace (for backtick template literals).
/// Collapses newlines and multiple spaces into single spaces, trims leading/trailing whitespace.
+ /// Also HTML-escapes double quotes to " for valid HTML attribute values.
fn appendNormalizedWhitespace(self: *Compiler, s: []const u8) !void {
var in_whitespace = true; // Start true to skip leading whitespace
for (s) |c| {
@@ -713,7 +714,8 @@ const Compiler = struct {
} else {
const escaped: []const u8 = switch (c) {
'\\' => "\\\\",
- '"' => "\\\"",
+ // Escape double quotes as HTML entity for valid attribute values
+ '"' => """,
else => &[_]u8{c},
};
try self.buf.appendSlice(self.allocator, escaped);
@@ -776,16 +778,25 @@ const Compiler = struct {
try self.appendStatic("\"");
}
- if (e.classes.len > 0) {
- try self.appendStatic(" class=\"");
- for (e.classes, 0..) |cls, i| {
- if (i > 0) try self.appendStatic(" ");
- try self.appendStatic(cls);
+ // Check if there's a class attribute that needs to be merged with shorthand classes
+ var class_attr_value: ?[]const u8 = null;
+ var class_attr_escaped: bool = true;
+ for (e.attributes) |attr| {
+ if (std.mem.eql(u8, attr.name, "class")) {
+ class_attr_value = attr.value;
+ class_attr_escaped = attr.escaped;
+ break;
}
- try self.appendStatic("\"");
}
+ // Emit merged class attribute (shorthand classes + class attribute value)
+ if (e.classes.len > 0 or class_attr_value != null) {
+ try self.emitMergedClassAttribute(e.classes, class_attr_value, class_attr_escaped);
+ }
+
+ // Emit other attributes (skip class since we handled it above)
for (e.attributes) |attr| {
+ if (std.mem.eql(u8, attr.name, "class")) continue; // Already handled
if (attr.value) |v| {
try self.emitAttribute(attr.name, v, attr.escaped);
} else {
@@ -822,6 +833,96 @@ const Compiler = struct {
try self.appendStatic(">");
}
+ /// Emits a merged class attribute combining shorthand classes and class attribute value
+ fn emitMergedClassAttribute(self: *Compiler, shorthand_classes: []const []const u8, attr_value: ?[]const u8, escaped: bool) !void {
+ _ = escaped;
+
+ if (attr_value) |value| {
+ // Check for string concatenation first: "literal" + variable
+ if (findConcatOperator(value)) |concat_pos| {
+ // Has concatenation - need runtime handling
+ try self.flush();
+ try self.writeIndent();
+ try self.writer.writeAll("try o.appendSlice(a, \" class=\\\"\");\n");
+
+ // Add shorthand classes first
+ if (shorthand_classes.len > 0) {
+ try self.writeIndent();
+ try self.writer.writeAll("try o.appendSlice(a, \"");
+ for (shorthand_classes, 0..) |cls, i| {
+ if (i > 0) try self.writer.writeAll(" ");
+ try self.writer.writeAll(cls);
+ }
+ try self.writer.writeAll(" \");\n"); // trailing space before concat value
+ }
+
+ // Emit the concatenation expression
+ try self.emitConcatExpr(value, concat_pos);
+
+ try self.writeIndent();
+ try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n");
+ return;
+ }
+
+ // Check if attribute value is static (string literal) or dynamic
+ const is_static = value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`');
+ const is_array = value.len >= 2 and value[0] == '[' and value[value.len - 1] == ']';
+
+ if (is_static or is_array) {
+ // Static value - can merge at compile time
+ try self.appendStatic(" class=\"");
+ // First add shorthand classes
+ for (shorthand_classes, 0..) |cls, i| {
+ if (i > 0) try self.appendStatic(" ");
+ try self.appendStatic(cls);
+ }
+ // Then add attribute value
+ if (shorthand_classes.len > 0) try self.appendStatic(" ");
+ if (is_array) {
+ try self.appendStatic(parseArrayToSpaceSeparated(value));
+ } else if (value[0] == '`') {
+ try self.appendNormalizedWhitespace(value[1 .. value.len - 1]);
+ } else {
+ try self.appendStatic(value[1 .. value.len - 1]);
+ }
+ try self.appendStatic("\"");
+ } else {
+ // Dynamic value - need runtime concatenation
+ try self.flush();
+ try self.writeIndent();
+ try self.writer.writeAll("try o.appendSlice(a, \" class=\\\"\");\n");
+
+ // Add shorthand classes first
+ if (shorthand_classes.len > 0) {
+ try self.writeIndent();
+ try self.writer.writeAll("try o.appendSlice(a, \"");
+ for (shorthand_classes, 0..) |cls, i| {
+ if (i > 0) try self.writer.writeAll(" ");
+ try self.writer.writeAll(cls);
+ }
+ try self.writer.writeAll(" \");\n"); // trailing space before dynamic value
+ }
+
+ // Add dynamic value
+ 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");
+ }
+ } else {
+ // No attribute value, just shorthand classes
+ try self.appendStatic(" class=\"");
+ for (shorthand_classes, 0..) |cls, i| {
+ if (i > 0) try self.appendStatic(" ");
+ try self.appendStatic(cls);
+ }
+ try self.appendStatic("\"");
+ }
+ }
+
fn emitText(self: *Compiler, segs: []const ast.TextSegment) anyerror!void {
for (segs) |seg| {
switch (seg) {