Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0bbee089f | |||
| 66981d6908 | |||
| 70ba7af27d | |||
| d53ff24931 | |||
| 752b64d0a9 | |||
| ca573f3166 | |||
| 0f2f19f9b1 | |||
| 654b45ee10 | |||
| 714db30a8c | |||
| 510dcfbb03 | |||
| 5841ec38d8 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,6 +2,11 @@
|
|||||||
zig-out/
|
zig-out/
|
||||||
zig-cache/
|
zig-cache/
|
||||||
.zig-cache/
|
.zig-cache/
|
||||||
|
.pugz-cache/
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# compiled template file
|
||||||
|
generated.zig
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
129
CLAUDE.md
129
CLAUDE.md
@@ -6,20 +6,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering.
|
Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- don not auto commit, user will do it.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
- `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)
|
||||||
|
- `zig build bench-interpreted` - Inpterpret trmplates
|
||||||
|
|
||||||
## 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 +40,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 +149,8 @@ The lexer tracks several states for handling complex syntax:
|
|||||||
- `in_raw_block` / `raw_block_indent` / `raw_block_started` - For dot block text (e.g., `script.`)
|
- `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 +219,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 +251,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 +273,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 +348,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 +453,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 +478,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
|
||||||
|
|||||||
198
README.md
198
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Pugz
|
# Pugz
|
||||||
|
|
||||||
A Pug template engine for Zig.
|
A Pug template engine for Zig, supporting both build-time compilation and runtime interpretation.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -18,29 +18,85 @@ A Pug template engine for Zig.
|
|||||||
Add pugz as a dependency in your `build.zig.zon`:
|
Add pugz as a dependency in your `build.zig.zon`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
zig fetch --save "git+https://code.patial.tech/zig/pugz#main"
|
zig fetch --save "git+https://github.com/ankitpatial/pugz#main"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then in your `build.zig`, add the `pugz` module as a dependency:
|
> **Note:** The primary repository is hosted at `code.patial.tech`. GitHub is a mirror. For dependencies, prefer the GitHub mirror for better availability.
|
||||||
|
|
||||||
```zig
|
---
|
||||||
const pugz = b.dependency("pugz", .{
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
exe.root_module.addImport("pugz", pugz.module("pugz"));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
**Important:** Always use an arena allocator for rendering. The render function creates many small allocations that should be freed together. Using a general-purpose allocator without freeing will cause memory leaks.
|
### Compiled Mode (Build-Time)
|
||||||
|
|
||||||
|
Templates are converted to native Zig code at build time. No parsing happens at runtime.
|
||||||
|
|
||||||
|
**build.zig:**
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const pugz_dep = b.dependency("pugz", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const build_templates = @import("pugz").build_templates;
|
||||||
|
const compiled_templates = build_templates.compileTemplates(b, .{
|
||||||
|
.source_dir = "views",
|
||||||
|
});
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "myapp",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
|
||||||
|
.{ .name = "tpls", .module = compiled_templates },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
b.installArtifact(exe);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
const tpls = @import("tpls");
|
||||||
|
|
||||||
|
pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
return try tpls.home(arena.allocator(), .{
|
||||||
|
.title = "Welcome",
|
||||||
|
.user = .{ .name = "Alice" },
|
||||||
|
.items = &[_][]const u8{ "One", "Two", "Three" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Interpreted Mode (Runtime)
|
||||||
|
|
||||||
|
Templates are parsed and evaluated at runtime. Useful for development or dynamic templates.
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const pugz = @import("pugz");
|
const pugz = @import("pugz");
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
const engine = pugz.ViewEngine.init(.{
|
var engine = pugz.ViewEngine.init(.{
|
||||||
.views_dir = "views",
|
.views_dir = "views",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,29 +112,10 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### With http.zig
|
**Inline template strings:**
|
||||||
|
|
||||||
When using with http.zig, use `res.arena` which is automatically freed after each response:
|
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
fn handler(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
const html = try pugz.renderTemplate(allocator,
|
||||||
const html = app.view.render(res.arena, "index", .{
|
|
||||||
.title = "Hello",
|
|
||||||
}) catch |err| {
|
|
||||||
res.status = 500;
|
|
||||||
res.body = @errorName(err);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
res.content_type = .HTML;
|
|
||||||
res.body = html;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Template String
|
|
||||||
|
|
||||||
```zig
|
|
||||||
const html = try engine.renderTpl(allocator,
|
|
||||||
\\h1 Hello, #{name}!
|
\\h1 Hello, #{name}!
|
||||||
\\ul
|
\\ul
|
||||||
\\ each item in items
|
\\ each item in items
|
||||||
@@ -89,67 +126,76 @@ const html = try engine.renderTpl(allocator,
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
---
|
||||||
|
|
||||||
### Run Tests
|
### With http.zig
|
||||||
|
|
||||||
```bash
|
```zig
|
||||||
zig build test
|
fn handler(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
// Compiled mode
|
||||||
|
const html = try tpls.home(res.arena, .{
|
||||||
|
.title = "Hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.content_type = .HTML;
|
||||||
|
res.body = html;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run Benchmarks
|
---
|
||||||
|
|
||||||
```bash
|
## Memory Management
|
||||||
zig build bench # Run rendering benchmarks
|
|
||||||
zig build bench-2 # Run comparison benchmarks
|
Always use an `ArenaAllocator` for rendering. Template rendering creates many small allocations that should be freed together.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const html = try engine.render(arena.allocator(), "index", data);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Template Syntax
|
---
|
||||||
|
|
||||||
```pug
|
## Documentation
|
||||||
doctype html
|
|
||||||
html
|
|
||||||
head
|
|
||||||
title= title
|
|
||||||
body
|
|
||||||
h1.header Hello, #{name}!
|
|
||||||
|
|
||||||
if authenticated
|
- [Template Syntax](docs/syntax.md) - Complete syntax reference
|
||||||
p Welcome back!
|
- [API Reference](docs/api.md) - Detailed API documentation
|
||||||
else
|
|
||||||
a(href="/login") Sign in
|
|
||||||
|
|
||||||
ul
|
---
|
||||||
each item in items
|
|
||||||
li= item
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
### Rendering Benchmarks (`zig build bench`)
|
Same templates and data (`src/benchmarks/templates/`), MacBook Air M2, 2000 iterations, best of 5 runs.
|
||||||
|
|
||||||
20,000 iterations on MacBook Air M2:
|
| Template | Pug.js | Pugz Compiled | Diff | Pugz Interpreted | Diff |
|
||||||
|
|----------|--------|---------------|------|------------------|------|
|
||||||
|
| simple-0 | 0.4ms | 0.1ms | +4x | 0.4ms | 1x |
|
||||||
|
| simple-1 | 1.3ms | 0.6ms | +2.2x | 5.8ms | -4.5x |
|
||||||
|
| simple-2 | 1.6ms | 0.5ms | +3.2x | 4.6ms | -2.9x |
|
||||||
|
| if-expression | 0.5ms | 0.2ms | +2.5x | 4.1ms | -8.2x |
|
||||||
|
| projects-escaped | 4.2ms | 0.6ms | +7x | 5.8ms | -1.4x |
|
||||||
|
| search-results | 14.7ms | 5.3ms | +2.8x | 50.7ms | -3.4x |
|
||||||
|
| friends | 145.5ms | 50.4ms | +2.9x | 450.8ms | -3.1x |
|
||||||
|
|
||||||
| Template | Avg | Renders/sec | Output |
|
- Pug.js and Pugz Compiled: render-only (pre-compiled)
|
||||||
|----------|-----|-------------|--------|
|
- Pugz Interpreted: parse + render on each iteration
|
||||||
| Simple | 11.81 us | 84,701 | 155 bytes |
|
- Diff: +Nx = N times faster, -Nx = N times slower
|
||||||
| Medium | 21.10 us | 47,404 | 1,211 bytes |
|
|
||||||
| Complex | 33.48 us | 29,872 | 4,852 bytes |
|
|
||||||
|
|
||||||
### Comparison Benchmarks (`zig build bench-2`)
|
---
|
||||||
ref: https://github.com/itsarnaud/template-engine-bench
|
|
||||||
|
|
||||||
2,000 iterations vs Pug.js:
|
## Development
|
||||||
|
|
||||||
| Template | Pugz | Pug.js | Speedup |
|
```bash
|
||||||
|----------|------|--------|---------|
|
zig build test # Run all tests
|
||||||
| simple-0 | 0.5ms | 2ms | 3.8x |
|
zig build bench-compiled # Benchmark compiled mode
|
||||||
| simple-1 | 6.7ms | 9ms | 1.3x |
|
zig build bench-interpreted # Benchmark interpreted mode
|
||||||
| simple-2 | 5.4ms | 9ms | 1.7x |
|
|
||||||
| if-expression | 4.4ms | 12ms | 2.7x |
|
# Pug.js benchmark (for comparison)
|
||||||
| projects-escaped | 7.3ms | 86ms | 11.7x |
|
cd src/benchmarks/pugjs && npm install && npm run bench
|
||||||
| search-results | 70.6ms | 41ms | 0.6x |
|
```
|
||||||
| friends | 682.1ms | 110ms | 0.2x |
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
99
build.zig
99
build.zig
@@ -1,11 +1,15 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Re-export build_templates for use by dependent packages
|
||||||
|
pub const build_templates = @import("src/build_templates.zig");
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
const mod = b.addModule("pugz", .{
|
const mod = b.addModule("pugz", .{
|
||||||
.root_source_file = b.path("src/root.zig"),
|
.root_source_file = b.path("src/root.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creates an executable that will run `test` blocks from the provided module.
|
// Creates an executable that will run `test` blocks from the provided module.
|
||||||
@@ -78,101 +82,62 @@ pub fn build(b: *std.Build) void {
|
|||||||
test_unit_step.dependOn(&run_mod_tests.step);
|
test_unit_step.dependOn(&run_mod_tests.step);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// Example: demo - Template Inheritance Demo with http.zig
|
// Compiled Templates Benchmark (compare with Pug.js bench.js)
|
||||||
|
// Uses auto-generated templates from src/benchmarks/templates/
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
const httpz_dep = b.dependency("httpz", .{
|
const mod_fast = b.addModule("pugz-fast", .{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = .ReleaseFast,
|
||||||
});
|
});
|
||||||
|
|
||||||
const demo = b.addExecutable(.{
|
const bench_templates = build_templates.compileTemplates(b, .{
|
||||||
.name = "demo",
|
.source_dir = "src/benchmarks/templates",
|
||||||
.root_module = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/examples/demo/main.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
.imports = &.{
|
|
||||||
.{ .name = "pugz", .module = mod },
|
|
||||||
.{ .name = "httpz", .module = httpz_dep.module("httpz") },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
b.installArtifact(demo);
|
const bench_compiled = b.addExecutable(.{
|
||||||
|
.name = "bench-compiled",
|
||||||
const run_demo = b.addRunArtifact(demo);
|
|
||||||
run_demo.step.dependOn(b.getInstallStep());
|
|
||||||
|
|
||||||
const demo_step = b.step("demo", "Run the template inheritance demo web app");
|
|
||||||
demo_step.dependOn(&run_demo.step);
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// Benchmark executable
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
const bench = b.addExecutable(.{
|
|
||||||
.name = "bench",
|
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path("src/benchmarks/benchmark.zig"),
|
.root_source_file = b.path("src/benchmarks/bench.zig"),
|
||||||
.target = target,
|
|
||||||
.optimize = .ReleaseFast, // Always use ReleaseFast for benchmarks
|
|
||||||
.imports = &.{
|
|
||||||
.{ .name = "pugz", .module = mod },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
b.installArtifact(bench);
|
|
||||||
|
|
||||||
const run_bench = b.addRunArtifact(bench);
|
|
||||||
run_bench.step.dependOn(b.getInstallStep());
|
|
||||||
|
|
||||||
const bench_step = b.step("bench", "Run rendering benchmarks");
|
|
||||||
bench_step.dependOn(&run_bench.step);
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// Comparison Benchmark Tests (template-engine-bench templates)
|
|
||||||
// Run all: zig build test-bench
|
|
||||||
// Run one: zig build test-bench -- simple-0
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
const bench_tests = b.addTest(.{
|
|
||||||
.root_module = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/benchmarks/benchmark_2.zig"),
|
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = .ReleaseFast,
|
.optimize = .ReleaseFast,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "pugz", .module = mod },
|
.{ .name = "pugz", .module = mod_fast },
|
||||||
|
.{ .name = "tpls", .module = bench_templates },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
.filters = if (b.args) |args| args else &.{},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const run_bench_tests = b.addRunArtifact(bench_tests);
|
b.installArtifact(bench_compiled);
|
||||||
|
|
||||||
const bench_test_step = b.step("bench-2", "Run comparison benchmarks (template-engine-bench)");
|
const run_bench_compiled = b.addRunArtifact(bench_compiled);
|
||||||
bench_test_step.dependOn(&run_bench_tests.step);
|
run_bench_compiled.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
const bench_compiled_step = b.step("bench-compiled", "Benchmark compiled templates (compare with Pug.js)");
|
||||||
|
bench_compiled_step.dependOn(&run_bench_compiled.step);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// Profile executable (for CPU profiling)
|
// Interpreted (Runtime) Benchmark
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
const profile = b.addExecutable(.{
|
const bench_interpreted = b.addExecutable(.{
|
||||||
.name = "profile",
|
.name = "bench-interpreted",
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path("src/benchmarks/profile_friends.zig"),
|
.root_source_file = b.path("src/benchmarks/bench_interpreted.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = .ReleaseFast,
|
.optimize = .ReleaseFast,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "pugz", .module = mod },
|
.{ .name = "pugz", .module = mod_fast },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
b.installArtifact(profile);
|
b.installArtifact(bench_interpreted);
|
||||||
|
|
||||||
const run_profile = b.addRunArtifact(profile);
|
const run_bench_interpreted = b.addRunArtifact(bench_interpreted);
|
||||||
run_profile.step.dependOn(b.getInstallStep());
|
run_bench_interpreted.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
const profile_step = b.step("profile", "Run friends template for profiling");
|
const bench_interpreted_step = b.step("bench-interpreted", "Benchmark interpreted (runtime) templates");
|
||||||
profile_step.dependOn(&run_profile.step);
|
bench_interpreted_step.dependOn(&run_bench_interpreted.step);
|
||||||
|
|
||||||
// Just like flags, top level steps are also listed in the `--help` menu.
|
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
.{
|
.{
|
||||||
.name = .pugz,
|
.name = .pugz,
|
||||||
.version = "0.1.3",
|
.version = "0.1.4",
|
||||||
.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",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
289
docs/api.md
Normal file
289
docs/api.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# Pugz API Reference
|
||||||
|
|
||||||
|
## Compiled Mode
|
||||||
|
|
||||||
|
### Build Setup
|
||||||
|
|
||||||
|
In `build.zig`:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const pugz_dep = b.dependency("pugz", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const build_templates = @import("pugz").build_templates;
|
||||||
|
const compiled_templates = build_templates.compileTemplates(b, .{
|
||||||
|
.source_dir = "views", // Required: directory containing .pug files
|
||||||
|
.extension = ".pug", // Optional: default ".pug"
|
||||||
|
});
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "myapp",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
|
||||||
|
.{ .name = "tpls", .module = compiled_templates },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
b.installArtifact(exe);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Compiled Templates
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
const tpls = @import("tpls");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
// Template function name is derived from filename
|
||||||
|
// views/home.pug -> tpls.home()
|
||||||
|
// views/pages/about.pug -> tpls.pages_about()
|
||||||
|
const html = try tpls.home(arena.allocator(), .{
|
||||||
|
.title = "Welcome",
|
||||||
|
.items = &[_][]const u8{ "One", "Two" },
|
||||||
|
});
|
||||||
|
|
||||||
|
std.debug.print("{s}\n", .{html});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Names
|
||||||
|
|
||||||
|
File paths are converted to function names:
|
||||||
|
- `home.pug` → `home()`
|
||||||
|
- `pages/about.pug` → `pages_about()`
|
||||||
|
- `admin-panel.pug` → `admin_panel()`
|
||||||
|
|
||||||
|
List all available templates:
|
||||||
|
```zig
|
||||||
|
for (tpls.template_names) |name| {
|
||||||
|
std.debug.print("{s}\n", .{name});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interpreted Mode
|
||||||
|
|
||||||
|
### ViewEngine
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
const pugz = @import("pugz");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
// Initialize engine
|
||||||
|
var engine = pugz.ViewEngine.init(.{
|
||||||
|
.views_dir = "views", // Required: root views directory
|
||||||
|
.mixins_dir = "mixins", // Optional: default "mixins"
|
||||||
|
.extension = ".pug", // Optional: default ".pug"
|
||||||
|
.pretty = true, // Optional: default true
|
||||||
|
});
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
// Render template (path relative to views_dir, no extension needed)
|
||||||
|
const html = try engine.render(arena.allocator(), "pages/home", .{
|
||||||
|
.title = "Hello",
|
||||||
|
.name = "World",
|
||||||
|
});
|
||||||
|
|
||||||
|
std.debug.print("{s}\n", .{html});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### renderTemplate
|
||||||
|
|
||||||
|
For inline template strings:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const html = try pugz.renderTemplate(allocator,
|
||||||
|
\\h1 Hello, #{name}!
|
||||||
|
\\ul
|
||||||
|
\\ each item in items
|
||||||
|
\\ li= item
|
||||||
|
, .{
|
||||||
|
.name = "World",
|
||||||
|
.items = &[_][]const u8{ "one", "two", "three" },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Types
|
||||||
|
|
||||||
|
Templates accept Zig structs as data. Supported field types:
|
||||||
|
|
||||||
|
| Zig Type | Template Usage |
|
||||||
|
|----------|----------------|
|
||||||
|
| `[]const u8` | `#{field}` |
|
||||||
|
| `i64`, `i32`, etc. | `#{field}` (converted to string) |
|
||||||
|
| `bool` | `if field` |
|
||||||
|
| `[]const T` | `each item in field` |
|
||||||
|
| `?T` (optional) | `if field` (null = false) |
|
||||||
|
| nested struct | `#{field.subfield}` |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const data = .{
|
||||||
|
.title = "My Page",
|
||||||
|
.count = 42,
|
||||||
|
.show_header = true,
|
||||||
|
.items = &[_][]const u8{ "a", "b", "c" },
|
||||||
|
.user = .{
|
||||||
|
.name = "Alice",
|
||||||
|
.email = "alice@example.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = try tpls.home(allocator, data);
|
||||||
|
```
|
||||||
|
|
||||||
|
Template:
|
||||||
|
```pug
|
||||||
|
h1= title
|
||||||
|
p Count: #{count}
|
||||||
|
if show_header
|
||||||
|
header Welcome
|
||||||
|
ul
|
||||||
|
each item in items
|
||||||
|
li= item
|
||||||
|
p #{user.name} (#{user.email})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
Recommended project layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
myproject/
|
||||||
|
├── build.zig
|
||||||
|
├── build.zig.zon
|
||||||
|
├── src/
|
||||||
|
│ └── main.zig
|
||||||
|
└── views/
|
||||||
|
├── mixins/
|
||||||
|
│ ├── buttons.pug
|
||||||
|
│ └── cards.pug
|
||||||
|
├── layouts/
|
||||||
|
│ └── base.pug
|
||||||
|
├── partials/
|
||||||
|
│ ├── header.pug
|
||||||
|
│ └── footer.pug
|
||||||
|
└── pages/
|
||||||
|
├── home.pug
|
||||||
|
└── about.pug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixin Resolution
|
||||||
|
|
||||||
|
Mixins are resolved in order:
|
||||||
|
1. Defined in the current template
|
||||||
|
2. Loaded from `views/mixins/*.pug` (lazy-loaded on first use)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web Framework Integration
|
||||||
|
|
||||||
|
### http.zig
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
const httpz = @import("httpz");
|
||||||
|
const tpls = @import("tpls");
|
||||||
|
|
||||||
|
const App = struct {
|
||||||
|
// app state
|
||||||
|
};
|
||||||
|
|
||||||
|
fn handler(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
_ = app;
|
||||||
|
_ = req;
|
||||||
|
|
||||||
|
const html = try tpls.home(res.arena, .{
|
||||||
|
.title = "Hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.content_type = .HTML;
|
||||||
|
res.body = html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using ViewEngine with http.zig
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const App = struct {
|
||||||
|
engine: pugz.ViewEngine,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn handler(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
_ = req;
|
||||||
|
|
||||||
|
const html = app.engine.render(res.arena, "home", .{
|
||||||
|
.title = "Hello",
|
||||||
|
}) catch |err| {
|
||||||
|
res.status = 500;
|
||||||
|
res.body = @errorName(err);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
res.content_type = .HTML;
|
||||||
|
res.body = html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const html = engine.render(allocator, "home", data) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound => // template file not found
|
||||||
|
error.ParseError => // invalid template syntax
|
||||||
|
error.OutOfMemory => // allocation failed
|
||||||
|
else => // other errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
Always use `ArenaAllocator` for template rendering:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// Per-request pattern
|
||||||
|
fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
return try tpls.home(arena.allocator(), .{ .title = "Hello" });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The arena pattern is efficient because:
|
||||||
|
- Template rendering creates many small allocations
|
||||||
|
- All allocations are freed at once with `arena.deinit()`
|
||||||
|
- No need to track individual allocations
|
||||||
340
docs/syntax.md
Normal file
340
docs/syntax.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# Pugz Template Syntax
|
||||||
|
|
||||||
|
Complete reference for Pugz template syntax.
|
||||||
|
|
||||||
|
## Tags & Nesting
|
||||||
|
|
||||||
|
Indentation defines nesting. Default tag is `div`.
|
||||||
|
|
||||||
|
```pug
|
||||||
|
div
|
||||||
|
h1 Title
|
||||||
|
p Paragraph
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<div><h1>Title</h1><p>Paragraph</p></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Classes & IDs
|
||||||
|
|
||||||
|
Shorthand syntax using `.` for classes and `#` for IDs.
|
||||||
|
|
||||||
|
```pug
|
||||||
|
div#main.container.active
|
||||||
|
.box
|
||||||
|
#sidebar
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<div id="main" class="container active"></div>
|
||||||
|
<div class="box"></div>
|
||||||
|
<div id="sidebar"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
```pug
|
||||||
|
a(href="/link" target="_blank") Click
|
||||||
|
input(type="checkbox" checked)
|
||||||
|
button(disabled=false)
|
||||||
|
button(disabled=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<a href="/link" target="_blank">Click</a>
|
||||||
|
<input type="checkbox" checked="checked" />
|
||||||
|
<button></button>
|
||||||
|
<button disabled="disabled"></button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Boolean attributes: `false` omits the attribute, `true` renders `attr="attr"`.
|
||||||
|
|
||||||
|
## Text Content
|
||||||
|
|
||||||
|
### Inline text
|
||||||
|
|
||||||
|
```pug
|
||||||
|
p Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
### Piped text
|
||||||
|
|
||||||
|
```pug
|
||||||
|
p
|
||||||
|
| Line one
|
||||||
|
| Line two
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block text (dot syntax)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
script.
|
||||||
|
console.log('hello');
|
||||||
|
console.log('world');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Literal HTML
|
||||||
|
|
||||||
|
```pug
|
||||||
|
<p>Passed through as-is</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interpolation
|
||||||
|
|
||||||
|
### Escaped (safe)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
p Hello #{name}
|
||||||
|
p= variable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unescaped (raw HTML)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
p Hello !{rawHtml}
|
||||||
|
p!= rawVariable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tag interpolation
|
||||||
|
|
||||||
|
```pug
|
||||||
|
p This is #[em emphasized] text
|
||||||
|
p Click #[a(href="/") here] to continue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditionals
|
||||||
|
|
||||||
|
### if / else if / else
|
||||||
|
|
||||||
|
```pug
|
||||||
|
if condition
|
||||||
|
p Yes
|
||||||
|
else if other
|
||||||
|
p Maybe
|
||||||
|
else
|
||||||
|
p No
|
||||||
|
```
|
||||||
|
|
||||||
|
### unless
|
||||||
|
|
||||||
|
```pug
|
||||||
|
unless loggedIn
|
||||||
|
p Please login
|
||||||
|
```
|
||||||
|
|
||||||
|
### String comparison
|
||||||
|
|
||||||
|
```pug
|
||||||
|
if status == "active"
|
||||||
|
p Active
|
||||||
|
```
|
||||||
|
|
||||||
|
## Iteration
|
||||||
|
|
||||||
|
### each
|
||||||
|
|
||||||
|
```pug
|
||||||
|
each item in items
|
||||||
|
li= item
|
||||||
|
```
|
||||||
|
|
||||||
|
### with index
|
||||||
|
|
||||||
|
```pug
|
||||||
|
each val, index in list
|
||||||
|
li #{index}: #{val}
|
||||||
|
```
|
||||||
|
|
||||||
|
### with else (empty collection)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
each item in items
|
||||||
|
li= item
|
||||||
|
else
|
||||||
|
li No items
|
||||||
|
```
|
||||||
|
|
||||||
|
### Objects
|
||||||
|
|
||||||
|
```pug
|
||||||
|
each val, key in object
|
||||||
|
p #{key}: #{val}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested iteration
|
||||||
|
|
||||||
|
```pug
|
||||||
|
each friend in friends
|
||||||
|
li #{friend.name}
|
||||||
|
each tag in friend.tags
|
||||||
|
span= tag
|
||||||
|
```
|
||||||
|
|
||||||
|
## Case / When
|
||||||
|
|
||||||
|
```pug
|
||||||
|
case status
|
||||||
|
when "active"
|
||||||
|
p Active
|
||||||
|
when "pending"
|
||||||
|
p Pending
|
||||||
|
default
|
||||||
|
p Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mixins
|
||||||
|
|
||||||
|
### Basic mixin
|
||||||
|
|
||||||
|
```pug
|
||||||
|
mixin button(text)
|
||||||
|
button= text
|
||||||
|
|
||||||
|
+button("Click me")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default parameters
|
||||||
|
|
||||||
|
```pug
|
||||||
|
mixin button(text, type="primary")
|
||||||
|
button(class="btn btn-" + type)= text
|
||||||
|
|
||||||
|
+button("Click me")
|
||||||
|
+button("Submit", "success")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block content
|
||||||
|
|
||||||
|
```pug
|
||||||
|
mixin card(title)
|
||||||
|
.card
|
||||||
|
h3= title
|
||||||
|
block
|
||||||
|
|
||||||
|
+card("My Card")
|
||||||
|
p Card content here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rest arguments
|
||||||
|
|
||||||
|
```pug
|
||||||
|
mixin list(id, ...items)
|
||||||
|
ul(id=id)
|
||||||
|
each item in items
|
||||||
|
li= item
|
||||||
|
|
||||||
|
+list("mylist", "a", "b", "c")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attributes pass-through
|
||||||
|
|
||||||
|
```pug
|
||||||
|
mixin link(href, text)
|
||||||
|
a(href=href)&attributes(attributes)= text
|
||||||
|
|
||||||
|
+link("/home", "Home")(class="nav-link" data-id="1")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Inheritance
|
||||||
|
|
||||||
|
### Base layout (layout.pug)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title= title
|
||||||
|
block styles
|
||||||
|
body
|
||||||
|
block content
|
||||||
|
block scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Child template
|
||||||
|
|
||||||
|
```pug
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1 Page Title
|
||||||
|
p Page content
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block modes
|
||||||
|
|
||||||
|
```pug
|
||||||
|
block append scripts
|
||||||
|
script(src="extra.js")
|
||||||
|
|
||||||
|
block prepend styles
|
||||||
|
link(rel="stylesheet" href="extra.css")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Includes
|
||||||
|
|
||||||
|
```pug
|
||||||
|
include header.pug
|
||||||
|
include partials/footer.pug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### HTML comment (rendered)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
// This renders as HTML comment
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<!-- This renders as HTML comment -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Silent comment (not rendered)
|
||||||
|
|
||||||
|
```pug
|
||||||
|
//- This is a silent comment
|
||||||
|
```
|
||||||
|
|
||||||
|
## Block Expansion
|
||||||
|
|
||||||
|
Colon for inline nesting:
|
||||||
|
|
||||||
|
```pug
|
||||||
|
a: img(src="logo.png")
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<a><img src="logo.png" /></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-Closing Tags
|
||||||
|
|
||||||
|
Explicit self-closing with `/`:
|
||||||
|
|
||||||
|
```pug
|
||||||
|
foo/
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<foo />
|
||||||
|
```
|
||||||
|
|
||||||
|
Void elements (`br`, `hr`, `img`, `input`, `meta`, `link`, etc.) are automatically self-closing.
|
||||||
|
|
||||||
|
## Doctype
|
||||||
|
|
||||||
|
```pug
|
||||||
|
doctype html
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
```
|
||||||
50
examples/demo/build.zig
Normal file
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;
|
||||||
|
}
|
||||||
2
examples/demo/views/404.pug
Normal file
2
examples/demo/views/404.pug
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
p
|
||||||
|
| Route no found
|
||||||
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
|
||||||
@@ -13,3 +13,9 @@ block content
|
|||||||
ul
|
ul
|
||||||
each val in items
|
each val in items
|
||||||
li= val
|
li= val
|
||||||
|
input(data-json=`
|
||||||
|
{
|
||||||
|
"very-long": "piece of ",
|
||||||
|
"data": true
|
||||||
|
}
|
||||||
|
`)
|
||||||
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;
|
||||||
|
}
|
||||||
154
src/benchmarks/bench_interpreted.zig
Normal file
154
src/benchmarks/bench_interpreted.zig
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//! Pugz Benchmark - Interpreted (Runtime) Mode
|
||||||
|
//!
|
||||||
|
//! This benchmark uses the ViewEngine to render templates at runtime,
|
||||||
|
//! reading from the same template/data files as the compiled benchmark.
|
||||||
|
//!
|
||||||
|
//! Run: zig build bench-interpreted
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const pugz = @import("pugz");
|
||||||
|
|
||||||
|
const iterations: usize = 2000;
|
||||||
|
const templates_dir = "src/benchmarks/templates";
|
||||||
|
|
||||||
|
// Data structures matching JSON files
|
||||||
|
const SubFriend = struct {
|
||||||
|
id: i64,
|
||||||
|
name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Friend = struct {
|
||||||
|
name: []const u8,
|
||||||
|
balance: []const u8,
|
||||||
|
age: i64,
|
||||||
|
address: []const u8,
|
||||||
|
picture: []const u8,
|
||||||
|
company: []const u8,
|
||||||
|
email: []const u8,
|
||||||
|
emailHref: []const u8,
|
||||||
|
about: []const u8,
|
||||||
|
tags: []const []const u8,
|
||||||
|
friends: []const SubFriend,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Account = struct {
|
||||||
|
balance: i64,
|
||||||
|
balanceFormatted: []const u8,
|
||||||
|
status: []const u8,
|
||||||
|
negative: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Project = struct {
|
||||||
|
name: []const u8,
|
||||||
|
url: []const u8,
|
||||||
|
description: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchRecord = struct {
|
||||||
|
imgUrl: []const u8,
|
||||||
|
viewItemUrl: []const u8,
|
||||||
|
title: []const u8,
|
||||||
|
description: []const u8,
|
||||||
|
featured: bool,
|
||||||
|
sizes: ?[]const []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
|
||||||
|
std.debug.print("║ Interpreted (Runtime) Benchmark ({d} iterations) ║\n", .{iterations});
|
||||||
|
std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir});
|
||||||
|
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
|
||||||
|
|
||||||
|
// Load JSON data
|
||||||
|
std.debug.print("\nLoading JSON data...\n", .{});
|
||||||
|
|
||||||
|
var data_arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer data_arena.deinit();
|
||||||
|
const data_alloc = data_arena.allocator();
|
||||||
|
|
||||||
|
const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json");
|
||||||
|
const simple1 = try loadJson(struct {
|
||||||
|
name: []const u8,
|
||||||
|
messageCount: i64,
|
||||||
|
colors: []const []const u8,
|
||||||
|
primary: bool,
|
||||||
|
}, data_alloc, "simple-1.json");
|
||||||
|
const simple2 = try loadJson(struct {
|
||||||
|
header: []const u8,
|
||||||
|
header2: []const u8,
|
||||||
|
header3: []const u8,
|
||||||
|
header4: []const u8,
|
||||||
|
header5: []const u8,
|
||||||
|
header6: []const u8,
|
||||||
|
list: []const []const u8,
|
||||||
|
}, data_alloc, "simple-2.json");
|
||||||
|
const if_expr = try loadJson(struct { accounts: []const Account }, data_alloc, "if-expression.json");
|
||||||
|
const projects = try loadJson(struct {
|
||||||
|
title: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
projects: []const Project,
|
||||||
|
}, data_alloc, "projects-escaped.json");
|
||||||
|
const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json");
|
||||||
|
const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json");
|
||||||
|
|
||||||
|
// Load template sources
|
||||||
|
const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug");
|
||||||
|
const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug");
|
||||||
|
const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug");
|
||||||
|
const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug");
|
||||||
|
const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug");
|
||||||
|
const search_tpl = try loadTemplate(data_alloc, "search-results.pug");
|
||||||
|
const friends_tpl = try loadTemplate(data_alloc, "friends.pug");
|
||||||
|
|
||||||
|
std.debug.print("Loaded. Starting benchmark...\n\n", .{});
|
||||||
|
|
||||||
|
var total: f64 = 0;
|
||||||
|
|
||||||
|
total += try bench("simple-0", allocator, simple0_tpl, simple0);
|
||||||
|
total += try bench("simple-1", allocator, simple1_tpl, simple1);
|
||||||
|
total += try bench("simple-2", allocator, simple2_tpl, simple2);
|
||||||
|
total += try bench("if-expression", allocator, if_expr_tpl, if_expr);
|
||||||
|
total += try bench("projects-escaped", allocator, projects_tpl, projects);
|
||||||
|
total += try bench("search-results", allocator, search_tpl, search);
|
||||||
|
total += try bench("friends", allocator, friends_tpl, friends_data);
|
||||||
|
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total });
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T {
|
||||||
|
const path = templates_dir ++ "/" ++ filename;
|
||||||
|
const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024);
|
||||||
|
const parsed = try std.json.parseFromSlice(T, alloc, content, .{});
|
||||||
|
return parsed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 {
|
||||||
|
const path = templates_dir ++ "/" ++ filename;
|
||||||
|
return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench(
|
||||||
|
name: []const u8,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
template: []const u8,
|
||||||
|
data: anytype,
|
||||||
|
) !f64 {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
for (0..iterations) |_| {
|
||||||
|
_ = arena.reset(.retain_capacity);
|
||||||
|
_ = try pugz.renderTemplate(arena.allocator(), template, data);
|
||||||
|
}
|
||||||
|
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0;
|
||||||
|
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms });
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
@@ -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", .{});
|
|
||||||
}
|
|
||||||
86
src/benchmarks/pugjs/bench.js
Normal file
86
src/benchmarks/pugjs/bench.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Pug.js Benchmark - Comparison with Pugz
|
||||||
|
*
|
||||||
|
* Run: npm install && npm run bench
|
||||||
|
*
|
||||||
|
* Both Pug.js and Pugz benchmarks read from the same files:
|
||||||
|
* ../templates/*.pug (templates)
|
||||||
|
* ../templates/*.json (data)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const pug = require('pug');
|
||||||
|
|
||||||
|
const iterations = 2000;
|
||||||
|
const templatesDir = path.join(__dirname, '..', 'templates');
|
||||||
|
|
||||||
|
const benchmarks = [
|
||||||
|
'simple-0',
|
||||||
|
'simple-1',
|
||||||
|
'simple-2',
|
||||||
|
'if-expression',
|
||||||
|
'projects-escaped',
|
||||||
|
'search-results',
|
||||||
|
'friends',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Load templates and data from shared files BEFORE benchmarking
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Loading templates and data...");
|
||||||
|
|
||||||
|
const templates = {};
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
for (const name of benchmarks) {
|
||||||
|
templates[name] = fs.readFileSync(path.join(templatesDir, `${name}.pug`), 'utf8');
|
||||||
|
data[name] = JSON.parse(fs.readFileSync(path.join(templatesDir, `${name}.json`), 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile all templates BEFORE benchmarking
|
||||||
|
const compiled = {};
|
||||||
|
for (const name of benchmarks) {
|
||||||
|
compiled[name] = pug.compile(templates[name], { pretty: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Templates compiled. Starting benchmark...\n");
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Benchmark
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
console.log("╔═══════════════════════════════════════════════════════════════╗");
|
||||||
|
console.log(`║ Pug.js Benchmark (${iterations} iterations) ║`);
|
||||||
|
console.log("║ Templates: src/benchmarks/templates/*.pug ║");
|
||||||
|
console.log("║ Data: src/benchmarks/templates/*.json ║");
|
||||||
|
console.log("╚═══════════════════════════════════════════════════════════════╝");
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const name of benchmarks) {
|
||||||
|
const compiledFn = compiled[name];
|
||||||
|
const templateData = data[name];
|
||||||
|
|
||||||
|
// Warmup
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
compiledFn(templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark
|
||||||
|
const start = process.hrtime.bigint();
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
compiledFn(templateData);
|
||||||
|
}
|
||||||
|
const end = process.hrtime.bigint();
|
||||||
|
|
||||||
|
const ms = Number(end - start) / 1_000_000;
|
||||||
|
total += ms;
|
||||||
|
console.log(` ${name.padEnd(20)} => ${ms.toFixed(1).padStart(7)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log(` ${"TOTAL".padEnd(20)} => ${total.toFixed(1).padStart(7)}ms`);
|
||||||
|
console.log("");
|
||||||
576
src/benchmarks/pugjs/package-lock.json
generated
Normal file
576
src/benchmarks/pugjs/package-lock.json
generated
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
{
|
||||||
|
"name": "pugjs-benchmark",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "pugjs-benchmark",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"pug": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.28.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
|
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.28.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "7.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asap": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/assert-never": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/babel-walk": {
|
||||||
|
"version": "3.0.0-canary-5",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
||||||
|
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.9.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/character-parser": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-regex": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/constantinople": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.6.0",
|
||||||
|
"@babel/types": "^7.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/doctypes": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-expression": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^7.1.1",
|
||||||
|
"object-assign": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/is-regex": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-stringify": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jstransformer": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-promise": "^2.0.0",
|
||||||
|
"promise": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/promise": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asap": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pug-code-gen": "^3.0.3",
|
||||||
|
"pug-filters": "^4.0.0",
|
||||||
|
"pug-lexer": "^5.0.1",
|
||||||
|
"pug-linker": "^4.0.0",
|
||||||
|
"pug-load": "^3.0.0",
|
||||||
|
"pug-parser": "^6.0.0",
|
||||||
|
"pug-runtime": "^3.0.1",
|
||||||
|
"pug-strip-comments": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-attrs": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"constantinople": "^4.0.1",
|
||||||
|
"js-stringify": "^1.0.2",
|
||||||
|
"pug-runtime": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-code-gen": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"constantinople": "^4.0.1",
|
||||||
|
"doctypes": "^1.1.0",
|
||||||
|
"js-stringify": "^1.0.2",
|
||||||
|
"pug-attrs": "^3.0.0",
|
||||||
|
"pug-error": "^2.1.0",
|
||||||
|
"pug-runtime": "^3.0.1",
|
||||||
|
"void-elements": "^3.1.0",
|
||||||
|
"with": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-error": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pug-filters": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"constantinople": "^4.0.1",
|
||||||
|
"jstransformer": "1.0.0",
|
||||||
|
"pug-error": "^2.0.0",
|
||||||
|
"pug-walk": "^2.0.0",
|
||||||
|
"resolve": "^1.15.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-lexer": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"character-parser": "^2.2.0",
|
||||||
|
"is-expression": "^4.0.0",
|
||||||
|
"pug-error": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-linker": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pug-error": "^2.0.0",
|
||||||
|
"pug-walk": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-load": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"pug-walk": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-parser": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pug-error": "^2.0.0",
|
||||||
|
"token-stream": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-runtime": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pug-strip-comments": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pug-error": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pug-walk": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.16.1",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/token-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/with": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.9.6",
|
||||||
|
"@babel/types": "^7.9.6",
|
||||||
|
"assert-never": "^1.2.1",
|
||||||
|
"babel-walk": "3.0.0-canary-5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/benchmarks/pugjs/package.json
Normal file
12
src/benchmarks/pugjs/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "pugjs-benchmark",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Pug.js benchmark for comparison with Pugz",
|
||||||
|
"main": "bench.js",
|
||||||
|
"scripts": {
|
||||||
|
"bench": "node bench.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pug": "^3.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
3064
src/benchmarks/templates/friends.json
Normal file
3064
src/benchmarks/templates/friends.json
Normal file
File diff suppressed because it is too large
Load Diff
28
src/benchmarks/templates/if-expression.json
Normal file
28
src/benchmarks/templates/if-expression.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"balance": 0,
|
||||||
|
"balanceFormatted": "$0.00",
|
||||||
|
"status": "open",
|
||||||
|
"negative": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"balance": 10,
|
||||||
|
"balanceFormatted": "$10.00",
|
||||||
|
"status": "closed",
|
||||||
|
"negative": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"balance": -100,
|
||||||
|
"balanceFormatted": "$-100.00",
|
||||||
|
"status": "suspended",
|
||||||
|
"negative": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"balance": 999,
|
||||||
|
"balanceFormatted": "$999.00",
|
||||||
|
"status": "open",
|
||||||
|
"negative": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
src/benchmarks/templates/projects-escaped.json
Normal file
41
src/benchmarks/templates/projects-escaped.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"title": "Projects",
|
||||||
|
"text": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"name": "<strong>Facebook</strong>",
|
||||||
|
"url": "http://facebook.com",
|
||||||
|
"description": "Social network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<strong>Google</strong>",
|
||||||
|
"url": "http://google.com",
|
||||||
|
"description": "Search engine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<strong>Twitter</strong>",
|
||||||
|
"url": "http://twitter.com",
|
||||||
|
"description": "Microblogging service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<strong>Amazon</strong>",
|
||||||
|
"url": "http://amazon.com",
|
||||||
|
"description": "Online retailer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<strong>eBay</strong>",
|
||||||
|
"url": "http://ebay.com",
|
||||||
|
"description": "Online auction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<strong>Wikipedia</strong>",
|
||||||
|
"url": "http://wikipedia.org",
|
||||||
|
"description": "A free encyclopedia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<strong>LiveJournal</strong>",
|
||||||
|
"url": "http://livejournal.com",
|
||||||
|
"description": "Blogging platform"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
278
src/benchmarks/templates/search-results.json
Normal file
278
src/benchmarks/templates/search-results.json
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
{
|
||||||
|
"searchRecords": [
|
||||||
|
{
|
||||||
|
"imgUrl": "img1.jpg",
|
||||||
|
"viewItemUrl": "http://foo/1",
|
||||||
|
"title": "Namebox",
|
||||||
|
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img2.jpg",
|
||||||
|
"viewItemUrl": "http://foo/2",
|
||||||
|
"title": "Arctiq",
|
||||||
|
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
|
||||||
|
"featured": false,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img3.jpg",
|
||||||
|
"viewItemUrl": "http://foo/3",
|
||||||
|
"title": "Niquent",
|
||||||
|
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img4.jpg",
|
||||||
|
"viewItemUrl": "http://foo/4",
|
||||||
|
"title": "Remotion",
|
||||||
|
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img5.jpg",
|
||||||
|
"viewItemUrl": "http://foo/5",
|
||||||
|
"title": "Octocore",
|
||||||
|
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img6.jpg",
|
||||||
|
"viewItemUrl": "http://foo/6",
|
||||||
|
"title": "Spherix",
|
||||||
|
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img7.jpg",
|
||||||
|
"viewItemUrl": "http://foo/7",
|
||||||
|
"title": "Quarex",
|
||||||
|
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img8.jpg",
|
||||||
|
"viewItemUrl": "http://foo/8",
|
||||||
|
"title": "Supremia",
|
||||||
|
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
|
||||||
|
"featured": false,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img9.jpg",
|
||||||
|
"viewItemUrl": "http://foo/9",
|
||||||
|
"title": "Amtap",
|
||||||
|
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
|
||||||
|
"featured": false,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img10.jpg",
|
||||||
|
"viewItemUrl": "http://foo/10",
|
||||||
|
"title": "Qiao",
|
||||||
|
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
|
||||||
|
"featured": false,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img11.jpg",
|
||||||
|
"viewItemUrl": "http://foo/11",
|
||||||
|
"title": "Pushcart",
|
||||||
|
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img12.jpg",
|
||||||
|
"viewItemUrl": "http://foo/12",
|
||||||
|
"title": "Eweville",
|
||||||
|
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
|
||||||
|
"featured": false,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img13.jpg",
|
||||||
|
"viewItemUrl": "http://foo/13",
|
||||||
|
"title": "Senmei",
|
||||||
|
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img14.jpg",
|
||||||
|
"viewItemUrl": "http://foo/14",
|
||||||
|
"title": "Maximind",
|
||||||
|
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img15.jpg",
|
||||||
|
"viewItemUrl": "http://foo/15",
|
||||||
|
"title": "Blurrybus",
|
||||||
|
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img16.jpg",
|
||||||
|
"viewItemUrl": "http://foo/16",
|
||||||
|
"title": "Virva",
|
||||||
|
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img17.jpg",
|
||||||
|
"viewItemUrl": "http://foo/17",
|
||||||
|
"title": "Centregy",
|
||||||
|
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img18.jpg",
|
||||||
|
"viewItemUrl": "http://foo/18",
|
||||||
|
"title": "Dancerity",
|
||||||
|
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img19.jpg",
|
||||||
|
"viewItemUrl": "http://foo/19",
|
||||||
|
"title": "Oceanica",
|
||||||
|
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
|
||||||
|
"featured": true,
|
||||||
|
"sizes": [
|
||||||
|
"S",
|
||||||
|
"M",
|
||||||
|
"L",
|
||||||
|
"XL",
|
||||||
|
"XXL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imgUrl": "img20.jpg",
|
||||||
|
"viewItemUrl": "http://foo/20",
|
||||||
|
"title": "Synkgen",
|
||||||
|
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
|
||||||
|
"featured": false,
|
||||||
|
"sizes": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,33 +1,17 @@
|
|||||||
.search-results-container
|
.search-results.view-gallery
|
||||||
.searching#searching
|
each searchRecord in searchRecords
|
||||||
.wait-indicator-icon Searching...
|
.search-item
|
||||||
#resultsContainer
|
.search-item-container.drop-shadow
|
||||||
.hd
|
.img-container
|
||||||
span.count
|
img(src=searchRecord.imgUrl)
|
||||||
span#count= totalCount
|
h4.title
|
||||||
| results
|
a(href=searchRecord.viewItemUrl)= searchRecord.title
|
||||||
.view-modifiers
|
| #{searchRecord.description}
|
||||||
.view-select
|
if searchRecord.featured
|
||||||
| View:
|
div Featured!
|
||||||
.view-icon.view-icon-selected#viewIconGallery
|
if searchRecord.sizes
|
||||||
i.icon-th
|
div
|
||||||
.view-icon#viewIconList
|
| Sizes available:
|
||||||
i.icon-th-list
|
ul
|
||||||
#resultsTarget
|
each size in searchRecord.sizes
|
||||||
.search-results.view-gallery
|
li= size
|
||||||
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
|
|
||||||
|
|||||||
3
src/benchmarks/templates/simple-0.json
Normal file
3
src/benchmarks/templates/simple-0.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"name": "John"
|
||||||
|
}
|
||||||
19
src/benchmarks/templates/simple-1.json
Normal file
19
src/benchmarks/templates/simple-1.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "George Washington",
|
||||||
|
"messageCount": 999,
|
||||||
|
"colors": [
|
||||||
|
"red",
|
||||||
|
"green",
|
||||||
|
"blue",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"pink",
|
||||||
|
"black",
|
||||||
|
"white",
|
||||||
|
"beige",
|
||||||
|
"brown",
|
||||||
|
"cyan",
|
||||||
|
"magenta"
|
||||||
|
],
|
||||||
|
"primary": true
|
||||||
|
}
|
||||||
20
src/benchmarks/templates/simple-2.json
Normal file
20
src/benchmarks/templates/simple-2.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"header": "Header",
|
||||||
|
"header2": "Header2",
|
||||||
|
"header3": "Header3",
|
||||||
|
"header4": "Header4",
|
||||||
|
"header5": "Header5",
|
||||||
|
"header6": "Header6",
|
||||||
|
"list": [
|
||||||
|
"1000000000",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"10"
|
||||||
|
]
|
||||||
|
}
|
||||||
1459
src/build_templates.zig
Normal file
1459
src/build_templates.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ const whitespace_sensitive = std.StaticStringMap(void).initComptime(.{
|
|||||||
pub const CodeGen = struct {
|
pub const CodeGen = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
options: Options,
|
options: Options,
|
||||||
output: std.ArrayListUnmanaged(u8),
|
output: std.ArrayList(u8),
|
||||||
depth: usize,
|
depth: usize,
|
||||||
/// Track if we're inside a whitespace-sensitive element.
|
/// Track if we're inside a whitespace-sensitive element.
|
||||||
preserve_whitespace: bool,
|
preserve_whitespace: bool,
|
||||||
|
|||||||
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.ArrayList(u8),
|
||||||
|
depth: u8,
|
||||||
|
|
||||||
|
fn init(allocator: std.mem.Allocator) Compiler {
|
||||||
|
return .{
|
||||||
|
.alloc = allocator,
|
||||||
|
.out = .{},
|
||||||
|
.depth = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Compiler) void {
|
||||||
|
self.out.deinit(self.alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile(self: *Compiler, name: []const u8, doc: ast.Document) ![]u8 {
|
||||||
|
// Header
|
||||||
|
try self.w(
|
||||||
|
\\const std = @import("std");
|
||||||
|
\\
|
||||||
|
\\/// HTML escape lookup table
|
||||||
|
\\const esc_table = blk: {
|
||||||
|
\\ var t: [256]?[]const u8 = .{null} ** 256;
|
||||||
|
\\ t['&'] = "&";
|
||||||
|
\\ t['<'] = "<";
|
||||||
|
\\ t['>'] = ">";
|
||||||
|
\\ t['"'] = """;
|
||||||
|
\\ t['\''] = "'";
|
||||||
|
\\ break :blk t;
|
||||||
|
\\};
|
||||||
|
\\
|
||||||
|
\\fn esc(out: *std.ArrayList(u8), s: []const u8) !void {
|
||||||
|
\\ var i: usize = 0;
|
||||||
|
\\ for (s, 0..) |c, j| {
|
||||||
|
\\ if (esc_table[c]) |e| {
|
||||||
|
\\ if (j > i) try out.appendSlice(s[i..j]);
|
||||||
|
\\ try out.appendSlice(e);
|
||||||
|
\\ i = j + 1;
|
||||||
|
\\ }
|
||||||
|
\\ }
|
||||||
|
\\ if (i < s.len) try out.appendSlice(s[i..]);
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\fn toStr(v: anytype) []const u8 {
|
||||||
|
\\ const T = @TypeOf(v);
|
||||||
|
\\ if (T == []const u8) return v;
|
||||||
|
\\ if (@typeInfo(T) == .optional) {
|
||||||
|
\\ if (v) |inner| return toStr(inner);
|
||||||
|
\\ return "";
|
||||||
|
\\ }
|
||||||
|
\\ return "";
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function signature
|
||||||
|
try self.w("pub fn ");
|
||||||
|
try self.w(name);
|
||||||
|
try self.w("(out: *std.ArrayList(u8), data: anytype) !void {\n");
|
||||||
|
self.depth = 1;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
for (doc.nodes) |n| {
|
||||||
|
try self.node(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.w("}\n");
|
||||||
|
return try self.alloc.dupe(u8, self.out.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(self: *Compiler, n: ast.Node) anyerror!void {
|
||||||
|
switch (n) {
|
||||||
|
.doctype => |d| try self.doctype(d),
|
||||||
|
.element => |e| try self.element(e),
|
||||||
|
.text => |t| try self.text(t.segments),
|
||||||
|
.conditional => |c| try self.conditional(c),
|
||||||
|
.each => |e| try self.each(e),
|
||||||
|
.raw_text => |r| try self.raw(r.content),
|
||||||
|
.comment => |c| if (c.rendered) try self.comment(c),
|
||||||
|
.code => |c| try self.code(c),
|
||||||
|
.document => |d| for (d.nodes) |child| try self.node(child),
|
||||||
|
.mixin_def, .mixin_call, .mixin_block, .@"while", .case, .block, .include, .extends => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doctype(self: *Compiler, d: ast.Doctype) !void {
|
||||||
|
try self.indent();
|
||||||
|
if (std.mem.eql(u8, d.value, "html")) {
|
||||||
|
try self.w("try out.appendSlice(\"<!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;
|
|
||||||
}
|
|
||||||
@@ -128,8 +128,8 @@ pub const Lexer = struct {
|
|||||||
pos: usize,
|
pos: usize,
|
||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
indent_stack: std.ArrayListUnmanaged(usize),
|
indent_stack: std.ArrayList(usize),
|
||||||
tokens: std.ArrayListUnmanaged(Token),
|
tokens: std.ArrayList(Token),
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
at_line_start: bool,
|
at_line_start: bool,
|
||||||
current_indent: usize,
|
current_indent: usize,
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
/// Parses all tokens and returns the document AST.
|
/// Parses all tokens and returns the document AST.
|
||||||
pub fn parse(self: *Parser) Error!ast.Document {
|
pub fn parse(self: *Parser) Error!ast.Document {
|
||||||
var nodes = std.ArrayListUnmanaged(Node).empty;
|
var nodes = std.ArrayList(Node).empty;
|
||||||
errdefer nodes.deinit(self.allocator);
|
errdefer nodes.deinit(self.allocator);
|
||||||
|
|
||||||
var extends_path: ?[]const u8 = null;
|
var extends_path: ?[]const u8 = null;
|
||||||
@@ -122,9 +122,9 @@ pub const Parser = struct {
|
|||||||
/// Parses an HTML element with optional tag, classes, id, attributes, and children.
|
/// Parses an HTML element with optional tag, classes, id, attributes, and children.
|
||||||
fn parseElement(self: *Parser) Error!Node {
|
fn parseElement(self: *Parser) Error!Node {
|
||||||
var tag: []const u8 = "div"; // default tag
|
var tag: []const u8 = "div"; // default tag
|
||||||
var classes = std.ArrayListUnmanaged([]const u8).empty;
|
var classes = std.ArrayList([]const u8).empty;
|
||||||
var id: ?[]const u8 = null;
|
var id: ?[]const u8 = null;
|
||||||
var attributes = std.ArrayListUnmanaged(Attribute).empty;
|
var attributes = std.ArrayList(Attribute).empty;
|
||||||
var spread_attributes: ?[]const u8 = null;
|
var spread_attributes: ?[]const u8 = null;
|
||||||
var self_closing = false;
|
var self_closing = false;
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ pub const Parser = struct {
|
|||||||
self.skipWhitespace();
|
self.skipWhitespace();
|
||||||
|
|
||||||
// Parse the inline nested element
|
// Parse the inline nested element
|
||||||
var children = std.ArrayListUnmanaged(Node).empty;
|
var children = std.ArrayList(Node).empty;
|
||||||
errdefer children.deinit(self.allocator);
|
errdefer children.deinit(self.allocator);
|
||||||
|
|
||||||
if (try self.parseNode()) |child| {
|
if (try self.parseNode()) |child| {
|
||||||
@@ -223,7 +223,7 @@ pub const Parser = struct {
|
|||||||
_ = self.advance();
|
_ = self.advance();
|
||||||
const raw_content = try self.parseRawTextBlock();
|
const raw_content = try self.parseRawTextBlock();
|
||||||
|
|
||||||
var children = std.ArrayListUnmanaged(Node).empty;
|
var children = std.ArrayList(Node).empty;
|
||||||
errdefer children.deinit(self.allocator);
|
errdefer children.deinit(self.allocator);
|
||||||
try children.append(self.allocator, .{ .raw_text = .{ .content = raw_content } });
|
try children.append(self.allocator, .{ .raw_text = .{ .content = raw_content } });
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse children if indented
|
// Parse children if indented
|
||||||
var children = std.ArrayListUnmanaged(Node).empty;
|
var children = std.ArrayList(Node).empty;
|
||||||
errdefer children.deinit(self.allocator);
|
errdefer children.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -267,7 +267,7 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parses attributes within parentheses.
|
/// Parses attributes within parentheses.
|
||||||
fn parseAttributes(self: *Parser, attributes: *std.ArrayListUnmanaged(Attribute)) Error!void {
|
fn parseAttributes(self: *Parser, attributes: *std.ArrayList(Attribute)) Error!void {
|
||||||
while (!self.check(.rparen) and !self.isAtEnd()) {
|
while (!self.check(.rparen) and !self.isAtEnd()) {
|
||||||
// Skip commas
|
// Skip commas
|
||||||
if (self.check(.comma)) {
|
if (self.check(.comma)) {
|
||||||
@@ -302,7 +302,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
/// Parses text segments (literals and interpolations).
|
/// Parses text segments (literals and interpolations).
|
||||||
fn parseTextSegments(self: *Parser) Error![]TextSegment {
|
fn parseTextSegments(self: *Parser) Error![]TextSegment {
|
||||||
var segments = std.ArrayListUnmanaged(TextSegment).empty;
|
var segments = std.ArrayList(TextSegment).empty;
|
||||||
errdefer segments.deinit(self.allocator);
|
errdefer segments.deinit(self.allocator);
|
||||||
|
|
||||||
while (self.check(.text) or self.check(.interp_start) or self.check(.interp_start_unesc) or self.check(.tag_interp_start)) {
|
while (self.check(.text) or self.check(.interp_start) or self.check(.interp_start_unesc) or self.check(.tag_interp_start)) {
|
||||||
@@ -338,9 +338,9 @@ pub const Parser = struct {
|
|||||||
_ = self.advance(); // skip #[
|
_ = self.advance(); // skip #[
|
||||||
|
|
||||||
var tag: []const u8 = "span"; // default tag
|
var tag: []const u8 = "span"; // default tag
|
||||||
var classes = std.ArrayListUnmanaged([]const u8).empty;
|
var classes = std.ArrayList([]const u8).empty;
|
||||||
var id: ?[]const u8 = null;
|
var id: ?[]const u8 = null;
|
||||||
var attributes = std.ArrayListUnmanaged(Attribute).empty;
|
var attributes = std.ArrayList(Attribute).empty;
|
||||||
|
|
||||||
errdefer classes.deinit(self.allocator);
|
errdefer classes.deinit(self.allocator);
|
||||||
errdefer attributes.deinit(self.allocator);
|
errdefer attributes.deinit(self.allocator);
|
||||||
@@ -369,7 +369,7 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse inner text segments (may contain nested interpolations)
|
// Parse inner text segments (may contain nested interpolations)
|
||||||
var text_segments = std.ArrayListUnmanaged(TextSegment).empty;
|
var text_segments = std.ArrayList(TextSegment).empty;
|
||||||
errdefer text_segments.deinit(self.allocator);
|
errdefer text_segments.deinit(self.allocator);
|
||||||
|
|
||||||
while (!self.check(.tag_interp_end) and !self.check(.newline) and !self.isAtEnd()) {
|
while (!self.check(.tag_interp_end) and !self.check(.newline) and !self.isAtEnd()) {
|
||||||
@@ -415,7 +415,7 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parses children within an indented block.
|
/// Parses children within an indented block.
|
||||||
fn parseChildren(self: *Parser, children: *std.ArrayListUnmanaged(Node)) Error!void {
|
fn parseChildren(self: *Parser, children: *std.ArrayList(Node)) Error!void {
|
||||||
while (!self.check(.dedent) and !self.isAtEnd()) {
|
while (!self.check(.dedent) and !self.isAtEnd()) {
|
||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
if (self.check(.dedent) or self.isAtEnd()) break;
|
if (self.check(.dedent) or self.isAtEnd()) break;
|
||||||
@@ -433,7 +433,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
/// Parses a raw text block (after `.`).
|
/// Parses a raw text block (after `.`).
|
||||||
fn parseRawTextBlock(self: *Parser) Error![]const u8 {
|
fn parseRawTextBlock(self: *Parser) Error![]const u8 {
|
||||||
var lines = std.ArrayListUnmanaged(u8).empty;
|
var lines = std.ArrayList(u8).empty;
|
||||||
errdefer lines.deinit(self.allocator);
|
errdefer lines.deinit(self.allocator);
|
||||||
|
|
||||||
while (!self.check(.dedent) and !self.isAtEnd()) {
|
while (!self.check(.dedent) and !self.isAtEnd()) {
|
||||||
@@ -470,7 +470,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
/// Parses conditional (if/else if/else/unless).
|
/// Parses conditional (if/else if/else/unless).
|
||||||
fn parseConditional(self: *Parser) Error!Node {
|
fn parseConditional(self: *Parser) Error!Node {
|
||||||
var branches = std.ArrayListUnmanaged(ast.Conditional.Branch).empty;
|
var branches = std.ArrayList(ast.Conditional.Branch).empty;
|
||||||
errdefer branches.deinit(self.allocator);
|
errdefer branches.deinit(self.allocator);
|
||||||
|
|
||||||
// Parse initial if/unless
|
// Parse initial if/unless
|
||||||
@@ -483,7 +483,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse body
|
// Parse body
|
||||||
var body = std.ArrayListUnmanaged(Node).empty;
|
var body = std.ArrayList(Node).empty;
|
||||||
errdefer body.deinit(self.allocator);
|
errdefer body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -512,7 +512,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
var else_body = std.ArrayListUnmanaged(Node).empty;
|
var else_body = std.ArrayList(Node).empty;
|
||||||
errdefer else_body.deinit(self.allocator);
|
errdefer else_body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -590,7 +590,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse body
|
// Parse body
|
||||||
var body = std.ArrayListUnmanaged(Node).empty;
|
var body = std.ArrayList(Node).empty;
|
||||||
errdefer body.deinit(self.allocator);
|
errdefer body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -599,7 +599,7 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for else branch
|
// Check for else branch
|
||||||
var else_children = std.ArrayListUnmanaged(Node).empty;
|
var else_children = std.ArrayList(Node).empty;
|
||||||
errdefer else_children.deinit(self.allocator);
|
errdefer else_children.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.kw_else)) {
|
if (self.check(.kw_else)) {
|
||||||
@@ -629,7 +629,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
var body = std.ArrayListUnmanaged(Node).empty;
|
var body = std.ArrayList(Node).empty;
|
||||||
errdefer body.deinit(self.allocator);
|
errdefer body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -651,10 +651,10 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
var whens = std.ArrayListUnmanaged(ast.Case.When).empty;
|
var whens = std.ArrayList(ast.Case.When).empty;
|
||||||
errdefer whens.deinit(self.allocator);
|
errdefer whens.deinit(self.allocator);
|
||||||
|
|
||||||
var default_children = std.ArrayListUnmanaged(Node).empty;
|
var default_children = std.ArrayList(Node).empty;
|
||||||
errdefer default_children.deinit(self.allocator);
|
errdefer default_children.deinit(self.allocator);
|
||||||
|
|
||||||
// Parse indented when/default clauses
|
// Parse indented when/default clauses
|
||||||
@@ -675,7 +675,7 @@ pub const Parser = struct {
|
|||||||
value = try self.parseRestOfLine();
|
value = try self.parseRestOfLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
var when_children = std.ArrayListUnmanaged(Node).empty;
|
var when_children = std.ArrayList(Node).empty;
|
||||||
errdefer when_children.deinit(self.allocator);
|
errdefer when_children.deinit(self.allocator);
|
||||||
var has_break = false;
|
var has_break = false;
|
||||||
|
|
||||||
@@ -778,8 +778,8 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse parameters if present
|
// Parse parameters if present
|
||||||
var params = std.ArrayListUnmanaged([]const u8).empty;
|
var params = std.ArrayList([]const u8).empty;
|
||||||
var defaults = std.ArrayListUnmanaged(?[]const u8).empty;
|
var defaults = std.ArrayList(?[]const u8).empty;
|
||||||
errdefer params.deinit(self.allocator);
|
errdefer params.deinit(self.allocator);
|
||||||
errdefer defaults.deinit(self.allocator);
|
errdefer defaults.deinit(self.allocator);
|
||||||
|
|
||||||
@@ -830,7 +830,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse body
|
// Parse body
|
||||||
var body = std.ArrayListUnmanaged(Node).empty;
|
var body = std.ArrayList(Node).empty;
|
||||||
errdefer body.deinit(self.allocator);
|
errdefer body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -851,8 +851,8 @@ pub const Parser = struct {
|
|||||||
fn parseMixinCall(self: *Parser) Error!Node {
|
fn parseMixinCall(self: *Parser) Error!Node {
|
||||||
const name = self.advance().value; // +name
|
const name = self.advance().value; // +name
|
||||||
|
|
||||||
var args = std.ArrayListUnmanaged([]const u8).empty;
|
var args = std.ArrayList([]const u8).empty;
|
||||||
var attributes = std.ArrayListUnmanaged(Attribute).empty;
|
var attributes = std.ArrayList(Attribute).empty;
|
||||||
errdefer args.deinit(self.allocator);
|
errdefer args.deinit(self.allocator);
|
||||||
errdefer attributes.deinit(self.allocator);
|
errdefer attributes.deinit(self.allocator);
|
||||||
|
|
||||||
@@ -894,7 +894,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse block content
|
// Parse block content
|
||||||
var block_children = std.ArrayListUnmanaged(Node).empty;
|
var block_children = std.ArrayList(Node).empty;
|
||||||
errdefer block_children.deinit(self.allocator);
|
errdefer block_children.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -979,7 +979,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse body
|
// Parse body
|
||||||
var body = std.ArrayListUnmanaged(Node).empty;
|
var body = std.ArrayList(Node).empty;
|
||||||
errdefer body.deinit(self.allocator);
|
errdefer body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -1011,7 +1011,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse body
|
// Parse body
|
||||||
var body = std.ArrayListUnmanaged(Node).empty;
|
var body = std.ArrayList(Node).empty;
|
||||||
errdefer body.deinit(self.allocator);
|
errdefer body.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -1052,7 +1052,7 @@ pub const Parser = struct {
|
|||||||
self.skipNewlines();
|
self.skipNewlines();
|
||||||
|
|
||||||
// Parse nested comment content
|
// Parse nested comment content
|
||||||
var children = std.ArrayListUnmanaged(Node).empty;
|
var children = std.ArrayList(Node).empty;
|
||||||
errdefer children.deinit(self.allocator);
|
errdefer children.deinit(self.allocator);
|
||||||
|
|
||||||
if (self.check(.indent)) {
|
if (self.check(.indent)) {
|
||||||
@@ -1091,7 +1091,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
/// Parses rest of line as text.
|
/// Parses rest of line as text.
|
||||||
fn parseRestOfLine(self: *Parser) Error![]const u8 {
|
fn parseRestOfLine(self: *Parser) Error![]const u8 {
|
||||||
var result = std.ArrayListUnmanaged(u8).empty;
|
var result = std.ArrayList(u8).empty;
|
||||||
errdefer result.deinit(self.allocator);
|
errdefer result.deinit(self.allocator);
|
||||||
|
|
||||||
while (!self.check(.newline) and !self.check(.indent) and !self.check(.dedent) and !self.isAtEnd()) {
|
while (!self.check(.newline) and !self.check(.indent) and !self.check(.dedent) and !self.isAtEnd()) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
290
src/runtime.zig
290
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) {
|
||||||
@@ -101,7 +128,7 @@ pub const Context = struct {
|
|||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
/// Stack of variable scopes (innermost last).
|
/// Stack of variable scopes (innermost last).
|
||||||
/// We keep all scopes allocated and track active depth with scope_depth.
|
/// We keep all scopes allocated and track active depth with scope_depth.
|
||||||
scopes: std.ArrayListUnmanaged(std.StringHashMapUnmanaged(Value)),
|
scopes: std.ArrayList(std.StringHashMapUnmanaged(Value)),
|
||||||
/// Current active scope depth (scopes[0..scope_depth] are active).
|
/// Current active scope depth (scopes[0..scope_depth] are active).
|
||||||
scope_depth: usize,
|
scope_depth: usize,
|
||||||
/// Mixin definitions available in this context.
|
/// Mixin definitions available in this context.
|
||||||
@@ -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| {
|
||||||
@@ -194,7 +242,7 @@ const BlockDef = struct {
|
|||||||
pub const Runtime = struct {
|
pub const Runtime = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
context: *Context,
|
context: *Context,
|
||||||
output: std.ArrayListUnmanaged(u8),
|
output: std.ArrayList(u8),
|
||||||
depth: usize,
|
depth: usize,
|
||||||
options: Options,
|
options: Options,
|
||||||
/// File resolver for loading external templates.
|
/// File resolver for loading external templates.
|
||||||
@@ -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| {
|
||||||
@@ -382,7 +431,7 @@ pub const Runtime = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect all classes: shorthand classes + class attributes (may be arrays)
|
// Collect all classes: shorthand classes + class attributes (may be arrays)
|
||||||
var all_classes = std.ArrayListUnmanaged(u8).empty;
|
var all_classes = std.ArrayList(u8).empty;
|
||||||
defer all_classes.deinit(self.allocator);
|
defer all_classes.deinit(self.allocator);
|
||||||
|
|
||||||
// Add shorthand classes first (e.g., .bang)
|
// Add shorthand classes first (e.g., .bang)
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1394,7 +1580,7 @@ fn parseArrayToSpaceSeparated(allocator: std.mem.Allocator, input: []const u8) !
|
|||||||
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
||||||
if (content.len == 0) return "";
|
if (content.len == 0) return "";
|
||||||
|
|
||||||
var result = std.ArrayListUnmanaged(u8).empty;
|
var result = std.ArrayList(u8).empty;
|
||||||
errdefer result.deinit(allocator);
|
errdefer result.deinit(allocator);
|
||||||
|
|
||||||
var pos: usize = 0;
|
var pos: usize = 0;
|
||||||
@@ -1457,7 +1643,7 @@ fn parseObjectToCSS(allocator: std.mem.Allocator, input: []const u8) ![]const u8
|
|||||||
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
||||||
if (content.len == 0) return "";
|
if (content.len == 0) return "";
|
||||||
|
|
||||||
var result = std.ArrayListUnmanaged(u8).empty;
|
var result = std.ArrayList(u8).empty;
|
||||||
errdefer result.deinit(allocator);
|
errdefer result.deinit(allocator);
|
||||||
|
|
||||||
var pos: usize = 0;
|
var pos: usize = 0;
|
||||||
@@ -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