Files
pugz/CLAUDE.md
2026-01-24 23:53:19 +05:30

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 in zig-out/)
  • zig build test - Run all tests
  • zig build bench-v1 - Run v1 template benchmark
  • zig build bench-interpreted - Run interpreted templates benchmark

Architecture Overview

Compilation Pipeline

Source → Lexer → Tokens → StripComments → Parser → AST → Linker → Codegen → HTML

Two Rendering Modes

  1. Static compilation (pug.compile): Outputs HTML directly
  2. 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) binds url field to href
  • Buffered code: p= message outputs the message field
  • 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) - Initialize
  • Lexer.getTokens() - Returns token slice
  • Lexer.last_error - Check for errors after failed getTokens()

Parser (parser.zig)

  • Parser.init(allocator, tokens, filename, source) - Initialize
  • Parser.parse() - Returns AST root node
  • Parser.err - Check for errors after failed parse()

Codegen (codegen.zig)

  • Compiler.init(allocator, options) - Initialize
  • Compiler.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 function
  • appendEscaped(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

  1. No JavaScript expressions: - var x = 1 not supported
  2. No nested field access: #{user.name} not supported, only #{name}
  3. No filters: :markdown, :coffee etc. not implemented
  4. String fields only: Data binding works best with []const u8 fields

Error Handling

Uses error unions with detailed PugError context including line, column, and source snippet:

  • LexerError - Tokenization errors
  • ParserError - Syntax errors
  • ViewEngineError - 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