Initial commit: Pugz - Pug-like HTML template engine in Zig

Features:
- Lexer with indentation tracking and raw text block support
- Parser producing AST from token stream
- Runtime with variable interpolation, conditionals, loops
- Mixin support (params, defaults, rest args, block content, attributes)
- Template inheritance (extends/block/append/prepend)
- Plain text (piped, dot blocks, literal HTML)
- Tag interpolation (#[tag text])
- Block expansion with colon
- Self-closing tags (void elements + explicit /)
- Case/when statements
- Comments (rendered and silent)

All 113 tests passing.
This commit is contained in:
2026-01-17 18:32:29 +05:30
parent 71f4ec4ffc
commit 6ab3f14897
28 changed files with 7693 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
//! Pugz Template Inheritance Demo
//!
//! A web application demonstrating Pug-style template inheritance
//! using the Pugz template engine with http.zig server.
//!
//! Routes:
//! GET / - Home page (layout.pug)
//! GET /page-a - Page A with custom scripts and content
//! GET /page-b - Page B with sub-layout
//! GET /append - Page with block append
//! GET /append-opt - Page with optional block syntax
const std = @import("std");
const httpz = @import("httpz");
const pugz = @import("pugz");
const Allocator = std.mem.Allocator;
/// Application state shared across all requests
const App = struct {
allocator: Allocator,
views_dir: []const u8,
/// File resolver for loading templates from disk
pub fn fileResolver(allocator: Allocator, path: []const u8) ?[]const u8 {
const file = std.fs.cwd().openFile(path, .{}) catch return null;
defer file.close();
return file.readToEndAlloc(allocator, 1024 * 1024) catch null;
}
/// Render a template with data
pub fn render(self: *App, template_name: []const u8, data: anytype) ![]u8 {
// Build full path
const template_path = try std.fs.path.join(self.allocator, &.{ self.views_dir, template_name });
defer self.allocator.free(template_path);
// Load template source
const source = fileResolver(self.allocator, template_path) orelse {
return error.TemplateNotFound;
};
defer self.allocator.free(source);
// Parse template
var lexer = pugz.Lexer.init(self.allocator, source);
const tokens = try lexer.tokenize();
var parser = pugz.Parser.init(self.allocator, tokens);
const doc = try parser.parse();
// Setup context with data
var ctx = pugz.runtime.Context.init(self.allocator);
defer ctx.deinit();
try ctx.pushScope();
inline for (std.meta.fields(@TypeOf(data))) |field| {
const value = @field(data, field.name);
try ctx.set(field.name, pugz.runtime.toValue(self.allocator, value));
}
// Render with file resolver for includes/extends
var runtime = pugz.runtime.Runtime.init(self.allocator, &ctx, .{
.file_resolver = fileResolver,
.base_dir = self.views_dir,
});
defer runtime.deinit();
return runtime.renderOwned(doc);
}
};
/// Handler for GET /
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.render("layout.pug", .{
.title = "Home",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /page-a - demonstrates extends and block override
fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.render("page-a.pug", .{
.title = "Page A - Pets",
.items = &[_][]const u8{ "A", "B", "C" },
.n = 0,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /page-b - demonstrates sub-layout inheritance
fn pageB(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.render("page-b.pug", .{
.title = "Page B - Sub Layout",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /append - demonstrates block append
fn pageAppend(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.render("page-append.pug", .{
.title = "Page Append",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /append-opt - demonstrates optional block keyword
fn pageAppendOptional(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.render("page-appen-optional-blk.pug", .{
.title = "Page Append Optional",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Views directory - relative to current working directory
const views_dir = "src/examples/app_01/views";
var app = App{
.allocator = allocator,
.views_dir = views_dir,
};
var server = try httpz.Server(*App).init(allocator, .{ .port = 8080 }, &app);
defer server.deinit();
var router = try server.router(.{});
// Routes
router.get("/", index, .{});
router.get("/page-a", pageA, .{});
router.get("/page-b", pageB, .{});
router.get("/append", pageAppend, .{});
router.get("/append-opt", pageAppendOptional, .{});
std.debug.print(
\\
\\Pugz Template Inheritance Demo
\\==============================
\\Server running at http://localhost:8080
\\
\\Routes:
\\ GET / - Home page (base layout)
\\ GET /page-a - Page with custom scripts and content blocks
\\ GET /page-b - Page with sub-layout inheritance
\\ GET /append - Page with block append
\\ GET /append-opt - Page with optional block keyword
\\
\\Press Ctrl+C to stop.
\\
, .{});
try server.listen();
}