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, "hello

some thing

ballahballah"); + { + const text = "click me "; + const @"type" = "secondary"; + try o.appendSlice(a, ""); + try esc(&o, a, strVal(text)); + 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, "

nothing

nothing

some footer content

"); + 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!


one
two
sdfsdfsbtn
"); + { + const name = "firstName"; + const label = "First Name"; + const placeholder = "first name"; + try o.appendSlice(a, "
"); + try esc(&o, a, strVal(label)); + try o.appendSlice(a, "
"); + } + try o.appendSlice(a, "
"); + { + const name = "lastName"; + const label = "Last Name"; + const placeholder = "last name"; + try o.appendSlice(a, "
"); + try esc(&o, a, strVal(label)); + try o.appendSlice(a, "
"); + } + try o.appendSlice(a, "sumit"); + if (truthy(@field(d, "error"))) { + { + const message = @field(d, "error"); + { + try o.appendSlice(a, ""); + try esc(&o, a, strVal(message)); + try o.appendSlice(a, ""); + } + } + } + try o.appendSlice(a, "

some footer content

"); + 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, "

nothing

nothing

some footer content

"); + 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, "

some footer content

"); + 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, "Users

User 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, "

some footer content

"); + 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, "

"); + try esc(&o, a, strVal(@field(d, "header"))); + try o.appendSlice(a, "

"); + try esc(&o, a, strVal(@field(d, "header2"))); + try o.appendSlice(a, "

"); + try esc(&o, a, strVal(@field(d, "header3"))); + try o.appendSlice(a, "

"); + try esc(&o, a, strVal(@field(d, "header4"))); + try o.appendSlice(a, "

"); + try esc(&o, a, strVal(@field(d, "header5"))); + try o.appendSlice(a, "
"); + try esc(&o, a, strVal(@field(d, "header6"))); + 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.title)); + 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) {