10 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 compiles Pug templates directly to HTML (unlike JS pug which compiles to JavaScript functions). 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-v1- Run v1 template benchmarkzig build bench-interpreted- Run interpreted templates benchmark
Architecture Overview
Compilation Pipeline
Source → Lexer → Tokens → StripComments → Parser → AST → Linker → Codegen → HTML
Two Rendering Modes
- Static compilation (
pug.compile): Outputs HTML directly - Data binding (
template.renderWithData): Supports#{field}interpolation with Zig structs
Core Modules
| Module | File | Purpose |
|---|---|---|
| Lexer | src/lexer.zig |
Tokenizes Pug source into tokens |
| Parser | src/parser.zig |
Builds AST from tokens |
| Runtime | src/runtime.zig |
Shared utilities (HTML escaping, etc.) |
| Error | src/error.zig |
Error formatting with source context |
| Walk | src/walk.zig |
AST traversal with visitor pattern |
| Strip Comments | src/strip_comments.zig |
Token filtering for comments |
| Load | src/load.zig |
File loading for includes/extends |
| Linker | src/linker.zig |
Template inheritance (extends/blocks) |
| Codegen | src/codegen.zig |
AST to HTML generation |
| Template | src/template.zig |
Data binding renderer |
| Pug | src/pug.zig |
Main entry point |
| ViewEngine | src/view_engine.zig |
High-level API for web servers |
| Root | src/root.zig |
Public library API exports |
Test Files
- src/tests/general_test.zig - Comprehensive integration tests
- src/tests/doctype_test.zig - Doctype-specific tests
- src/tests/check_list_test.zig - Template output validation tests
- src/lexer_test.zig - Lexer unit tests
- src/parser_test.zig - Parser unit tests
API Usage
Static Compilation (no data)
const std = @import("std");
const pug = @import("pugz").pug;
pub fn main() !void {
const allocator = std.heap.page_allocator;
var result = try pug.compile(allocator, "p Hello World", .{});
defer result.deinit(allocator);
std.debug.print("{s}\n", .{result.html}); // <p>Hello World</p>
}
Dynamic Rendering with Data
const std = @import("std");
const pugz = @import("pugz");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const html = try pugz.renderTemplate(arena.allocator(),
\\h1 #{title}
\\p #{message}
, .{
.title = "Welcome",
.message = "Hello, World!",
});
std.debug.print("{s}\n", .{html});
// Output: <h1>Welcome</h1><p>Hello, World!</p>
}
Data Binding Features
- Interpolation:
#{fieldName}in text content - Attribute binding:
a(href=url)bindsurlfield to href - Buffered code:
p= messageoutputs themessagefield - Auto-escaping: HTML is escaped by default (XSS protection)
const html = try pugz.renderTemplate(allocator,
\\a(href=url, class=style) #{text}
, .{
.url = "https://example.com",
.style = "btn",
.text = "Click me!",
});
// Output: <a href="https://example.com" class="btn">Click me!</a>
ViewEngine (for Web Servers)
const std = @import("std");
const pugz = @import("pugz");
const engine = pugz.ViewEngine.init(.{
.views_dir = "src/views",
.extension = ".pug",
});
// In request handler
pub fn handleRequest(allocator: std.mem.Allocator) ![]const u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
return try engine.render(arena.allocator(), "pages/home", .{
.title = "Home",
.user = .{ .name = "Alice" },
});
}
Compile Options
pub const CompileOptions = struct {
filename: ?[]const u8 = null, // For error messages
basedir: ?[]const u8 = null, // For absolute includes
pretty: bool = false, // Pretty print output
strip_unbuffered_comments: bool = true,
strip_buffered_comments: bool = false,
debug: bool = false,
doctype: ?[]const u8 = null,
};
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 Notes
Lexer (lexer.zig)
Lexer.init(allocator, source, options)- InitializeLexer.getTokens()- Returns token sliceLexer.last_error- Check for errors after failedgetTokens()
Parser (parser.zig)
Parser.init(allocator, tokens, filename, source)- InitializeParser.parse()- Returns AST root nodeParser.err- Check for errors after failedparse()
Codegen (codegen.zig)
Compiler.init(allocator, options)- InitializeCompiler.compile(ast)- Returns HTML string
Walk (walk.zig)
- Uses O(1) stack operations (append/pop) not O(n) insert/remove
getParent(index)uses reverse indexing (0 = immediate parent)initWithCapacity()for pre-allocation optimization
Runtime (runtime.zig)
escapeChar(c)- Shared HTML escape functionappendEscaped(list, allocator, str)- Append with escaping
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
Security Note: By default, #{} and = escape HTML entities (<, >, &, ", ') to prevent XSS attacks. Only use !{} or != for content you fully trust.
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
Conditionals
if condition
p Yes
else if other
p Maybe
else
p No
unless loggedIn
p Please login
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
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")
Includes & Inheritance
include header.pug
extends layout.pug
block content
h1 Page Title
Comments
// This renders as HTML comment
//- This is a silent comment (not in output)
Benchmark Results (2000 iterations)
| Template | Time |
|---|---|
| simple-0 | 0.8ms |
| simple-1 | 11.6ms |
| simple-2 | 8.2ms |
| if-expression | 7.4ms |
| projects-escaped | 7.1ms |
| search-results | 13.4ms |
| friends | 22.9ms |
| TOTAL | 71.3ms |
Limitations vs JS Pug
- No JavaScript expressions:
- var x = 1not supported - No nested field access:
#{user.name}not supported, only#{name} - No filters:
:markdown,:coffeeetc. not implemented - String fields only: Data binding works best with
[]const u8fields
Error Handling
Uses error unions with detailed PugError context including line, column, and source snippet:
LexerError- Tokenization errorsParserError- Syntax errorsViewEngineError- Template not found, parse errors
File Structure
src/
├── root.zig # Public library API
├── view_engine.zig # High-level ViewEngine
├── pug.zig # Main entry point (static compilation)
├── template.zig # Data binding renderer
├── lexer.zig # Tokenizer
├── lexer_test.zig # Lexer tests
├── parser.zig # AST parser
├── parser_test.zig # Parser tests
├── runtime.zig # Shared utilities
├── error.zig # Error formatting
├── walk.zig # AST traversal
├── strip_comments.zig # Comment filtering
├── load.zig # File loading
├── linker.zig # Template inheritance
├── codegen.zig # HTML generation
├── tests/ # Integration tests
│ ├── general_test.zig
│ ├── doctype_test.zig
│ └── check_list_test.zig
└── benchmarks/ # Performance benchmarks
├── bench_v1.zig
└── bench_interpreted.zig