Files
pugz/CLAUDE.md

482 lines
13 KiB
Markdown
Raw Permalink Normal View History

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Purpose
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.
2026-01-22 21:28:25 +05:30
## Rules
- don not auto commit, user will do it.
## Build Commands
- `zig build` - Build the project (output in `zig-out/`)
- `zig build test` - Run all tests
2026-01-22 11:10:47 +05:30
- `zig build bench-compiled` - Run compiled templates benchmark (compare with Pug.js)
2026-01-22 21:28:25 +05:30
- `zig build bench-interpreted` - Inpterpret trmplates
## Architecture Overview
2026-01-22 11:10:47 +05:30
The template engine supports two rendering modes:
2026-01-22 11:10:47 +05:30
### 1. Runtime Rendering (Interpreted)
```
Source → Lexer → Tokens → Parser → AST → Runtime → HTML
```
2026-01-22 11:10:47 +05:30
### 2. Build-Time Compilation (Compiled)
```
Source → Lexer → Tokens → Parser → AST → build_templates.zig → generated.zig → Native Zig Code
```
The compiled mode is **~3x faster** than Pug.js.
### Core Modules
| Module | Purpose |
|--------|---------|
| **src/lexer.zig** | Tokenizes Pug source into tokens. Handles indentation tracking, raw text blocks, interpolation. |
| **src/parser.zig** | Converts token stream into AST. Handles nesting via indent/dedent tokens. |
| **src/ast.zig** | AST node definitions (Element, Text, Conditional, Each, Mixin, etc.) |
| **src/runtime.zig** | Evaluates AST with data context, produces final HTML. Handles variable interpolation, conditionals, loops, mixins. |
2026-01-22 11:10:47 +05:30
| **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. |
2026-01-22 11:10:47 +05:30
| **src/root.zig** | Public library API - exports `ViewEngine`, `renderTemplate()`, `build_templates` and core types. |
### Test Files
- **src/tests/general_test.zig** - Comprehensive integration tests for all features
2026-01-22 11:10:47 +05:30
- **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
**Important**: The runtime is designed to work with `ArenaAllocator`:
```zig
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); // Frees all template memory at once
const html = try pugz.renderTemplate(arena.allocator(), template, data);
```
This pattern is recommended because template rendering creates many small allocations that are all freed together after the response is sent.
## Key Implementation Details
### Lexer State Machine
The lexer tracks several states for handling complex syntax:
- `in_raw_block` / `raw_block_indent` / `raw_block_started` - For dot block text (e.g., `script.`)
- `indent_stack` - Stack-based indent/dedent token generation
2026-01-22 11:10:47 +05:30
**Important**: The lexer distinguishes between `#id` (ID selector), `#{expr}` (interpolation), and `#[tag]` (tag interpolation) by looking ahead at the next character.
### Token Types
Key tokens: `tag`, `class`, `id`, `lparen`, `rparen`, `attr_name`, `attr_value`, `text`, `interp_start`, `interp_end`, `indent`, `dedent`, `dot_block`, `pipe_text`, `literal_html`, `self_close`, `mixin_call`, etc.
### AST Node Types
- `element` - HTML elements with tag, classes, id, attributes, children
- `text` - Text with segments (literal, escaped interpolation, unescaped interpolation, tag interpolation)
- `conditional` - if/else if/else/unless branches
- `each` - Iteration with value, optional index, else branch
- `mixin_def` / `mixin_call` - Mixin definitions and invocations
- `block` - Named blocks for template inheritance
- `include` / `extends` - File inclusion and inheritance
- `raw_text` - Literal HTML or text blocks
### Runtime Value System
```zig
pub const Value = union(enum) {
null,
bool: bool,
int: i64,
float: f64,
string: []const u8,
array: []const Value,
object: std.StringHashMapUnmanaged(Value),
};
```
The `toValue()` function converts Zig structs to runtime Values automatically.
## Supported Pug Features
### Tags & Nesting
```pug
div
h1 Title
p Paragraph
```
### Classes & IDs (shorthand)
```pug
div#main.container.active
.box // defaults to div
#sidebar // defaults to div
```
### Attributes
```pug
a(href="/link" target="_blank") Click
input(type="checkbox" checked)
div(style={color: 'red'})
div(class=['foo', 'bar'])
button(disabled=false) // omitted when false
button(disabled=true) // disabled="disabled"
```
### Text & Interpolation
```pug
p Hello #{name} // escaped interpolation
p Hello !{rawHtml} // unescaped interpolation
p= variable // buffered code (escaped)
p!= rawVariable // buffered code (unescaped)
| Piped text line
p.
Multi-line
text block
<p>Literal HTML</p> // passed through as-is
2026-01-22 11:10:47 +05:30
// Interpolation-only text works too
h1.header #{title} // renders <h1 class="header">Title Value</h1>
```
### Tag Interpolation
```pug
p This is #[em emphasized] text
p Click #[a(href="/") here] to continue
```
### Block Expansion
```pug
a: img(src="logo.png") // colon for inline nesting
```
### Explicit Self-Closing
```pug
foo/ // renders as <foo />
```
### Conditionals
```pug
if condition
p Yes
else if other
p Maybe
else
p No
unless loggedIn
p Please login
2026-01-22 11:10:47 +05:30
// String comparison in conditions
if status == "active"
p Active
```
### Iteration
```pug
each item in items
li= item
each val, index in list
li #{index}: #{val}
each item in items
li= item
else
li No items
// Works with objects too (key as index)
each val, key in object
p #{key}: #{val}
2026-01-22 11:10:47 +05:30
// Nested iteration with field access
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
```pug
mixin button(text, type="primary")
button(class="btn btn-" + type)= text
+button("Click me")
+button("Submit", "success")
// With block content
mixin card(title)
.card
h3= title
block
+card("My Card")
p Card content here
// Rest arguments
mixin list(id, ...items)
ul(id=id)
each item in items
li= item
+list("mylist", "a", "b", "c")
// Attributes pass-through
mixin link(href, text)
a(href=href)&attributes(attributes)= text
+link("/home", "Home")(class="nav-link" data-id="1")
```
### Includes & Inheritance
```pug
include header.pug
extends layout.pug
block content
h1 Page Title
// Block modes
block append scripts
script(src="extra.js")
block prepend styles
link(rel="stylesheet" href="extra.css")
```
### Comments
```pug
// This renders as HTML comment
//- This is a silent comment (not in output)
```
## Server Usage
2026-01-22 11:10:47 +05:30
### Compiled Templates (Recommended for Production)
Use build-time compilation for best performance. See "Build-Time Template Compilation" section above.
### ViewEngine (Runtime Rendering)
2026-01-22 11:10:47 +05:30
The `ViewEngine` provides runtime template rendering with lazy-loading:
```zig
const std = @import("std");
const pugz = @import("pugz");
// Initialize once at server startup
var engine = try pugz.ViewEngine.init(allocator, .{
.views_dir = "src/views", // Root views directory
.mixins_dir = "mixins", // Mixins dir for lazy-loading (optional, default: "mixins")
.extension = ".pug", // File extension (default: .pug)
.pretty = true, // Pretty-print output (default: true)
});
defer engine.deinit();
// In request handler - use arena allocator per request
pub fn handleRequest(engine: *pugz.ViewEngine, allocator: std.mem.Allocator) ![]u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
// Template path is relative to views_dir, extension added automatically
return try engine.render(arena.allocator(), "pages/home", .{
.title = "Home",
.user = .{ .name = "Alice" },
});
}
```
### Mixin Resolution (Lazy Loading)
Mixins are resolved in the following order:
1. **Same template** - Mixins defined in the current template file
2. **Mixins directory** - If not found, searches `views/mixins/*.pug` files (lazy-loaded on first use)
This lazy-loading approach means:
- Mixins are only parsed when first called
- No upfront loading of all mixin files at server startup
- Templates can override mixins from the mixins directory by defining them locally
### Directory Structure
```
src/views/
├── mixins/ # Lazy-loaded mixins (searched when mixin not found in template)
│ ├── buttons.pug # mixin btn(text), mixin btn-link(href, text)
│ └── cards.pug # mixin card(title), mixin card-simple(title, body)
├── layouts/
│ └── base.pug # Base layout with blocks
├── partials/
│ ├── header.pug
│ └── footer.pug
└── pages/
├── home.pug # extends layouts/base
└── about.pug # extends layouts/base
```
Templates can use:
- `extends layouts/base` - Paths relative to views_dir
- `include partials/header` - Paths relative to views_dir
- `+btn("Click")` - Mixins from mixins/ dir loaded on-demand
### Low-Level API
For inline templates or custom use cases:
```zig
const std = @import("std");
const pugz = @import("pugz");
pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
return try pugz.renderTemplate(arena.allocator(),
\\html
\\ head
\\ title= title
\\ body
\\ h1 Hello, #{name}!
\\ if showList
\\ ul
\\ each item in items
\\ li= item
, .{
.title = "My Page",
.name = "World",
.showList = true,
.items = &[_][]const u8{ "One", "Two", "Three" },
});
}
```
## Testing
Run tests with `zig build test`. Tests cover:
- Basic element parsing and rendering
- Class and ID shorthand syntax
- Attribute parsing (quoted, unquoted, boolean, object literals)
- Text interpolation (escaped, unescaped, tag interpolation)
2026-01-22 11:10:47 +05:30
- Interpolation-only text (e.g., `h1.class #{var}`)
- Conditionals (if/else if/else/unless)
2026-01-22 11:10:47 +05:30
- Iteration (each with index, else branch, objects, nested loops)
- Case/when statements
- Mixin definitions and calls (with defaults, rest args, block content, attributes)
- Plain text (piped, dot blocks, literal HTML)
- Self-closing tags (void elements, explicit `/`)
- Block expansion with colon
- Comments (rendered and silent)
2026-01-22 11:10:47 +05:30
- String comparison in conditions
## Error Handling
The lexer and parser return errors for invalid syntax:
- `ParserError.UnexpectedToken`
- `ParserError.MissingCondition`
- `ParserError.MissingMixinName`
- `RuntimeError.ParseError` (wrapped for convenience API)
## Future Improvements
Potential areas for enhancement:
- Filter support (`:markdown`, `:stylus`, etc.)
- More complete JavaScript expression evaluation
- Source maps for debugging
2026-01-22 11:10:47 +05:30
- Mixin support in compiled templates