14 KiB
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.
Rules
- Do not auto commit, user will do it.
- At the start of each new session, read this CLAUDE.md file to understand project context and rules.
- When the user specifies a new rule, update this CLAUDE.md file to include it.
- Code comments are required but must be meaningful, not bloated. Focus on explaining "why" not "what". Avoid obvious comments like "// increment counter" - instead explain complex logic, non-obvious decisions, or tricky edge cases.
Build Commands
zig build- Build the project (output inzig-out/)zig build test- Run all testszig build bench-compiled- Run compiled templates benchmark (compare with Pug.js)zig build bench-interpreted- Inpterpret trmplates
Architecture Overview
The template engine supports two rendering modes:
1. Runtime Rendering (Interpreted)
Source → Lexer → Tokens → Parser → AST → Runtime → HTML
2. Build-Time Compilation (Compiled)
Source → Lexer → Tokens → Parser → AST → build_templates.zig → generated.zig → Native Zig Code
The compiled mode is ~3x faster than Pug.js.
Core Modules
| Module | Purpose |
|---|---|
| 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. |
| src/build_templates.zig | Build-time template compiler. Generates optimized Zig code from .pug templates. |
| src/view_engine.zig | High-level ViewEngine for web servers. Manages views directory, auto-loads mixins. |
| src/root.zig | Public library API - exports ViewEngine, renderTemplate(), build_templates and core types. |
Test Files
- src/tests/general_test.zig - Comprehensive integration tests for all features
- src/tests/doctype_test.zig - Doctype-specific tests
- src/tests/inheritance_test.zig - Template inheritance tests
Build-Time Template Compilation
For maximum performance, templates can be compiled to native Zig code at build time.
Setup in build.zig
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
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
appendSlicecalls - 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:
# 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:
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
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, childrentext- Text with segments (literal, escaped interpolation, unescaped interpolation, tag interpolation)conditional- if/else if/else/unless brancheseach- Iteration with value, optional index, else branchmixin_def/mixin_call- Mixin definitions and invocationsblock- Named blocks for template inheritanceinclude/extends- File inclusion and inheritanceraw_text- Literal HTML or text blocks
Runtime Value System
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
div
h1 Title
p Paragraph
Classes & IDs (shorthand)
div#main.container.active
.box // defaults to div
#sidebar // defaults to div
Attributes
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
p Hello #{name} // escaped interpolation (SAFE - default)
p Hello !{rawHtml} // unescaped interpolation (UNSAFE - trusted content only)
p= variable // buffered code (escaped, SAFE)
p!= rawVariable // buffered code (unescaped, UNSAFE)
| Piped text line
p.
Multi-line
text block
<p>Literal HTML</p> // passed through as-is
// Interpolation-only text works too
h1.header #{title} // renders <h1 class="header">Title Value</h1>
Security Note: By default, #{} and = escape HTML entities (<, >, &, ", ') to prevent XSS attacks. Only use !{} or != for content you fully trust (e.g., pre-sanitized HTML from your own code). Never use unescaped output for user-provided data.
Tag Interpolation
p This is #[em emphasized] text
p Click #[a(href="/") here] to continue
Block Expansion
a: img(src="logo.png") // colon for inline nesting
Explicit Self-Closing
foo/ // renders as <foo />
Conditionals
if condition
p Yes
else if other
p Maybe
else
p No
unless loggedIn
p Please login
// String comparison in conditions
if status == "active"
p Active
Iteration
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}
// Nested iteration with field access
each friend in friends
li #{friend.name}
each tag in friend.tags
span= tag
Case/When
case status
when "active"
p Active
when "pending"
p Pending
default
p Unknown
Mixins
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
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
// This renders as HTML comment
//- This is a silent comment (not in output)
Server Usage
Compiled Templates (Recommended for Production)
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:
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:
- Same template - Mixins defined in the current template file
- Mixins directory - If not found, searches
views/mixins/*.pugfiles (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_dirinclude 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:
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)
- Interpolation-only text (e.g.,
h1.class #{var}) - Conditionals (if/else if/else/unless)
- Iteration (each with index, else branch, objects, nested loops)
- Case/when statements
- Mixin definitions and calls (with defaults, rest args, block content, attributes)
- Plain text (piped, dot blocks, literal HTML)
- Self-closing tags (void elements, explicit
/) - Block expansion with colon
- Comments (rendered and silent)
- String comparison in conditions
Error Handling
The lexer and parser return errors for invalid syntax:
ParserError.UnexpectedTokenParserError.MissingConditionParserError.MissingMixinNameRuntimeError.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
- Mixin support in compiled templates