diff --git a/README.md b/README.md index 99e9454..ea58f93 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pugz -A Pug template engine for Zig. +A Pug template engine for Zig, supporting both build-time compilation and runtime interpretation. ## Features @@ -23,27 +23,80 @@ zig fetch --save "git+https://github.com/ankitpatial/pugz#main" > **Note:** The primary repository is hosted at `code.patial.tech`. GitHub is a mirror. For dependencies, prefer the GitHub mirror for better availability. - -Then in your `build.zig`, add the `pugz` module as a dependency: - -```zig -const pugz = b.dependency("pugz", .{ - .target = target, - .optimize = optimize, -}); -exe.root_module.addImport("pugz", pugz.module("pugz")); -``` +--- ## Usage -**Important:** Always use an arena allocator for rendering. The render function creates many small allocations that should be freed together. Using a general-purpose allocator without freeing will cause memory leaks. +### Compiled Mode (Build-Time) + +Templates are converted to native Zig code at build time. No parsing happens at runtime. + +**build.zig:** + +```zig +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const pugz_dep = b.dependency("pugz", .{ + .target = target, + .optimize = optimize, + }); + + const build_templates = @import("pugz").build_templates; + const compiled_templates = build_templates.compileTemplates(b, .{ + .source_dir = "views", + }); + + const exe = b.addExecutable(.{ + .name = "myapp", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "pugz", .module = pugz_dep.module("pugz") }, + .{ .name = "tpls", .module = compiled_templates }, + }, + }), + }); + + b.installArtifact(exe); +} +``` + +**Usage:** + +```zig +const std = @import("std"); +const tpls = @import("tpls"); + +pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + return try tpls.home(arena.allocator(), .{ + .title = "Welcome", + .user = .{ .name = "Alice" }, + .items = &[_][]const u8{ "One", "Two", "Three" }, + }); +} +``` + +--- + +### Interpreted Mode (Runtime) + +Templates are parsed and evaluated at runtime. Useful for development or dynamic templates. ```zig const std = @import("std"); const pugz = @import("pugz"); pub fn main() !void { - const engine = pugz.ViewEngine.init(.{ + var engine = pugz.ViewEngine.init(.{ .views_dir = "views", }); @@ -59,29 +112,10 @@ pub fn main() !void { } ``` -### With http.zig - -When using with http.zig, use `res.arena` which is automatically freed after each response: +**Inline template strings:** ```zig -fn handler(app: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = app.view.render(res.arena, "index", .{ - .title = "Hello", - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} -``` - -### Template String - -```zig -const html = try engine.renderTpl(allocator, +const html = try pugz.renderTemplate(allocator, \\h1 Hello, #{name}! \\ul \\ each item in items @@ -92,67 +126,76 @@ const html = try engine.renderTpl(allocator, }); ``` -## Development +--- -### Run Tests +### With http.zig -```bash -zig build test +```zig +fn handler(_: *App, _: *httpz.Request, res: *httpz.Response) !void { + // Compiled mode + const html = try tpls.home(res.arena, .{ + .title = "Hello", + }); + + res.content_type = .HTML; + res.body = html; +} ``` -### Run Benchmarks +--- -```bash -zig build bench # Run rendering benchmarks -zig build bench-2 # Run comparison benchmarks +## Memory Management + +Always use an `ArenaAllocator` for rendering. Template rendering creates many small allocations that should be freed together. + +```zig +var arena = std.heap.ArenaAllocator.init(allocator); +defer arena.deinit(); + +const html = try engine.render(arena.allocator(), "index", data); ``` -## Template Syntax +--- -```pug -doctype html -html - head - title= title - body - h1.header Hello, #{name}! - - if authenticated - p Welcome back! - else - a(href="/login") Sign in - - ul - each item in items - li= item -``` +## Documentation + +- [Template Syntax](docs/syntax.md) - Complete syntax reference +- [API Reference](docs/api.md) - Detailed API documentation + +--- ## Benchmarks -### Rendering Benchmarks (`zig build bench`) +Same templates and data (`src/benchmarks/templates/`), MacBook Air M2, 2000 iterations, best of 5 runs. -20,000 iterations on MacBook Air M2: +| Template | Pug.js | Pugz Compiled | Diff | Pugz Interpreted | Diff | +|----------|--------|---------------|------|------------------|------| +| simple-0 | 0.4ms | 0.1ms | +4x | 0.4ms | 1x | +| simple-1 | 1.3ms | 0.6ms | +2.2x | 5.8ms | -4.5x | +| simple-2 | 1.6ms | 0.5ms | +3.2x | 4.6ms | -2.9x | +| if-expression | 0.5ms | 0.2ms | +2.5x | 4.1ms | -8.2x | +| projects-escaped | 4.2ms | 0.6ms | +7x | 5.8ms | -1.4x | +| search-results | 14.7ms | 5.3ms | +2.8x | 50.7ms | -3.4x | +| friends | 145.5ms | 50.4ms | +2.9x | 450.8ms | -3.1x | -| Template | Avg | Renders/sec | Output | -|----------|-----|-------------|--------| -| Simple | 11.81 us | 84,701 | 155 bytes | -| Medium | 21.10 us | 47,404 | 1,211 bytes | -| Complex | 33.48 us | 29,872 | 4,852 bytes | +- Pug.js and Pugz Compiled: render-only (pre-compiled) +- Pugz Interpreted: parse + render on each iteration +- Diff: +Nx = N times faster, -Nx = N times slower -### Comparison Benchmarks (`zig build bench-2`) -ref: https://github.com/itsarnaud/template-engine-bench +--- -2,000 iterations vs Pug.js: +## Development -| Template | Pugz | Pug.js | Speedup | -|----------|------|--------|---------| -| simple-0 | 0.5ms | 2ms | 3.8x | -| simple-1 | 6.7ms | 9ms | 1.3x | -| simple-2 | 5.4ms | 9ms | 1.7x | -| if-expression | 4.4ms | 12ms | 2.7x | -| projects-escaped | 7.3ms | 86ms | 11.7x | -| search-results | 70.6ms | 41ms | 0.6x | -| friends | 682.1ms | 110ms | 0.2x | +```bash +zig build test # Run all tests +zig build bench-compiled # Benchmark compiled mode +zig build bench-interpreted # Benchmark interpreted mode + +# Pug.js benchmark (for comparison) +cd src/benchmarks/pugjs && npm install && npm run bench +``` + +--- ## License diff --git a/build.zig b/build.zig index ac92f19..9262d0e 100644 --- a/build.zig +++ b/build.zig @@ -1,11 +1,15 @@ const std = @import("std"); +// Re-export build_templates for use by dependent packages +pub const build_templates = @import("src/build_templates.zig"); + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const mod = b.addModule("pugz", .{ .root_source_file = b.path("src/root.zig"), .target = target, + .optimize = optimize, }); // Creates an executable that will run `test` blocks from the provided module. @@ -78,101 +82,62 @@ pub fn build(b: *std.Build) void { test_unit_step.dependOn(&run_mod_tests.step); // ───────────────────────────────────────────────────────────────────────── - // Example: demo - Template Inheritance Demo with http.zig + // Compiled Templates Benchmark (compare with Pug.js bench.js) + // Uses auto-generated templates from src/benchmarks/templates/ // ───────────────────────────────────────────────────────────────────────── - const httpz_dep = b.dependency("httpz", .{ + const mod_fast = b.addModule("pugz-fast", .{ + .root_source_file = b.path("src/root.zig"), .target = target, - .optimize = optimize, + .optimize = .ReleaseFast, }); - const demo = b.addExecutable(.{ - .name = "demo", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/examples/demo/main.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "pugz", .module = mod }, - .{ .name = "httpz", .module = httpz_dep.module("httpz") }, - }, - }), + const bench_templates = build_templates.compileTemplates(b, .{ + .source_dir = "src/benchmarks/templates", }); - b.installArtifact(demo); - - const run_demo = b.addRunArtifact(demo); - run_demo.step.dependOn(b.getInstallStep()); - - const demo_step = b.step("demo", "Run the template inheritance demo web app"); - demo_step.dependOn(&run_demo.step); - - // ───────────────────────────────────────────────────────────────────────── - // Benchmark executable - // ───────────────────────────────────────────────────────────────────────── - const bench = b.addExecutable(.{ - .name = "bench", + const bench_compiled = b.addExecutable(.{ + .name = "bench-compiled", .root_module = b.createModule(.{ - .root_source_file = b.path("src/benchmarks/benchmark.zig"), - .target = target, - .optimize = .ReleaseFast, // Always use ReleaseFast for benchmarks - .imports = &.{ - .{ .name = "pugz", .module = mod }, - }, - }), - }); - - b.installArtifact(bench); - - const run_bench = b.addRunArtifact(bench); - run_bench.step.dependOn(b.getInstallStep()); - - const bench_step = b.step("bench", "Run rendering benchmarks"); - bench_step.dependOn(&run_bench.step); - - // ───────────────────────────────────────────────────────────────────────── - // Comparison Benchmark Tests (template-engine-bench templates) - // Run all: zig build test-bench - // Run one: zig build test-bench -- simple-0 - // ───────────────────────────────────────────────────────────────────────── - const bench_tests = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path("src/benchmarks/benchmark_2.zig"), + .root_source_file = b.path("src/benchmarks/bench.zig"), .target = target, .optimize = .ReleaseFast, .imports = &.{ - .{ .name = "pugz", .module = mod }, + .{ .name = "pugz", .module = mod_fast }, + .{ .name = "tpls", .module = bench_templates }, }, }), - .filters = if (b.args) |args| args else &.{}, }); - const run_bench_tests = b.addRunArtifact(bench_tests); + b.installArtifact(bench_compiled); - const bench_test_step = b.step("bench-2", "Run comparison benchmarks (template-engine-bench)"); - bench_test_step.dependOn(&run_bench_tests.step); + const run_bench_compiled = b.addRunArtifact(bench_compiled); + run_bench_compiled.step.dependOn(b.getInstallStep()); + + const bench_compiled_step = b.step("bench-compiled", "Benchmark compiled templates (compare with Pug.js)"); + bench_compiled_step.dependOn(&run_bench_compiled.step); // ───────────────────────────────────────────────────────────────────────── - // Profile executable (for CPU profiling) + // Interpreted (Runtime) Benchmark // ───────────────────────────────────────────────────────────────────────── - const profile = b.addExecutable(.{ - .name = "profile", + const bench_interpreted = b.addExecutable(.{ + .name = "bench-interpreted", .root_module = b.createModule(.{ - .root_source_file = b.path("src/benchmarks/profile_friends.zig"), + .root_source_file = b.path("src/benchmarks/bench_interpreted.zig"), .target = target, .optimize = .ReleaseFast, .imports = &.{ - .{ .name = "pugz", .module = mod }, + .{ .name = "pugz", .module = mod_fast }, }, }), }); - b.installArtifact(profile); + b.installArtifact(bench_interpreted); - const run_profile = b.addRunArtifact(profile); - run_profile.step.dependOn(b.getInstallStep()); + const run_bench_interpreted = b.addRunArtifact(bench_interpreted); + run_bench_interpreted.step.dependOn(b.getInstallStep()); - const profile_step = b.step("profile", "Run friends template for profiling"); - profile_step.dependOn(&run_profile.step); + const bench_interpreted_step = b.step("bench-interpreted", "Benchmark interpreted (runtime) templates"); + bench_interpreted_step.dependOn(&run_bench_interpreted.step); // Just like flags, top level steps are also listed in the `--help` menu. // diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..bafaae6 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,289 @@ +# Pugz API Reference + +## Compiled Mode + +### Build Setup + +In `build.zig`: + +```zig +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const pugz_dep = b.dependency("pugz", .{ + .target = target, + .optimize = optimize, + }); + + const build_templates = @import("pugz").build_templates; + const compiled_templates = build_templates.compileTemplates(b, .{ + .source_dir = "views", // Required: directory containing .pug files + .extension = ".pug", // Optional: default ".pug" + }); + + const exe = b.addExecutable(.{ + .name = "myapp", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "pugz", .module = pugz_dep.module("pugz") }, + .{ .name = "tpls", .module = compiled_templates }, + }, + }), + }); + + b.installArtifact(exe); +} +``` + +### Using Compiled Templates + +```zig +const std = @import("std"); +const tpls = @import("tpls"); + +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + // Template function name is derived from filename + // views/home.pug -> tpls.home() + // views/pages/about.pug -> tpls.pages_about() + const html = try tpls.home(arena.allocator(), .{ + .title = "Welcome", + .items = &[_][]const u8{ "One", "Two" }, + }); + + std.debug.print("{s}\n", .{html}); +} +``` + +### Template Names + +File paths are converted to function names: +- `home.pug` → `home()` +- `pages/about.pug` → `pages_about()` +- `admin-panel.pug` → `admin_panel()` + +List all available templates: +```zig +for (tpls.template_names) |name| { + std.debug.print("{s}\n", .{name}); +} +``` + +--- + +## Interpreted Mode + +### ViewEngine + +```zig +const std = @import("std"); +const pugz = @import("pugz"); + +pub fn main() !void { + // Initialize engine + var engine = pugz.ViewEngine.init(.{ + .views_dir = "views", // Required: root views directory + .mixins_dir = "mixins", // Optional: default "mixins" + .extension = ".pug", // Optional: default ".pug" + .pretty = true, // Optional: default true + }); + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + // Render template (path relative to views_dir, no extension needed) + const html = try engine.render(arena.allocator(), "pages/home", .{ + .title = "Hello", + .name = "World", + }); + + std.debug.print("{s}\n", .{html}); +} +``` + +### renderTemplate + +For inline template strings: + +```zig +const html = try pugz.renderTemplate(allocator, + \\h1 Hello, #{name}! + \\ul + \\ each item in items + \\ li= item +, .{ + .name = "World", + .items = &[_][]const u8{ "one", "two", "three" }, +}); +``` + +--- + +## Data Types + +Templates accept Zig structs as data. Supported field types: + +| Zig Type | Template Usage | +|----------|----------------| +| `[]const u8` | `#{field}` | +| `i64`, `i32`, etc. | `#{field}` (converted to string) | +| `bool` | `if field` | +| `[]const T` | `each item in field` | +| `?T` (optional) | `if field` (null = false) | +| nested struct | `#{field.subfield}` | + +### Example + +```zig +const data = .{ + .title = "My Page", + .count = 42, + .show_header = true, + .items = &[_][]const u8{ "a", "b", "c" }, + .user = .{ + .name = "Alice", + .email = "alice@example.com", + }, +}; + +const html = try tpls.home(allocator, data); +``` + +Template: +```pug +h1= title +p Count: #{count} +if show_header + header Welcome +ul + each item in items + li= item +p #{user.name} (#{user.email}) +``` + +--- + +## Directory Structure + +Recommended project layout: + +``` +myproject/ +├── build.zig +├── build.zig.zon +├── src/ +│ └── main.zig +└── views/ + ├── mixins/ + │ ├── buttons.pug + │ └── cards.pug + ├── layouts/ + │ └── base.pug + ├── partials/ + │ ├── header.pug + │ └── footer.pug + └── pages/ + ├── home.pug + └── about.pug +``` + +### Mixin Resolution + +Mixins are resolved in order: +1. Defined in the current template +2. Loaded from `views/mixins/*.pug` (lazy-loaded on first use) + +--- + +## Web Framework Integration + +### http.zig + +```zig +const std = @import("std"); +const httpz = @import("httpz"); +const tpls = @import("tpls"); + +const App = struct { + // app state +}; + +fn handler(app: *App, req: *httpz.Request, res: *httpz.Response) !void { + _ = app; + _ = req; + + const html = try tpls.home(res.arena, .{ + .title = "Hello", + }); + + res.content_type = .HTML; + res.body = html; +} +``` + +### Using ViewEngine with http.zig + +```zig +const App = struct { + engine: pugz.ViewEngine, +}; + +fn handler(app: *App, req: *httpz.Request, res: *httpz.Response) !void { + _ = req; + + const html = app.engine.render(res.arena, "home", .{ + .title = "Hello", + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} +``` + +--- + +## Error Handling + +```zig +const html = engine.render(allocator, "home", data) catch |err| { + switch (err) { + error.FileNotFound => // template file not found + error.ParseError => // invalid template syntax + error.OutOfMemory => // allocation failed + else => // other errors + } +}; +``` + +--- + +## Memory Management + +Always use `ArenaAllocator` for template rendering: + +```zig +// Per-request pattern +fn handleRequest(allocator: std.mem.Allocator) ![]u8 { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + return try tpls.home(arena.allocator(), .{ .title = "Hello" }); +} +``` + +The arena pattern is efficient because: +- Template rendering creates many small allocations +- All allocations are freed at once with `arena.deinit()` +- No need to track individual allocations diff --git a/docs/syntax.md b/docs/syntax.md new file mode 100644 index 0000000..d75ad5a --- /dev/null +++ b/docs/syntax.md @@ -0,0 +1,340 @@ +# Pugz Template Syntax + +Complete reference for Pugz template syntax. + +## Tags & Nesting + +Indentation defines nesting. Default tag is `div`. + +```pug +div + h1 Title + p Paragraph +``` + +Output: +```html +
Paragraph
Passed through as-is
+``` + +## Interpolation + +### Escaped (safe) + +```pug +p Hello #{name} +p= variable +``` + +### Unescaped (raw HTML) + +```pug +p Hello !{rawHtml} +p!= rawVariable +``` + +### Tag interpolation + +```pug +p This is #[em emphasized] text +p Click #[a(href="/") here] to continue +``` + +## Conditionals + +### if / else if / else + +```pug +if condition + p Yes +else if other + p Maybe +else + p No +``` + +### unless + +```pug +unless loggedIn + p Please login +``` + +### String comparison + +```pug +if status == "active" + p Active +``` + +## Iteration + +### each + +```pug +each item in items + li= item +``` + +### with index + +```pug +each val, index in list + li #{index}: #{val} +``` + +### with else (empty collection) + +```pug +each item in items + li= item +else + li No items +``` + +### Objects + +```pug +each val, key in object + p #{key}: #{val} +``` + +### Nested iteration + +```pug +each friend in friends + li #{friend.name} + each tag in friend.tags + span= tag +``` + +## Case / When + +```pug +case status + when "active" + p Active + when "pending" + p Pending + default + p Unknown +``` + +## Mixins + +### Basic mixin + +```pug +mixin button(text) + button= text + ++button("Click me") +``` + +### Default parameters + +```pug +mixin button(text, type="primary") + button(class="btn btn-" + type)= text + ++button("Click me") ++button("Submit", "success") +``` + +### Block content + +```pug +mixin card(title) + .card + h3= title + block + ++card("My Card") + p Card content here +``` + +### Rest arguments + +```pug +mixin list(id, ...items) + ul(id=id) + each item in items + li= item + ++list("mylist", "a", "b", "c") +``` + +### Attributes pass-through + +```pug +mixin link(href, text) + a(href=href)&attributes(attributes)= text + ++link("/home", "Home")(class="nav-link" data-id="1") +``` + +## Template Inheritance + +### Base layout (layout.pug) + +```pug +doctype html +html + head + title= title + block styles + body + block content + block scripts +``` + +### Child template + +```pug +extends layout.pug + +block content + h1 Page Title + p Page content +``` + +### Block modes + +```pug +block append scripts + script(src="extra.js") + +block prepend styles + link(rel="stylesheet" href="extra.css") +``` + +## Includes + +```pug +include header.pug +include partials/footer.pug +``` + +## Comments + +### HTML comment (rendered) + +```pug +// This renders as HTML comment +``` + +Output: +```html + +``` + +### Silent comment (not rendered) + +```pug +//- This is a silent comment +``` + +## Block Expansion + +Colon for inline nesting: + +```pug +a: img(src="logo.png") +``` + +Output: +```html +
+```
+
+## Self-Closing Tags
+
+Explicit self-closing with `/`:
+
+```pug
+foo/
+```
+
+Output:
+```html
+