//! Pugz Compiler - Compiles Pug templates to efficient Zig functions. //! //! Generates Zig source code that can be @import'd and called directly, //! avoiding AST interpretation overhead entirely. const std = @import("std"); const ast = @import("ast.zig"); const Lexer = @import("lexer.zig").Lexer; const Parser = @import("parser.zig").Parser; /// Compiles a Pug source string to a Zig function. pub fn compileSource(allocator: std.mem.Allocator, name: []const u8, source: []const u8) ![]u8 { var lexer = Lexer.init(allocator, source); defer lexer.deinit(); const tokens = try lexer.tokenize(); var parser = Parser.init(allocator, tokens); const doc = try parser.parse(); return compileDoc(allocator, name, doc); } /// Compiles an AST Document to a Zig function. pub fn compileDoc(allocator: std.mem.Allocator, name: []const u8, doc: ast.Document) ![]u8 { var c = Compiler.init(allocator); defer c.deinit(); return c.compile(name, doc); } const Compiler = struct { alloc: std.mem.Allocator, out: std.ArrayList(u8), depth: u8, fn init(allocator: std.mem.Allocator) Compiler { return .{ .alloc = allocator, .out = .{}, .depth = 0, }; } fn deinit(self: *Compiler) void { self.out.deinit(self.alloc); } fn compile(self: *Compiler, name: []const u8, doc: ast.Document) ![]u8 { // Header try self.w( \\const std = @import("std"); \\ \\/// HTML escape lookup table \\const esc_table = blk: { \\ var t: [256]?[]const u8 = .{null} ** 256; \\ t['&'] = "&"; \\ t['<'] = "<"; \\ t['>'] = ">"; \\ t['"'] = """; \\ t['\''] = "'"; \\ break :blk t; \\}; \\ \\fn esc(out: *std.ArrayList(u8), s: []const u8) !void { \\ var i: usize = 0; \\ for (s, 0..) |c, j| { \\ if (esc_table[c]) |e| { \\ if (j > i) try out.appendSlice(s[i..j]); \\ try out.appendSlice(e); \\ i = j + 1; \\ } \\ } \\ if (i < s.len) try out.appendSlice(s[i..]); \\} \\ \\fn toStr(v: anytype) []const u8 { \\ const T = @TypeOf(v); \\ if (T == []const u8) return v; \\ if (@typeInfo(T) == .optional) { \\ if (v) |inner| return toStr(inner); \\ return ""; \\ } \\ return ""; \\} \\ \\ ); // Function signature try self.w("pub fn "); try self.w(name); try self.w("(out: *std.ArrayList(u8), data: anytype) !void {\n"); self.depth = 1; // Body for (doc.nodes) |n| { try self.node(n); } try self.w("}\n"); return try self.alloc.dupe(u8, self.out.items); } fn node(self: *Compiler, n: ast.Node) anyerror!void { switch (n) { .doctype => |d| try self.doctype(d), .element => |e| try self.element(e), .text => |t| try self.text(t.segments), .conditional => |c| try self.conditional(c), .each => |e| try self.each(e), .raw_text => |r| try self.raw(r.content), .comment => |c| if (c.rendered) try self.comment(c), .code => |c| try self.code(c), .document => |d| for (d.nodes) |child| try self.node(child), .mixin_def, .mixin_call, .mixin_block, .@"while", .case, .block, .include, .extends => {}, } } fn doctype(self: *Compiler, d: ast.Doctype) !void { try self.indent(); if (std.mem.eql(u8, d.value, "html")) { try self.w("try out.appendSlice(\"\");\n"); } else { try self.w("try out.appendSlice(\"\");\n"); } } fn element(self: *Compiler, e: ast.Element) anyerror!void { const is_void = isVoid(e.tag) or e.self_closing; // Open tag try self.indent(); try self.w("try out.appendSlice(\"<"); try self.w(e.tag); // ID if (e.id) |id| { try self.w(" id=\\\""); try self.wEsc(id); try self.w("\\\""); } // Classes if (e.classes.len > 0) { try self.w(" class=\\\""); for (e.classes, 0..) |cls, i| { if (i > 0) try self.w(" "); try self.wEsc(cls); } try self.w("\\\""); } // Static attributes (close the appendSlice, handle dynamic separately) var has_dynamic = false; for (e.attributes) |attr| { if (attr.value) |v| { if (isDynamic(v)) { has_dynamic = true; continue; } try self.w(" "); try self.w(attr.name); try self.w("=\\\""); try self.wEsc(stripQuotes(v)); try self.w("\\\""); } else { try self.w(" "); try self.w(attr.name); try self.w("=\\\""); try self.w(attr.name); try self.w("\\\""); } } if (is_void and !has_dynamic) { try self.w(" />\");\n"); return; } if (!has_dynamic and e.inline_text == null and e.buffered_code == null) { try self.w(">\");\n"); } else { try self.w("\");\n"); } // Dynamic attributes for (e.attributes) |attr| { if (attr.value) |v| { if (isDynamic(v)) { try self.indent(); try self.w("try out.appendSlice(\" "); try self.w(attr.name); try self.w("=\\\"\");\n"); try self.indent(); try self.expr(v, attr.escaped); try self.indent(); try self.w("try out.appendSlice(\"\\\"\");\n"); } } } if (has_dynamic or e.inline_text != null or e.buffered_code != null) { try self.indent(); if (is_void) { try self.w("try out.appendSlice(\" />\");\n"); return; } try self.w("try out.appendSlice(\">\");\n"); } // Inline text if (e.inline_text) |segs| { try self.text(segs); } // Buffered code (p= expr) if (e.buffered_code) |bc| { try self.indent(); try self.expr(bc.expression, bc.escaped); } // Children self.depth += 1; for (e.children) |child| { try self.node(child); } self.depth -= 1; // Close tag try self.indent(); try self.w("try out.appendSlice(\"\");\n"); } fn text(self: *Compiler, segs: []const ast.TextSegment) anyerror!void { for (segs) |seg| { switch (seg) { .literal => |lit| { try self.indent(); try self.w("try out.appendSlice(\""); try self.wEsc(lit); try self.w("\");\n"); }, .interp_escaped => |e| { try self.indent(); try self.expr(e, true); }, .interp_unescaped => |e| { try self.indent(); try self.expr(e, false); }, .interp_tag => |t| try self.inlineTag(t), } } } fn inlineTag(self: *Compiler, t: ast.InlineTag) anyerror!void { try self.indent(); try self.w("try out.appendSlice(\"<"); try self.w(t.tag); if (t.id) |id| { try self.w(" id=\\\""); try self.wEsc(id); try self.w("\\\""); } if (t.classes.len > 0) { try self.w(" class=\\\""); for (t.classes, 0..) |cls, i| { if (i > 0) try self.w(" "); try self.wEsc(cls); } try self.w("\\\""); } try self.w(">\");\n"); try self.text(t.text_segments); try self.indent(); try self.w("try out.appendSlice(\"\");\n"); } fn conditional(self: *Compiler, c: ast.Conditional) anyerror!void { for (c.branches, 0..) |br, i| { try self.indent(); if (i == 0) { if (br.is_unless) { try self.w("if (!"); } else { try self.w("if ("); } try self.cond(br.condition orelse "true"); try self.w(") {\n"); } else if (br.condition) |cnd| { try self.w("} else if ("); try self.cond(cnd); try self.w(") {\n"); } else { try self.w("} else {\n"); } self.depth += 1; for (br.children) |child| try self.node(child); self.depth -= 1; } try self.indent(); try self.w("}\n"); } fn cond(self: *Compiler, c: []const u8) !void { // Check for field access: convert "field" to "@hasField(...) and data.field" // and "obj.field" to "obj.field" (assuming obj is a loop var) if (std.mem.indexOfScalar(u8, c, '.')) |_| { try self.w(c); } else { try self.w("@hasField(@TypeOf(data), \""); try self.w(c); try self.w("\") and @field(data, \""); try self.w(c); try self.w("\") != null"); } } fn each(self: *Compiler, e: ast.Each) anyerror!void { // Parse collection - could be "items" or "obj.items" const col = e.collection; try self.indent(); if (std.mem.indexOfScalar(u8, col, '.')) |dot| { // Nested: for (parent.field) |item| try self.w("for ("); try self.w(col[0..dot]); try self.w("."); try self.w(col[dot + 1 ..]); try self.w(") |"); } else { // Top-level: for (data.field) |item| try self.w("if (@hasField(@TypeOf(data), \""); try self.w(col); try self.w("\")) {\n"); self.depth += 1; try self.indent(); try self.w("for (@field(data, \""); try self.w(col); try self.w("\")) |"); } try self.w(e.value_name); if (e.index_name) |idx| { try self.w(", "); try self.w(idx); } try self.w("| {\n"); self.depth += 1; for (e.children) |child| try self.node(child); self.depth -= 1; try self.indent(); try self.w("}\n"); // Close the hasField block for top-level if (std.mem.indexOfScalar(u8, col, '.') == null) { self.depth -= 1; try self.indent(); try self.w("}\n"); } } fn code(self: *Compiler, c: ast.Code) !void { try self.indent(); try self.expr(c.expression, c.escaped); } fn expr(self: *Compiler, e: []const u8, escaped: bool) !void { // Parse: "name" (data field), "item.name" (loop var field) if (std.mem.indexOfScalar(u8, e, '.')) |dot| { const base = e[0..dot]; const field = e[dot + 1 ..]; if (escaped) { try self.w("try esc(out, toStr("); try self.w(base); try self.w("."); try self.w(field); try self.w("));\n"); } else { try self.w("try out.appendSlice(toStr("); try self.w(base); try self.w("."); try self.w(field); try self.w("));\n"); } } else { if (escaped) { try self.w("try esc(out, toStr(@field(data, \""); try self.w(e); try self.w("\")));\n"); } else { try self.w("try out.appendSlice(toStr(@field(data, \""); try self.w(e); try self.w("\")));\n"); } } } fn raw(self: *Compiler, content: []const u8) !void { try self.indent(); try self.w("try out.appendSlice(\""); try self.wEsc(content); try self.w("\");\n"); } fn comment(self: *Compiler, c: ast.Comment) !void { try self.indent(); try self.w("try out.appendSlice(\"\");\n"); } // Helpers fn indent(self: *Compiler) !void { for (0..self.depth) |_| try self.out.appendSlice(self.alloc, " "); } fn w(self: *Compiler, s: []const u8) !void { try self.out.appendSlice(self.alloc, s); } fn wEsc(self: *Compiler, s: []const u8) !void { for (s) |c| { switch (c) { '\\' => try self.out.appendSlice(self.alloc, "\\\\"), '"' => try self.out.appendSlice(self.alloc, "\\\""), '\n' => try self.out.appendSlice(self.alloc, "\\n"), '\r' => try self.out.appendSlice(self.alloc, "\\r"), '\t' => try self.out.appendSlice(self.alloc, "\\t"), else => try self.out.append(self.alloc, c), } } } }; fn isDynamic(v: []const u8) bool { if (v.len < 2) return true; return v[0] != '"' and v[0] != '\''; } fn stripQuotes(v: []const u8) []const u8 { if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) { return v[1 .. v.len - 1]; } return v; } fn isVoid(tag: []const u8) bool { const voids = std.StaticStringMap(void).initComptime(.{ .{ "area", {} }, .{ "base", {} }, .{ "br", {} }, .{ "col", {} }, .{ "embed", {} }, .{ "hr", {} }, .{ "img", {} }, .{ "input", {} }, .{ "link", {} }, .{ "meta", {} }, .{ "param", {} }, .{ "source", {} }, .{ "track", {} }, .{ "wbr", {} }, }); return voids.has(tag); } test "compile simple template" { const allocator = std.testing.allocator; const source = "p Hello"; const code = try compileSource(allocator, "simple", source); defer allocator.free(code); std.debug.print("\n{s}\n", .{code}); }