15 Commits

Author SHA1 Message Date
c0bbee089f chore: bump version to 0.1.4 2026-01-22 23:23:41 +05:30
66981d6908 fix: build template tag with json data issue. 2026-01-22 23:19:39 +05:30
70ba7af27d fix: issue with extends layouts 2026-01-22 23:08:53 +05:30
d53ff24931 fix: template fn name fix for numeric named templates like 404.pug 2026-01-22 22:48:22 +05:30
752b64d0a9 Update CLAUDE.md 2026-01-22 21:28:25 +05:30
ca573f3166 Replace deprecated ArrayListUnmanaged with ArrayList
std.ArrayListUnmanaged is now std.ArrayList in Zig 0.15.
The old managed ArrayList is deprecated as std.array_list.Managed.
2026-01-22 12:45:49 +05:30
0f2f19f9b1 Fix strVal to handle pointer-to-array types correctly
- For pointer-to-array (*[N]u8), explicitly slice using @ptrCast
- Prevents returning dangling pointer to stack memory
2026-01-22 12:39:02 +05:30
654b45ee10 Compiled temapltes.
Benchmark cleanup
2026-01-22 11:10:47 +05:30
714db30a8c Add documentation and interpreted benchmark
- Add docs/syntax.md: complete template syntax reference
- Add docs/api.md: detailed API documentation
- Add src/benchmarks/bench_interpreted.zig: runtime benchmark
- Update build.zig: add bench-interpreted step
- Update README.md: simplified with docs links and benchmark table
2026-01-22 11:08:52 +05:30
510dcfbb03 bench data 2026-01-21 23:41:36 +05:30
5841ec38d8 Update README: clarify GitHub as mirror, prefer for dependencies 2026-01-20 18:08:42 +05:30
e2a1271425 v0.1.3: Add scoped warnings for skipped errors
- Add scoped logger (pugz/runtime) for better log filtering
- Add warnings when mixin not found
- Add warnings for mixin attribute failures
- Add warnings for mixin directory/file lookup failures
- Add warnings for mixin parse/tokenize failures
- Use 'action first, then reason' pattern in all log messages
2026-01-19 19:31:39 +05:30
a498eea0bc v0.1.2: Bump version 2026-01-19 19:20:33 +05:30
c172009799 v0.1.1: Add warning log when mixin is not found 2026-01-19 19:19:28 +05:30
8ff473839c Bump version to 0.1.0 2026-01-19 19:11:36 +05:30
50 changed files with 8124 additions and 1482 deletions

5
.gitignore vendored
View File

@@ -2,6 +2,11 @@
zig-out/
zig-cache/
.zig-cache/
.pugz-cache/
node_modules
# compiled template file
generated.zig
# IDE
.vscode/

129
CLAUDE.md
View File

@@ -6,20 +6,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering.
## Rules
- don not auto commit, user will do it.
## Build Commands
- `zig build` - Build the project (output in `zig-out/`)
- `zig build test` - Run all tests
- `zig build app-01` - Run the example web app (http://localhost:8080)
- `zig build bench-compiled` - Run compiled templates benchmark (compare with Pug.js)
- `zig build bench-interpreted` - Inpterpret trmplates
## Architecture Overview
The template engine follows a classic compiler pipeline:
The template engine supports two rendering modes:
### 1. Runtime Rendering (Interpreted)
```
Source → Lexer → Tokens → Parser → AST → Runtime → HTML
```
### 2. Build-Time Compilation (Compiled)
```
Source → Lexer → Tokens → Parser → AST → build_templates.zig → generated.zig → Native Zig Code
```
The compiled mode is **~3x faster** than Pug.js.
### Core Modules
| Module | Purpose |
@@ -28,13 +40,93 @@ Source → Lexer → Tokens → Parser → AST → Runtime → HTML
| **src/parser.zig** | Converts token stream into AST. Handles nesting via indent/dedent tokens. |
| **src/ast.zig** | AST node definitions (Element, Text, Conditional, Each, Mixin, etc.) |
| **src/runtime.zig** | Evaluates AST with data context, produces final HTML. Handles variable interpolation, conditionals, loops, mixins. |
| **src/codegen.zig** | Static HTML generation (without runtime evaluation). Outputs placeholders for dynamic content. |
| **src/build_templates.zig** | Build-time template compiler. Generates optimized Zig code from `.pug` templates. |
| **src/view_engine.zig** | High-level ViewEngine for web servers. Manages views directory, auto-loads mixins. |
| **src/root.zig** | Public library API - exports `ViewEngine`, `renderTemplate()` and core types. |
| **src/root.zig** | Public library API - exports `ViewEngine`, `renderTemplate()`, `build_templates` and core types. |
### Test Files
- **src/tests/general_test.zig** - Comprehensive integration tests for all features
- **src/tests/doctype_test.zig** - Doctype-specific tests
- **src/tests/inheritance_test.zig** - Template inheritance tests
## Build-Time Template Compilation
For maximum performance, templates can be compiled to native Zig code at build time.
### Setup in build.zig
```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const pugz_dep = b.dependency("pugz", .{});
// Compile templates at build time
const build_templates = @import("pugz").build_templates;
const compiled_templates = build_templates.compileTemplates(b, .{
.source_dir = "views", // Directory containing .pug files
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "tpls", .module = compiled_templates },
},
}),
});
}
```
### Usage in Code
```zig
const tpls = @import("tpls");
pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
// Zero-cost template rendering - just native Zig code
return try tpls.home(allocator, .{
.title = "Welcome",
.user = .{ .name = "Alice", .email = "alice@example.com" },
.items = &[_][]const u8{ "One", "Two", "Three" },
});
}
```
### Generated Code Features
The compiler generates optimized Zig code with:
- **Static string merging** - Consecutive static content merged into single `appendSlice` calls
- **Zero allocation for static templates** - Returns string literal directly
- **Type-safe data access** - Uses `@field(d, "name")` for compile-time checked field access
- **Automatic type conversion** - `strVal()` helper converts integers to strings
- **Optional handling** - Nullable slices handled with `orelse &.{}`
- **HTML escaping** - Lookup table for fast character escaping
### Benchmark Results (2000 iterations)
| Template | Pug.js | Pugz | Speedup |
|----------|--------|------|---------|
| simple-0 | 0.8ms | 0.1ms | **8x** |
| simple-1 | 1.4ms | 0.6ms | **2.3x** |
| simple-2 | 1.8ms | 0.6ms | **3x** |
| if-expression | 0.6ms | 0.2ms | **3x** |
| projects-escaped | 4.4ms | 0.6ms | **7.3x** |
| search-results | 15.2ms | 5.6ms | **2.7x** |
| friends | 153.5ms | 54.0ms | **2.8x** |
| **TOTAL** | **177.6ms** | **61.6ms** | **~3x faster** |
Run benchmarks:
```bash
# Pugz (Zig)
zig build bench-compiled
# Pug.js (for comparison)
cd src/benchmarks/pugjs && npm install && npm run bench
```
## Memory Management
@@ -57,6 +149,8 @@ The lexer tracks several states for handling complex syntax:
- `in_raw_block` / `raw_block_indent` / `raw_block_started` - For dot block text (e.g., `script.`)
- `indent_stack` - Stack-based indent/dedent token generation
**Important**: The lexer distinguishes between `#id` (ID selector), `#{expr}` (interpolation), and `#[tag]` (tag interpolation) by looking ahead at the next character.
### Token Types
Key tokens: `tag`, `class`, `id`, `lparen`, `rparen`, `attr_name`, `attr_value`, `text`, `interp_start`, `interp_end`, `indent`, `dedent`, `dot_block`, `pipe_text`, `literal_html`, `self_close`, `mixin_call`, etc.
@@ -125,6 +219,9 @@ p.
Multi-line
text block
<p>Literal HTML</p> // passed through as-is
// Interpolation-only text works too
h1.header #{title} // renders <h1 class="header">Title Value</h1>
```
### Tag Interpolation
@@ -154,6 +251,10 @@ else
unless loggedIn
p Please login
// String comparison in conditions
if status == "active"
p Active
```
### Iteration
@@ -172,6 +273,12 @@ else
// Works with objects too (key as index)
each val, key in object
p #{key}: #{val}
// Nested iteration with field access
each friend in friends
li #{friend.name}
each tag in friend.tags
span= tag
```
### Case/When
@@ -241,9 +348,13 @@ block prepend styles
## Server Usage
### ViewEngine (Recommended)
### Compiled Templates (Recommended for Production)
The `ViewEngine` provides the simplest API for web servers:
Use build-time compilation for best performance. See "Build-Time Template Compilation" section above.
### ViewEngine (Runtime Rendering)
The `ViewEngine` provides runtime template rendering with lazy-loading:
```zig
const std = @import("std");
@@ -342,14 +453,16 @@ Run tests with `zig build test`. Tests cover:
- Class and ID shorthand syntax
- Attribute parsing (quoted, unquoted, boolean, object literals)
- Text interpolation (escaped, unescaped, tag interpolation)
- Interpolation-only text (e.g., `h1.class #{var}`)
- Conditionals (if/else if/else/unless)
- Iteration (each with index, else branch, objects)
- Iteration (each with index, else branch, objects, nested loops)
- Case/when statements
- Mixin definitions and calls (with defaults, rest args, block content, attributes)
- Plain text (piped, dot blocks, literal HTML)
- Self-closing tags (void elements, explicit `/`)
- Block expansion with colon
- Comments (rendered and silent)
- String comparison in conditions
## Error Handling
@@ -365,4 +478,4 @@ Potential areas for enhancement:
- Filter support (`:markdown`, `:stylus`, etc.)
- More complete JavaScript expression evaluation
- Source maps for debugging
- Compile-time template validation
- Mixin support in compiled templates

202
README.md
View File

@@ -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
@@ -18,29 +18,85 @@ A Pug template engine for Zig.
Add pugz as a dependency in your `build.zig.zon`:
```bash
zig fetch --save "git+https://code.patial.tech/zig/pugz#main"
zig fetch --save "git+https://github.com/ankitpatial/pugz#main"
```
Then in your `build.zig`, add the `pugz` module as a dependency:
> **Note:** The primary repository is hosted at `code.patial.tech`. GitHub is a mirror. For dependencies, prefer the GitHub mirror for better availability.
```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",
});
@@ -56,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
@@ -89,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

View File

@@ -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.
//

View File

@@ -1,20 +1,13 @@
.{
.name = .pugz,
.version = "0.0.0",
.version = "0.1.4",
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.httpz = .{
.url = "git+https://github.com/karlseguin/http.zig?ref=master#9ef2ffe8d611ff2e1081e5cf39cb4632c145c5b9",
.hash = "httpz-0.0.0-PNVzrIowBwAFr_kqBN1W4KBMC2Ofutasj2ZfNAIcfTzF",
},
},
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
"examples",
},
}

289
docs/api.md Normal file
View File

@@ -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

340
docs/syntax.md Normal file
View File

@@ -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
<div><h1>Title</h1><p>Paragraph</p></div>
```
## Classes & IDs
Shorthand syntax using `.` for classes and `#` for IDs.
```pug
div#main.container.active
.box
#sidebar
```
Output:
```html
<div id="main" class="container active"></div>
<div class="box"></div>
<div id="sidebar"></div>
```
## Attributes
```pug
a(href="/link" target="_blank") Click
input(type="checkbox" checked)
button(disabled=false)
button(disabled=true)
```
Output:
```html
<a href="/link" target="_blank">Click</a>
<input type="checkbox" checked="checked" />
<button></button>
<button disabled="disabled"></button>
```
Boolean attributes: `false` omits the attribute, `true` renders `attr="attr"`.
## Text Content
### Inline text
```pug
p Hello World
```
### Piped text
```pug
p
| Line one
| Line two
```
### Block text (dot syntax)
```pug
script.
console.log('hello');
console.log('world');
```
### Literal HTML
```pug
<p>Passed through as-is</p>
```
## 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
<!-- This renders as HTML comment -->
```
### Silent comment (not rendered)
```pug
//- This is a silent comment
```
## Block Expansion
Colon for inline nesting:
```pug
a: img(src="logo.png")
```
Output:
```html
<a><img src="logo.png" /></a>
```
## Self-Closing Tags
Explicit self-closing with `/`:
```pug
foo/
```
Output:
```html
<foo />
```
Void elements (`br`, `hr`, `img`, `input`, `meta`, `link`, etc.) are automatically self-closing.
## Doctype
```pug
doctype html
```
Output:
```html
<!DOCTYPE html>
```

50
examples/demo/build.zig Normal file
View File

@@ -0,0 +1,50 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Get dependencies
const pugz_dep = b.dependency("pugz", .{
.target = target,
.optimize = optimize,
});
const httpz_dep = b.dependency("httpz", .{
.target = target,
.optimize = optimize,
});
// Compile templates at build time using pugz's build_templates
// Generates views/generated.zig with all templates
const build_templates = @import("pugz").build_templates;
const compiled_templates = build_templates.compileTemplates(b, .{
.source_dir = "views",
});
// Main executable
const exe = b.addExecutable(.{
.name = "demo",
.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 = "httpz", .module = httpz_dep.module("httpz") },
.{ .name = "tpls", .module = compiled_templates },
},
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the demo server");
run_step.dependOn(&run_cmd.step);
}

View File

@@ -0,0 +1,21 @@
.{
.name = .demo,
.version = "0.0.1",
.fingerprint = 0xd642dfa01393173d,
.minimum_zig_version = "0.15.2",
.dependencies = .{
.pugz = .{
.path = "../..",
},
.httpz = .{
.url = "git+https://github.com/karlseguin/http.zig?ref=master#9ef2ffe8d611ff2e1081e5cf39cb4632c145c5b9",
.hash = "httpz-0.0.0-PNVzrIowBwAFr_kqBN1W4KBMC2Ofutasj2ZfNAIcfTzF",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"views",
},
}

156
examples/demo/src/main.zig Normal file
View File

@@ -0,0 +1,156 @@
//! Pugz Demo - Interpreted vs Compiled Templates
//!
//! This demo shows two approaches:
//! 1. **Interpreted** (ViewEngine) - supports extends/blocks, parsed at runtime
//! 2. **Compiled** (build-time) - 3x faster, templates compiled to Zig code
//!
//! Routes:
//! GET / - Compiled home page (fast)
//! GET /users - Compiled users list (fast)
//! GET /interpreted - Interpreted with inheritance (flexible)
//! GET /page-a - Interpreted page A
const std = @import("std");
const httpz = @import("httpz");
const pugz = @import("pugz");
// Compiled templates - generated at build time from views/compiled/*.pug
const tpls = @import("tpls");
const Allocator = std.mem.Allocator;
/// Application state shared across all requests
const App = struct {
allocator: Allocator,
view: pugz.ViewEngine,
pub fn init(allocator: Allocator) App {
return .{
.allocator = allocator,
.view = pugz.ViewEngine.init(.{
.views_dir = "views",
}),
};
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leak");
const allocator = gpa.allocator();
var app = App.init(allocator);
const port = 8080;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app);
defer server.deinit();
var router = try server.router(.{});
// Compiled template routes (fast - 3x faster than Pug.js)
router.get("/", indexCompiled, .{});
router.get("/users", usersCompiled, .{});
// Interpreted template routes (flexible - supports extends/blocks)
router.get("/interpreted", indexInterpreted, .{});
router.get("/page-a", pageA, .{});
std.debug.print(
\\
\\Pugz Demo - Interpreted vs Compiled Templates
\\=============================================
\\Server running at http://localhost:{d}
\\
\\Compiled routes (3x faster than Pug.js):
\\ GET / - Home page (compiled)
\\ GET /users - Users list (compiled)
\\
\\Interpreted routes (supports extends/blocks):
\\ GET /interpreted - Home with ViewEngine
\\ GET /page-a - Page with inheritance
\\
\\Press Ctrl+C to stop.
\\
, .{port});
try server.listen();
}
// ─────────────────────────────────────────────────────────────────────────────
// Compiled template handlers (fast - no parsing at runtime)
// ─────────────────────────────────────────────────────────────────────────────
/// GET / - Compiled home page
fn indexCompiled(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = tpls.home(res.arena, .{
.title = "Welcome - Compiled",
.authenticated = true,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /users - Compiled users list
fn usersCompiled(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
const User = struct {
name: []const u8,
email: []const u8,
};
const html = tpls.users(res.arena, .{
.title = "Users - Compiled",
.users = &[_]User{
.{ .name = "Alice", .email = "alice@example.com" },
.{ .name = "Bob", .email = "bob@example.com" },
.{ .name = "Charlie", .email = "charlie@example.com" },
},
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
// ─────────────────────────────────────────────────────────────────────────────
// Interpreted template handlers (flexible - supports inheritance)
// ─────────────────────────────────────────────────────────────────────────────
/// GET /interpreted - Uses ViewEngine (parsed at runtime)
fn indexInterpreted(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "index", .{
.title = "Home - Interpreted",
.authenticated = true,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /page-a - Demonstrates extends and block override
fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "page-a", .{
.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;
}

View File

@@ -0,0 +1,2 @@
p
| Route no found

View File

@@ -0,0 +1,15 @@
doctype html
html
head
title #{title}
link(rel="stylesheet" href="/style.css")
body
header
h1 #{title}
if authenticated
span.user Welcome back!
main
p This page is rendered using a compiled template.
p Compiled templates are 3x faster than Pug.js!
footer
p &copy; 2024 Pugz Demo

View File

@@ -13,3 +13,9 @@ block content
ul
each val in items
li= val
input(data-json=`
{
"very-long": "piece of ",
"data": true
}
`)

View File

@@ -0,0 +1,11 @@
doctype html
html
head
title Users
body
h1 User List
ul.user-list
each user in users
li.user
strong= user.name
span.email= user.email

172
src/benchmarks/bench.zig Normal file
View File

@@ -0,0 +1,172 @@
//! Pugz Benchmark - Compiled Templates vs Pug.js
//!
//! Both Pugz and Pug.js benchmarks read from the same files:
//! src/benchmarks/templates/*.pug (templates)
//! src/benchmarks/templates/*.json (data)
//!
//! Run Pugz: zig build bench-all-compiled
//! Run Pug.js: cd src/benchmarks/pugjs && npm install && npm run bench
const std = @import("std");
const tpls = @import("tpls");
const iterations: usize = 2000;
const templates_dir = "src/benchmarks/templates";
// ═══════════════════════════════════════════════════════════════════════════
// Data structures matching JSON files
// ═══════════════════════════════════════════════════════════════════════════
const SubFriend = struct {
id: i64,
name: []const u8,
};
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i64,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: []const []const u8,
friends: []const SubFriend,
};
const Account = struct {
balance: i64,
balanceFormatted: []const u8,
status: []const u8,
negative: bool,
};
const Project = struct {
name: []const u8,
url: []const u8,
description: []const u8,
};
const SearchRecord = struct {
imgUrl: []const u8,
viewItemUrl: []const u8,
title: []const u8,
description: []const u8,
featured: bool,
sizes: ?[]const []const u8,
};
// ═══════════════════════════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════════════════════════
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("\n", .{});
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Compiled Zig Templates Benchmark ({d} iterations) ║\n", .{iterations});
std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir});
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
// ─────────────────────────────────────────────────────────────────────────
// Load JSON data
// ─────────────────────────────────────────────────────────────────────────
std.debug.print("\nLoading JSON data...\n", .{});
var data_arena = std.heap.ArenaAllocator.init(allocator);
defer data_arena.deinit();
const data_alloc = data_arena.allocator();
// Load all JSON files
const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json");
const simple1 = try loadJson(struct {
name: []const u8,
messageCount: i64,
colors: []const []const u8,
primary: bool,
}, data_alloc, "simple-1.json");
const simple2 = try loadJson(struct {
header: []const u8,
header2: []const u8,
header3: []const u8,
header4: []const u8,
header5: []const u8,
header6: []const u8,
list: []const []const u8,
}, data_alloc, "simple-2.json");
const if_expr = try loadJson(struct { accounts: []const Account }, data_alloc, "if-expression.json");
const projects = try loadJson(struct {
title: []const u8,
text: []const u8,
projects: []const Project,
}, data_alloc, "projects-escaped.json");
const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json");
const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json");
std.debug.print("Loaded. Starting benchmark...\n\n", .{});
var total: f64 = 0;
// ─────────────────────────────────────────────────────────────────────────
// Benchmark each template
// ─────────────────────────────────────────────────────────────────────────
// simple-0
total += try bench("simple-0", allocator, tpls.simple_0, simple0);
// simple-1
total += try bench("simple-1", allocator, tpls.simple_1, simple1);
// simple-2
total += try bench("simple-2", allocator, tpls.simple_2, simple2);
// if-expression
total += try bench("if-expression", allocator, tpls.if_expression, if_expr);
// projects-escaped
total += try bench("projects-escaped", allocator, tpls.projects_escaped, projects);
// search-results
total += try bench("search-results", allocator, tpls.search_results, search);
// friends
total += try bench("friends", allocator, tpls.friends, friends_data);
// ─────────────────────────────────────────────────────────────────────────
// Summary
// ─────────────────────────────────────────────────────────────────────────
std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total });
std.debug.print("\n", .{});
}
fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T {
const path = templates_dir ++ "/" ++ filename;
const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024);
const parsed = try std.json.parseFromSlice(T, alloc, content, .{});
return parsed.value;
}
fn bench(
name: []const u8,
allocator: std.mem.Allocator,
comptime render_fn: anytype,
data: anytype,
) !f64 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
_ = try render_fn(arena.allocator(), data);
}
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0;
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms });
return ms;
}

View File

@@ -0,0 +1,154 @@
//! Pugz Benchmark - Interpreted (Runtime) Mode
//!
//! This benchmark uses the ViewEngine to render templates at runtime,
//! reading from the same template/data files as the compiled benchmark.
//!
//! Run: zig build bench-interpreted
const std = @import("std");
const pugz = @import("pugz");
const iterations: usize = 2000;
const templates_dir = "src/benchmarks/templates";
// Data structures matching JSON files
const SubFriend = struct {
id: i64,
name: []const u8,
};
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i64,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: []const []const u8,
friends: []const SubFriend,
};
const Account = struct {
balance: i64,
balanceFormatted: []const u8,
status: []const u8,
negative: bool,
};
const Project = struct {
name: []const u8,
url: []const u8,
description: []const u8,
};
const SearchRecord = struct {
imgUrl: []const u8,
viewItemUrl: []const u8,
title: []const u8,
description: []const u8,
featured: bool,
sizes: ?[]const []const u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("\n", .{});
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Interpreted (Runtime) Benchmark ({d} iterations) ║\n", .{iterations});
std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir});
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
// Load JSON data
std.debug.print("\nLoading JSON data...\n", .{});
var data_arena = std.heap.ArenaAllocator.init(allocator);
defer data_arena.deinit();
const data_alloc = data_arena.allocator();
const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json");
const simple1 = try loadJson(struct {
name: []const u8,
messageCount: i64,
colors: []const []const u8,
primary: bool,
}, data_alloc, "simple-1.json");
const simple2 = try loadJson(struct {
header: []const u8,
header2: []const u8,
header3: []const u8,
header4: []const u8,
header5: []const u8,
header6: []const u8,
list: []const []const u8,
}, data_alloc, "simple-2.json");
const if_expr = try loadJson(struct { accounts: []const Account }, data_alloc, "if-expression.json");
const projects = try loadJson(struct {
title: []const u8,
text: []const u8,
projects: []const Project,
}, data_alloc, "projects-escaped.json");
const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json");
const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json");
// Load template sources
const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug");
const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug");
const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug");
const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug");
const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug");
const search_tpl = try loadTemplate(data_alloc, "search-results.pug");
const friends_tpl = try loadTemplate(data_alloc, "friends.pug");
std.debug.print("Loaded. Starting benchmark...\n\n", .{});
var total: f64 = 0;
total += try bench("simple-0", allocator, simple0_tpl, simple0);
total += try bench("simple-1", allocator, simple1_tpl, simple1);
total += try bench("simple-2", allocator, simple2_tpl, simple2);
total += try bench("if-expression", allocator, if_expr_tpl, if_expr);
total += try bench("projects-escaped", allocator, projects_tpl, projects);
total += try bench("search-results", allocator, search_tpl, search);
total += try bench("friends", allocator, friends_tpl, friends_data);
std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total });
std.debug.print("\n", .{});
}
fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T {
const path = templates_dir ++ "/" ++ filename;
const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024);
const parsed = try std.json.parseFromSlice(T, alloc, content, .{});
return parsed.value;
}
fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 {
const path = templates_dir ++ "/" ++ filename;
return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024);
}
fn bench(
name: []const u8,
allocator: std.mem.Allocator,
template: []const u8,
data: anytype,
) !f64 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
_ = try pugz.renderTemplate(arena.allocator(), template, data);
}
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0;
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms });
return ms;
}

View File

@@ -1,388 +0,0 @@
//! Pugz Rendering Benchmark
//!
//! Measures template rendering performance with various template complexities.
//! Run with: zig build bench
//!
//! Metrics reported:
//! - Total time for N iterations
//! - Average time per render
//! - Renders per second
//! - Memory usage per render
const std = @import("std");
const pugz = @import("pugz");
const Allocator = std.mem.Allocator;
/// Benchmark configuration
const Config = struct {
warmup_iterations: usize = 200,
benchmark_iterations: usize = 20_000,
show_output: bool = false,
};
/// Benchmark result
const Result = struct {
name: []const u8,
iterations: usize,
total_ns: u64,
min_ns: u64,
max_ns: u64,
avg_ns: u64,
ops_per_sec: f64,
bytes_per_render: usize,
arena_peak_bytes: usize,
pub fn print(self: Result) void {
std.debug.print("\n{s}\n", .{self.name});
std.debug.print(" Iterations: {d:>10}\n", .{self.iterations});
std.debug.print(" Total time: {d:>10.2} ms\n", .{@as(f64, @floatFromInt(self.total_ns)) / 1_000_000.0});
std.debug.print(" Avg per render: {d:>10.2} us\n", .{@as(f64, @floatFromInt(self.avg_ns)) / 1_000.0});
std.debug.print(" Min: {d:>10.2} us\n", .{@as(f64, @floatFromInt(self.min_ns)) / 1_000.0});
std.debug.print(" Max: {d:>10.2} us\n", .{@as(f64, @floatFromInt(self.max_ns)) / 1_000.0});
std.debug.print(" Renders/sec: {d:>10.0}\n", .{self.ops_per_sec});
std.debug.print(" Output size: {d:>10} bytes\n", .{self.bytes_per_render});
std.debug.print(" Memory/render: {d:>10} bytes\n", .{self.arena_peak_bytes});
}
};
/// Run a benchmark for a template
fn runBenchmark(
allocator: Allocator,
comptime name: []const u8,
template: []const u8,
data: anytype,
config: Config,
) !Result {
// Warmup phase
for (0..config.warmup_iterations) |_| {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
_ = try pugz.renderTemplate(arena.allocator(), template, data);
}
// Benchmark phase
var total_ns: u64 = 0;
var min_ns: u64 = std.math.maxInt(u64);
var max_ns: u64 = 0;
var output_size: usize = 0;
var peak_memory: usize = 0;
var timer = try std.time.Timer.start();
for (0..config.benchmark_iterations) |i| {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
timer.reset();
const result = try pugz.renderTemplate(arena.allocator(), template, data);
const elapsed = timer.read();
total_ns += elapsed;
min_ns = @min(min_ns, elapsed);
max_ns = @max(max_ns, elapsed);
if (i == 0) {
output_size = result.len;
// Measure memory used by arena (query state before deinit)
const state = arena.queryCapacity();
peak_memory = state;
if (config.show_output) {
std.debug.print("\n--- {s} output ---\n{s}\n", .{ name, result });
}
}
}
const avg_ns = total_ns / config.benchmark_iterations;
const ops_per_sec = @as(f64, @floatFromInt(config.benchmark_iterations)) / (@as(f64, @floatFromInt(total_ns)) / 1_000_000_000.0);
return .{
.name = name,
.iterations = config.benchmark_iterations,
.total_ns = total_ns,
.min_ns = min_ns,
.max_ns = max_ns,
.avg_ns = avg_ns,
.ops_per_sec = ops_per_sec,
.bytes_per_render = output_size,
.arena_peak_bytes = peak_memory,
};
}
/// Simple template - just a few elements
const simple_template =
\\doctype html
\\html
\\ head
\\ title= title
\\ body
\\ h1 Hello, #{name}!
\\ p Welcome to our site.
;
/// Medium template - with conditionals and loops
const medium_template =
\\doctype html
\\html
\\ head
\\ title= title
\\ meta(charset="utf-8")
\\ meta(name="viewport" content="width=device-width, initial-scale=1")
\\ body
\\ header
\\ nav.navbar
\\ a.brand(href="/") Brand
\\ ul.nav-links
\\ each link in navLinks
\\ li
\\ a(href=link.href)= link.text
\\ main.container
\\ h1= title
\\ if showIntro
\\ p.intro Welcome, #{userName}!
\\ section.content
\\ each item in items
\\ .card
\\ h3= item.title
\\ p= item.description
\\ footer
\\ p Copyright 2024
;
/// Complex template - with mixins, nested loops, conditionals
const complex_template =
\\mixin card(title, description)
\\ .card
\\ .card-header
\\ h3= title
\\ .card-body
\\ p= description
\\ block
\\
\\mixin button(text, type="primary")
\\ button(class="btn btn-" + type)= text
\\
\\mixin navItem(href, text)
\\ li
\\ a(href=href)= text
\\
\\doctype html
\\html
\\ head
\\ title= title
\\ meta(charset="utf-8")
\\ meta(name="viewport" content="width=device-width, initial-scale=1")
\\ link(rel="stylesheet" href="/css/style.css")
\\ body
\\ header.site-header
\\ .container
\\ a.logo(href="/")
\\ img(src="/img/logo.png" alt="Logo")
\\ nav.main-nav
\\ ul
\\ each link in navLinks
\\ +navItem(link.href, link.text)
\\ .user-menu
\\ if user
\\ span.greeting Hello, #{user.name}!
\\ +button("Logout", "secondary")
\\ else
\\ +button("Login")
\\ +button("Sign Up", "success")
\\ main.site-content
\\ .container
\\ .page-header
\\ h1= pageTitle
\\ if subtitle
\\ p.subtitle= subtitle
\\ .content-grid
\\ each category in categories
\\ section.category
\\ h2= category.name
\\ .cards
\\ each item in category.items
\\ +card(item.title, item.description)
\\ .card-footer
\\ +button("View Details")
\\ aside.sidebar
\\ .widget
\\ h4 Recent Posts
\\ ul.post-list
\\ each post in recentPosts
\\ li
\\ a(href=post.url)= post.title
\\ .widget
\\ h4 Tags
\\ .tag-cloud
\\ each tag in allTags
\\ span.tag= tag
\\ footer.site-footer
\\ .container
\\ .footer-grid
\\ .footer-col
\\ h4 About
\\ p Some description text here.
\\ .footer-col
\\ h4 Links
\\ ul
\\ each link in footerLinks
\\ li
\\ a(href=link.href)= link.text
\\ .footer-col
\\ h4 Contact
\\ p Email: contact@example.com
\\ .copyright
\\ p Copyright #{year} Example Inc.
;
pub fn main() !void {
// Use GPA with leak detection enabled
var gpa = std.heap.GeneralPurposeAllocator(.{
.stack_trace_frames = 10,
.safety = true,
}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("\n⚠️ MEMORY LEAK DETECTED!\n", .{});
} else {
std.debug.print("\n✓ No memory leaks detected.\n", .{});
}
}
const allocator = gpa.allocator();
const config = Config{
.warmup_iterations = 200,
.benchmark_iterations = 20_000,
.show_output = false,
};
std.debug.print("\n", .{});
std.debug.print("╔══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Pugz Template Rendering Benchmark ║\n", .{});
std.debug.print("╠══════════════════════════════════════════════════════════════╣\n", .{});
std.debug.print("║ Warmup iterations: {d:>6} ║\n", .{config.warmup_iterations});
std.debug.print("║ Benchmark iterations: {d:>6} ║\n", .{config.benchmark_iterations});
std.debug.print("╚══════════════════════════════════════════════════════════════╝\n", .{});
// Simple template benchmark
const simple_result = try runBenchmark(
allocator,
"Simple Template (basic elements, interpolation)",
simple_template,
.{
.title = "Welcome",
.name = "World",
},
config,
);
simple_result.print();
// Medium template benchmark
const NavLink = struct { href: []const u8, text: []const u8 };
const Item = struct { title: []const u8, description: []const u8 };
const medium_result = try runBenchmark(
allocator,
"Medium Template (loops, conditionals, nested elements)",
medium_template,
.{
.title = "Dashboard",
.userName = "Alice",
.showIntro = true,
.navLinks = &[_]NavLink{
.{ .href = "/", .text = "Home" },
.{ .href = "/about", .text = "About" },
.{ .href = "/contact", .text = "Contact" },
},
.items = &[_]Item{
.{ .title = "Item 1", .description = "Description for item 1" },
.{ .title = "Item 2", .description = "Description for item 2" },
.{ .title = "Item 3", .description = "Description for item 3" },
.{ .title = "Item 4", .description = "Description for item 4" },
},
},
config,
);
medium_result.print();
// Complex template benchmark
const User = struct { name: []const u8 };
const SimpleItem = struct { title: []const u8, description: []const u8 };
const Category = struct { name: []const u8, items: []const SimpleItem };
const Post = struct { url: []const u8, title: []const u8 };
const FooterLink = struct { href: []const u8, text: []const u8 };
const complex_result = try runBenchmark(
allocator,
"Complex Template (mixins, nested loops, conditionals)",
complex_template,
.{
.title = "Example Site",
.pageTitle = "Welcome to Our Site",
.subtitle = "The best place on the web",
.year = "2024",
.user = User{ .name = "Alice" },
.navLinks = &[_]NavLink{
.{ .href = "/", .text = "Home" },
.{ .href = "/products", .text = "Products" },
.{ .href = "/about", .text = "About" },
.{ .href = "/contact", .text = "Contact" },
},
.categories = &[_]Category{
.{
.name = "Featured",
.items = &[_]SimpleItem{
.{ .title = "Product A", .description = "Amazing product A" },
.{ .title = "Product B", .description = "Wonderful product B" },
},
},
.{
.name = "Popular",
.items = &[_]SimpleItem{
.{ .title = "Product C", .description = "Popular product C" },
.{ .title = "Product D", .description = "Trending product D" },
},
},
},
.recentPosts = &[_]Post{
.{ .url = "/blog/post-1", .title = "First Blog Post" },
.{ .url = "/blog/post-2", .title = "Second Blog Post" },
.{ .url = "/blog/post-3", .title = "Third Blog Post" },
},
.allTags = &[_][]const u8{ "tech", "news", "tutorial", "review", "guide" },
.footerLinks = &[_]FooterLink{
.{ .href = "/privacy", .text = "Privacy Policy" },
.{ .href = "/terms", .text = "Terms of Service" },
.{ .href = "/sitemap", .text = "Sitemap" },
},
},
config,
);
complex_result.print();
// Summary
std.debug.print("\n", .{});
std.debug.print("╔══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Summary ║\n", .{});
std.debug.print("╠══════════════════════════════════════════════════════════════╣\n", .{});
std.debug.print("║ Template │ Avg (us) │ Renders/sec │ Output (bytes) ║\n", .{});
std.debug.print("╠──────────────────┼──────────┼─────────────┼─────────────────╣\n", .{});
std.debug.print("║ Simple │ {d:>8.2} │ {d:>11.0} │ {d:>15} ║\n", .{
@as(f64, @floatFromInt(simple_result.avg_ns)) / 1_000.0,
simple_result.ops_per_sec,
simple_result.bytes_per_render,
});
std.debug.print("║ Medium │ {d:>8.2} │ {d:>11.0} │ {d:>15} ║\n", .{
@as(f64, @floatFromInt(medium_result.avg_ns)) / 1_000.0,
medium_result.ops_per_sec,
medium_result.bytes_per_render,
});
std.debug.print("║ Complex │ {d:>8.2} │ {d:>11.0} │ {d:>15} ║\n", .{
@as(f64, @floatFromInt(complex_result.avg_ns)) / 1_000.0,
complex_result.ops_per_sec,
complex_result.bytes_per_render,
});
std.debug.print("╚══════════════════════════════════════════════════════════════╝\n", .{});
std.debug.print("\n", .{});
}

View File

@@ -1,479 +0,0 @@
//! Pugz Benchmark - Comparison with template-engine-bench
//!
//! These benchmarks use the exact same templates from:
//! https://github.com/itsarnaud/template-engine-bench
//!
//! Run individual benchmarks:
//! zig build test-bench -- simple-0
//! zig build test-bench -- friends
//!
//! Run all benchmarks:
//! zig build test-bench
//!
//! Pug.js reference (2000 iterations on MacBook Air M2):
//! - simple-0: pug => 2ms
//! - simple-1: pug => 9ms
//! - simple-2: pug => 9ms
//! - if-expression: pug => 12ms
//! - projects-escaped: pug => 86ms
//! - search-results: pug => 41ms
//! - friends: pug => 110ms
const std = @import("std");
const pugz = @import("pugz");
const iterations: usize = 2000;
// ═══════════════════════════════════════════════════════════════════════════
// simple-0
// ═══════════════════════════════════════════════════════════════════════════
const simple_0_tpl = "h1 Hello, #{name}";
test "bench: simple-0" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leak!");
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), simple_0_tpl, .{
.name = "John",
});
total_ns += timer.read();
}
printResult("simple-0", total_ns, 2);
}
// ═══════════════════════════════════════════════════════════════════════════
// simple-1
// ═══════════════════════════════════════════════════════════════════════════
const simple_1_tpl =
\\.simple-1(style="background-color: blue; border: 1px solid black")
\\ .colors
\\ span.hello Hello #{name}!
\\ strong You have #{messageCount} messages!
\\ if colors
\\ ul
\\ each color in colors
\\ li.color= color
\\ else
\\ div No colors!
\\ if primary
\\ button(type="button" class="primary") Click me!
\\ else
\\ button(type="button" class="secondary") Click me!
;
test "bench: simple-1" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.name = "George Washington",
.messageCount = 999,
.colors = &[_][]const u8{ "red", "green", "blue", "yellow", "orange", "pink", "black", "white", "beige", "brown", "cyan", "magenta" },
.primary = true,
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), simple_1_tpl, data);
total_ns += timer.read();
}
printResult("simple-1", total_ns, 9);
}
// ═══════════════════════════════════════════════════════════════════════════
// simple-2
// ═══════════════════════════════════════════════════════════════════════════
const simple_2_tpl =
\\div
\\ h1.header #{header}
\\ h2.header2 #{header2}
\\ h3.header3 #{header3}
\\ h4.header4 #{header4}
\\ h5.header5 #{header5}
\\ h6.header6 #{header6}
\\ ul.list
\\ each item in list
\\ li.item #{item}
;
test "bench: simple-2" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.header = "Header",
.header2 = "Header2",
.header3 = "Header3",
.header4 = "Header4",
.header5 = "Header5",
.header6 = "Header6",
.list = &[_][]const u8{ "1000000000", "2", "3", "4", "5", "6", "7", "8", "9", "10" },
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), simple_2_tpl, data);
total_ns += timer.read();
}
printResult("simple-2", total_ns, 9);
}
// ═══════════════════════════════════════════════════════════════════════════
// if-expression
// ═══════════════════════════════════════════════════════════════════════════
const if_expression_tpl =
\\each account in accounts
\\ div
\\ if account.status == "closed"
\\ div Your account has been closed!
\\ if account.status == "suspended"
\\ div Your account has been temporarily suspended
\\ if account.status == "open"
\\ div
\\ | Bank balance:
\\ if account.negative
\\ span.negative= account.balanceFormatted
\\ else
\\ span.positive= account.balanceFormatted
;
const Account = struct {
balance: i32,
balanceFormatted: []const u8,
status: []const u8,
negative: bool,
};
test "bench: if-expression" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.accounts = &[_]Account{
.{ .balance = 0, .balanceFormatted = "$0.00", .status = "open", .negative = false },
.{ .balance = 10, .balanceFormatted = "$10.00", .status = "closed", .negative = false },
.{ .balance = -100, .balanceFormatted = "$-100.00", .status = "suspended", .negative = true },
.{ .balance = 999, .balanceFormatted = "$999.00", .status = "open", .negative = false },
},
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), if_expression_tpl, data);
total_ns += timer.read();
}
printResult("if-expression", total_ns, 12);
}
// ═══════════════════════════════════════════════════════════════════════════
// projects-escaped
// ═══════════════════════════════════════════════════════════════════════════
const projects_escaped_tpl =
\\doctype html
\\html
\\ head
\\ title #{title}
\\ body
\\ p #{text}
\\ each project in projects
\\ a(href=project.url) #{project.name}
\\ p #{project.description}
\\ else
\\ p No projects
;
const Project = struct {
name: []const u8,
url: []const u8,
description: []const u8,
};
test "bench: projects-escaped" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.title = "Projects",
.text = "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
.projects = &[_]Project{
.{ .name = "<strong>Facebook</strong>", .url = "http://facebook.com", .description = "Social network" },
.{ .name = "<strong>Google</strong>", .url = "http://google.com", .description = "Search engine" },
.{ .name = "<strong>Twitter</strong>", .url = "http://twitter.com", .description = "Microblogging service" },
.{ .name = "<strong>Amazon</strong>", .url = "http://amazon.com", .description = "Online retailer" },
.{ .name = "<strong>eBay</strong>", .url = "http://ebay.com", .description = "Online auction" },
.{ .name = "<strong>Wikipedia</strong>", .url = "http://wikipedia.org", .description = "A free encyclopedia" },
.{ .name = "<strong>LiveJournal</strong>", .url = "http://livejournal.com", .description = "Blogging platform" },
},
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), projects_escaped_tpl, data);
total_ns += timer.read();
}
printResult("projects-escaped", total_ns, 86);
}
// ═══════════════════════════════════════════════════════════════════════════
// search-results
// ═══════════════════════════════════════════════════════════════════════════
// Simplified to match original JS benchmark template exactly
const search_results_tpl =
\\.search-results.view-gallery
\\ each searchRecord in searchRecords
\\ .search-item
\\ .search-item-container.drop-shadow
\\ .img-container
\\ img(src=searchRecord.imgUrl)
\\ h4.title
\\ a(href=searchRecord.viewItemUrl)= searchRecord.title
\\ | #{searchRecord.description}
\\ if searchRecord.featured
\\ div Featured!
\\ if searchRecord.sizes
\\ div
\\ | Sizes available:
\\ ul
\\ each size in searchRecord.sizes
\\ li= size
;
const SearchRecord = struct {
imgUrl: []const u8,
viewItemUrl: []const u8,
title: []const u8,
description: []const u8,
featured: bool,
sizes: ?[]const []const u8,
};
test "bench: search-results" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const sizes = &[_][]const u8{ "S", "M", "L", "XL", "XXL" };
// Long descriptions matching original benchmark (Lorem ipsum paragraphs)
const desc1 = "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing. Dolore adipisicing pariatur in fugiat nulla voluptate aliquip esse laboris quis exercitation aliqua labore.";
const desc2 = "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute. Esse sunt laborum excepteur sint elit sit esse ad.";
const desc3 = "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod qui anim cillum sint. Dolor exercitation tempor aliquip sunt nisi ipsum ullamco adipisicing.";
const desc4 = "Est ad amet irure veniam dolore velit amet irure fugiat ut elit. Tempor fugiat dolor tempor aute enim. Ad sint mollit laboris id sint ullamco eu do irure nostrud magna sunt voluptate.";
const desc5 = "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit reprehenderit sunt. Exercitation esse irure magna proident ex ut elit magna mollit aliqua amet.";
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.searchRecords = &[_]SearchRecord{
.{ .imgUrl = "img1.jpg", .viewItemUrl = "http://foo/1", .title = "Namebox", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img2.jpg", .viewItemUrl = "http://foo/2", .title = "Arctiq", .description = desc2, .featured = false, .sizes = sizes },
.{ .imgUrl = "img3.jpg", .viewItemUrl = "http://foo/3", .title = "Niquent", .description = desc3, .featured = true, .sizes = sizes },
.{ .imgUrl = "img4.jpg", .viewItemUrl = "http://foo/4", .title = "Remotion", .description = desc4, .featured = true, .sizes = sizes },
.{ .imgUrl = "img5.jpg", .viewItemUrl = "http://foo/5", .title = "Octocore", .description = desc5, .featured = true, .sizes = sizes },
.{ .imgUrl = "img6.jpg", .viewItemUrl = "http://foo/6", .title = "Spherix", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img7.jpg", .viewItemUrl = "http://foo/7", .title = "Quarex", .description = desc2, .featured = true, .sizes = sizes },
.{ .imgUrl = "img8.jpg", .viewItemUrl = "http://foo/8", .title = "Supremia", .description = desc3, .featured = false, .sizes = sizes },
.{ .imgUrl = "img9.jpg", .viewItemUrl = "http://foo/9", .title = "Amtap", .description = desc4, .featured = false, .sizes = sizes },
.{ .imgUrl = "img10.jpg", .viewItemUrl = "http://foo/10", .title = "Qiao", .description = desc5, .featured = false, .sizes = sizes },
.{ .imgUrl = "img11.jpg", .viewItemUrl = "http://foo/11", .title = "Pushcart", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img12.jpg", .viewItemUrl = "http://foo/12", .title = "Eweville", .description = desc2, .featured = false, .sizes = sizes },
.{ .imgUrl = "img13.jpg", .viewItemUrl = "http://foo/13", .title = "Senmei", .description = desc3, .featured = true, .sizes = sizes },
.{ .imgUrl = "img14.jpg", .viewItemUrl = "http://foo/14", .title = "Maximind", .description = desc4, .featured = true, .sizes = sizes },
.{ .imgUrl = "img15.jpg", .viewItemUrl = "http://foo/15", .title = "Blurrybus", .description = desc5, .featured = true, .sizes = sizes },
.{ .imgUrl = "img16.jpg", .viewItemUrl = "http://foo/16", .title = "Virva", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img17.jpg", .viewItemUrl = "http://foo/17", .title = "Centregy", .description = desc2, .featured = true, .sizes = sizes },
.{ .imgUrl = "img18.jpg", .viewItemUrl = "http://foo/18", .title = "Dancerity", .description = desc3, .featured = true, .sizes = sizes },
.{ .imgUrl = "img19.jpg", .viewItemUrl = "http://foo/19", .title = "Oceanica", .description = desc4, .featured = true, .sizes = sizes },
.{ .imgUrl = "img20.jpg", .viewItemUrl = "http://foo/20", .title = "Synkgen", .description = desc5, .featured = false, .sizes = null },
},
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), search_results_tpl, data);
total_ns += timer.read();
}
printResult("search-results", total_ns, 41);
}
// ═══════════════════════════════════════════════════════════════════════════
// friends
// ═══════════════════════════════════════════════════════════════════════════
const friends_tpl =
\\doctype html
\\html(lang="en")
\\ head
\\ meta(charset="UTF-8")
\\ title Friends
\\ body
\\ div.friends
\\ each friend in friends
\\ div.friend
\\ ul
\\ li Name: #{friend.name}
\\ li Balance: #{friend.balance}
\\ li Age: #{friend.age}
\\ li Address: #{friend.address}
\\ li Image:
\\ img(src=friend.picture)
\\ li Company: #{friend.company}
\\ li Email:
\\ a(href=friend.emailHref) #{friend.email}
\\ li About: #{friend.about}
\\ if friend.tags
\\ li Tags:
\\ ul
\\ each tag in friend.tags
\\ li #{tag}
\\ if friend.friends
\\ li Friends:
\\ ul
\\ each subFriend in friend.friends
\\ li #{subFriend.name} (#{subFriend.id})
;
const SubFriend = struct {
id: i32,
name: []const u8,
};
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i32,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: ?[]const []const u8,
friends: ?[]const SubFriend,
};
test "bench: friends" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leadk");
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const friend_tags = &[_][]const u8{ "id", "amet", "non", "ut", "dolore", "commodo", "consequat" };
const sub_friends = &[_]SubFriend{
.{ .id = 0, .name = "Gates Lewis" },
.{ .id = 1, .name = "Britt Stokes" },
.{ .id = 2, .name = "Reed Wade" },
};
var friends_data: [100]Friend = undefined;
for (&friends_data, 0..) |*f, i| {
f.* = .{
.name = "Gardner Alvarez",
.balance = "$1,509.00",
.age = 30 + @as(i32, @intCast(i % 20)),
.address = "282 Lancaster Avenue, Bowden, Kansas, 666",
.picture = "http://placehold.it/32x32",
.company = "Dentrex",
.email = "gardneralvarez@dentrex.com",
.emailHref = "mailto:gardneralvarez@dentrex.com",
.about = "Minim elit tempor enim voluptate labore do non nisi sint nulla deserunt officia proident excepteur.",
.tags = friend_tags,
.friends = sub_friends,
};
}
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), friends_tpl, .{
.friends = &friends_data,
});
total_ns += timer.read();
}
printResult("friends", total_ns, 110);
}
// ═══════════════════════════════════════════════════════════════════════════
// Helper
// ═══════════════════════════════════════════════════════════════════════════
fn printResult(name: []const u8, total_ns: u64, pug_ref_ms: f64) void {
const total_ms = @as(f64, @floatFromInt(total_ns)) / 1_000_000.0;
const avg_us = @as(f64, @floatFromInt(total_ns)) / @as(f64, @floatFromInt(iterations)) / 1_000.0;
const speedup = pug_ref_ms / total_ms;
std.debug.print("\n{s:<20} => {d:>6.1}ms ({d:.2}us/render) | Pug.js: {d:.0}ms | {d:.1}x\n", .{
name,
total_ms,
avg_us,
pug_ref_ms,
speedup,
});
}

View File

@@ -1,170 +0,0 @@
const std = @import("std");
const pugz = @import("pugz");
const friends_tpl =
\\doctype html
\\html(lang="en")
\\ head
\\ meta(charset="UTF-8")
\\ title Friends
\\ body
\\ div.friends
\\ each friend in friends
\\ div.friend
\\ ul
\\ li Name: #{friend.name}
\\ li Balance: #{friend.balance}
\\ li Age: #{friend.age}
\\ li Address: #{friend.address}
\\ li Image:
\\ img(src=friend.picture)
\\ li Company: #{friend.company}
\\ li Email:
\\ a(href=friend.emailHref) #{friend.email}
\\ li About: #{friend.about}
\\ if friend.tags
\\ li Tags:
\\ ul
\\ each tag in friend.tags
\\ li #{tag}
\\ if friend.friends
\\ li Friends:
\\ ul
\\ each subFriend in friend.friends
\\ li #{subFriend.name} (#{subFriend.id})
;
const SubFriend = struct { id: i32, name: []const u8 };
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i32,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: ?[]const []const u8,
friends: ?[]const SubFriend,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const engine = pugz.ViewEngine.init(.{});
const friend_tags = &[_][]const u8{ "id", "amet", "non", "ut", "dolore", "commodo", "consequat" };
const sub_friends = &[_]SubFriend{
.{ .id = 0, .name = "Gates Lewis" },
.{ .id = 1, .name = "Britt Stokes" },
.{ .id = 2, .name = "Reed Wade" },
};
var friends_data: [100]Friend = undefined;
for (&friends_data, 0..) |*f, i| {
f.* = .{
.name = "Gardner Alvarez",
.balance = "$1,509.00",
.age = 30 + @as(i32, @intCast(i % 20)),
.address = "282 Lancaster Avenue, Bowden, Kansas, 666",
.picture = "http://placehold.it/32x32",
.company = "Dentrex",
.email = "gardneralvarez@dentrex.com",
.emailHref = "mailto:gardneralvarez@dentrex.com",
.about = "Minim elit tempor enim voluptate labore do non nisi sint nulla deserunt officia proident excepteur.",
.tags = friend_tags,
.friends = sub_friends,
};
}
const data = .{ .friends = &friends_data };
// Warmup
for (0..10) |_| {
_ = arena.reset(.retain_capacity);
_ = try engine.renderTpl(arena.allocator(), friends_tpl, data);
}
// Get output size
_ = arena.reset(.retain_capacity);
const output = try engine.renderTpl(arena.allocator(), friends_tpl, data);
const output_size = output.len;
// Profile render
const iterations: usize = 500;
var total_render: u64 = 0;
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), friends_tpl, data);
total_render += timer.read();
}
const avg_render_us = @as(f64, @floatFromInt(total_render)) / @as(f64, @floatFromInt(iterations)) / 1000.0;
const total_ms = @as(f64, @floatFromInt(total_render)) / 1_000_000.0;
// Header
std.debug.print("\n", .{});
std.debug.print("╔══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ FRIENDS TEMPLATE CPU PROFILE ║\n", .{});
std.debug.print("╠══════════════════════════════════════════════════════════════╣\n", .{});
std.debug.print("║ Iterations: {d:<6} Output size: {d:<6} bytes ║\n", .{ iterations, output_size });
std.debug.print("╚══════════════════════════════════════════════════════════════╝\n\n", .{});
// Results
std.debug.print("┌────────────────────────────────────┬─────────────────────────┐\n", .{});
std.debug.print("│ Metric │ Value │\n", .{});
std.debug.print("├────────────────────────────────────┼─────────────────────────┤\n", .{});
std.debug.print("│ Total time │ {d:>10.1} ms │\n", .{total_ms});
std.debug.print("│ Avg per render │ {d:>10.1} µs │\n", .{avg_render_us});
std.debug.print("│ Renders/sec │ {d:>10.0} │\n", .{1_000_000.0 / avg_render_us});
std.debug.print("└────────────────────────────────────┴─────────────────────────┘\n", .{});
// Template complexity breakdown
std.debug.print("\n📋 Template Complexity:\n", .{});
std.debug.print(" • 100 friends (outer loop)\n", .{});
std.debug.print(" • 7 tags per friend (nested loop) = 700 tag iterations\n", .{});
std.debug.print(" • 3 sub-friends per friend (nested loop) = 300 sub-friend iterations\n", .{});
std.debug.print(" • Total loop iterations: 100 + 700 + 300 = 1,100\n", .{});
std.debug.print(" • ~10 interpolations per friend = 1,000+ variable lookups\n", .{});
std.debug.print(" • 2 conditionals per friend = 200 conditional evaluations\n", .{});
// Cost breakdown estimate
const loop_iterations: f64 = 1100;
const var_lookups: f64 = 1500; // approximate
std.debug.print("\n💡 Estimated Cost Breakdown (per render):\n", .{});
std.debug.print(" Total: {d:.1} µs\n", .{avg_render_us});
std.debug.print(" Per loop iteration: ~{d:.2} µs ({d:.0} iterations)\n", .{ avg_render_us / loop_iterations, loop_iterations });
std.debug.print(" Per variable lookup: ~{d:.3} µs ({d:.0} lookups)\n", .{ avg_render_us / var_lookups, var_lookups });
// Comparison
std.debug.print("\n📊 Comparison with Pug.js:\n", .{});
const pugjs_us: f64 = 55.0; // From benchmark: 110ms / 2000 = 55µs
std.debug.print(" Pug.js: {d:.1} µs/render\n", .{pugjs_us});
std.debug.print(" Pugz: {d:.1} µs/render\n", .{avg_render_us});
const ratio = avg_render_us / pugjs_us;
if (ratio > 1.0) {
std.debug.print(" Status: Pugz is {d:.1}x SLOWER\n", .{ratio});
} else {
std.debug.print(" Status: Pugz is {d:.1}x FASTER\n", .{1.0 / ratio});
}
std.debug.print("\nKey Bottlenecks (likely):\n", .{});
std.debug.print(" 1. Data conversion: Zig struct -> pugz.Value (comptime reflection)\n", .{});
std.debug.print(" 2. Variable lookup: HashMap get() for each interpolation\n", .{});
std.debug.print(" 3. AST traversal: Walking tree nodes vs Pug.js compiled JS functions\n", .{});
std.debug.print(" 4. Loop scope: Creating/clearing scope per loop iteration\n", .{});
std.debug.print("\nAlready optimized:\n", .{});
std.debug.print(" - Scope pooling (reuse hashmap capacity)\n", .{});
std.debug.print(" - Batched HTML escaping\n", .{});
std.debug.print(" - Arena allocator with retain_capacity\n", .{});
}

View File

@@ -0,0 +1,86 @@
/**
* Pug.js Benchmark - Comparison with Pugz
*
* Run: npm install && npm run bench
*
* Both Pug.js and Pugz benchmarks read from the same files:
* ../templates/*.pug (templates)
* ../templates/*.json (data)
*/
const fs = require('fs');
const path = require('path');
const pug = require('pug');
const iterations = 2000;
const templatesDir = path.join(__dirname, '..', 'templates');
const benchmarks = [
'simple-0',
'simple-1',
'simple-2',
'if-expression',
'projects-escaped',
'search-results',
'friends',
];
// ═══════════════════════════════════════════════════════════════════════════
// Load templates and data from shared files BEFORE benchmarking
// ═══════════════════════════════════════════════════════════════════════════
console.log("");
console.log("Loading templates and data...");
const templates = {};
const data = {};
for (const name of benchmarks) {
templates[name] = fs.readFileSync(path.join(templatesDir, `${name}.pug`), 'utf8');
data[name] = JSON.parse(fs.readFileSync(path.join(templatesDir, `${name}.json`), 'utf8'));
}
// Compile all templates BEFORE benchmarking
const compiled = {};
for (const name of benchmarks) {
compiled[name] = pug.compile(templates[name], { pretty: true });
}
console.log("Templates compiled. Starting benchmark...\n");
// ═══════════════════════════════════════════════════════════════════════════
// Benchmark
// ═══════════════════════════════════════════════════════════════════════════
console.log("╔═══════════════════════════════════════════════════════════════╗");
console.log(`║ Pug.js Benchmark (${iterations} iterations) ║`);
console.log("║ Templates: src/benchmarks/templates/*.pug ║");
console.log("║ Data: src/benchmarks/templates/*.json ║");
console.log("╚═══════════════════════════════════════════════════════════════╝");
let total = 0;
for (const name of benchmarks) {
const compiledFn = compiled[name];
const templateData = data[name];
// Warmup
for (let i = 0; i < 100; i++) {
compiledFn(templateData);
}
// Benchmark
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
compiledFn(templateData);
}
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1_000_000;
total += ms;
console.log(` ${name.padEnd(20)} => ${ms.toFixed(1).padStart(7)}ms`);
}
console.log("");
console.log(` ${"TOTAL".padEnd(20)} => ${total.toFixed(1).padStart(7)}ms`);
console.log("");

576
src/benchmarks/pugjs/package-lock.json generated Normal file
View File

@@ -0,0 +1,576 @@
{
"name": "pugjs-benchmark",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pugjs-benchmark",
"version": "1.0.0",
"dependencies": {
"pug": "^3.0.3"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.6"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/assert-never": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz",
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
"license": "MIT"
},
"node_modules/babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.9.6"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
"license": "MIT",
"dependencies": {
"is-regex": "^1.0.3"
}
},
"node_modules/constantinople": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.1"
}
},
"node_modules/doctypes": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-expression": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
"license": "MIT",
"dependencies": {
"acorn": "^7.1.1",
"object-assign": "^4.1.1"
}
},
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/js-stringify": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
"license": "MIT"
},
"node_modules/jstransformer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
"license": "MIT",
"dependencies": {
"is-promise": "^2.0.0",
"promise": "^7.0.1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"license": "MIT",
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/pug": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
"license": "MIT",
"dependencies": {
"pug-code-gen": "^3.0.3",
"pug-filters": "^4.0.0",
"pug-lexer": "^5.0.1",
"pug-linker": "^4.0.0",
"pug-load": "^3.0.0",
"pug-parser": "^6.0.0",
"pug-runtime": "^3.0.1",
"pug-strip-comments": "^2.0.0"
}
},
"node_modules/pug-attrs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"js-stringify": "^1.0.2",
"pug-runtime": "^3.0.0"
}
},
"node_modules/pug-code-gen": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"doctypes": "^1.1.0",
"js-stringify": "^1.0.2",
"pug-attrs": "^3.0.0",
"pug-error": "^2.1.0",
"pug-runtime": "^3.0.1",
"void-elements": "^3.1.0",
"with": "^7.0.0"
}
},
"node_modules/pug-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
"license": "MIT"
},
"node_modules/pug-filters": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"jstransformer": "1.0.0",
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0",
"resolve": "^1.15.1"
}
},
"node_modules/pug-lexer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
"license": "MIT",
"dependencies": {
"character-parser": "^2.2.0",
"is-expression": "^4.0.0",
"pug-error": "^2.0.0"
}
},
"node_modules/pug-linker": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-load": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0",
"token-stream": "1.0.0"
}
},
"node_modules/pug-runtime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
"license": "MIT"
},
"node_modules/pug-strip-comments": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0"
}
},
"node_modules/pug-walk": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/token-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
"license": "MIT"
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/with": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"assert-never": "^1.2.1",
"babel-walk": "3.0.0-canary-5"
},
"engines": {
"node": ">= 10.0.0"
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "pugjs-benchmark",
"version": "1.0.0",
"description": "Pug.js benchmark for comparison with Pugz",
"main": "bench.js",
"scripts": {
"bench": "node bench.js"
},
"dependencies": {
"pug": "^3.0.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"accounts": [
{
"balance": 0,
"balanceFormatted": "$0.00",
"status": "open",
"negative": false
},
{
"balance": 10,
"balanceFormatted": "$10.00",
"status": "closed",
"negative": false
},
{
"balance": -100,
"balanceFormatted": "$-100.00",
"status": "suspended",
"negative": true
},
{
"balance": 999,
"balanceFormatted": "$999.00",
"status": "open",
"negative": false
}
]
}

View File

@@ -0,0 +1,41 @@
{
"title": "Projects",
"text": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
"projects": [
{
"name": "<strong>Facebook</strong>",
"url": "http://facebook.com",
"description": "Social network"
},
{
"name": "<strong>Google</strong>",
"url": "http://google.com",
"description": "Search engine"
},
{
"name": "<strong>Twitter</strong>",
"url": "http://twitter.com",
"description": "Microblogging service"
},
{
"name": "<strong>Amazon</strong>",
"url": "http://amazon.com",
"description": "Online retailer"
},
{
"name": "<strong>eBay</strong>",
"url": "http://ebay.com",
"description": "Online auction"
},
{
"name": "<strong>Wikipedia</strong>",
"url": "http://wikipedia.org",
"description": "A free encyclopedia"
},
{
"name": "<strong>LiveJournal</strong>",
"url": "http://livejournal.com",
"description": "Blogging platform"
}
]
}

View File

@@ -0,0 +1,278 @@
{
"searchRecords": [
{
"imgUrl": "img1.jpg",
"viewItemUrl": "http://foo/1",
"title": "Namebox",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img2.jpg",
"viewItemUrl": "http://foo/2",
"title": "Arctiq",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img3.jpg",
"viewItemUrl": "http://foo/3",
"title": "Niquent",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img4.jpg",
"viewItemUrl": "http://foo/4",
"title": "Remotion",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img5.jpg",
"viewItemUrl": "http://foo/5",
"title": "Octocore",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img6.jpg",
"viewItemUrl": "http://foo/6",
"title": "Spherix",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img7.jpg",
"viewItemUrl": "http://foo/7",
"title": "Quarex",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img8.jpg",
"viewItemUrl": "http://foo/8",
"title": "Supremia",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img9.jpg",
"viewItemUrl": "http://foo/9",
"title": "Amtap",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img10.jpg",
"viewItemUrl": "http://foo/10",
"title": "Qiao",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img11.jpg",
"viewItemUrl": "http://foo/11",
"title": "Pushcart",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img12.jpg",
"viewItemUrl": "http://foo/12",
"title": "Eweville",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img13.jpg",
"viewItemUrl": "http://foo/13",
"title": "Senmei",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img14.jpg",
"viewItemUrl": "http://foo/14",
"title": "Maximind",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img15.jpg",
"viewItemUrl": "http://foo/15",
"title": "Blurrybus",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img16.jpg",
"viewItemUrl": "http://foo/16",
"title": "Virva",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img17.jpg",
"viewItemUrl": "http://foo/17",
"title": "Centregy",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img18.jpg",
"viewItemUrl": "http://foo/18",
"title": "Dancerity",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img19.jpg",
"viewItemUrl": "http://foo/19",
"title": "Oceanica",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img20.jpg",
"viewItemUrl": "http://foo/20",
"title": "Synkgen",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": false,
"sizes": null
}
]
}

View File

@@ -1,33 +1,17 @@
.search-results-container
.searching#searching
.wait-indicator-icon Searching...
#resultsContainer
.hd
span.count
span#count= totalCount
| results
.view-modifiers
.view-select
| View:
.view-icon.view-icon-selected#viewIconGallery
i.icon-th
.view-icon#viewIconList
i.icon-th-list
#resultsTarget
.search-results.view-gallery
each searchRecord in searchRecords
.search-item
.search-item-container.drop-shadow
.img-container
img(src=searchRecord.imgUrl)
h4.title
a(href=searchRecord.viewItemUrl)= searchRecord.title
| #{searchRecord.description}
if searchRecord.featured
div Featured!
if searchRecord.sizes
div
| Sizes available:
ul
each size in searchRecord.sizes
li= size
.search-results.view-gallery
each searchRecord in searchRecords
.search-item
.search-item-container.drop-shadow
.img-container
img(src=searchRecord.imgUrl)
h4.title
a(href=searchRecord.viewItemUrl)= searchRecord.title
| #{searchRecord.description}
if searchRecord.featured
div Featured!
if searchRecord.sizes
div
| Sizes available:
ul
each size in searchRecord.sizes
li= size

View File

@@ -0,0 +1,3 @@
{
"name": "John"
}

View File

@@ -0,0 +1,19 @@
{
"name": "George Washington",
"messageCount": 999,
"colors": [
"red",
"green",
"blue",
"yellow",
"orange",
"pink",
"black",
"white",
"beige",
"brown",
"cyan",
"magenta"
],
"primary": true
}

View File

@@ -0,0 +1,20 @@
{
"header": "Header",
"header2": "Header2",
"header3": "Header3",
"header4": "Header4",
"header5": "Header5",
"header6": "Header6",
"list": [
"1000000000",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10"
]
}

1459
src/build_templates.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@ const whitespace_sensitive = std.StaticStringMap(void).initComptime(.{
pub const CodeGen = struct {
allocator: std.mem.Allocator,
options: Options,
output: std.ArrayListUnmanaged(u8),
output: std.ArrayList(u8),
depth: usize,
/// Track if we're inside a whitespace-sensitive element.
preserve_whitespace: bool,

472
src/compiler.zig Normal file
View File

@@ -0,0 +1,472 @@
//! 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['&'] = "&amp;";
\\ t['<'] = "&lt;";
\\ t['>'] = "&gt;";
\\ t['"'] = "&quot;";
\\ t['\''] = "&#x27;";
\\ 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(\"<!DOCTYPE html>\");\n");
} else {
try self.w("try out.appendSlice(\"<!DOCTYPE ");
try self.wEsc(d.value);
try self.w(">\");\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(\"</");
try self.w(e.tag);
try self.w(">\");\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(\"</");
try self.w(t.tag);
try self.w(">\");\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(\"<!-- ");
try self.wEsc(c.content);
try self.w(" -->\");\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});
}

View File

@@ -1,148 +0,0 @@
//! Pugz Template Inheritance Demo
//!
//! A web application demonstrating Pug-style template inheritance
//! using the Pugz ViewEngine 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,
view: pugz.ViewEngine,
pub fn init(allocator: Allocator) App {
return .{
.allocator = allocator,
.view = pugz.ViewEngine.init(.{
.views_dir = "src/examples/demo/views",
}),
};
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leak");
const allocator = gpa.allocator();
// Initialize view engine once at startup
var app = App.init(allocator);
const port = 8080;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &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:{d}
\\
\\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.
\\
, .{port});
try server.listen();
}
/// Handler for GET /
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
// Use res.arena - memory is automatically freed after response is sent
const html = app.view.render(res.arena, "index", .{
.title = "Home",
.authenticated = true,
}) 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.view.render(res.arena, "page-a", .{
.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.view.render(res.arena, "page-b", .{
.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.view.render(res.arena, "page-append", .{
.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.view.render(res.arena, "page-appen-optional-blk", .{
.title = "Page Append Optional",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}

View File

@@ -128,8 +128,8 @@ pub const Lexer = struct {
pos: usize,
line: usize,
column: usize,
indent_stack: std.ArrayListUnmanaged(usize),
tokens: std.ArrayListUnmanaged(Token),
indent_stack: std.ArrayList(usize),
tokens: std.ArrayList(Token),
allocator: std.mem.Allocator,
at_line_start: bool,
current_indent: usize,
@@ -1174,8 +1174,12 @@ pub const Lexer = struct {
if (self.peek() != ' ') return;
const next = self.peekAt(1);
const next2 = self.peekAt(2);
// Don't consume if followed by another selector, attribute, or special syntax
if (next == '.' or next == '#' or next == '(' or next == '=' or next == ':' or
// BUT: #{...} and #[...] are interpolation, not ID selectors
const is_id_selector = next == '#' and next2 != '{' and next2 != '[';
if (next == '.' or is_id_selector or next == '(' or next == '=' or next == ':' or
next == '\n' or next == '\r' or next == 0)
{
return;

View File

@@ -54,7 +54,7 @@ pub const Parser = struct {
/// Parses all tokens and returns the document AST.
pub fn parse(self: *Parser) Error!ast.Document {
var nodes = std.ArrayListUnmanaged(Node).empty;
var nodes = std.ArrayList(Node).empty;
errdefer nodes.deinit(self.allocator);
var extends_path: ?[]const u8 = null;
@@ -122,9 +122,9 @@ pub const Parser = struct {
/// Parses an HTML element with optional tag, classes, id, attributes, and children.
fn parseElement(self: *Parser) Error!Node {
var tag: []const u8 = "div"; // default tag
var classes = std.ArrayListUnmanaged([]const u8).empty;
var classes = std.ArrayList([]const u8).empty;
var id: ?[]const u8 = null;
var attributes = std.ArrayListUnmanaged(Attribute).empty;
var attributes = std.ArrayList(Attribute).empty;
var spread_attributes: ?[]const u8 = null;
var self_closing = false;
@@ -174,7 +174,7 @@ pub const Parser = struct {
self.skipWhitespace();
// Parse the inline nested element
var children = std.ArrayListUnmanaged(Node).empty;
var children = std.ArrayList(Node).empty;
errdefer children.deinit(self.allocator);
if (try self.parseNode()) |child| {
@@ -223,7 +223,7 @@ pub const Parser = struct {
_ = self.advance();
const raw_content = try self.parseRawTextBlock();
var children = std.ArrayListUnmanaged(Node).empty;
var children = std.ArrayList(Node).empty;
errdefer children.deinit(self.allocator);
try children.append(self.allocator, .{ .raw_text = .{ .content = raw_content } });
@@ -245,7 +245,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse children if indented
var children = std.ArrayListUnmanaged(Node).empty;
var children = std.ArrayList(Node).empty;
errdefer children.deinit(self.allocator);
if (self.check(.indent)) {
@@ -267,7 +267,7 @@ pub const Parser = struct {
}
/// Parses attributes within parentheses.
fn parseAttributes(self: *Parser, attributes: *std.ArrayListUnmanaged(Attribute)) Error!void {
fn parseAttributes(self: *Parser, attributes: *std.ArrayList(Attribute)) Error!void {
while (!self.check(.rparen) and !self.isAtEnd()) {
// Skip commas
if (self.check(.comma)) {
@@ -302,7 +302,7 @@ pub const Parser = struct {
/// Parses text segments (literals and interpolations).
fn parseTextSegments(self: *Parser) Error![]TextSegment {
var segments = std.ArrayListUnmanaged(TextSegment).empty;
var segments = std.ArrayList(TextSegment).empty;
errdefer segments.deinit(self.allocator);
while (self.check(.text) or self.check(.interp_start) or self.check(.interp_start_unesc) or self.check(.tag_interp_start)) {
@@ -338,9 +338,9 @@ pub const Parser = struct {
_ = self.advance(); // skip #[
var tag: []const u8 = "span"; // default tag
var classes = std.ArrayListUnmanaged([]const u8).empty;
var classes = std.ArrayList([]const u8).empty;
var id: ?[]const u8 = null;
var attributes = std.ArrayListUnmanaged(Attribute).empty;
var attributes = std.ArrayList(Attribute).empty;
errdefer classes.deinit(self.allocator);
errdefer attributes.deinit(self.allocator);
@@ -369,7 +369,7 @@ pub const Parser = struct {
}
// Parse inner text segments (may contain nested interpolations)
var text_segments = std.ArrayListUnmanaged(TextSegment).empty;
var text_segments = std.ArrayList(TextSegment).empty;
errdefer text_segments.deinit(self.allocator);
while (!self.check(.tag_interp_end) and !self.check(.newline) and !self.isAtEnd()) {
@@ -415,7 +415,7 @@ pub const Parser = struct {
}
/// Parses children within an indented block.
fn parseChildren(self: *Parser, children: *std.ArrayListUnmanaged(Node)) Error!void {
fn parseChildren(self: *Parser, children: *std.ArrayList(Node)) Error!void {
while (!self.check(.dedent) and !self.isAtEnd()) {
self.skipNewlines();
if (self.check(.dedent) or self.isAtEnd()) break;
@@ -433,7 +433,7 @@ pub const Parser = struct {
/// Parses a raw text block (after `.`).
fn parseRawTextBlock(self: *Parser) Error![]const u8 {
var lines = std.ArrayListUnmanaged(u8).empty;
var lines = std.ArrayList(u8).empty;
errdefer lines.deinit(self.allocator);
while (!self.check(.dedent) and !self.isAtEnd()) {
@@ -470,7 +470,7 @@ pub const Parser = struct {
/// Parses conditional (if/else if/else/unless).
fn parseConditional(self: *Parser) Error!Node {
var branches = std.ArrayListUnmanaged(ast.Conditional.Branch).empty;
var branches = std.ArrayList(ast.Conditional.Branch).empty;
errdefer branches.deinit(self.allocator);
// Parse initial if/unless
@@ -483,7 +483,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse body
var body = std.ArrayListUnmanaged(Node).empty;
var body = std.ArrayList(Node).empty;
errdefer body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -512,7 +512,7 @@ pub const Parser = struct {
self.skipNewlines();
var else_body = std.ArrayListUnmanaged(Node).empty;
var else_body = std.ArrayList(Node).empty;
errdefer else_body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -590,7 +590,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse body
var body = std.ArrayListUnmanaged(Node).empty;
var body = std.ArrayList(Node).empty;
errdefer body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -599,7 +599,7 @@ pub const Parser = struct {
}
// Check for else branch
var else_children = std.ArrayListUnmanaged(Node).empty;
var else_children = std.ArrayList(Node).empty;
errdefer else_children.deinit(self.allocator);
if (self.check(.kw_else)) {
@@ -629,7 +629,7 @@ pub const Parser = struct {
self.skipNewlines();
var body = std.ArrayListUnmanaged(Node).empty;
var body = std.ArrayList(Node).empty;
errdefer body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -651,10 +651,10 @@ pub const Parser = struct {
self.skipNewlines();
var whens = std.ArrayListUnmanaged(ast.Case.When).empty;
var whens = std.ArrayList(ast.Case.When).empty;
errdefer whens.deinit(self.allocator);
var default_children = std.ArrayListUnmanaged(Node).empty;
var default_children = std.ArrayList(Node).empty;
errdefer default_children.deinit(self.allocator);
// Parse indented when/default clauses
@@ -675,7 +675,7 @@ pub const Parser = struct {
value = try self.parseRestOfLine();
}
var when_children = std.ArrayListUnmanaged(Node).empty;
var when_children = std.ArrayList(Node).empty;
errdefer when_children.deinit(self.allocator);
var has_break = false;
@@ -778,8 +778,8 @@ pub const Parser = struct {
}
// Parse parameters if present
var params = std.ArrayListUnmanaged([]const u8).empty;
var defaults = std.ArrayListUnmanaged(?[]const u8).empty;
var params = std.ArrayList([]const u8).empty;
var defaults = std.ArrayList(?[]const u8).empty;
errdefer params.deinit(self.allocator);
errdefer defaults.deinit(self.allocator);
@@ -830,7 +830,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse body
var body = std.ArrayListUnmanaged(Node).empty;
var body = std.ArrayList(Node).empty;
errdefer body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -851,8 +851,8 @@ pub const Parser = struct {
fn parseMixinCall(self: *Parser) Error!Node {
const name = self.advance().value; // +name
var args = std.ArrayListUnmanaged([]const u8).empty;
var attributes = std.ArrayListUnmanaged(Attribute).empty;
var args = std.ArrayList([]const u8).empty;
var attributes = std.ArrayList(Attribute).empty;
errdefer args.deinit(self.allocator);
errdefer attributes.deinit(self.allocator);
@@ -894,7 +894,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse block content
var block_children = std.ArrayListUnmanaged(Node).empty;
var block_children = std.ArrayList(Node).empty;
errdefer block_children.deinit(self.allocator);
if (self.check(.indent)) {
@@ -979,7 +979,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse body
var body = std.ArrayListUnmanaged(Node).empty;
var body = std.ArrayList(Node).empty;
errdefer body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -1011,7 +1011,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse body
var body = std.ArrayListUnmanaged(Node).empty;
var body = std.ArrayList(Node).empty;
errdefer body.deinit(self.allocator);
if (self.check(.indent)) {
@@ -1052,7 +1052,7 @@ pub const Parser = struct {
self.skipNewlines();
// Parse nested comment content
var children = std.ArrayListUnmanaged(Node).empty;
var children = std.ArrayList(Node).empty;
errdefer children.deinit(self.allocator);
if (self.check(.indent)) {
@@ -1091,7 +1091,7 @@ pub const Parser = struct {
/// Parses rest of line as text.
fn parseRestOfLine(self: *Parser) Error![]const u8 {
var result = std.ArrayListUnmanaged(u8).empty;
var result = std.ArrayList(u8).empty;
errdefer result.deinit(self.allocator);
while (!self.check(.newline) and !self.check(.indent) and !self.check(.dedent) and !self.isAtEnd()) {

View File

@@ -55,6 +55,11 @@ pub const renderTemplate = runtime.renderTemplate;
// High-level API
pub const ViewEngine = view_engine.ViewEngine;
pub const CompiledTemplate = view_engine.CompiledTemplate;
// Build-time template compilation
pub const build_templates = @import("build_templates.zig");
pub const compileTemplates = build_templates.compileTemplates;
test {
_ = @import("std").testing.refAllDecls(@This());

View File

@@ -26,6 +26,8 @@ const ast = @import("ast.zig");
const Lexer = @import("lexer.zig").Lexer;
const Parser = @import("parser.zig").Parser;
const log = std.log.scoped(.@"pugz/runtime");
/// A value in the template context.
pub const Value = union(enum) {
/// Null/undefined value.
@@ -44,18 +46,45 @@ pub const Value = union(enum) {
object: std.StringHashMapUnmanaged(Value),
/// Returns the value as a string for output.
/// For integers, uses pre-computed strings for small values to avoid allocation.
pub fn toString(self: Value, allocator: std.mem.Allocator) ![]const u8 {
// Fast path: strings are most common in templates (branch hint)
if (self == .string) {
@branchHint(.likely);
return self.string;
}
return switch (self) {
.string => unreachable, // handled above
.null => "",
.bool => |b| if (b) "true" else "false",
.int => |i| try std.fmt.allocPrint(allocator, "{d}", .{i}),
.int => |i| blk: {
// Fast path for common small integers (0-99)
if (i >= 0 and i < 100) {
break :blk small_int_strings[@intCast(i)];
}
// Allocate for larger integers
break :blk try std.fmt.allocPrint(allocator, "{d}", .{i});
},
.float => |f| try std.fmt.allocPrint(allocator, "{d}", .{f}),
.string => |s| s,
.array => "[Array]",
.object => "[Object]",
};
}
/// Pre-computed strings for small integers 0-99 (common in loops)
const small_int_strings = [_][]const u8{
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
};
/// Returns the value as a boolean for conditionals.
pub fn isTruthy(self: Value) bool {
return switch (self) {
@@ -99,7 +128,7 @@ pub const Context = struct {
allocator: std.mem.Allocator,
/// Stack of variable scopes (innermost last).
/// We keep all scopes allocated and track active depth with scope_depth.
scopes: std.ArrayListUnmanaged(std.StringHashMapUnmanaged(Value)),
scopes: std.ArrayList(std.StringHashMapUnmanaged(Value)),
/// Current active scope depth (scopes[0..scope_depth] are active).
scope_depth: usize,
/// Mixin definitions available in this context.
@@ -153,10 +182,31 @@ pub const Context = struct {
try current.put(self.allocator, name, value);
}
/// Gets or creates a slot for a variable, returning a pointer to the value.
/// Use this for loop variables that are updated repeatedly.
pub fn getOrPutPtr(self: *Context, name: []const u8) !*Value {
if (self.scope_depth == 0) {
try self.pushScope();
}
const current = &self.scopes.items[self.scope_depth - 1];
const gop = try current.getOrPut(self.allocator, name);
if (!gop.found_existing) {
gop.value_ptr.* = Value.null;
}
return gop.value_ptr;
}
/// Gets a variable, searching from innermost to outermost scope.
pub fn get(self: *Context, name: []const u8) ?Value {
// Search from innermost to outermost scope
var i = self.scope_depth;
// Fast path: most lookups are in the innermost scope
if (self.scope_depth > 0) {
@branchHint(.likely);
if (self.scopes.items[self.scope_depth - 1].get(name)) |value| {
return value;
}
}
// Search remaining scopes (less common)
var i = self.scope_depth -| 1;
while (i > 0) {
i -= 1;
if (self.scopes.items[i].get(name)) |value| {
@@ -192,7 +242,7 @@ const BlockDef = struct {
pub const Runtime = struct {
allocator: std.mem.Allocator,
context: *Context,
output: std.ArrayListUnmanaged(u8),
output: std.ArrayList(u8),
depth: usize,
options: Options,
/// File resolver for loading external templates.
@@ -247,7 +297,8 @@ pub const Runtime = struct {
/// Renders the document and returns the HTML output.
pub fn render(self: *Runtime, doc: ast.Document) Error![]const u8 {
try self.output.ensureTotalCapacity(self.allocator, 1024);
// Pre-allocate buffer - 256KB handles most large templates without realloc
try self.output.ensureTotalCapacity(self.allocator, 256 * 1024);
// Handle template inheritance
if (doc.extends_path) |extends_path| {
@@ -380,7 +431,7 @@ pub const Runtime = struct {
}
// Collect all classes: shorthand classes + class attributes (may be arrays)
var all_classes = std.ArrayListUnmanaged(u8).empty;
var all_classes = std.ArrayList(u8).empty;
defer all_classes.deinit(self.allocator);
// Add shorthand classes first (e.g., .bang)
@@ -598,11 +649,18 @@ pub const Runtime = struct {
try self.context.pushScope();
defer self.context.popScope();
// Get direct pointers to loop variables - avoids hash lookup per iteration
const value_ptr = try self.context.getOrPutPtr(each.value_name);
const index_ptr: ?*Value = if (each.index_name) |idx_name|
try self.context.getOrPutPtr(idx_name)
else
null;
for (items, 0..) |item, index| {
// Just overwrite the loop variable (no scope push/pop per iteration)
try self.context.set(each.value_name, item);
if (each.index_name) |idx_name| {
try self.context.set(idx_name, Value.integer(@intCast(index)));
// Direct pointer update - no hash lookup!
value_ptr.* = item;
if (index_ptr) |ptr| {
ptr.* = Value.integer(@intCast(index));
}
for (each.children) |child| {
@@ -622,19 +680,24 @@ pub const Runtime = struct {
try self.context.pushScope();
defer self.context.popScope();
// Get direct pointers to loop variables
const value_ptr = try self.context.getOrPutPtr(each.value_name);
const index_ptr: ?*Value = if (each.index_name) |idx_name|
try self.context.getOrPutPtr(idx_name)
else
null;
var iter = obj.iterator();
var index: usize = 0;
while (iter.next()) |entry| {
// Just overwrite the loop variable (no scope push/pop per iteration)
try self.context.set(each.value_name, entry.value_ptr.*);
if (each.index_name) |idx_name| {
try self.context.set(idx_name, Value.str(entry.key_ptr.*));
// Direct pointer update - no hash lookup!
value_ptr.* = entry.value_ptr.*;
if (index_ptr) |ptr| {
ptr.* = Value.str(entry.key_ptr.*);
}
for (each.children) |child| {
try self.visitNode(child);
}
index += 1;
}
},
else => {
@@ -765,8 +828,11 @@ pub const Runtime = struct {
}
}
// If still not found, skip this mixin call
const mixin_def = mixin orelse return;
// If still not found, log warning and skip this mixin call
const mixin_def = mixin orelse {
log.warn("skipping, mixin '{s}' not found", .{call.name});
return;
};
try self.context.pushScope();
defer self.context.popScope();
@@ -790,9 +856,13 @@ pub const Runtime = struct {
if (attr.value) |val| {
// Strip quotes from attribute value for the object
const clean_val = try self.evaluateString(val);
attrs_obj.put(self.allocator, attr.name, Value.str(clean_val)) catch {};
attrs_obj.put(self.allocator, attr.name, Value.str(clean_val)) catch |err| {
log.warn("skipping attribute, failed to set '{s}': {}", .{ attr.name, err });
};
} else {
attrs_obj.put(self.allocator, attr.name, Value.boolean(true)) catch {};
attrs_obj.put(self.allocator, attr.name, Value.boolean(true)) catch |err| {
log.warn("skipping attribute, failed to set '{s}': {}", .{ attr.name, err });
};
}
}
try self.context.set("attributes", .{ .object = attrs_obj });
@@ -851,10 +921,16 @@ pub const Runtime = struct {
const resolver = self.file_resolver orelse return null;
// First try: look for a file named {name}.pug
const specific_path = std.fs.path.join(self.allocator, &.{ self.mixins_dir, name }) catch return null;
const specific_path = std.fs.path.join(self.allocator, &.{ self.mixins_dir, name }) catch |err| {
log.warn("skipping mixin lookup, failed to join path for '{s}': {}", .{ name, err });
return null;
};
defer self.allocator.free(specific_path);
const with_ext = std.fmt.allocPrint(self.allocator, "{s}.pug", .{specific_path}) catch return null;
const with_ext = std.fmt.allocPrint(self.allocator, "{s}.pug", .{specific_path}) catch |err| {
log.warn("skipping mixin lookup, failed to allocate path for '{s}': {}", .{ name, err });
return null;
};
defer self.allocator.free(with_ext);
if (resolver(self.allocator, with_ext)) |source| {
@@ -869,17 +945,29 @@ pub const Runtime = struct {
// Second try: iterate through all .pug files in mixins directory
// Use cwd().openDir for relative paths, openDirAbsolute for absolute paths
var dir = if (std.fs.path.isAbsolute(self.mixins_dir))
std.fs.openDirAbsolute(self.mixins_dir, .{ .iterate = true }) catch return null
std.fs.openDirAbsolute(self.mixins_dir, .{ .iterate = true }) catch |err| {
log.warn("skipping mixins directory scan, failed to open '{s}': {}", .{ self.mixins_dir, err });
return null;
}
else
std.fs.cwd().openDir(self.mixins_dir, .{ .iterate = true }) catch return null;
std.fs.cwd().openDir(self.mixins_dir, .{ .iterate = true }) catch |err| {
log.warn("skipping mixins directory scan, failed to open '{s}': {}", .{ self.mixins_dir, err });
return null;
};
defer dir.close();
var iter = dir.iterate();
while (iter.next() catch return null) |entry| {
while (iter.next() catch |err| {
log.warn("skipping mixins directory scan, iteration failed: {}", .{err});
return null;
}) |entry| {
if (entry.kind != .file) continue;
if (!std.mem.endsWith(u8, entry.name, ".pug")) continue;
const file_path = std.fs.path.join(self.allocator, &.{ self.mixins_dir, entry.name }) catch continue;
const file_path = std.fs.path.join(self.allocator, &.{ self.mixins_dir, entry.name }) catch |err| {
log.warn("skipping mixin file, failed to join path for '{s}': {}", .{ entry.name, err });
continue;
};
defer self.allocator.free(file_path);
if (resolver(self.allocator, file_path)) |source| {
@@ -898,11 +986,17 @@ pub const Runtime = struct {
/// Parses a source file and extracts a mixin definition by name.
fn parseMixinFromSource(self: *Runtime, source: []const u8, name: []const u8) ?ast.MixinDef {
var lexer = Lexer.init(self.allocator, source);
const tokens = lexer.tokenize() catch return null;
const tokens = lexer.tokenize() catch |err| {
log.warn("skipping mixin file, tokenize failed for '{s}': {}", .{ name, err });
return null;
};
// Note: lexer is not deinitialized - tokens contain slices into source
var parser = Parser.init(self.allocator, tokens);
const doc = parser.parse() catch return null;
const doc = parser.parse() catch |err| {
log.warn("skipping mixin file, parse failed for '{s}': {}", .{ name, err });
return null;
};
// Find the mixin definition with the matching name
for (doc.nodes) |node| {
@@ -999,8 +1093,60 @@ pub const Runtime = struct {
// ─────────────────────────────────────────────────────────────────────────
/// Evaluates a simple expression (variable lookup or literal).
/// Optimized for common cases: simple variable names without operators.
fn evaluateExpression(self: *Runtime, expr: []const u8) Value {
const trimmed = std.mem.trim(u8, expr, " \t");
// Fast path: empty expression
if (expr.len == 0) return Value.null;
const first = expr[0];
// Ultra-fast path: identifier starting with a-z (most common case)
// Covers: friend, name, friend.name, friend.email, tag, etc.
if (first >= 'a' and first <= 'z') {
// Scan for operators - if none found, direct variable lookup
for (expr) |c| {
// Check for operators that require complex evaluation
if (c == '+' or c == '[' or c == '(' or c == '{' or c == ' ' or c == '\t') {
break;
}
} else {
// No operators found - direct variable lookup (most common path)
return self.lookupVariable(expr);
}
}
// Fast path: check if expression needs trimming
const last = expr[expr.len - 1];
const needs_trim = first == ' ' or first == '\t' or last == ' ' or last == '\t';
const trimmed = if (needs_trim) std.mem.trim(u8, expr, " \t") else expr;
if (trimmed.len == 0) return Value.null;
// Fast path: simple variable lookup (no special chars except dots)
// Most expressions in templates are just variable names like "name" or "friend.email"
const first_char = trimmed[0];
if (first_char != '"' and first_char != '\'' and first_char != '-' and
(first_char < '0' or first_char > '9'))
{
// Quick scan: if no special operators, go straight to variable lookup
var has_operator = false;
for (trimmed) |c| {
if (c == '+' or c == '[' or c == '(' or c == '{') {
has_operator = true;
break;
}
}
if (!has_operator) {
// Check for boolean/null literals
if (trimmed.len <= 5) {
if (std.mem.eql(u8, trimmed, "true")) return Value.boolean(true);
if (std.mem.eql(u8, trimmed, "false")) return Value.boolean(false);
if (std.mem.eql(u8, trimmed, "null")) return Value.null;
}
// Simple variable lookup
return self.lookupVariable(trimmed);
}
}
// Check for string concatenation with + operator
// e.g., "btn btn-" + type or "hello " + name + "!"
@@ -1020,8 +1166,8 @@ pub const Runtime = struct {
// Check for string literal
if (trimmed.len >= 2) {
if ((trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') or
(trimmed[0] == '\'' and trimmed[trimmed.len - 1] == '\''))
if ((first_char == '"' and trimmed[trimmed.len - 1] == '"') or
(first_char == '\'' and trimmed[trimmed.len - 1] == '\''))
{
return Value.str(trimmed[1 .. trimmed.len - 1]);
}
@@ -1032,7 +1178,7 @@ pub const Runtime = struct {
return Value.integer(i);
} else |_| {}
// Check for boolean literals
// Check for boolean literals (fallback for complex expressions)
if (std.mem.eql(u8, trimmed, "true")) return Value.boolean(true);
if (std.mem.eql(u8, trimmed, "false")) return Value.boolean(false);
if (std.mem.eql(u8, trimmed, "null")) return Value.null;
@@ -1080,21 +1226,47 @@ pub const Runtime = struct {
}
/// Looks up a variable with dot notation support.
/// Optimized for the common case of single property access (e.g., "friend.name").
fn lookupVariable(self: *Runtime, path: []const u8) Value {
var parts = std.mem.splitScalar(u8, path, '.');
const first = parts.first();
var current = self.context.get(first) orelse return Value.null;
while (parts.next()) |part| {
switch (current) {
.object => |obj| {
current = obj.get(part) orelse return Value.null;
},
else => return Value.null,
// Fast path: find first dot position
var dot_pos: ?usize = null;
for (path, 0..) |c, i| {
if (c == '.') {
dot_pos = i;
break;
}
}
if (dot_pos == null) {
// No dots - simple variable lookup
return self.context.get(path) orelse Value.null;
}
// Has dots - get base variable first
const base_name = path[0..dot_pos.?];
var current = self.context.get(base_name) orelse return Value.null;
// Property access loop - objects are most common
var pos = dot_pos.? + 1;
while (pos < path.len) {
// Find next dot or end
var end = pos;
while (end < path.len and path[end] != '.') {
end += 1;
}
const prop = path[pos..end];
// Most values are objects in property chains (branch hint)
if (current == .object) {
@branchHint(.likely);
current = current.object.get(prop) orelse return Value.null;
} else {
return Value.null;
}
pos = end + 1;
}
return current;
}
@@ -1302,34 +1474,81 @@ pub const Runtime = struct {
}
fn write(self: *Runtime, str: []const u8) Error!void {
try self.output.appendSlice(self.allocator, str);
// Use addManyAsSlice for potentially faster bulk copy
const dest = try self.output.addManyAsSlice(self.allocator, str.len);
@memcpy(dest, str);
}
fn writeEscaped(self: *Runtime, str: []const u8) Error!void {
var start: usize = 0;
// Fast path: use SIMD-friendly byte scan for escape characters
// Check if any escaping needed using a simple loop (compiler can vectorize)
var escape_needed: usize = str.len;
for (str, 0..) |c, i| {
const escape: ?[]const u8 = switch (c) {
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
'"' => "&quot;",
'\'' => "&#x27;",
else => null,
};
if (escape) |esc| {
// Use a lookup instead of multiple comparisons
if (escape_table[c]) {
escape_needed = i;
break;
}
}
// No escaping needed - single fast write
if (escape_needed == str.len) {
const dest = try self.output.addManyAsSlice(self.allocator, str.len);
@memcpy(dest, str);
return;
}
// Write prefix that doesn't need escaping
if (escape_needed > 0) {
const dest = try self.output.addManyAsSlice(self.allocator, escape_needed);
@memcpy(dest, str[0..escape_needed]);
}
// Slow path: escape remaining characters
var start = escape_needed;
for (str[escape_needed..], escape_needed..) |c, i| {
if (escape_table[c]) {
// Write accumulated non-escaped chars first
if (i > start) {
try self.output.appendSlice(self.allocator, str[start..i]);
const chunk = str[start..i];
const dest = try self.output.addManyAsSlice(self.allocator, chunk.len);
@memcpy(dest, chunk);
}
try self.output.appendSlice(self.allocator, esc);
const esc = escape_strings[c];
const dest = try self.output.addManyAsSlice(self.allocator, esc.len);
@memcpy(dest, esc);
start = i + 1;
}
}
// Write remaining non-escaped chars
if (start < str.len) {
try self.output.appendSlice(self.allocator, str[start..]);
const chunk = str[start..];
const dest = try self.output.addManyAsSlice(self.allocator, chunk.len);
@memcpy(dest, chunk);
}
}
/// Lookup table for characters that need HTML escaping
const escape_table = blk: {
var table: [256]bool = [_]bool{false} ** 256;
table['&'] = true;
table['<'] = true;
table['>'] = true;
table['"'] = true;
table['\''] = true;
break :blk table;
};
/// Escape strings for each character
const escape_strings = blk: {
var strings: [256][]const u8 = [_][]const u8{""} ** 256;
strings['&'] = "&amp;";
strings['<'] = "&lt;";
strings['>'] = "&gt;";
strings['"'] = "&quot;";
strings['\''] = "&#x27;";
break :blk strings;
};
};
// ─────────────────────────────────────────────────────────────────────────────
@@ -1361,7 +1580,7 @@ fn parseArrayToSpaceSeparated(allocator: std.mem.Allocator, input: []const u8) !
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
if (content.len == 0) return "";
var result = std.ArrayListUnmanaged(u8).empty;
var result = std.ArrayList(u8).empty;
errdefer result.deinit(allocator);
var pos: usize = 0;
@@ -1424,7 +1643,7 @@ fn parseObjectToCSS(allocator: std.mem.Allocator, input: []const u8) ![]const u8
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
if (content.len == 0) return "";
var result = std.ArrayListUnmanaged(u8).empty;
var result = std.ArrayList(u8).empty;
errdefer result.deinit(allocator);
var pos: usize = 0;
@@ -1549,6 +1768,7 @@ pub fn render(allocator: std.mem.Allocator, doc: ast.Document, data: anytype) ![
}
/// Converts a Zig value to a runtime Value.
/// For best performance, use an arena allocator.
pub fn toValue(allocator: std.mem.Allocator, v: anytype) Value {
const T = @TypeOf(v);
@@ -1595,11 +1815,12 @@ pub fn toValue(allocator: std.mem.Allocator, v: anytype) Value {
return Value.null;
},
.@"struct" => |info| {
// Convert struct to object
// Convert struct to object - pre-allocate for known field count
var obj = std.StringHashMapUnmanaged(Value).empty;
obj.ensureTotalCapacity(allocator, info.fields.len) catch return Value.null;
inline for (info.fields) |field| {
const field_value = @field(v, field.name);
obj.put(allocator, field.name, toValue(allocator, field_value)) catch return Value.null;
obj.putAssumeCapacity(field.name, toValue(allocator, field_value));
}
return .{ .object = obj };
},

View File

@@ -14,6 +14,27 @@ test "Simple interpolation" {
);
}
test "Interpolation only as text" {
try expectOutput(
"h1.header #{header}",
.{ .header = "MyHeader" },
"<h1 class=\"header\">MyHeader</h1>",
);
}
test "Interpolation in each loop" {
try expectOutput(
\\ul.list
\\ each item in list
\\ li.item #{item}
, .{ .list = &[_][]const u8{ "a", "b" } },
\\<ul class="list">
\\ <li class="item">a</li>
\\ <li class="item">b</li>
\\</ul>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Test Case 2: Attributes with inline text
// ─────────────────────────────────────────────────────────────────────────────
@@ -737,4 +758,3 @@ test "Mixin with string concatenation in class" {
\\<button class="btn btn-secondary">Click me</button>
);
}

View File

@@ -4,12 +4,13 @@
//! - Views directory configuration
//! - Lazy-loading mixins from a mixins subdirectory (on-demand)
//! - Relative path resolution for includes and extends
//! - **Compiled templates** for maximum performance (parse once, render many)
//!
//! Mixins are resolved in the following order:
//! 1. Mixins defined in the same template file
//! 2. Mixins from the mixins directory (lazy-loaded when first called)
//!
//! Example:
//! ## Basic Usage
//! ```zig
//! const engine = ViewEngine.init(.{
//! .views_dir = "src/views",
@@ -24,6 +25,19 @@
//! const out = try engine.renderTpl(allocator, tpl, .{ .title = "Hello" });
//! defer allocator.free(out);
//! ```
//!
//! ## Compiled Templates (High Performance)
//! For maximum performance, compile templates once and render many times:
//! ```zig
//! // At startup: compile template (keeps AST in memory)
//! var compiled = try CompiledTemplate.init(gpa, "h1 Hello, #{name}!");
//! defer compiled.deinit();
//!
//! // Per request: render with arena (fast, zero parsing overhead)
//! var arena = std.heap.ArenaAllocator.init(gpa);
//! defer arena.deinit();
//! const html = try compiled.render(arena.allocator(), .{ .name = "World" });
//! ```
const std = @import("std");
const Lexer = @import("lexer.zig").Lexer;
@@ -172,6 +186,151 @@ pub const ViewEngine = struct {
}
};
// ─────────────────────────────────────────────────────────────────────────────
// CompiledTemplate - Parse once, render many times
// ─────────────────────────────────────────────────────────────────────────────
const ast = @import("ast.zig");
/// A pre-compiled template that can be rendered multiple times with different data.
/// This is the fastest way to render templates - parsing happens once at startup,
/// and each render only needs to evaluate the AST with new data.
///
/// Memory layout:
/// - The CompiledTemplate owns an arena that holds all AST nodes and source strings
/// - Call render() with a per-request arena allocator for output
/// - Call deinit() when the template is no longer needed
///
/// Example:
/// ```zig
/// // Compile once at startup
/// var tpl = try CompiledTemplate.init(gpa, "h1 Hello, #{name}!");
/// defer tpl.deinit();
///
/// // Render many times with different data
/// for (requests) |req| {
/// var arena = std.heap.ArenaAllocator.init(gpa);
/// defer arena.deinit();
/// const html = try tpl.render(arena.allocator(), .{ .name = req.name });
/// // send html...
/// }
/// ```
pub const CompiledTemplate = struct {
/// Arena holding all compiled template data (AST, source slices)
arena: std.heap.ArenaAllocator,
/// The parsed document AST
doc: ast.Document,
/// Runtime options
options: RenderOptions,
pub const RenderOptions = struct {
pretty: bool = true,
base_dir: []const u8 = ".",
mixins_dir: []const u8 = "",
};
/// Compiles a template string into a reusable CompiledTemplate.
/// The backing_allocator is used for the internal arena that holds the AST.
pub fn init(backing_allocator: std.mem.Allocator, source: []const u8) !CompiledTemplate {
return initWithOptions(backing_allocator, source, .{});
}
/// Compiles a template with custom options.
pub fn initWithOptions(backing_allocator: std.mem.Allocator, source: []const u8, options: RenderOptions) !CompiledTemplate {
var arena = std.heap.ArenaAllocator.init(backing_allocator);
errdefer arena.deinit();
const alloc = arena.allocator();
// Copy source into arena (AST slices point into it)
const owned_source = try alloc.dupe(u8, source);
// Tokenize
var lexer = Lexer.init(alloc, owned_source);
// Don't deinit lexer - arena owns all memory
const tokens = lexer.tokenize() catch return ViewEngineError.ParseError;
// Parse
var parser = Parser.init(alloc, tokens);
const doc = parser.parse() catch return ViewEngineError.ParseError;
return .{
.arena = arena,
.doc = doc,
.options = options,
};
}
/// Compiles a template from a file.
pub fn initFromFile(backing_allocator: std.mem.Allocator, path: []const u8, options: RenderOptions) !CompiledTemplate {
const source = std.fs.cwd().readFileAlloc(backing_allocator, path, 5 * 1024 * 1024) catch {
return ViewEngineError.TemplateNotFound;
};
defer backing_allocator.free(source);
return initWithOptions(backing_allocator, source, options);
}
/// Releases all memory used by the compiled template.
pub fn deinit(self: *CompiledTemplate) void {
self.arena.deinit();
}
/// Renders the compiled template with the given data.
/// Use a per-request arena allocator for best performance.
pub fn render(self: *const CompiledTemplate, allocator: std.mem.Allocator, data: anytype) ![]u8 {
// Create context with data
var ctx = Context.init(allocator);
defer ctx.deinit();
// Populate context from data struct
try ctx.pushScope();
inline for (std.meta.fields(@TypeOf(data))) |field| {
const value = @field(data, field.name);
try ctx.set(field.name, runtime.toValue(allocator, value));
}
// Create runtime
var rt = Runtime.init(allocator, &ctx, .{
.pretty = self.options.pretty,
.base_dir = self.options.base_dir,
.mixins_dir = self.options.mixins_dir,
.file_resolver = null,
});
defer rt.deinit();
return rt.renderOwned(self.doc);
}
/// Renders with a pre-converted Value context (avoids toValue overhead).
pub fn renderWithValue(self: *const CompiledTemplate, allocator: std.mem.Allocator, data: runtime.Value) ![]u8 {
var ctx = Context.init(allocator);
defer ctx.deinit();
// Populate context from Value object
try ctx.pushScope();
switch (data) {
.object => |obj| {
var iter = obj.iterator();
while (iter.next()) |entry| {
try ctx.set(entry.key_ptr.*, entry.value_ptr.*);
}
},
else => {},
}
var rt = Runtime.init(allocator, &ctx, .{
.pretty = self.options.pretty,
.base_dir = self.options.base_dir,
.mixins_dir = self.options.mixins_dir,
.file_resolver = null,
});
defer rt.deinit();
return rt.renderOwned(self.doc);
}
};
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
@@ -180,3 +339,41 @@ test "ViewEngine resolves paths correctly" {
// This test requires a views directory - skip in unit tests
// Full integration tests are in src/tests/
}
test "CompiledTemplate basic usage" {
const allocator = std.testing.allocator;
var tpl = try CompiledTemplate.init(allocator, "h1 Hello, #{name}!");
defer tpl.deinit();
// Render multiple times
for (0..3) |_| {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const html = try tpl.render(arena.allocator(), .{ .name = "World" });
try std.testing.expectEqualStrings("<h1>Hello, World!</h1>\n", html);
}
}
test "CompiledTemplate with loop" {
const allocator = std.testing.allocator;
var tpl = try CompiledTemplate.init(allocator,
\\ul
\\ each item in items
\\ li= item
);
defer tpl.deinit();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const html = try tpl.render(arena.allocator(), .{
.items = &[_][]const u8{ "a", "b", "c" },
});
try std.testing.expect(std.mem.indexOf(u8, html, "<li>a</li>") != null);
try std.testing.expect(std.mem.indexOf(u8, html, "<li>b</li>") != null);
try std.testing.expect(std.mem.indexOf(u8, html, "<li>c</li>") != null);
}