Espanol | English
A high-performance template engine inspired by Pug, implemented in Zig with full JavaScript support.
doctype html
html(lang="en")
head
title #{pageTitle.toUpperCase()}
body
h1.greeting Hello #{name}!
p Age next year: #{age + 1}
if isActive
p.status Active user
each item in items
li= item
- ✅ Complete Pug syntax - Tags, attributes, classes, IDs, doctype
- ✅ Full UTF-8 support - Accented characters (á, é, ñ, ü), emoji 🎉, all Unicode
- ✅ JavaScript ES5.1 - Interpolations with methods, operators and expressions
- ✅ Real JavaScript engine - Powered by mujs
- ✅ Conditionals - if/else/else if/unless with full JavaScript expressions (>, <, >=, <=, ==, &&, ||)
- ✅ Loops - each/while with array support
- ✅ Mixins - Reusable components with arguments
- ✅ Template inheritance - extends/block
- ✅ JSON variables - Full support for strings, numbers, bools, arrays, and objects
- ✅ Attribute expressions - Dynamic attribute values (
class=myVar) - Buffered/unbuffered code -
=,!=, and-operators - ✅ Documentation comments -
//!for file metadata (ignored by parser) - Node.js addon - Native integration via N-API
- ✅ Bun.js compatible - 2-5x faster than Node.js
- ✅ C API - Full C-compatible API for FFI integration
- ✅ C++ API - Modern C++ wrapper with RAII, exceptions, STL (C++11/17)
- ✅ Editor support - VS Code, Sublime Text, CodeMirror
- ✅ No dependencies - Only Zig 0.15.2 and embedded mujs
- ⚡ Fast - Native compilation in Zig with optimizations
- Secure - HTML escaping and XSS prevention
- Works on Termux/Android (CLI binary)
- 87 unit tests - Comprehensive test coverage
Platform Notes:
- Termux/Android: CLI binary works perfectly. Node.js addon compiles but cannot be loaded due to Android restrictions. See docs/en/TERMUX.md.
- Windows: Pre-compiled binaries temporarily unavailable due to MinGW/MSVC toolchain incompatibility. Windows users can:
- Build locally with MinGW-w64/MSYS2
- Use WSL (Windows Subsystem for Linux)
- We're actively working on a solution. Track progress in GitHub Issues.
- Zig 0.15.2 (download)
git clone https://github.com/carlos-sweb/zig-pug
cd zig-pug
zig build# Install system-wide
make install
# Uninstall
make uninstall# Run the compiled binary
./zig-out/bin/zpug template.zpug
# Or if installed
zpug template.zpugzig-pug includes a powerful command line interface:
# Compile file to stdout
zpug template.zpug
# Compile with output file
zpug -i template.zpug -o output.html
# With variables (simple)
zpug template.zpug --var name=Alice --var age=25
# With arrays (CSV format)
zpug template.zpug --array items=apple,banana,orange
# With JSON (objects and complex structures)
zpug template.zpug --json user='{"name":"Alice","age":30}'
# With JSON file (arrays, objects, nested data)
zpug template.zpug --vars data.json
# Pretty-print with comments (development mode)
zpug -p template.zpug -o dev.html
# Pretty-print without comments (readable mode)
zpug -F template.zpug -o readable.html
# Minify output (production mode)
zpug -m template.zpug -o minified.html
# Default (production: no comments, minified)
zpug template.zpug -o output.html
# From stdin
cat template.zpug | zpug --stdin > output.htmlExample with inline arrays and objects:
# CSV arrays (simple values)
zpug template.zpug --array fruits=apple,banana,orange --array scores=95,87,92
# JSON objects
zpug template.zpug --json user='{"name":"Alice","age":30,"email":"alice@example.com"}'
# JSON arrays (for complex data)
zpug template.zpug --json items='["apple","banana","orange"]'
# Mix all types
zpug template.zpug \
--var title="Dashboard" \
--array tags=prod,stable \
--json user='{"name":"Alice","role":"admin"}' \
--prettyExample JSON variables file:
{
"user": {
"name": "Alice",
"age": 30,
"email": "alice@example.com"
},
"items": ["apple", "banana", "orange"],
"active": true
}See complete CLI documentation
template.zpug:
doctype html
html(lang="en")
head
meta(charset="UTF-8")
title #{pageTitle}
body
- var greeting = "Hello"
h1.main-title= greeting + " " + userName
if isLoggedIn
p.status Welcome back, #{userName}!
ul.menu
each item in menuItems
li
a(href=item.url)= item.title
else
p Please log in
mixin button(text, type)
button(class=type)= text
+button("Click me", "btn-primary")
Compile with:
zpug template.zpug --vars data.json -o output.htmlOutput:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>My Page</title></head><body><h1 class="main-title">Hello Alice</h1><p class="status">Welcome back, Alice!</p><ul class="menu"><li><a href="/home">Home</a></li><li><a href="/profile">Profile</a></li></ul><button class="btn-primary">Click me</button></body></html>doctype html
// Output: <!DOCTYPE html>
doctype xml
// Output: <!DOCTYPE xml>
// Simple tags
div
p Hello
span World
// Multiple classes (concatenated)
div.box.highlight.active
// Output: <div class="box highlight active">
// Classes and IDs
div.container
p#main-text
button.btn.btn-primary#submit
// Implicit divs (NEW in v4.0.0!)
.wrapper // <div class="wrapper">
#header // <div id="header">
(data-role="main") // <div data-role="main">
#app.container(data-v="2") // <div id="app" class="container" data-v="2">
// Attributes (static)
a(href="https://example.com" target="_blank") Link
input(type="text" name="username" required)
// Attributes (dynamic expressions)
- var myClass = "active"
- var myUrl = "/home"
button(class=myClass) Click
a(href=myUrl) Link
// Output: <button class="active">Click</button>
// Complex expressions (v4.0.0+)
- var alertType = "success"
- var userId = 42
div(class="alert alert-"+alertType)
div(id="user-"+userId)
// Output: <div class="alert alert-success">
// <div id="user-42">
// Operators: +, -, ., [], <, >
a(href=user.profile.url) Profile
div(data-first=items[0]) First Item
// Multiple lines
div(
class="card"
id="user-card"
data-user-id="123"
)
// Unbuffered code (executes but doesn't output)
- var name = "Alice"
- var age = 30
- var doubled = age * 2
// Buffered code inline (tag= syntax)
p= name
// Output: <p>Alice</p>
h1= name.toUpperCase()
// Output: <h1>ALICE</h1>
// Unescaped buffered code (tag!=)
- var html = "<strong>Bold</strong>"
div= html
// Output: <div><strong>Bold</strong></div>
div!= html
// Output: <div><strong>Bold</strong></div>
// Simple variables
p Hello #{name}
// String methods
p #{name.toUpperCase()}
p #{email.toLowerCase()}
// Arithmetic
p Age: #{age}
p Next year: #{age + 1}
p Double: #{age * 2}
// Objects (from JSON)
p Name: #{user.name}
p Email: #{user.email}
p Age: #{user.age}
// Arrays (from JSON)
p First: #{items[0]}
p Count: #{items.length}
// Complex expressions
p Full: #{firstName + ' ' + lastName}
p Status: #{age >= 18 ? 'Adult' : 'Minor'}
// Math
p Max: #{Math.max(10, 20)}
p Random: #{Math.floor(Math.random() * 100)}
All interpolations are automatically escaped for security:
// Escaped by default (safe)
p #{userInput}
// Input: <script>alert('xss')</script>
// Output: <p><script>alert('xss')</script></p>
// Unescaped (for trusted HTML only)
p !{trustedHtml}
// Input: <strong>Bold</strong>
// Output: <p><strong>Bold</strong></p>
Escaped characters: & < > " '
!{} with HTML you control. Never with user input.
Full support for if, else, else if, and unless with complete JavaScript expressions:
// Basic if/else
if isLoggedIn
p Welcome back!
else
p Please log in
// Multiple else if (unlimited chaining)
if score > 90
p Grade A
else if score > 80
p Grade B
else if score > 70
p Grade C
else
p Grade F
// unless (negation)
unless isAdmin
p Access denied
// Property access
if user.isPremium
p Premium features enabled
else
p Upgrade to premium
// Comparison operators (>, <, >=, <=, ==)
if age >= 18
p Adult content
else if age >= 13
p Teen content
else
p Kids content
// Logical operators (&&, ||)
if age >= 18 && hasLicense
p Can drive
else
p Cannot drive
// String equality
if status == "active"
p Account active
else if status == "pending"
p Pending approval
else
p Account inactive
// Complex expressions
if (isAdmin || isModerator) && user.isActive
p Administrative access
else
p Regular access
// Array length checks
if items.length > 0
p Cart has #{items.length} items
else
p Cart is empty
// Nested conditions
if user.isActive
if user.isPremium
p Premium active user
else
p Regular active user
else
p Inactive account
Supported features:
- ✅ Property access:
user.isPremium,array.length - ✅ Comparison operators:
>,<,>=,<=,== - ✅ Logical operators:
&&(AND),||(OR) - ✅ String equality:
status == "active" - ✅ Unlimited
else ifchaining - ✅ Nested conditionals
- ✅ Complex combined expressions
// Each with arrays
each item in items
li= item
// Each with index
each item, i in items
li #{i}: #{item}
// Optional chaining (NEW!) - Works everywhere
// Safely access properties that may not exist
// In interpolations
p #{user?.name}
p #{user?.profile?.bio}
// In buffered code
h1= user?.name
p= product?.description
// In attributes
a(href=user?.website)
div(class=item?.theme)
// In loops
each tag in product?.tags
span.tag= tag
// Nested optional chaining
each item in data?.products?.featured
li= item
// While loops
- var count = 0
while count < 5
p Count: #{count}
- count = count + 1
Optional Chaining (?.): Works in all contexts (interpolations, buffered code, attributes, loops). Eliminates manual hasOwnProperty checks. If the property doesn't exist, returns empty value—no errors thrown.
// Define mixin
mixin greeting(name)
p Hello, #{name}!
mixin button(text, type)
button(class=type)= text
// Use mixins
+greeting("World")
+greeting("Alice")
+button("Click me", "btn-primary")
+button("Cancel", "btn-secondary")
// Output:
// <p>Hello, World!</p>
// <p>Hello, Alice!</p>
// <button class="btn-primary">Click me</button>
// <button class="btn-secondary">Cancel</button>
Build reusable layouts with template inheritance using extends and block:
// layout.zpug - Base layout
doctype html
html
head
title
block title
| Default Title
body
header
h1 My Website
main
block content
p Default content
footer
block footer
p © 2024
// page.zpug - Extends layout
extends layout.zpug
block title
| Home Page
block content
h2 Welcome
p This replaces the default block content
// Output:
// <!DOCTYPE html>
// <html>
// <head><title>Home Page</title></head>
// <body>
// <header><h1>My Website</h1></header>
// <main>
// <h2>Welcome</h2>
// <p>This replaces the default block content</p>
// </main>
// <footer><p>© 2024</p></footer>
// </body>
// </html>
Block Modes:
// Replace (default) - Replaces parent block completely
block content
p New content
// Append - Adds after parent block
block append content
p Added after default
// Prepend - Adds before parent block
block prepend content
p Added before default
Path Syntax:
// Unquoted paths (recommended)
extends layout.zpug
extends ../layouts/base.zpug
// Quoted paths
extends "layout.zpug"
extends "../layouts/base.zpug"
See examples/extends/ for complete working examples.
//! Documentation comment (completely ignored)
//! Can appear before doctype declarations
// Buffered comment (visible in HTML with --pretty)
// This appears only in development mode
//- Unbuffered comment (never in HTML)
//- This is only in source, never compiled
// Security: Comments are escaped
// Comment with --> injection attempt
// Output: <!-- Comment with - -> injection attempt -->
Comment Types:
| Syntax | Name | Processed? | In HTML? | Use Case |
|---|---|---|---|---|
//! |
Documentation | ❌ No | ❌ No | File metadata, author notes |
// |
Buffered | ✅ Yes | ✅ Yes (--pretty only) | Development debugging |
//- |
Unbuffered | ✅ Yes | ❌ No | Code comments |
Comment Behavior:
- Documentation comments (
//!): Completely ignored by tokenizer (can appear beforedoctype) - Production mode (default): All buffered comments (
//) are stripped for minimal file size - Development mode (
--pretty): Buffered comments (//) are included for debugging - Readable mode (
--format): Pretty-print without comments - Unbuffered comments (
//-): Always stripped in all modes
# Production: no comments, minified
zpug template.zpug -o output.html
# Development: with comments and indentation
zpug --pretty template.zpug -o output.html
# Readable: indentation without comments
zpug --format template.zpug -o output.htmlThis matches industry standards (Pug, HTML minifiers) where production output is optimized and development output is readable.
Full Unicode support for international characters in all template elements:
doctype html
html(lang="es")
head
title Página en Español
body
h1 Bienvenido 🎉
// Spanish
p.información Este es un párrafo con acentos: José, María, Ángel
// Portuguese
p.português Programação em português com ã, õ, ç
// French
p.français Génération française avec é, è, ê, ç
// German
p#größe Deutsche Größe mit ä, ö, ü, ß
// Emoji and symbols
p Symbols: © ™ € £ ¥ • Emoji: 🚀 ✨ 💻 🌍
Supported:
- ✅ Accented characters in text:
á é í ó ú ñ ü ç - ✅ Accented characters in class names:
.información - ✅ Accented characters in IDs:
#descripción - ✅ Accented characters in comments:
// útil - ✅ Emoji and Unicode symbols:
🎉 © ™ € - ✅ All UTF-8 sequences (1-4 bytes)
const std = @import("std");
const parser = @import("parser.zig");
const compiler = @import("compiler.zig");
const runtime = @import("runtime.zig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Create JavaScript runtime
var js_runtime = try runtime.JsRuntime.init(allocator);
defer js_runtime.deinit();
// Set variables
try js_runtime.setString("name", "Alice");
try js_runtime.setNumber("age", 25);
try js_runtime.setBool("active", true);
// Parse template
const source =
\\doctype html
\\html
\\ body
\\ h1= name
\\ p Age: #{age}
;
var pars = try parser.Parser.init(allocator, source);
defer pars.deinit();
const tree = try pars.parse();
// Compile to HTML
var comp = try compiler.Compiler.init(allocator, js_runtime);
defer comp.deinit();
const html = try comp.compile(tree);
defer allocator.free(html);
std.debug.print("{s}\n", .{html});
}// Set arrays
const items = [_][]const u8{ "apple", "banana", "orange" };
for (items) |item| {
// Arrays are set via JavaScript code
_ = try js_runtime.eval("var items = ['apple', 'banana', 'orange']");
}
// Set objects
_ = try js_runtime.eval("var user = {name: 'Bob', age: 30}");
// Access properties
const name = try js_runtime.eval("user.name");
defer allocator.free(name);- GETTING-STARTED.md - Step-by-step guide
- CLI.md - Command line interface
- PUG-SYNTAX.md - Complete syntax reference
- NODEJS-INTEGRATION.md - Node.js integration (N-API)
- ZIG-PACKAGE.md - Usage as Zig dependency
- TERMUX.md - Compilation on Termux/Android
- LOOPS-INCLUDES-CACHE.md - Loops, includes and cache
- API-REFERENCE.md - API documentation
- EXAMPLES.md - Practical examples
- TESTS.md - Test documentation (87 tests)
# Run all tests
zig build test
# View detailed results
zig build test --summary allTest status: ✅ All 87 tests passing
See docs/tests/ for detailed test documentation.
zig-pug uses a two-phase architecture for processing templates:
Source (*.zpug)
↓
Tokenizer (lexical analysis)
↓
Parser (syntactic analysis)
↓
AST (abstract syntax tree)
↓
Compiler ← JS Runtime (mujs)
↓
HTML (output)
zig-pug uses a separation of concerns approach:
Phase 1: Parser (Zig)
- Recognizes operators as tokens (
>=,&&,||, etc.) - Reconstructs JavaScript expressions as strings
- Does NOT evaluate expressions
Phase 2: mujs (C)
- Evaluates JavaScript expressions
- Handles all operators, property access, methods
- Returns results to the compiler
Example:
if age >= 18 && hasLicense
p Can driveParser produces: "age>=18&&hasLicense" (string)
mujs evaluates: 25 >= 18 && true → true (result)
This architecture provides:
- ✅ Complete JavaScript ES5.1 support automatically
- ✅ Simple parser code (just string concatenation)
- ✅ Reliable evaluation (mujs is battle-tested)
- ✅ Same approach as Pug.js (delegates to JavaScript engine)
For details: See docs/en/ARCHITECTURE.md
- HTML escaping - Pre-calculated buffer size (single allocation)
- JS code generation - Pre-allocated buffers for arrays/objects
- mujs - Compiled with -O2 optimization
- Release builds - Forced ReleaseFast for mujs compatibility
zig-pug uses mujs as its JavaScript engine:
- Version: mujs 1.3.8
- Standard: ES5.1 compliant
- Size: 590 KB
- Dependencies: None (only libm)
- Used by: MuPDF, Ghostscript
✅ Fully supported:
- String methods, Number methods, Array methods
- Object property access, Arithmetic/Comparison/Logical operators
- Ternary operator, Math object, JSON object
❌ Not supported (ES6+):
- Arrow functions, Template literals, let/const, Async/await, Classes
For template engines, ES5.1 is completely sufficient.
Phase 1: Critical Bugs
- Multiple classes concatenation
- Loop iterator parsing
- Mixin arguments
- Comment escaping (security)
Phase 2: API & Variables
- JSON arrays support (
--vars) - JSON objects support
- Attribute expressions (
class=myVar) - Unbuffered code (
-lines)
Phase 3: UX Improvements
- Error messages with line numbers and hints
- Tag= and tag!= syntax
- Doctype support
Phase 4: Performance
- Optimized HTML escaping
- Optimized JS code generation
Phase 5: Testing
- 87 comprehensive unit tests
- Test documentation
Phase 6: Production Features
- Comment handling (production vs development modes)
- Pretty printing (HTML indentation)
- Watch mode (
-w)
See PLAN.md for the complete development plan.
Compilation Pipeline:
- Tokenizer (Español) - Lexical analysis: state machine, token types, indentation handling
- Parser (Español) - Syntax analysis: recursive descent, AST building, error handling
- AST (Español) - Abstract Syntax Tree: node types, structure, visitor pattern
- Compiler (Español) - HTML generation: JavaScript evaluation, security, template features
Language APIs:
- C/C++ Guide (Español) - Getting started guide for using zig-pug from C/C++
- C API Reference (Español) - Complete C API reference with examples, error handling, and thread safety
- Node.js API - Node.js addon documentation with TypeScript support
Additional Resources:
- CLI Documentation - Complete command-line interface guide
- C Examples - Complete C examples with Makefile and CMake
- pkg-config Usage - Using zpug from C/C++ projects
- English:
- Español:
Contributions are welcome! Please:
- Fork the project
- Create a branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
MIT License - see LICENSE for details
- Pug - Original inspiration
- Zig - Programming language
- mujs - Embedded JavaScript engine
- Artifex Software - Creators of mujs
- Issues: GitHub Issues
Made with ❤️ using Zig 0.15.2 and mujs