Compiled temapltes.
Benchmark cleanup
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,6 +5,9 @@ zig-cache/
|
|||||||
.pugz-cache/
|
.pugz-cache/
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# compiled template file
|
||||||
|
generated.zig
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
125
CLAUDE.md
125
CLAUDE.md
@@ -10,16 +10,24 @@ Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug
|
|||||||
|
|
||||||
- `zig build` - Build the project (output in `zig-out/`)
|
- `zig build` - Build the project (output in `zig-out/`)
|
||||||
- `zig build test` - Run all tests
|
- `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)
|
||||||
|
|
||||||
## Architecture Overview
|
## 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
|
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
|
### Core Modules
|
||||||
|
|
||||||
| Module | Purpose |
|
| Module | Purpose |
|
||||||
@@ -28,13 +36,93 @@ Source → Lexer → Tokens → Parser → AST → Runtime → HTML
|
|||||||
| **src/parser.zig** | Converts token stream into AST. Handles nesting via indent/dedent tokens. |
|
| **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/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/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/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
|
### Test Files
|
||||||
|
|
||||||
- **src/tests/general_test.zig** - Comprehensive integration tests for all features
|
- **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
|
## Memory Management
|
||||||
|
|
||||||
@@ -57,6 +145,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.`)
|
- `in_raw_block` / `raw_block_indent` / `raw_block_started` - For dot block text (e.g., `script.`)
|
||||||
- `indent_stack` - Stack-based indent/dedent token generation
|
- `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
|
### 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.
|
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 +215,9 @@ p.
|
|||||||
Multi-line
|
Multi-line
|
||||||
text block
|
text block
|
||||||
<p>Literal HTML</p> // passed through as-is
|
<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
|
### Tag Interpolation
|
||||||
@@ -154,6 +247,10 @@ else
|
|||||||
|
|
||||||
unless loggedIn
|
unless loggedIn
|
||||||
p Please login
|
p Please login
|
||||||
|
|
||||||
|
// String comparison in conditions
|
||||||
|
if status == "active"
|
||||||
|
p Active
|
||||||
```
|
```
|
||||||
|
|
||||||
### Iteration
|
### Iteration
|
||||||
@@ -172,6 +269,12 @@ else
|
|||||||
// Works with objects too (key as index)
|
// Works with objects too (key as index)
|
||||||
each val, key in object
|
each val, key in object
|
||||||
p #{key}: #{val}
|
p #{key}: #{val}
|
||||||
|
|
||||||
|
// Nested iteration with field access
|
||||||
|
each friend in friends
|
||||||
|
li #{friend.name}
|
||||||
|
each tag in friend.tags
|
||||||
|
span= tag
|
||||||
```
|
```
|
||||||
|
|
||||||
### Case/When
|
### Case/When
|
||||||
@@ -241,9 +344,13 @@ block prepend styles
|
|||||||
|
|
||||||
## Server Usage
|
## 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
|
```zig
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
@@ -342,14 +449,16 @@ Run tests with `zig build test`. Tests cover:
|
|||||||
- Class and ID shorthand syntax
|
- Class and ID shorthand syntax
|
||||||
- Attribute parsing (quoted, unquoted, boolean, object literals)
|
- Attribute parsing (quoted, unquoted, boolean, object literals)
|
||||||
- Text interpolation (escaped, unescaped, tag interpolation)
|
- Text interpolation (escaped, unescaped, tag interpolation)
|
||||||
|
- Interpolation-only text (e.g., `h1.class #{var}`)
|
||||||
- Conditionals (if/else if/else/unless)
|
- 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
|
- Case/when statements
|
||||||
- Mixin definitions and calls (with defaults, rest args, block content, attributes)
|
- Mixin definitions and calls (with defaults, rest args, block content, attributes)
|
||||||
- Plain text (piped, dot blocks, literal HTML)
|
- Plain text (piped, dot blocks, literal HTML)
|
||||||
- Self-closing tags (void elements, explicit `/`)
|
- Self-closing tags (void elements, explicit `/`)
|
||||||
- Block expansion with colon
|
- Block expansion with colon
|
||||||
- Comments (rendered and silent)
|
- Comments (rendered and silent)
|
||||||
|
- String comparison in conditions
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
@@ -365,4 +474,4 @@ Potential areas for enhancement:
|
|||||||
- Filter support (`:markdown`, `:stylus`, etc.)
|
- Filter support (`:markdown`, `:stylus`, etc.)
|
||||||
- More complete JavaScript expression evaluation
|
- More complete JavaScript expression evaluation
|
||||||
- Source maps for debugging
|
- Source maps for debugging
|
||||||
- Compile-time template validation
|
- Mixin support in compiled templates
|
||||||
|
|||||||
@@ -3,18 +3,11 @@
|
|||||||
.version = "0.1.3",
|
.version = "0.1.3",
|
||||||
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{},
|
||||||
.httpz = .{
|
|
||||||
.url = "git+https://github.com/karlseguin/http.zig?ref=master#9ef2ffe8d611ff2e1081e5cf39cb4632c145c5b9",
|
|
||||||
.hash = "httpz-0.0.0-PNVzrIowBwAFr_kqBN1W4KBMC2Ofutasj2ZfNAIcfTzF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
.paths = .{
|
.paths = .{
|
||||||
"build.zig",
|
"build.zig",
|
||||||
"build.zig.zon",
|
"build.zig.zon",
|
||||||
"src",
|
"src",
|
||||||
// For example...
|
"examples",
|
||||||
//"LICENSE",
|
|
||||||
//"README.md",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
50
examples/demo/build.zig
Normal file
50
examples/demo/build.zig
Normal 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);
|
||||||
|
}
|
||||||
21
examples/demo/build.zig.zon
Normal file
21
examples/demo/build.zig.zon
Normal 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
156
examples/demo/src/main.zig
Normal 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;
|
||||||
|
}
|
||||||
15
examples/demo/views/home.pug
Normal file
15
examples/demo/views/home.pug
Normal 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 © 2024 Pugz Demo
|
||||||
11
examples/demo/views/users.pug
Normal file
11
examples/demo/views/users.pug
Normal 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
172
src/benchmarks/bench.zig
Normal 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;
|
||||||
|
}
|
||||||
@@ -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", .{});
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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", .{});
|
|
||||||
}
|
|
||||||
686
src/build_templates.zig
Normal file
686
src/build_templates.zig
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
//! Pugz Build Step - Compile .pug templates to Zig code at build time.
|
||||||
|
//!
|
||||||
|
//! Generates a single `generated.zig` file in the views folder containing:
|
||||||
|
//! - Shared helper functions (esc, truthy)
|
||||||
|
//! - All compiled template render functions
|
||||||
|
//!
|
||||||
|
//! ## Usage in build.zig:
|
||||||
|
//! ```zig
|
||||||
|
//! const build_templates = @import("pugz").build_templates;
|
||||||
|
//! const templates = build_templates.compileTemplates(b, .{
|
||||||
|
//! .source_dir = "views",
|
||||||
|
//! });
|
||||||
|
//! exe.root_module.addImport("templates", templates);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Usage in code:
|
||||||
|
//! ```zig
|
||||||
|
//! const tpls = @import("templates");
|
||||||
|
//! const html = try tpls.home(allocator, .{ .title = "Welcome" });
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Lexer = @import("lexer.zig").Lexer;
|
||||||
|
const Parser = @import("parser.zig").Parser;
|
||||||
|
const ast = @import("ast.zig");
|
||||||
|
|
||||||
|
pub const Options = struct {
|
||||||
|
source_dir: []const u8 = "views",
|
||||||
|
extension: []const u8 = ".pug",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn compileTemplates(b: *std.Build, options: Options) *std.Build.Module {
|
||||||
|
const gen_step = TemplateGenStep.create(b, options);
|
||||||
|
return b.createModule(.{
|
||||||
|
.root_source_file = gen_step.getOutput(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateGenStep = struct {
|
||||||
|
step: std.Build.Step,
|
||||||
|
options: Options,
|
||||||
|
generated_file: std.Build.GeneratedFile,
|
||||||
|
|
||||||
|
fn create(b: *std.Build, options: Options) *TemplateGenStep {
|
||||||
|
const self = b.allocator.create(TemplateGenStep) catch @panic("OOM");
|
||||||
|
self.* = .{
|
||||||
|
.step = std.Build.Step.init(.{
|
||||||
|
.id = .custom,
|
||||||
|
.name = "pugz-compile-templates",
|
||||||
|
.owner = b,
|
||||||
|
.makeFn = make,
|
||||||
|
}),
|
||||||
|
.options = options,
|
||||||
|
.generated_file = .{ .step = &self.step },
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getOutput(self: *TemplateGenStep) std.Build.LazyPath {
|
||||||
|
return .{ .generated = .{ .file = &self.generated_file } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
|
||||||
|
const self: *TemplateGenStep = @fieldParentPtr("step", step);
|
||||||
|
const b = step.owner;
|
||||||
|
const allocator = b.allocator;
|
||||||
|
|
||||||
|
var templates = std.ArrayListUnmanaged(TemplateInfo){};
|
||||||
|
defer templates.deinit(allocator);
|
||||||
|
try findTemplates(allocator, self.options.source_dir, "", self.options.extension, &templates);
|
||||||
|
|
||||||
|
const out_path = try std.fs.path.join(allocator, &.{ self.options.source_dir, "generated.zig" });
|
||||||
|
try generateSingleFile(allocator, self.options.source_dir, out_path, templates.items);
|
||||||
|
|
||||||
|
self.generated_file.path = out_path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TemplateInfo = struct {
|
||||||
|
rel_path: []const u8,
|
||||||
|
zig_name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn findTemplates(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
base_dir: []const u8,
|
||||||
|
sub_path: []const u8,
|
||||||
|
extension: []const u8,
|
||||||
|
templates: *std.ArrayListUnmanaged(TemplateInfo),
|
||||||
|
) !void {
|
||||||
|
const full_path = if (sub_path.len > 0)
|
||||||
|
try std.fs.path.join(allocator, &.{ base_dir, sub_path })
|
||||||
|
else
|
||||||
|
try allocator.dupe(u8, base_dir);
|
||||||
|
defer allocator.free(full_path);
|
||||||
|
|
||||||
|
var dir = std.fs.cwd().openDir(full_path, .{ .iterate = true }) catch |err| {
|
||||||
|
std.log.warn("Cannot open directory {s}: {}", .{ full_path, err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
var iter = dir.iterate();
|
||||||
|
while (try iter.next()) |entry| {
|
||||||
|
const name = try allocator.dupe(u8, entry.name);
|
||||||
|
|
||||||
|
if (entry.kind == .directory) {
|
||||||
|
const new_sub = if (sub_path.len > 0)
|
||||||
|
try std.fs.path.join(allocator, &.{ sub_path, name })
|
||||||
|
else
|
||||||
|
name;
|
||||||
|
try findTemplates(allocator, base_dir, new_sub, extension, templates);
|
||||||
|
} else if (entry.kind == .file and std.mem.endsWith(u8, name, extension)) {
|
||||||
|
const rel_path = if (sub_path.len > 0)
|
||||||
|
try std.fs.path.join(allocator, &.{ sub_path, name })
|
||||||
|
else
|
||||||
|
name;
|
||||||
|
|
||||||
|
const without_ext = rel_path[0 .. rel_path.len - extension.len];
|
||||||
|
const zig_name = try pathToIdent(allocator, without_ext);
|
||||||
|
|
||||||
|
try templates.append(allocator, .{
|
||||||
|
.rel_path = rel_path,
|
||||||
|
.zig_name = zig_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pathToIdent(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
|
||||||
|
var result = try allocator.alloc(u8, path.len);
|
||||||
|
for (path, 0..) |c, i| {
|
||||||
|
result[i] = switch (c) {
|
||||||
|
'/', '\\', '-', '.' => '_',
|
||||||
|
else => c,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateSingleFile(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
source_dir: []const u8,
|
||||||
|
out_path: []const u8,
|
||||||
|
templates: []const TemplateInfo,
|
||||||
|
) !void {
|
||||||
|
var out = std.ArrayListUnmanaged(u8){};
|
||||||
|
defer out.deinit(allocator);
|
||||||
|
|
||||||
|
const w = out.writer(allocator);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
try w.writeAll(
|
||||||
|
\\//! Auto-generated by pugz.compileTemplates()
|
||||||
|
\\//! Do not edit manually - regenerate by running: zig build
|
||||||
|
\\
|
||||||
|
\\const std = @import("std");
|
||||||
|
\\const Allocator = std.mem.Allocator;
|
||||||
|
\\const ArrayList = std.ArrayList(u8);
|
||||||
|
\\
|
||||||
|
\\// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
\\// Helpers
|
||||||
|
\\// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
\\
|
||||||
|
\\const esc_lut: [256]?[]const u8 = blk: {
|
||||||
|
\\ var t: [256]?[]const u8 = .{null} ** 256;
|
||||||
|
\\ t['&'] = "&";
|
||||||
|
\\ t['<'] = "<";
|
||||||
|
\\ t['>'] = ">";
|
||||||
|
\\ t['"'] = """;
|
||||||
|
\\ t['\''] = "'";
|
||||||
|
\\ break :blk t;
|
||||||
|
\\};
|
||||||
|
\\
|
||||||
|
\\fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void {
|
||||||
|
\\ var i: usize = 0;
|
||||||
|
\\ for (s, 0..) |c, j| {
|
||||||
|
\\ if (esc_lut[c]) |e| {
|
||||||
|
\\ if (j > i) try o.appendSlice(a, s[i..j]);
|
||||||
|
\\ try o.appendSlice(a, e);
|
||||||
|
\\ i = j + 1;
|
||||||
|
\\ }
|
||||||
|
\\ }
|
||||||
|
\\ if (i < s.len) try o.appendSlice(a, s[i..]);
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\fn truthy(v: anytype) bool {
|
||||||
|
\\ return switch (@typeInfo(@TypeOf(v))) {
|
||||||
|
\\ .bool => v,
|
||||||
|
\\ .optional => v != null,
|
||||||
|
\\ .pointer => |p| if (p.size == .slice) v.len > 0 else true,
|
||||||
|
\\ .int, .comptime_int => v != 0,
|
||||||
|
\\ else => true,
|
||||||
|
\\ };
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\var int_buf: [32]u8 = undefined;
|
||||||
|
\\
|
||||||
|
\\fn strVal(v: anytype) []const u8 {
|
||||||
|
\\ const T = @TypeOf(v);
|
||||||
|
\\ return switch (@typeInfo(T)) {
|
||||||
|
\\ .pointer => |p| if (p.size == .slice) v else @compileError("unsupported pointer type"),
|
||||||
|
\\ .int, .comptime_int => std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0",
|
||||||
|
\\ .optional => if (v) |val| strVal(val) else "",
|
||||||
|
\\ else => @compileError("strVal: unsupported type " ++ @typeName(T)),
|
||||||
|
\\ };
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
\\// Templates
|
||||||
|
\\// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate each template
|
||||||
|
for (templates) |tpl| {
|
||||||
|
const src_path = try std.fs.path.join(allocator, &.{ source_dir, tpl.rel_path });
|
||||||
|
defer allocator.free(src_path);
|
||||||
|
|
||||||
|
const source = std.fs.cwd().readFileAlloc(allocator, src_path, 5 * 1024 * 1024) catch |err| {
|
||||||
|
std.log.err("Failed to read {s}: {}", .{ src_path, err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
defer allocator.free(source);
|
||||||
|
|
||||||
|
try compileTemplate(allocator, w, tpl.zig_name, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template names list
|
||||||
|
try w.writeAll("pub const template_names = [_][]const u8{\n");
|
||||||
|
for (templates) |tpl| {
|
||||||
|
try w.print(" \"{s}\",\n", .{tpl.zig_name});
|
||||||
|
}
|
||||||
|
try w.writeAll("};\n");
|
||||||
|
|
||||||
|
const file = try std.fs.cwd().createFile(out_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
try file.writeAll(out.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compileTemplate(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
w: std.ArrayListUnmanaged(u8).Writer,
|
||||||
|
name: []const u8,
|
||||||
|
source: []const u8,
|
||||||
|
) !void {
|
||||||
|
var lexer = Lexer.init(allocator, source);
|
||||||
|
defer lexer.deinit();
|
||||||
|
const tokens = lexer.tokenize() catch |err| {
|
||||||
|
std.log.err("Tokenize error in '{s}': {}", .{ name, err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
var parser = Parser.init(allocator, tokens);
|
||||||
|
const doc = parser.parse() catch |err| {
|
||||||
|
std.log.err("Parse error in '{s}': {}", .{ name, err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if template has content
|
||||||
|
var has_content = false;
|
||||||
|
for (doc.nodes) |node| {
|
||||||
|
if (nodeHasOutput(node)) {
|
||||||
|
has_content = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if template has any dynamic content
|
||||||
|
var has_dynamic = false;
|
||||||
|
for (doc.nodes) |node| {
|
||||||
|
if (nodeHasDynamic(node)) {
|
||||||
|
has_dynamic = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try w.print("pub fn {s}(a: Allocator, d: anytype) Allocator.Error![]u8 {{\n", .{name});
|
||||||
|
|
||||||
|
if (!has_content) {
|
||||||
|
// Empty template (extends-only, mixin definitions, etc.)
|
||||||
|
try w.writeAll(" _ = .{ a, d };\n");
|
||||||
|
try w.writeAll(" return \"\";\n");
|
||||||
|
} else if (!has_dynamic) {
|
||||||
|
// Static-only template - return literal string, no allocation
|
||||||
|
try w.writeAll(" _ = .{ a, d };\n");
|
||||||
|
var compiler = Compiler.init(allocator, w);
|
||||||
|
try w.writeAll(" return ");
|
||||||
|
for (doc.nodes) |node| {
|
||||||
|
try compiler.emitNode(node);
|
||||||
|
}
|
||||||
|
try compiler.flushAsReturn();
|
||||||
|
} else {
|
||||||
|
// Dynamic template - needs ArrayList
|
||||||
|
try w.writeAll(" var o: ArrayList = .empty;\n");
|
||||||
|
|
||||||
|
var compiler = Compiler.init(allocator, w);
|
||||||
|
for (doc.nodes) |node| {
|
||||||
|
try compiler.emitNode(node);
|
||||||
|
}
|
||||||
|
try compiler.flush();
|
||||||
|
|
||||||
|
try w.writeAll(" return o.items;\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
try w.writeAll("}\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nodeHasOutput(node: ast.Node) bool {
|
||||||
|
return switch (node) {
|
||||||
|
.doctype, .element, .text, .raw_text, .comment => true,
|
||||||
|
.conditional => |c| blk: {
|
||||||
|
for (c.branches) |br| {
|
||||||
|
for (br.children) |child| {
|
||||||
|
if (nodeHasOutput(child)) break :blk true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
},
|
||||||
|
.each => |e| blk: {
|
||||||
|
for (e.children) |child| {
|
||||||
|
if (nodeHasOutput(child)) break :blk true;
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
},
|
||||||
|
.document => |d| blk: {
|
||||||
|
for (d.nodes) |child| {
|
||||||
|
if (nodeHasOutput(child)) break :blk true;
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
},
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nodeHasDynamic(node: ast.Node) bool {
|
||||||
|
return switch (node) {
|
||||||
|
.element => |e| blk: {
|
||||||
|
if (e.buffered_code != null) break :blk true;
|
||||||
|
if (e.inline_text) |segs| {
|
||||||
|
for (segs) |seg| {
|
||||||
|
if (seg != .literal) break :blk true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (e.children) |child| {
|
||||||
|
if (nodeHasDynamic(child)) break :blk true;
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
},
|
||||||
|
.text => |t| blk: {
|
||||||
|
for (t.segments) |seg| {
|
||||||
|
if (seg != .literal) break :blk true;
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
},
|
||||||
|
.conditional, .each => true,
|
||||||
|
.document => |d| blk: {
|
||||||
|
for (d.nodes) |child| {
|
||||||
|
if (nodeHasDynamic(child)) break :blk true;
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
},
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Compiler = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
writer: std.ArrayListUnmanaged(u8).Writer,
|
||||||
|
buf: std.ArrayListUnmanaged(u8), // Buffer for merging static strings
|
||||||
|
depth: usize,
|
||||||
|
loop_vars: std.ArrayListUnmanaged([]const u8), // Track loop variable names
|
||||||
|
|
||||||
|
fn init(allocator: std.mem.Allocator, writer: std.ArrayListUnmanaged(u8).Writer) Compiler {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.writer = writer,
|
||||||
|
.buf = .{},
|
||||||
|
.depth = 1,
|
||||||
|
.loop_vars = .{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(self: *Compiler) !void {
|
||||||
|
if (self.buf.items.len > 0) {
|
||||||
|
try self.writeIndent();
|
||||||
|
try self.writer.writeAll("try o.appendSlice(a, \"");
|
||||||
|
try self.writer.writeAll(self.buf.items);
|
||||||
|
try self.writer.writeAll("\");\n");
|
||||||
|
self.buf.items.len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flushAsReturn(self: *Compiler) !void {
|
||||||
|
// For static-only templates - return string literal directly
|
||||||
|
try self.writer.writeAll("\"");
|
||||||
|
try self.writer.writeAll(self.buf.items);
|
||||||
|
try self.writer.writeAll("\";\n");
|
||||||
|
self.buf.items.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendStatic(self: *Compiler, s: []const u8) !void {
|
||||||
|
for (s) |c| {
|
||||||
|
const escaped: []const u8 = switch (c) {
|
||||||
|
'\\' => "\\\\",
|
||||||
|
'"' => "\\\"",
|
||||||
|
'\n' => "\\n",
|
||||||
|
'\r' => "\\r",
|
||||||
|
'\t' => "\\t",
|
||||||
|
else => &[_]u8{c},
|
||||||
|
};
|
||||||
|
try self.buf.appendSlice(self.allocator, escaped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeIndent(self: *Compiler) !void {
|
||||||
|
for (0..self.depth) |_| try self.writer.writeAll(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitNode(self: *Compiler, node: ast.Node) anyerror!void {
|
||||||
|
switch (node) {
|
||||||
|
.doctype => |dt| {
|
||||||
|
if (std.mem.eql(u8, dt.value, "html")) {
|
||||||
|
try self.appendStatic("<!DOCTYPE html>");
|
||||||
|
} else {
|
||||||
|
try self.appendStatic("<!DOCTYPE ");
|
||||||
|
try self.appendStatic(dt.value);
|
||||||
|
try self.appendStatic(">");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.element => |e| try self.emitElement(e),
|
||||||
|
.text => |t| try self.emitText(t.segments),
|
||||||
|
.raw_text => |r| try self.appendStatic(r.content),
|
||||||
|
.conditional => |c| try self.emitConditional(c),
|
||||||
|
.each => |e| try self.emitEach(e),
|
||||||
|
.comment => |c| if (c.rendered) {
|
||||||
|
try self.appendStatic("<!-- ");
|
||||||
|
try self.appendStatic(c.content);
|
||||||
|
try self.appendStatic(" -->");
|
||||||
|
},
|
||||||
|
.document => |dc| for (dc.nodes) |child| try self.emitNode(child),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitElement(self: *Compiler, e: ast.Element) anyerror!void {
|
||||||
|
const is_void = isVoidElement(e.tag) or e.self_closing;
|
||||||
|
|
||||||
|
// Open tag
|
||||||
|
try self.appendStatic("<");
|
||||||
|
try self.appendStatic(e.tag);
|
||||||
|
|
||||||
|
if (e.id) |id| {
|
||||||
|
try self.appendStatic(" id=\"");
|
||||||
|
try self.appendStatic(id);
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.classes.len > 0) {
|
||||||
|
try self.appendStatic(" class=\"");
|
||||||
|
for (e.classes, 0..) |cls, i| {
|
||||||
|
if (i > 0) try self.appendStatic(" ");
|
||||||
|
try self.appendStatic(cls);
|
||||||
|
}
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e.attributes) |attr| {
|
||||||
|
if (attr.value) |v| {
|
||||||
|
if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) {
|
||||||
|
try self.appendStatic(" ");
|
||||||
|
try self.appendStatic(attr.name);
|
||||||
|
try self.appendStatic("=\"");
|
||||||
|
try self.appendStatic(v[1 .. v.len - 1]);
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try self.appendStatic(" ");
|
||||||
|
try self.appendStatic(attr.name);
|
||||||
|
try self.appendStatic("=\"");
|
||||||
|
try self.appendStatic(attr.name);
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_void) {
|
||||||
|
try self.appendStatic(" />");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.appendStatic(">");
|
||||||
|
|
||||||
|
if (e.inline_text) |segs| {
|
||||||
|
try self.emitText(segs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.buffered_code) |bc| {
|
||||||
|
try self.emitExpr(bc.expression, bc.escaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e.children) |child| {
|
||||||
|
try self.emitNode(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.appendStatic("</");
|
||||||
|
try self.appendStatic(e.tag);
|
||||||
|
try self.appendStatic(">");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitText(self: *Compiler, segs: []const ast.TextSegment) anyerror!void {
|
||||||
|
for (segs) |seg| {
|
||||||
|
switch (seg) {
|
||||||
|
.literal => |lit| try self.appendStatic(lit),
|
||||||
|
.interp_escaped => |expr| try self.emitExpr(expr, true),
|
||||||
|
.interp_unescaped => |expr| try self.emitExpr(expr, false),
|
||||||
|
.interp_tag => |t| try self.emitInlineTag(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitInlineTag(self: *Compiler, t: ast.InlineTag) anyerror!void {
|
||||||
|
try self.appendStatic("<");
|
||||||
|
try self.appendStatic(t.tag);
|
||||||
|
if (t.id) |id| {
|
||||||
|
try self.appendStatic(" id=\"");
|
||||||
|
try self.appendStatic(id);
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
}
|
||||||
|
if (t.classes.len > 0) {
|
||||||
|
try self.appendStatic(" class=\"");
|
||||||
|
for (t.classes, 0..) |cls, i| {
|
||||||
|
if (i > 0) try self.appendStatic(" ");
|
||||||
|
try self.appendStatic(cls);
|
||||||
|
}
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
}
|
||||||
|
try self.appendStatic(">");
|
||||||
|
try self.emitText(t.text_segments);
|
||||||
|
try self.appendStatic("</");
|
||||||
|
try self.appendStatic(t.tag);
|
||||||
|
try self.appendStatic(">");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitExpr(self: *Compiler, expr: []const u8, escaped: bool) !void {
|
||||||
|
try self.flush(); // Dynamic content - flush static buffer first
|
||||||
|
try self.writeIndent();
|
||||||
|
|
||||||
|
// Generate the accessor expression
|
||||||
|
var accessor_buf: [512]u8 = undefined;
|
||||||
|
const accessor = self.buildAccessor(expr, &accessor_buf);
|
||||||
|
|
||||||
|
// Use strVal helper to handle type conversion
|
||||||
|
if (escaped) {
|
||||||
|
try self.writer.print("try esc(&o, a, strVal({s}));\n", .{accessor});
|
||||||
|
} else {
|
||||||
|
try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isLoopVar(self: *Compiler, name: []const u8) bool {
|
||||||
|
for (self.loop_vars.items) |v| {
|
||||||
|
if (std.mem.eql(u8, v, name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildAccessor(self: *Compiler, expr: []const u8, buf: []u8) []const u8 {
|
||||||
|
// Handle nested field access like friend.name, subFriend.id
|
||||||
|
if (std.mem.indexOfScalar(u8, expr, '.')) |dot| {
|
||||||
|
const base = expr[0..dot];
|
||||||
|
const rest = expr[dot + 1 ..];
|
||||||
|
// For loop variables like friend.name, access directly
|
||||||
|
return std.fmt.bufPrint(buf, "{s}.{s}", .{ base, rest }) catch expr;
|
||||||
|
} else {
|
||||||
|
// Check if it's a loop variable (like color, item, tag)
|
||||||
|
if (self.isLoopVar(expr)) {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
// For top-level like "name", access from d
|
||||||
|
return std.fmt.bufPrint(buf, "@field(d, \"{s}\")", .{expr}) catch expr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitConditional(self: *Compiler, c: ast.Conditional) anyerror!void {
|
||||||
|
try self.flush();
|
||||||
|
for (c.branches, 0..) |br, i| {
|
||||||
|
try self.writeIndent();
|
||||||
|
if (i == 0) {
|
||||||
|
if (br.is_unless) {
|
||||||
|
try self.writer.writeAll("if (!");
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll("if (");
|
||||||
|
}
|
||||||
|
try self.emitCondition(br.condition orelse "true");
|
||||||
|
try self.writer.writeAll(") {\n");
|
||||||
|
} else if (br.condition) |cond| {
|
||||||
|
try self.writer.writeAll("} else if (");
|
||||||
|
try self.emitCondition(cond);
|
||||||
|
try self.writer.writeAll(") {\n");
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll("} else {\n");
|
||||||
|
}
|
||||||
|
self.depth += 1;
|
||||||
|
for (br.children) |child| try self.emitNode(child);
|
||||||
|
try self.flush();
|
||||||
|
self.depth -= 1;
|
||||||
|
}
|
||||||
|
try self.writeIndent();
|
||||||
|
try self.writer.writeAll("}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitCondition(self: *Compiler, cond: []const u8) !void {
|
||||||
|
// Handle string equality: status == "closed" -> std.mem.eql(u8, status, "closed")
|
||||||
|
if (std.mem.indexOf(u8, cond, " == \"")) |eq_pos| {
|
||||||
|
const lhs = std.mem.trim(u8, cond[0..eq_pos], " ");
|
||||||
|
const rhs_start = eq_pos + 5; // skip ' == "'
|
||||||
|
if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| {
|
||||||
|
const rhs = cond[rhs_start .. rhs_start + rhs_end];
|
||||||
|
try self.writer.print("std.mem.eql(u8, {s}, \"{s}\")", .{ lhs, rhs });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle string inequality: status != "closed"
|
||||||
|
if (std.mem.indexOf(u8, cond, " != \"")) |eq_pos| {
|
||||||
|
const lhs = std.mem.trim(u8, cond[0..eq_pos], " ");
|
||||||
|
const rhs_start = eq_pos + 5;
|
||||||
|
if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| {
|
||||||
|
const rhs = cond[rhs_start .. rhs_start + rhs_end];
|
||||||
|
try self.writer.print("!std.mem.eql(u8, {s}, \"{s}\")", .{ lhs, rhs });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Regular field access
|
||||||
|
if (std.mem.indexOfScalar(u8, cond, '.')) |_| {
|
||||||
|
try self.writer.print("truthy({s})", .{cond});
|
||||||
|
} else {
|
||||||
|
try self.writer.print("truthy(@field(d, \"{s}\"))", .{cond});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitEach(self: *Compiler, e: ast.Each) anyerror!void {
|
||||||
|
try self.flush();
|
||||||
|
try self.writeIndent();
|
||||||
|
|
||||||
|
// Track this loop variable
|
||||||
|
try self.loop_vars.append(self.allocator, e.value_name);
|
||||||
|
|
||||||
|
// Generate the for loop - handle optional collections with orelse
|
||||||
|
if (std.mem.indexOfScalar(u8, e.collection, '.')) |dot| {
|
||||||
|
const base = e.collection[0..dot];
|
||||||
|
const field = e.collection[dot + 1 ..];
|
||||||
|
// Use orelse to handle optional slices
|
||||||
|
try self.writer.print("for (if (@typeInfo(@TypeOf({s}.{s})) == .optional) ({s}.{s} orelse &.{{}}) else {s}.{s}) |{s}", .{ base, field, base, field, base, field, e.value_name });
|
||||||
|
} else {
|
||||||
|
try self.writer.print("for (@field(d, \"{s}\")) |{s}", .{ e.collection, e.value_name });
|
||||||
|
}
|
||||||
|
if (e.index_name) |idx| {
|
||||||
|
try self.writer.print(", {s}", .{idx});
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("| {\n");
|
||||||
|
|
||||||
|
self.depth += 1;
|
||||||
|
for (e.children) |child| {
|
||||||
|
try self.emitNode(child);
|
||||||
|
}
|
||||||
|
try self.flush();
|
||||||
|
self.depth -= 1;
|
||||||
|
|
||||||
|
try self.writeIndent();
|
||||||
|
try self.writer.writeAll("}\n");
|
||||||
|
|
||||||
|
// Pop loop variable
|
||||||
|
_ = self.loop_vars.pop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isVoidElement(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);
|
||||||
|
}
|
||||||
472
src/compiler.zig
Normal file
472
src/compiler.zig
Normal 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.ArrayListUnmanaged(u8),
|
||||||
|
depth: u8,
|
||||||
|
|
||||||
|
fn init(allocator: std.mem.Allocator) Compiler {
|
||||||
|
return .{
|
||||||
|
.alloc = allocator,
|
||||||
|
.out = .{},
|
||||||
|
.depth = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Compiler) void {
|
||||||
|
self.out.deinit(self.alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile(self: *Compiler, name: []const u8, doc: ast.Document) ![]u8 {
|
||||||
|
// Header
|
||||||
|
try self.w(
|
||||||
|
\\const std = @import("std");
|
||||||
|
\\
|
||||||
|
\\/// HTML escape lookup table
|
||||||
|
\\const esc_table = blk: {
|
||||||
|
\\ var t: [256]?[]const u8 = .{null} ** 256;
|
||||||
|
\\ t['&'] = "&";
|
||||||
|
\\ t['<'] = "<";
|
||||||
|
\\ t['>'] = ">";
|
||||||
|
\\ t['"'] = """;
|
||||||
|
\\ t['\''] = "'";
|
||||||
|
\\ break :blk t;
|
||||||
|
\\};
|
||||||
|
\\
|
||||||
|
\\fn esc(out: *std.ArrayList(u8), s: []const u8) !void {
|
||||||
|
\\ var i: usize = 0;
|
||||||
|
\\ for (s, 0..) |c, j| {
|
||||||
|
\\ if (esc_table[c]) |e| {
|
||||||
|
\\ if (j > i) try out.appendSlice(s[i..j]);
|
||||||
|
\\ try out.appendSlice(e);
|
||||||
|
\\ i = j + 1;
|
||||||
|
\\ }
|
||||||
|
\\ }
|
||||||
|
\\ if (i < s.len) try out.appendSlice(s[i..]);
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\fn toStr(v: anytype) []const u8 {
|
||||||
|
\\ const T = @TypeOf(v);
|
||||||
|
\\ if (T == []const u8) return v;
|
||||||
|
\\ if (@typeInfo(T) == .optional) {
|
||||||
|
\\ if (v) |inner| return toStr(inner);
|
||||||
|
\\ return "";
|
||||||
|
\\ }
|
||||||
|
\\ return "";
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function signature
|
||||||
|
try self.w("pub fn ");
|
||||||
|
try self.w(name);
|
||||||
|
try self.w("(out: *std.ArrayList(u8), data: anytype) !void {\n");
|
||||||
|
self.depth = 1;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
for (doc.nodes) |n| {
|
||||||
|
try self.node(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.w("}\n");
|
||||||
|
return try self.alloc.dupe(u8, self.out.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(self: *Compiler, n: ast.Node) anyerror!void {
|
||||||
|
switch (n) {
|
||||||
|
.doctype => |d| try self.doctype(d),
|
||||||
|
.element => |e| try self.element(e),
|
||||||
|
.text => |t| try self.text(t.segments),
|
||||||
|
.conditional => |c| try self.conditional(c),
|
||||||
|
.each => |e| try self.each(e),
|
||||||
|
.raw_text => |r| try self.raw(r.content),
|
||||||
|
.comment => |c| if (c.rendered) try self.comment(c),
|
||||||
|
.code => |c| try self.code(c),
|
||||||
|
.document => |d| for (d.nodes) |child| try self.node(child),
|
||||||
|
.mixin_def, .mixin_call, .mixin_block, .@"while", .case, .block, .include, .extends => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doctype(self: *Compiler, d: ast.Doctype) !void {
|
||||||
|
try self.indent();
|
||||||
|
if (std.mem.eql(u8, d.value, "html")) {
|
||||||
|
try self.w("try out.appendSlice(\"<!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});
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1174,8 +1174,12 @@ pub const Lexer = struct {
|
|||||||
if (self.peek() != ' ') return;
|
if (self.peek() != ' ') return;
|
||||||
|
|
||||||
const next = self.peekAt(1);
|
const next = self.peekAt(1);
|
||||||
|
const next2 = self.peekAt(2);
|
||||||
|
|
||||||
// Don't consume if followed by another selector, attribute, or special syntax
|
// 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)
|
next == '\n' or next == '\r' or next == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ pub const renderTemplate = runtime.renderTemplate;
|
|||||||
|
|
||||||
// High-level API
|
// High-level API
|
||||||
pub const ViewEngine = view_engine.ViewEngine;
|
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 {
|
test {
|
||||||
_ = @import("std").testing.refAllDecls(@This());
|
_ = @import("std").testing.refAllDecls(@This());
|
||||||
|
|||||||
280
src/runtime.zig
280
src/runtime.zig
@@ -46,18 +46,45 @@ pub const Value = union(enum) {
|
|||||||
object: std.StringHashMapUnmanaged(Value),
|
object: std.StringHashMapUnmanaged(Value),
|
||||||
|
|
||||||
/// Returns the value as a string for output.
|
/// 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 {
|
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) {
|
return switch (self) {
|
||||||
|
.string => unreachable, // handled above
|
||||||
.null => "",
|
.null => "",
|
||||||
.bool => |b| if (b) "true" else "false",
|
.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}),
|
.float => |f| try std.fmt.allocPrint(allocator, "{d}", .{f}),
|
||||||
.string => |s| s,
|
|
||||||
.array => "[Array]",
|
.array => "[Array]",
|
||||||
.object => "[Object]",
|
.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.
|
/// Returns the value as a boolean for conditionals.
|
||||||
pub fn isTruthy(self: Value) bool {
|
pub fn isTruthy(self: Value) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@@ -155,10 +182,31 @@ pub const Context = struct {
|
|||||||
try current.put(self.allocator, name, value);
|
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.
|
/// Gets a variable, searching from innermost to outermost scope.
|
||||||
pub fn get(self: *Context, name: []const u8) ?Value {
|
pub fn get(self: *Context, name: []const u8) ?Value {
|
||||||
// Search from innermost to outermost scope
|
// Fast path: most lookups are in the innermost scope
|
||||||
var i = self.scope_depth;
|
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) {
|
while (i > 0) {
|
||||||
i -= 1;
|
i -= 1;
|
||||||
if (self.scopes.items[i].get(name)) |value| {
|
if (self.scopes.items[i].get(name)) |value| {
|
||||||
@@ -249,7 +297,8 @@ pub const Runtime = struct {
|
|||||||
|
|
||||||
/// Renders the document and returns the HTML output.
|
/// Renders the document and returns the HTML output.
|
||||||
pub fn render(self: *Runtime, doc: ast.Document) Error![]const u8 {
|
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
|
// Handle template inheritance
|
||||||
if (doc.extends_path) |extends_path| {
|
if (doc.extends_path) |extends_path| {
|
||||||
@@ -600,11 +649,18 @@ pub const Runtime = struct {
|
|||||||
try self.context.pushScope();
|
try self.context.pushScope();
|
||||||
defer self.context.popScope();
|
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| {
|
for (items, 0..) |item, index| {
|
||||||
// Just overwrite the loop variable (no scope push/pop per iteration)
|
// Direct pointer update - no hash lookup!
|
||||||
try self.context.set(each.value_name, item);
|
value_ptr.* = item;
|
||||||
if (each.index_name) |idx_name| {
|
if (index_ptr) |ptr| {
|
||||||
try self.context.set(idx_name, Value.integer(@intCast(index)));
|
ptr.* = Value.integer(@intCast(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (each.children) |child| {
|
for (each.children) |child| {
|
||||||
@@ -624,19 +680,24 @@ pub const Runtime = struct {
|
|||||||
try self.context.pushScope();
|
try self.context.pushScope();
|
||||||
defer self.context.popScope();
|
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 iter = obj.iterator();
|
||||||
var index: usize = 0;
|
|
||||||
while (iter.next()) |entry| {
|
while (iter.next()) |entry| {
|
||||||
// Just overwrite the loop variable (no scope push/pop per iteration)
|
// Direct pointer update - no hash lookup!
|
||||||
try self.context.set(each.value_name, entry.value_ptr.*);
|
value_ptr.* = entry.value_ptr.*;
|
||||||
if (each.index_name) |idx_name| {
|
if (index_ptr) |ptr| {
|
||||||
try self.context.set(idx_name, Value.str(entry.key_ptr.*));
|
ptr.* = Value.str(entry.key_ptr.*);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (each.children) |child| {
|
for (each.children) |child| {
|
||||||
try self.visitNode(child);
|
try self.visitNode(child);
|
||||||
}
|
}
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
@@ -1032,8 +1093,60 @@ pub const Runtime = struct {
|
|||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Evaluates a simple expression (variable lookup or literal).
|
/// 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 {
|
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
|
// Check for string concatenation with + operator
|
||||||
// e.g., "btn btn-" + type or "hello " + name + "!"
|
// e.g., "btn btn-" + type or "hello " + name + "!"
|
||||||
@@ -1053,8 +1166,8 @@ pub const Runtime = struct {
|
|||||||
|
|
||||||
// Check for string literal
|
// Check for string literal
|
||||||
if (trimmed.len >= 2) {
|
if (trimmed.len >= 2) {
|
||||||
if ((trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') or
|
if ((first_char == '"' and trimmed[trimmed.len - 1] == '"') or
|
||||||
(trimmed[0] == '\'' and trimmed[trimmed.len - 1] == '\''))
|
(first_char == '\'' and trimmed[trimmed.len - 1] == '\''))
|
||||||
{
|
{
|
||||||
return Value.str(trimmed[1 .. trimmed.len - 1]);
|
return Value.str(trimmed[1 .. trimmed.len - 1]);
|
||||||
}
|
}
|
||||||
@@ -1065,7 +1178,7 @@ pub const Runtime = struct {
|
|||||||
return Value.integer(i);
|
return Value.integer(i);
|
||||||
} else |_| {}
|
} 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, "true")) return Value.boolean(true);
|
||||||
if (std.mem.eql(u8, trimmed, "false")) return Value.boolean(false);
|
if (std.mem.eql(u8, trimmed, "false")) return Value.boolean(false);
|
||||||
if (std.mem.eql(u8, trimmed, "null")) return Value.null;
|
if (std.mem.eql(u8, trimmed, "null")) return Value.null;
|
||||||
@@ -1113,21 +1226,47 @@ pub const Runtime = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Looks up a variable with dot notation support.
|
/// 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 {
|
fn lookupVariable(self: *Runtime, path: []const u8) Value {
|
||||||
var parts = std.mem.splitScalar(u8, path, '.');
|
// Fast path: find first dot position
|
||||||
const first = parts.first();
|
var dot_pos: ?usize = null;
|
||||||
|
for (path, 0..) |c, i| {
|
||||||
var current = self.context.get(first) orelse return Value.null;
|
if (c == '.') {
|
||||||
|
dot_pos = i;
|
||||||
while (parts.next()) |part| {
|
break;
|
||||||
switch (current) {
|
|
||||||
.object => |obj| {
|
|
||||||
current = obj.get(part) orelse return Value.null;
|
|
||||||
},
|
|
||||||
else => return Value.null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1335,34 +1474,81 @@ pub const Runtime = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write(self: *Runtime, str: []const u8) Error!void {
|
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 {
|
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| {
|
for (str, 0..) |c, i| {
|
||||||
const escape: ?[]const u8 = switch (c) {
|
// Use a lookup instead of multiple comparisons
|
||||||
'&' => "&",
|
if (escape_table[c]) {
|
||||||
'<' => "<",
|
escape_needed = i;
|
||||||
'>' => ">",
|
break;
|
||||||
'"' => """,
|
}
|
||||||
'\'' => "'",
|
}
|
||||||
else => null,
|
|
||||||
};
|
// No escaping needed - single fast write
|
||||||
if (escape) |esc| {
|
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
|
// Write accumulated non-escaped chars first
|
||||||
if (i > start) {
|
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;
|
start = i + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Write remaining non-escaped chars
|
// Write remaining non-escaped chars
|
||||||
if (start < str.len) {
|
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['&'] = "&";
|
||||||
|
strings['<'] = "<";
|
||||||
|
strings['>'] = ">";
|
||||||
|
strings['"'] = """;
|
||||||
|
strings['\''] = "'";
|
||||||
|
break :blk strings;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1582,6 +1768,7 @@ pub fn render(allocator: std.mem.Allocator, doc: ast.Document, data: anytype) ![
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a Zig value to a runtime Value.
|
/// 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 {
|
pub fn toValue(allocator: std.mem.Allocator, v: anytype) Value {
|
||||||
const T = @TypeOf(v);
|
const T = @TypeOf(v);
|
||||||
|
|
||||||
@@ -1628,11 +1815,12 @@ pub fn toValue(allocator: std.mem.Allocator, v: anytype) Value {
|
|||||||
return Value.null;
|
return Value.null;
|
||||||
},
|
},
|
||||||
.@"struct" => |info| {
|
.@"struct" => |info| {
|
||||||
// Convert struct to object
|
// Convert struct to object - pre-allocate for known field count
|
||||||
var obj = std.StringHashMapUnmanaged(Value).empty;
|
var obj = std.StringHashMapUnmanaged(Value).empty;
|
||||||
|
obj.ensureTotalCapacity(allocator, info.fields.len) catch return Value.null;
|
||||||
inline for (info.fields) |field| {
|
inline for (info.fields) |field| {
|
||||||
const field_value = @field(v, field.name);
|
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 };
|
return .{ .object = obj };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
// 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>
|
\\<button class="btn btn-secondary">Click me</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
//! - Views directory configuration
|
//! - Views directory configuration
|
||||||
//! - Lazy-loading mixins from a mixins subdirectory (on-demand)
|
//! - Lazy-loading mixins from a mixins subdirectory (on-demand)
|
||||||
//! - Relative path resolution for includes and extends
|
//! - Relative path resolution for includes and extends
|
||||||
|
//! - **Compiled templates** for maximum performance (parse once, render many)
|
||||||
//!
|
//!
|
||||||
//! Mixins are resolved in the following order:
|
//! Mixins are resolved in the following order:
|
||||||
//! 1. Mixins defined in the same template file
|
//! 1. Mixins defined in the same template file
|
||||||
//! 2. Mixins from the mixins directory (lazy-loaded when first called)
|
//! 2. Mixins from the mixins directory (lazy-loaded when first called)
|
||||||
//!
|
//!
|
||||||
//! Example:
|
//! ## Basic Usage
|
||||||
//! ```zig
|
//! ```zig
|
||||||
//! const engine = ViewEngine.init(.{
|
//! const engine = ViewEngine.init(.{
|
||||||
//! .views_dir = "src/views",
|
//! .views_dir = "src/views",
|
||||||
@@ -24,6 +25,19 @@
|
|||||||
//! const out = try engine.renderTpl(allocator, tpl, .{ .title = "Hello" });
|
//! const out = try engine.renderTpl(allocator, tpl, .{ .title = "Hello" });
|
||||||
//! defer allocator.free(out);
|
//! 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 std = @import("std");
|
||||||
const Lexer = @import("lexer.zig").Lexer;
|
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
|
// Tests
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -180,3 +339,41 @@ test "ViewEngine resolves paths correctly" {
|
|||||||
// This test requires a views directory - skip in unit tests
|
// This test requires a views directory - skip in unit tests
|
||||||
// Full integration tests are in src/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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user