fix: add scoped error logging for lexer/parser errors

- Add std.log.scoped(.pugz) to template.zig and view_engine.zig
- Log detailed error info (code, line, column, message) when parsing fails
- Log template path context in ViewEngine on parse errors
- Remove debug print from lexer, use proper scoped logging instead
- Move benchmarks, docs, examples, playground, tests out of src/ to project root
- Update build.zig and documentation paths accordingly
- Bump version to 0.3.1
This commit is contained in:
2026-01-25 17:10:02 +05:30
parent 9d3b729c6c
commit aaf6a1af2d
1148 changed files with 57 additions and 330 deletions

View File

@@ -1,166 +0,0 @@
//! Pugz Benchmark - Template Rendering
//!
//! This benchmark parses templates ONCE, then renders 2000 times.
//! This matches how Pug.js benchmark works (compile once, render many).
//!
//! Run: zig build bench
const std = @import("std");
const pugz = @import("pugz");
const iterations: usize = 2000;
const templates_dir = "src/benchmarks/templates";
// Data structures matching JSON files
const SubFriend = struct {
id: i64,
name: []const u8,
};
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i64,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: []const []const u8,
friends: []const SubFriend,
};
const Account = struct {
balance: i64,
balanceFormatted: []const u8,
status: []const u8,
negative: bool,
};
const Project = struct {
name: []const u8,
url: []const u8,
description: []const u8,
};
const SearchRecord = struct {
imgUrl: []const u8,
viewItemUrl: []const u8,
title: []const u8,
description: []const u8,
featured: bool,
sizes: ?[]const []const u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("\n", .{});
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Pugz Benchmark ({d} iterations, parse once) ║\n", .{iterations});
std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir});
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
// Load JSON data
std.debug.print("\nLoading JSON data and parsing templates...\n", .{});
var data_arena = std.heap.ArenaAllocator.init(allocator);
defer data_arena.deinit();
const data_alloc = data_arena.allocator();
const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json");
const simple1 = try loadJson(struct {
name: []const u8,
messageCount: i64,
colors: []const []const u8,
primary: bool,
}, data_alloc, "simple-1.json");
const simple2 = try loadJson(struct {
header: []const u8,
header2: []const u8,
header3: []const u8,
header4: []const u8,
header5: []const u8,
header6: []const u8,
list: []const []const u8,
}, data_alloc, "simple-2.json");
const if_expr = try loadJson(struct { accounts: []const Account }, data_alloc, "if-expression.json");
const projects = try loadJson(struct {
title: []const u8,
text: []const u8,
projects: []const Project,
}, data_alloc, "projects-escaped.json");
const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json");
const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json");
// Load and PARSE templates ONCE (like Pug.js compiles once)
const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug");
const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug");
const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug");
const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug");
const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug");
const search_tpl = try loadTemplate(data_alloc, "search-results.pug");
const friends_tpl = try loadTemplate(data_alloc, "friends.pug");
// Parse templates once
const simple0_ast = try pugz.template.parse(data_alloc, simple0_tpl);
const simple1_ast = try pugz.template.parse(data_alloc, simple1_tpl);
const simple2_ast = try pugz.template.parse(data_alloc, simple2_tpl);
const if_expr_ast = try pugz.template.parse(data_alloc, if_expr_tpl);
const projects_ast = try pugz.template.parse(data_alloc, projects_tpl);
const search_ast = try pugz.template.parse(data_alloc, search_tpl);
const friends_ast = try pugz.template.parse(data_alloc, friends_tpl);
std.debug.print("Loaded. Starting benchmark (render only)...\n\n", .{});
var total: f64 = 0;
total += try bench("simple-0", allocator, simple0_ast, simple0);
total += try bench("simple-1", allocator, simple1_ast, simple1);
total += try bench("simple-2", allocator, simple2_ast, simple2);
total += try bench("if-expression", allocator, if_expr_ast, if_expr);
total += try bench("projects-escaped", allocator, projects_ast, projects);
total += try bench("search-results", allocator, search_ast, search);
total += try bench("friends", allocator, friends_ast, friends_data);
std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total });
std.debug.print("\n", .{});
}
fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T {
const path = templates_dir ++ "/" ++ filename;
const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024);
const parsed = try std.json.parseFromSlice(T, alloc, content, .{});
return parsed.value;
}
fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 {
const path = templates_dir ++ "/" ++ filename;
return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024);
}
fn bench(
name: []const u8,
allocator: std.mem.Allocator,
ast: *pugz.parser.Node,
data: anytype,
) !f64 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
_ = pugz.template.renderAst(arena.allocator(), ast, data) catch |err| {
std.debug.print(" {s:<20} => ERROR: {}\n", .{ name, err });
return 0;
};
}
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0;
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms });
return ms;
}

View File

@@ -1,86 +0,0 @@
/**
* Pug.js Benchmark - Comparison with Pugz
*
* Run: npm install && npm run bench
*
* Both Pug.js and Pugz benchmarks read from the same files:
* ../templates/*.pug (templates)
* ../templates/*.json (data)
*/
const fs = require('fs');
const path = require('path');
const pug = require('pug');
const iterations = 2000;
const templatesDir = path.join(__dirname, '..', 'templates');
const benchmarks = [
'simple-0',
'simple-1',
'simple-2',
'if-expression',
'projects-escaped',
'search-results',
'friends',
];
// ═══════════════════════════════════════════════════════════════════════════
// Load templates and data from shared files BEFORE benchmarking
// ═══════════════════════════════════════════════════════════════════════════
console.log("");
console.log("Loading templates and data...");
const templates = {};
const data = {};
for (const name of benchmarks) {
templates[name] = fs.readFileSync(path.join(templatesDir, `${name}.pug`), 'utf8');
data[name] = JSON.parse(fs.readFileSync(path.join(templatesDir, `${name}.json`), 'utf8'));
}
// Compile all templates BEFORE benchmarking
const compiled = {};
for (const name of benchmarks) {
compiled[name] = pug.compile(templates[name], { pretty: true });
}
console.log("Templates compiled. Starting benchmark...\n");
// ═══════════════════════════════════════════════════════════════════════════
// Benchmark
// ═══════════════════════════════════════════════════════════════════════════
console.log("╔═══════════════════════════════════════════════════════════════╗");
console.log(`║ Pug.js Benchmark (${iterations} iterations) ║`);
console.log("║ Templates: src/benchmarks/templates/*.pug ║");
console.log("║ Data: src/benchmarks/templates/*.json ║");
console.log("╚═══════════════════════════════════════════════════════════════╝");
let total = 0;
for (const name of benchmarks) {
const compiledFn = compiled[name];
const templateData = data[name];
// Warmup
for (let i = 0; i < 100; i++) {
compiledFn(templateData);
}
// Benchmark
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
compiledFn(templateData);
}
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1_000_000;
total += ms;
console.log(` ${name.padEnd(20)} => ${ms.toFixed(1).padStart(7)}ms`);
}
console.log("");
console.log(` ${"TOTAL".padEnd(20)} => ${total.toFixed(1).padStart(7)}ms`);
console.log("");

View File

@@ -1,576 +0,0 @@
{
"name": "pugjs-benchmark",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pugjs-benchmark",
"version": "1.0.0",
"dependencies": {
"pug": "^3.0.3"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.6"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/assert-never": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz",
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
"license": "MIT"
},
"node_modules/babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.9.6"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
"license": "MIT",
"dependencies": {
"is-regex": "^1.0.3"
}
},
"node_modules/constantinople": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.1"
}
},
"node_modules/doctypes": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-expression": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
"license": "MIT",
"dependencies": {
"acorn": "^7.1.1",
"object-assign": "^4.1.1"
}
},
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/js-stringify": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
"license": "MIT"
},
"node_modules/jstransformer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
"license": "MIT",
"dependencies": {
"is-promise": "^2.0.0",
"promise": "^7.0.1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"license": "MIT",
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/pug": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
"license": "MIT",
"dependencies": {
"pug-code-gen": "^3.0.3",
"pug-filters": "^4.0.0",
"pug-lexer": "^5.0.1",
"pug-linker": "^4.0.0",
"pug-load": "^3.0.0",
"pug-parser": "^6.0.0",
"pug-runtime": "^3.0.1",
"pug-strip-comments": "^2.0.0"
}
},
"node_modules/pug-attrs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"js-stringify": "^1.0.2",
"pug-runtime": "^3.0.0"
}
},
"node_modules/pug-code-gen": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"doctypes": "^1.1.0",
"js-stringify": "^1.0.2",
"pug-attrs": "^3.0.0",
"pug-error": "^2.1.0",
"pug-runtime": "^3.0.1",
"void-elements": "^3.1.0",
"with": "^7.0.0"
}
},
"node_modules/pug-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
"license": "MIT"
},
"node_modules/pug-filters": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
"license": "MIT",
"dependencies": {
"constantinople": "^4.0.1",
"jstransformer": "1.0.0",
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0",
"resolve": "^1.15.1"
}
},
"node_modules/pug-lexer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
"license": "MIT",
"dependencies": {
"character-parser": "^2.2.0",
"is-expression": "^4.0.0",
"pug-error": "^2.0.0"
}
},
"node_modules/pug-linker": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-load": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0",
"token-stream": "1.0.0"
}
},
"node_modules/pug-runtime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
"license": "MIT"
},
"node_modules/pug-strip-comments": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
"license": "MIT",
"dependencies": {
"pug-error": "^2.0.0"
}
},
"node_modules/pug-walk": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/token-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
"license": "MIT"
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/with": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"assert-never": "^1.2.1",
"babel-walk": "3.0.0-canary-5"
},
"engines": {
"node": ">= 10.0.0"
}
}
}
}

View File

@@ -1,12 +0,0 @@
{
"name": "pugjs-benchmark",
"version": "1.0.0",
"description": "Pug.js benchmark for comparison with Pugz",
"main": "bench.js",
"scripts": {
"bench": "node bench.js"
},
"dependencies": {
"pug": "^3.0.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
doctype html
html(lang="en")
head
meta(charset="UTF-8")
title Friends
body
div.friends
each friend in friends
div.friend
ul
li Name: #{friend.name}
li Balance: #{friend.balance}
li Age: #{friend.age}
li Address: #{friend.address}
li Image:
img(src=friend.picture)
li Company: #{friend.company}
li Email:
a(href=friend.emailHref) #{friend.email}
li About: #{friend.about}
if friend.tags
li Tags:
ul
each tag in friend.tags
li #{tag}
if friend.friends
li Friends:
ul
each subFriend in friend.friends
li #{subFriend.name} (#{subFriend.id})

View File

@@ -1,279 +0,0 @@
//! Auto-generated by pugz.compileTemplates()
//! Do not edit manually - regenerate by running: zig build
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList(u8);
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
const esc_lut: [256]?[]const u8 = blk: {
var t: [256]?[]const u8 = .{null} ** 256;
t['&'] = "&amp;";
t['<'] = "&lt;";
t['>'] = "&gt;";
t['"'] = "&quot;";
t['\''] = "&#x27;";
break :blk t;
};
fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void {
var i: usize = 0;
for (s, 0..) |c, j| {
if (esc_lut[c]) |e| {
if (j > i) try o.appendSlice(a, s[i..j]);
try o.appendSlice(a, e);
i = j + 1;
}
}
if (i < s.len) try o.appendSlice(a, s[i..]);
}
fn truthy(v: anytype) bool {
return switch (@typeInfo(@TypeOf(v))) {
.bool => v,
.optional => v != null,
.pointer => |p| if (p.size == .slice) v.len > 0 else true,
.int, .comptime_int => v != 0,
else => true,
};
}
var int_buf: [32]u8 = undefined;
fn strVal(v: anytype) []const u8 {
const T = @TypeOf(v);
switch (@typeInfo(T)) {
.pointer => |p| switch (p.size) {
.slice => return v,
.one => {
// For pointer-to-array, slice it
const child_info = @typeInfo(p.child);
if (child_info == .array) {
const arr_info = child_info.array;
const ptr: [*]const arr_info.child = @ptrCast(v);
return ptr[0..arr_info.len];
}
return strVal(v.*);
},
else => @compileError("unsupported pointer type"),
},
.array => @compileError("arrays must be passed by pointer"),
.int, .comptime_int => return std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0",
.optional => return if (v) |val| strVal(val) else "",
else => @compileError("strVal: unsupported type " ++ @typeName(T)),
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Templates
// ─────────────────────────────────────────────────────────────────────────────
pub fn simple_2(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<div><h1 class=\"header\">");
try esc(&o, a, strVal(@field(d, "header")));
try o.appendSlice(a, "</h1><h2 class=\"header2\">");
try esc(&o, a, strVal(@field(d, "header2")));
try o.appendSlice(a, "</h2><h3 class=\"header3\">");
try esc(&o, a, strVal(@field(d, "header3")));
try o.appendSlice(a, "</h3><h4 class=\"header4\">");
try esc(&o, a, strVal(@field(d, "header4")));
try o.appendSlice(a, "</h4><h5 class=\"header5\">");
try esc(&o, a, strVal(@field(d, "header5")));
try o.appendSlice(a, "</h5><h6 class=\"header6\">");
try esc(&o, a, strVal(@field(d, "header6")));
try o.appendSlice(a, "</h6><ul class=\"list\">");
for (@field(d, "list")) |item| {
try o.appendSlice(a, "<li class=\"item\">");
try esc(&o, a, strVal(item));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul></div>");
return o.items;
}
pub fn simple_1(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<div class=\"simple-1\" style=\"background-color: blue; border: 1px solid black\"><div class=\"colors\"><span class=\"hello\">Hello ");
try esc(&o, a, strVal(@field(d, "name")));
try o.appendSlice(a, "!<strong>You have ");
try esc(&o, a, strVal(@field(d, "messageCount")));
try o.appendSlice(a, " messages!</strong></span>");
if (@hasField(@TypeOf(d), "colors") and truthy(@field(d, "colors"))) {
try o.appendSlice(a, "<ul>");
for (@field(d, "colors")) |color| {
try o.appendSlice(a, "<li class=\"color\">");
try esc(&o, a, strVal(color));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul>");
} else {
try o.appendSlice(a, "<div>No colors!</div>");
}
try o.appendSlice(a, "</div>");
if (@hasField(@TypeOf(d), "primary") and truthy(@field(d, "primary"))) {
try o.appendSlice(a, "<button class=\"primary\" type=\"button\">Click me!</button>");
} else {
try o.appendSlice(a, "<button class=\"secondary\" type=\"button\">Click me!</button>");
}
try o.appendSlice(a, "</div>");
return o.items;
}
pub fn simple_0(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<h1>Hello, ");
try esc(&o, a, strVal(@field(d, "name")));
try o.appendSlice(a, "</h1>");
return o.items;
}
pub fn projects_escaped(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title></head><body><p>");
try esc(&o, a, strVal(@field(d, "text")));
try o.appendSlice(a, "</p>");
if (@field(d, "projects").len > 0) {
for (@field(d, "projects")) |project| {
try o.appendSlice(a, "<a");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(project.url));
try o.appendSlice(a, "\"");
try o.appendSlice(a, "></a><project class=\"name\"></project><p>");
try esc(&o, a, strVal(project.description));
try o.appendSlice(a, "</p>");
}
} else {
try o.appendSlice(a, "<p>No projects</p>");
}
try o.appendSlice(a, "</body></html>");
return o.items;
}
pub fn friends(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><title>Friends</title></head><body><div class=\"friends\">");
for (@field(d, "friends")) |friend| {
try o.appendSlice(a, "<div class=\"friend\"><ul><li>Name: ");
try esc(&o, a, strVal(friend.name));
try o.appendSlice(a, "</li><li>Balance: ");
try esc(&o, a, strVal(friend.balance));
try o.appendSlice(a, "</li><li>Age: ");
try esc(&o, a, strVal(friend.age));
try o.appendSlice(a, "</li><li>Address: ");
try esc(&o, a, strVal(friend.address));
try o.appendSlice(a, "</li><li>Image:<img");
try o.appendSlice(a, " src=\"");
try o.appendSlice(a, strVal(friend.picture));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " /></li><li>Company: ");
try esc(&o, a, strVal(friend.company));
try o.appendSlice(a, "</li><li>Email:<a");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(friend.emailHref));
try o.appendSlice(a, "\"");
try o.appendSlice(a, "></a><friend class=\"email\"></friend></li><li>About: ");
try esc(&o, a, strVal(friend.about));
try o.appendSlice(a, "</li>");
if (truthy(friend.tags)) {
try o.appendSlice(a, "<li>Tags:<ul>");
for (if (@typeInfo(@TypeOf(friend.tags)) == .optional) (friend.tags orelse &.{}) else friend.tags) |tag| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(tag));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul></li>");
}
if (truthy(friend.friends)) {
try o.appendSlice(a, "<li>Friends:<ul>");
for (if (@typeInfo(@TypeOf(friend.friends)) == .optional) (friend.friends orelse &.{}) else friend.friends) |subFriend| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(subFriend.name));
try o.appendSlice(a, " (");
try esc(&o, a, strVal(subFriend.id));
try o.appendSlice(a, ")</li>");
}
try o.appendSlice(a, "</ul></li>");
}
try o.appendSlice(a, "</ul></div>");
}
try o.appendSlice(a, "</div></body></html>");
return o.items;
}
pub fn search_results(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<div class=\"search-results view-gallery\">");
for (@field(d, "searchRecords")) |searchRecord| {
try o.appendSlice(a, "<div class=\"search-item\"><div class=\"search-item-container drop-shadow\"><div class=\"img-container\"><img");
try o.appendSlice(a, " src=\"");
try o.appendSlice(a, strVal(searchRecord.imgUrl));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " /></div><h4 class=\"title\"><a");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(searchRecord.viewItemUrl));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(searchRecord.title));
try o.appendSlice(a, "</a></h4>");
try esc(&o, a, strVal(searchRecord.description));
if (truthy(searchRecord.featured)) {
try o.appendSlice(a, "<div>Featured!</div>");
}
if (truthy(searchRecord.sizes)) {
try o.appendSlice(a, "<div>Sizes available:<ul>");
for (if (@typeInfo(@TypeOf(searchRecord.sizes)) == .optional) (searchRecord.sizes orelse &.{}) else searchRecord.sizes) |size| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(size));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul></div>");
}
try o.appendSlice(a, "</div></div>");
}
try o.appendSlice(a, "</div>");
return o.items;
}
pub fn if_expression(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
for (@field(d, "accounts")) |account| {
try o.appendSlice(a, "<div>");
if (std.mem.eql(u8, strVal(account.status), "closed")) {
try o.appendSlice(a, "<div>Your account has been closed!</div>");
}
if (std.mem.eql(u8, strVal(account.status), "suspended")) {
try o.appendSlice(a, "<div>Your account has been temporarily suspended</div>");
}
if (std.mem.eql(u8, strVal(account.status), "open")) {
try o.appendSlice(a, "<div>Bank balance:");
if (truthy(account.negative)) {
try o.appendSlice(a, "<span class=\"negative\">");
try esc(&o, a, strVal(account.balanceFormatted));
try o.appendSlice(a, "</span>");
} else {
try o.appendSlice(a, "<span class=\"positive\">");
try esc(&o, a, strVal(account.balanceFormatted));
try o.appendSlice(a, "</span>");
}
try o.appendSlice(a, "</div>");
}
try o.appendSlice(a, "</div>");
}
return o.items;
}
pub const template_names = [_][]const u8{
"simple_2",
"simple_1",
"simple_0",
"projects_escaped",
"friends",
"search_results",
"if_expression",
};

View File

@@ -1,28 +0,0 @@
{
"accounts": [
{
"balance": 0,
"balanceFormatted": "$0.00",
"status": "open",
"negative": false
},
{
"balance": 10,
"balanceFormatted": "$10.00",
"status": "closed",
"negative": false
},
{
"balance": -100,
"balanceFormatted": "$-100.00",
"status": "suspended",
"negative": true
},
{
"balance": 999,
"balanceFormatted": "$999.00",
"status": "open",
"negative": false
}
]
}

View File

@@ -1,13 +0,0 @@
each account in accounts
div
if account.status == "closed"
div Your account has been closed!
if account.status == "suspended"
div Your account has been temporarily suspended
if account.status == "open"
div
| Bank balance:
if account.negative
span.negative= account.balanceFormatted
else
span.positive= account.balanceFormatted

View File

@@ -1,41 +0,0 @@
{
"title": "Projects",
"text": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
"projects": [
{
"name": "<strong>Facebook</strong>",
"url": "http://facebook.com",
"description": "Social network"
},
{
"name": "<strong>Google</strong>",
"url": "http://google.com",
"description": "Search engine"
},
{
"name": "<strong>Twitter</strong>",
"url": "http://twitter.com",
"description": "Microblogging service"
},
{
"name": "<strong>Amazon</strong>",
"url": "http://amazon.com",
"description": "Online retailer"
},
{
"name": "<strong>eBay</strong>",
"url": "http://ebay.com",
"description": "Online auction"
},
{
"name": "<strong>Wikipedia</strong>",
"url": "http://wikipedia.org",
"description": "A free encyclopedia"
},
{
"name": "<strong>LiveJournal</strong>",
"url": "http://livejournal.com",
"description": "Blogging platform"
}
]
}

View File

@@ -1,11 +0,0 @@
doctype html
html
head
title #{title}
body
p #{text}
each project in projects
a(href=project.url) #{project.name}
p #{project.description}
else
p No projects

View File

@@ -1,278 +0,0 @@
{
"searchRecords": [
{
"imgUrl": "img1.jpg",
"viewItemUrl": "http://foo/1",
"title": "Namebox",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img2.jpg",
"viewItemUrl": "http://foo/2",
"title": "Arctiq",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img3.jpg",
"viewItemUrl": "http://foo/3",
"title": "Niquent",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img4.jpg",
"viewItemUrl": "http://foo/4",
"title": "Remotion",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img5.jpg",
"viewItemUrl": "http://foo/5",
"title": "Octocore",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img6.jpg",
"viewItemUrl": "http://foo/6",
"title": "Spherix",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img7.jpg",
"viewItemUrl": "http://foo/7",
"title": "Quarex",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img8.jpg",
"viewItemUrl": "http://foo/8",
"title": "Supremia",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img9.jpg",
"viewItemUrl": "http://foo/9",
"title": "Amtap",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img10.jpg",
"viewItemUrl": "http://foo/10",
"title": "Qiao",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img11.jpg",
"viewItemUrl": "http://foo/11",
"title": "Pushcart",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img12.jpg",
"viewItemUrl": "http://foo/12",
"title": "Eweville",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": false,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img13.jpg",
"viewItemUrl": "http://foo/13",
"title": "Senmei",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img14.jpg",
"viewItemUrl": "http://foo/14",
"title": "Maximind",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img15.jpg",
"viewItemUrl": "http://foo/15",
"title": "Blurrybus",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img16.jpg",
"viewItemUrl": "http://foo/16",
"title": "Virva",
"description": "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img17.jpg",
"viewItemUrl": "http://foo/17",
"title": "Centregy",
"description": "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img18.jpg",
"viewItemUrl": "http://foo/18",
"title": "Dancerity",
"description": "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img19.jpg",
"viewItemUrl": "http://foo/19",
"title": "Oceanica",
"description": "Est ad amet irure veniam dolore velit amet irure fugiat ut elit.",
"featured": true,
"sizes": [
"S",
"M",
"L",
"XL",
"XXL"
]
},
{
"imgUrl": "img20.jpg",
"viewItemUrl": "http://foo/20",
"title": "Synkgen",
"description": "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit.",
"featured": false,
"sizes": null
}
]
}

View File

@@ -1,17 +0,0 @@
.search-results.view-gallery
each searchRecord in searchRecords
.search-item
.search-item-container.drop-shadow
.img-container
img(src=searchRecord.imgUrl)
h4.title
a(href=searchRecord.viewItemUrl)= searchRecord.title
| #{searchRecord.description}
if searchRecord.featured
div Featured!
if searchRecord.sizes
div
| Sizes available:
ul
each size in searchRecord.sizes
li= size

View File

@@ -1,3 +0,0 @@
{
"name": "John"
}

View File

@@ -1 +0,0 @@
h1 Hello, #{name}

View File

@@ -1,19 +0,0 @@
{
"name": "George Washington",
"messageCount": 999,
"colors": [
"red",
"green",
"blue",
"yellow",
"orange",
"pink",
"black",
"white",
"beige",
"brown",
"cyan",
"magenta"
],
"primary": true
}

View File

@@ -1,14 +0,0 @@
.simple-1(style="background-color: blue; border: 1px solid black")
.colors
span.hello Hello #{name}!
strong You have #{messageCount} messages!
if colors
ul
each color in colors
li.color= color
else
div No colors!
if primary
button(type="button" class="primary") Click me!
else
button(type="button" class="secondary") Click me!

View File

@@ -1,20 +0,0 @@
{
"header": "Header",
"header2": "Header2",
"header3": "Header3",
"header4": "Header4",
"header5": "Header5",
"header6": "Header6",
"list": [
"1000000000",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10"
]
}

View File

@@ -1,10 +0,0 @@
div
h1.header #{header}
h2.header2 #{header2}
h3.header3 #{header3}
h4.header4 #{header4}
h5.header5 #{header5}
h6.header6 #{header6}
ul.list
each item in list
li.item #{item}

View File

@@ -1,289 +0,0 @@
# Pugz API Reference
## Compiled Mode
### Build Setup
In `build.zig`:
```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const pugz_dep = b.dependency("pugz", .{
.target = target,
.optimize = optimize,
});
const build_templates = @import("pugz").build_templates;
const compiled_templates = build_templates.compileTemplates(b, .{
.source_dir = "views", // Required: directory containing .pug files
.extension = ".pug", // Optional: default ".pug"
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "tpls", .module = compiled_templates },
},
}),
});
b.installArtifact(exe);
}
```
### Using Compiled Templates
```zig
const std = @import("std");
const tpls = @import("tpls");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
// Template function name is derived from filename
// views/home.pug -> tpls.home()
// views/pages/about.pug -> tpls.pages_about()
const html = try tpls.home(arena.allocator(), .{
.title = "Welcome",
.items = &[_][]const u8{ "One", "Two" },
});
std.debug.print("{s}\n", .{html});
}
```
### Template Names
File paths are converted to function names:
- `home.pug``home()`
- `pages/about.pug``pages_about()`
- `admin-panel.pug``admin_panel()`
List all available templates:
```zig
for (tpls.template_names) |name| {
std.debug.print("{s}\n", .{name});
}
```
---
## Interpreted Mode
### ViewEngine
```zig
const std = @import("std");
const pugz = @import("pugz");
pub fn main() !void {
// Initialize engine
var engine = pugz.ViewEngine.init(.{
.views_dir = "views", // Required: root views directory
.mixins_dir = "mixins", // Optional: default "mixins"
.extension = ".pug", // Optional: default ".pug"
.pretty = true, // Optional: default true
});
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
// Render template (path relative to views_dir, no extension needed)
const html = try engine.render(arena.allocator(), "pages/home", .{
.title = "Hello",
.name = "World",
});
std.debug.print("{s}\n", .{html});
}
```
### renderTemplate
For inline template strings:
```zig
const html = try pugz.renderTemplate(allocator,
\\h1 Hello, #{name}!
\\ul
\\ each item in items
\\ li= item
, .{
.name = "World",
.items = &[_][]const u8{ "one", "two", "three" },
});
```
---
## Data Types
Templates accept Zig structs as data. Supported field types:
| Zig Type | Template Usage |
|----------|----------------|
| `[]const u8` | `#{field}` |
| `i64`, `i32`, etc. | `#{field}` (converted to string) |
| `bool` | `if field` |
| `[]const T` | `each item in field` |
| `?T` (optional) | `if field` (null = false) |
| nested struct | `#{field.subfield}` |
### Example
```zig
const data = .{
.title = "My Page",
.count = 42,
.show_header = true,
.items = &[_][]const u8{ "a", "b", "c" },
.user = .{
.name = "Alice",
.email = "alice@example.com",
},
};
const html = try tpls.home(allocator, data);
```
Template:
```pug
h1= title
p Count: #{count}
if show_header
header Welcome
ul
each item in items
li= item
p #{user.name} (#{user.email})
```
---
## Directory Structure
Recommended project layout:
```
myproject/
├── build.zig
├── build.zig.zon
├── src/
│ └── main.zig
└── views/
├── mixins/
│ ├── buttons.pug
│ └── cards.pug
├── layouts/
│ └── base.pug
├── partials/
│ ├── header.pug
│ └── footer.pug
└── pages/
├── home.pug
└── about.pug
```
### Mixin Resolution
Mixins are resolved in order:
1. Defined in the current template
2. Loaded from `views/mixins/*.pug` (lazy-loaded on first use)
---
## Web Framework Integration
### http.zig
```zig
const std = @import("std");
const httpz = @import("httpz");
const tpls = @import("tpls");
const App = struct {
// app state
};
fn handler(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
_ = app;
_ = req;
const html = try tpls.home(res.arena, .{
.title = "Hello",
});
res.content_type = .HTML;
res.body = html;
}
```
### Using ViewEngine with http.zig
```zig
const App = struct {
engine: pugz.ViewEngine,
};
fn handler(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
_ = req;
const html = app.engine.render(res.arena, "home", .{
.title = "Hello",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
```
---
## Error Handling
```zig
const html = engine.render(allocator, "home", data) catch |err| {
switch (err) {
error.FileNotFound => // template file not found
error.ParseError => // invalid template syntax
error.OutOfMemory => // allocation failed
else => // other errors
}
};
```
---
## Memory Management
Always use `ArenaAllocator` for template rendering:
```zig
// Per-request pattern
fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
return try tpls.home(arena.allocator(), .{ .title = "Hello" });
}
```
The arena pattern is efficient because:
- Template rendering creates many small allocations
- All allocations are freed at once with `arena.deinit()`
- No need to track individual allocations

View File

@@ -1,340 +0,0 @@
# Pugz Template Syntax
Complete reference for Pugz template syntax.
## Tags & Nesting
Indentation defines nesting. Default tag is `div`.
```pug
div
h1 Title
p Paragraph
```
Output:
```html
<div><h1>Title</h1><p>Paragraph</p></div>
```
## Classes & IDs
Shorthand syntax using `.` for classes and `#` for IDs.
```pug
div#main.container.active
.box
#sidebar
```
Output:
```html
<div id="main" class="container active"></div>
<div class="box"></div>
<div id="sidebar"></div>
```
## Attributes
```pug
a(href="/link" target="_blank") Click
input(type="checkbox" checked)
button(disabled=false)
button(disabled=true)
```
Output:
```html
<a href="/link" target="_blank">Click</a>
<input type="checkbox" checked="checked" />
<button></button>
<button disabled="disabled"></button>
```
Boolean attributes: `false` omits the attribute, `true` renders `attr="attr"`.
## Text Content
### Inline text
```pug
p Hello World
```
### Piped text
```pug
p
| Line one
| Line two
```
### Block text (dot syntax)
```pug
script.
console.log('hello');
console.log('world');
```
### Literal HTML
```pug
<p>Passed through as-is</p>
```
## Interpolation
### Escaped (safe)
```pug
p Hello #{name}
p= variable
```
### Unescaped (raw HTML)
```pug
p Hello !{rawHtml}
p!= rawVariable
```
### Tag interpolation
```pug
p This is #[em emphasized] text
p Click #[a(href="/") here] to continue
```
## Conditionals
### if / else if / else
```pug
if condition
p Yes
else if other
p Maybe
else
p No
```
### unless
```pug
unless loggedIn
p Please login
```
### String comparison
```pug
if status == "active"
p Active
```
## Iteration
### each
```pug
each item in items
li= item
```
### with index
```pug
each val, index in list
li #{index}: #{val}
```
### with else (empty collection)
```pug
each item in items
li= item
else
li No items
```
### Objects
```pug
each val, key in object
p #{key}: #{val}
```
### Nested iteration
```pug
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
### Basic mixin
```pug
mixin button(text)
button= text
+button("Click me")
```
### Default parameters
```pug
mixin button(text, type="primary")
button(class="btn btn-" + type)= text
+button("Click me")
+button("Submit", "success")
```
### Block content
```pug
mixin card(title)
.card
h3= title
block
+card("My Card")
p Card content here
```
### Rest arguments
```pug
mixin list(id, ...items)
ul(id=id)
each item in items
li= item
+list("mylist", "a", "b", "c")
```
### Attributes pass-through
```pug
mixin link(href, text)
a(href=href)&attributes(attributes)= text
+link("/home", "Home")(class="nav-link" data-id="1")
```
## Template Inheritance
### Base layout (layout.pug)
```pug
doctype html
html
head
title= title
block styles
body
block content
block scripts
```
### Child template
```pug
extends layout.pug
block content
h1 Page Title
p Page content
```
### Block modes
```pug
block append scripts
script(src="extra.js")
block prepend styles
link(rel="stylesheet" href="extra.css")
```
## Includes
```pug
include header.pug
include partials/footer.pug
```
## Comments
### HTML comment (rendered)
```pug
// This renders as HTML comment
```
Output:
```html
<!-- This renders as HTML comment -->
```
### Silent comment (not rendered)
```pug
//- This is a silent comment
```
## Block Expansion
Colon for inline nesting:
```pug
a: img(src="logo.png")
```
Output:
```html
<a><img src="logo.png" /></a>
```
## Self-Closing Tags
Explicit self-closing with `/`:
```pug
foo/
```
Output:
```html
<foo />
```
Void elements (`br`, `hr`, `img`, `input`, `meta`, `link`, etc.) are automatically self-closing.
## Doctype
```pug
doctype html
```
Output:
```html
<!DOCTYPE html>
```

View File

@@ -1,42 +0,0 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Get dependencies
const pugz_dep = b.dependency("pugz", .{
.target = target,
.optimize = optimize,
});
const httpz_dep = b.dependency("httpz", .{
.target = target,
.optimize = optimize,
});
// Main executable
const exe = b.addExecutable(.{
.name = "demo",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "httpz", .module = httpz_dep.module("httpz") },
},
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the demo server");
run_step.dependOn(&run_cmd.step);
}

View File

@@ -1,21 +0,0 @@
.{
.name = .demo,
.version = "0.0.1",
.fingerprint = 0xd642dfa01393173d,
.minimum_zig_version = "0.15.2",
.dependencies = .{
.pugz = .{
.path = "../../..",
},
.httpz = .{
.url = "git+https://github.com/karlseguin/http.zig?ref=master#9ef2ffe8d611ff2e1081e5cf39cb4632c145c5b9",
.hash = "httpz-0.0.0-PNVzrIowBwAFr_kqBN1W4KBMC2Ofutasj2ZfNAIcfTzF",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"views",
},
}

View File

@@ -1,752 +0,0 @@
/* Pugz Store - Clean Modern CSS */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--text: #1f2937;
--text-muted: #6b7280;
--bg: #ffffff;
--bg-alt: #f9fafb;
--border: #e5e7eb;
--success: #10b981;
--radius: 8px;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--text);
background: var(--bg);
}
a {
color: var(--primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Layout */
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
.header {
background: var(--bg);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
}
.logo:hover {
text-decoration: none;
}
.nav {
display: flex;
gap: 24px;
}
.nav-link {
color: var(--text-muted);
font-weight: 500;
}
.nav-link:hover {
color: var(--primary);
text-decoration: none;
}
.header-actions {
display: flex;
align-items: center;
}
.cart-link {
color: var(--text);
font-weight: 500;
}
/* Footer */
.footer {
background: var(--text);
color: white;
padding: 40px 0;
margin-top: 60px;
}
.footer-content {
text-align: center;
}
.footer-content p {
color: #9ca3af;
}
/* Buttons */
.btn {
display: inline-block;
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
border-radius: var(--radius);
border: none;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.btn:hover {
text-decoration: none;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-outline {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
}
.btn-outline:hover {
border-color: var(--primary);
color: var(--primary);
}
.btn-sm {
padding: 6px 12px;
font-size: 13px;
}
.btn-block {
display: block;
width: 100%;
}
/* Hero Section */
.hero {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: 80px 0;
text-align: center;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
}
.hero p {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 32px;
}
.hero-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.hero .btn-outline {
border-color: rgba(255, 255, 255, 0.4);
color: white;
}
.hero .btn-outline:hover {
border-color: white;
background: rgba(255, 255, 255, 0.1);
}
/* Sections */
.section {
padding: 60px 0;
}
.section-alt {
background: var(--bg-alt);
}
.section h2 {
font-size: 1.75rem;
margin-bottom: 32px;
}
.page-header {
background: var(--bg-alt);
padding: 40px 0;
border-bottom: 1px solid var(--border);
}
.page-header h1 {
font-size: 2rem;
margin-bottom: 8px;
}
.page-header p {
color: var(--text-muted);
}
/* Feature Grid */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 24px;
}
.feature-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
}
.feature-card h3 {
font-size: 1.1rem;
margin-bottom: 12px;
}
.feature-card p {
color: var(--text-muted);
font-size: 14px;
}
.feature-card ul {
margin: 0;
padding-left: 20px;
color: var(--text-muted);
font-size: 14px;
}
.feature-card li {
margin-bottom: 4px;
}
/* Category Grid */
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
}
.category-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 32px 24px;
text-align: center;
transition: all 0.2s;
}
.category-card:hover {
border-color: var(--primary);
text-decoration: none;
transform: translateY(-2px);
}
.category-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background: var(--bg-alt);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 600;
color: var(--primary);
}
.category-card h3 {
font-size: 1rem;
color: var(--text);
margin-bottom: 4px;
}
.category-card span {
font-size: 14px;
color: var(--text-muted);
}
/* Product Grid */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
.product-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
transition: all 0.2s;
}
.product-card:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.product-image {
position: relative;
height: 180px;
background: var(--bg-alt);
}
.product-badge {
position: absolute;
top: 12px;
left: 12px;
background: #ef4444;
color: white;
padding: 4px 10px;
font-size: 12px;
font-weight: 600;
border-radius: 4px;
}
.product-info {
padding: 16px;
}
.product-category {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.product-name {
font-size: 1rem;
margin: 6px 0 12px;
}
.product-price {
font-size: 1.1rem;
font-weight: 600;
color: var(--text);
margin-bottom: 12px;
}
/* Products Toolbar */
.products-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.results-count {
color: var(--text-muted);
}
.sort-options {
display: flex;
align-items: center;
gap: 8px;
}
.sort-options label {
color: var(--text-muted);
font-size: 14px;
}
.sort-options select {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 14px;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
}
.page-link {
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-muted);
font-size: 14px;
}
.page-link:hover {
border-color: var(--primary);
color: var(--primary);
text-decoration: none;
}
.page-link.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
/* Cart */
.cart-layout {
display: grid;
grid-template-columns: 1fr 340px;
gap: 32px;
}
.cart-items {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.cart-item {
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: 20px;
padding: 20px;
align-items: center;
border-bottom: 1px solid var(--border);
}
.cart-item:last-child {
border-bottom: none;
}
.cart-item-info h3 {
font-size: 1rem;
margin-bottom: 4px;
}
.cart-item-price {
color: var(--text-muted);
font-size: 14px;
}
.cart-item-qty {
display: flex;
align-items: center;
}
.qty-btn {
width: 32px;
height: 32px;
border: 1px solid var(--border);
background: var(--bg);
cursor: pointer;
font-size: 16px;
}
.qty-input {
width: 48px;
height: 32px;
border: 1px solid var(--border);
border-left: none;
border-right: none;
text-align: center;
font-size: 14px;
}
.cart-item-total {
font-weight: 600;
min-width: 80px;
text-align: right;
}
.cart-item-remove {
width: 32px;
height: 32px;
border: none;
background: none;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
}
.cart-item-remove:hover {
color: #ef4444;
}
.cart-actions {
padding: 20px;
border-top: 1px solid var(--border);
}
.cart-summary {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
height: fit-content;
}
.cart-summary h3 {
font-size: 1.1rem;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 14px;
}
.summary-total {
font-size: 1.1rem;
font-weight: 600;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
/* About Page */
.about-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 40px;
}
.about-main h2 {
margin-bottom: 16px;
}
.about-main h3 {
margin: 24px 0 12px;
font-size: 1.1rem;
}
.about-main p {
color: var(--text-muted);
margin-bottom: 12px;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 10px 0;
border-bottom: 1px solid var(--border);
font-size: 14px;
}
.about-sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.info-card {
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
}
.info-card h3 {
font-size: 1rem;
margin-bottom: 12px;
}
.info-card ul {
margin: 0;
padding-left: 18px;
font-size: 14px;
color: var(--text-muted);
}
.info-card li {
margin-bottom: 6px;
}
/* Product Detail */
.product-detail {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.product-detail-image {
background: var(--bg-alt);
border-radius: var(--radius);
aspect-ratio: 1;
}
.product-image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.product-detail-info h1 {
font-size: 2rem;
margin: 8px 0 16px;
}
.product-price-large {
font-size: 1.75rem;
font-weight: 600;
color: var(--primary);
margin-bottom: 16px;
}
.product-description {
color: var(--text-muted);
margin-bottom: 24px;
line-height: 1.7;
}
.product-actions {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 24px;
}
.quantity-selector {
display: flex;
align-items: center;
gap: 8px;
}
.quantity-selector label {
font-weight: 500;
}
.product-meta {
padding-top: 24px;
border-top: 1px solid var(--border);
}
.product-meta p {
color: var(--text-muted);
font-size: 14px;
margin-bottom: 8px;
}
.breadcrumb {
font-size: 14px;
color: var(--text-muted);
}
.breadcrumb a {
color: var(--text-muted);
}
.breadcrumb a:hover {
color: var(--primary);
}
.breadcrumb span {
margin: 0 8px;
}
/* Error Page */
.error-page {
padding: 100px 0;
text-align: center;
}
.error-code {
font-size: 8rem;
font-weight: 700;
color: var(--border);
line-height: 1;
}
.error-content h2 {
margin: 16px 0 8px;
}
.error-content p {
color: var(--text-muted);
margin-bottom: 32px;
}
.error-actions {
display: flex;
gap: 12px;
justify-content: center;
}
/* Utility Classes */
.text-success {
color: var(--success);
}
.text-muted {
color: var(--text-muted);
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
flex-wrap: wrap;
gap: 16px;
}
.nav {
order: 3;
width: 100%;
justify-content: center;
}
.hero h1 {
font-size: 2rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
.cart-layout,
.about-grid {
grid-template-columns: 1fr;
}
.cart-item {
grid-template-columns: 1fr;
gap: 12px;
}
}

View File

@@ -1,433 +0,0 @@
//! Pugz Store Demo - A comprehensive e-commerce demo showcasing Pugz capabilities
//!
//! Features demonstrated:
//! - Template inheritance (extends/block)
//! - Partial includes (header, footer)
//! - Mixins with parameters (product-card, rating, forms)
//! - Conditionals and loops
//! - Data binding
//! - Pretty printing
//! - LRU cache with TTL
const std = @import("std");
const httpz = @import("httpz");
const pugz = @import("pugz");
const Allocator = std.mem.Allocator;
// ============================================================================
// Data Types
// ============================================================================
const Product = struct {
id: []const u8,
name: []const u8,
price: []const u8,
image: []const u8,
rating: u8,
category: []const u8,
categorySlug: []const u8,
sale: bool = false,
description: []const u8 = "",
reviewCount: []const u8 = "0",
};
const Category = struct {
name: []const u8,
slug: []const u8,
icon: []const u8,
count: []const u8,
active: bool = false,
};
const CartItem = struct {
id: []const u8,
name: []const u8,
price: []const u8,
image: []const u8,
variant: []const u8,
quantity: []const u8,
total: []const u8,
};
const Cart = struct {
items: []const CartItem,
subtotal: []const u8,
shipping: []const u8,
discount: ?[]const u8 = null,
discountCode: ?[]const u8 = null,
tax: []const u8,
total: []const u8,
};
const ShippingMethod = struct {
id: []const u8,
name: []const u8,
time: []const u8,
price: []const u8,
};
const State = struct {
code: []const u8,
name: []const u8,
};
// ============================================================================
// Sample Data
// ============================================================================
const sample_products = [_]Product{
.{
.id = "1",
.name = "Wireless Headphones",
.price = "79.99",
.image = "/images/headphones.jpg",
.rating = 4,
.category = "Electronics",
.categorySlug = "electronics",
.sale = true,
.description = "Premium wireless headphones with noise cancellation",
.reviewCount = "128",
},
.{
.id = "2",
.name = "Smart Watch Pro",
.price = "199.99",
.image = "/images/watch.jpg",
.rating = 5,
.category = "Electronics",
.categorySlug = "electronics",
.description = "Advanced fitness tracking and notifications",
.reviewCount = "256",
},
.{
.id = "3",
.name = "Laptop Stand",
.price = "49.99",
.image = "/images/stand.jpg",
.rating = 4,
.category = "Accessories",
.categorySlug = "accessories",
.description = "Ergonomic aluminum laptop stand",
.reviewCount = "89",
},
.{
.id = "4",
.name = "USB-C Hub",
.price = "39.99",
.image = "/images/hub.jpg",
.rating = 4,
.category = "Accessories",
.categorySlug = "accessories",
.sale = true,
.description = "7-in-1 USB-C hub with HDMI and card reader",
.reviewCount = "312",
},
.{
.id = "5",
.name = "Mechanical Keyboard",
.price = "129.99",
.image = "/images/keyboard.jpg",
.rating = 5,
.category = "Electronics",
.categorySlug = "electronics",
.description = "RGB mechanical keyboard with Cherry MX switches",
.reviewCount = "445",
},
.{
.id = "6",
.name = "Desk Lamp",
.price = "34.99",
.image = "/images/lamp.jpg",
.rating = 4,
.category = "Home Office",
.categorySlug = "home-office",
.description = "LED desk lamp with adjustable brightness",
.reviewCount = "67",
},
};
const sample_categories = [_]Category{
.{ .name = "Electronics", .slug = "electronics", .icon = "E", .count = "24" },
.{ .name = "Accessories", .slug = "accessories", .icon = "A", .count = "18" },
.{ .name = "Home Office", .slug = "home-office", .icon = "H", .count = "12" },
.{ .name = "Clothing", .slug = "clothing", .icon = "C", .count = "36" },
};
const sample_cart_items = [_]CartItem{
.{
.id = "1",
.name = "Wireless Headphones",
.price = "79.99",
.image = "/images/headphones.jpg",
.variant = "Black",
.quantity = "1",
.total = "79.99",
},
.{
.id = "5",
.name = "Mechanical Keyboard",
.price = "129.99",
.image = "/images/keyboard.jpg",
.variant = "RGB",
.quantity = "1",
.total = "129.99",
},
};
const sample_cart = Cart{
.items = &sample_cart_items,
.subtotal = "209.98",
.shipping = "0",
.tax = "18.90",
.total = "228.88",
};
const shipping_methods = [_]ShippingMethod{
.{ .id = "standard", .name = "Standard Shipping", .time = "5-7 business days", .price = "0" },
.{ .id = "express", .name = "Express Shipping", .time = "2-3 business days", .price = "9.99" },
.{ .id = "overnight", .name = "Overnight Shipping", .time = "Next business day", .price = "19.99" },
};
const us_states = [_]State{
.{ .code = "CA", .name = "California" },
.{ .code = "NY", .name = "New York" },
.{ .code = "TX", .name = "Texas" },
.{ .code = "FL", .name = "Florida" },
.{ .code = "WA", .name = "Washington" },
};
// ============================================================================
// Application
// ============================================================================
const App = struct {
allocator: Allocator,
view: pugz.ViewEngine,
pub fn init(allocator: Allocator) !App {
return .{
.allocator = allocator,
.view = try pugz.ViewEngine.init(allocator, .{
.views_dir = "views",
.pretty = true,
.max_cached_templates = 50,
.cache_ttl_seconds = 10, // 10s TTL for development
}),
};
}
pub fn deinit(self: *App) void {
self.view.deinit();
}
};
// ============================================================================
// Request Handlers
// ============================================================================
fn home(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/home", .{
.title = "Home",
.cartCount = "2",
.authenticated = true,
.items = &[_][]const u8{ "Wireless Headphones", "Smart Watch", "Laptop Stand", "USB-C Hub" },
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn products(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/products", .{
.title = "All Products",
.cartCount = "2",
.productCount = "6",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn productDetail(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
const id = req.param("id") orelse "1";
_ = id;
const html = app.view.render(res.arena, "pages/product-detail", .{
.cartCount = "2",
.productName = "Wireless Headphones",
.category = "Electronics",
.price = "79.99",
.description = "Premium wireless headphones with active noise cancellation. Experience crystal-clear audio whether you're working, traveling, or relaxing at home.",
.sku = "WH-001-BLK",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn cart(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/cart", .{
.title = "Shopping Cart",
.cartCount = "2",
.cartItems = &sample_cart_items,
.subtotal = sample_cart.subtotal,
.shipping = sample_cart.shipping,
.tax = sample_cart.tax,
.total = sample_cart.total,
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn about(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/about", .{
.title = "About",
.cartCount = "2",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn includeDemo(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/include-demo", .{
.title = "Include Demo",
.cartCount = "2",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn notFound(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
res.status = 404;
const html = app.view.render(res.arena, "pages/404", .{
.title = "Page Not Found",
.cartCount = "2",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn renderError(res: *httpz.Response, err: anyerror) void {
res.status = 500;
res.content_type = .HTML;
res.body = std.fmt.allocPrint(res.arena,
\\<!DOCTYPE html>
\\<html>
\\<head><title>Error</title></head>
\\<body>
\\<h1>500 - Server Error</h1>
\\<p>Error: {s}</p>
\\</body>
\\</html>
, .{@errorName(err)}) catch "Internal Server Error";
}
// ============================================================================
// Static Files
// ============================================================================
fn serveStatic(_: *App, req: *httpz.Request, res: *httpz.Response) !void {
const path = req.url.path;
// Strip leading slash and prepend public folder
const rel_path = if (path.len > 0 and path[0] == '/') path[1..] else path;
const full_path = std.fmt.allocPrint(res.arena, "public/{s}", .{rel_path}) catch {
res.status = 500;
res.body = "Internal Server Error";
return;
};
// Read file from disk
const content = std.fs.cwd().readFileAlloc(res.arena, full_path, 10 * 1024 * 1024) catch {
res.status = 404;
res.body = "Not Found";
return;
};
// Set content type based on extension
if (std.mem.endsWith(u8, path, ".css")) {
res.content_type = .CSS;
} else if (std.mem.endsWith(u8, path, ".js")) {
res.content_type = .JS;
} else if (std.mem.endsWith(u8, path, ".html")) {
res.content_type = .HTML;
}
res.body = content;
}
// ============================================================================
// Main
// ============================================================================
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leak");
const allocator = gpa.allocator();
var app = try App.init(allocator);
defer app.deinit();
const port = 8081;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app);
defer server.deinit();
var router = try server.router(.{});
// Pages
router.get("/", home, .{});
router.get("/products", products, .{});
router.get("/products/:id", productDetail, .{});
router.get("/cart", cart, .{});
router.get("/about", about, .{});
router.get("/include-demo", includeDemo, .{});
// Static files
router.get("/css/*", serveStatic, .{});
std.debug.print(
\\
\\ ____ ____ _
\\ | _ \ _ _ __ _ ____ / ___|| |_ ___ _ __ ___
\\ | |_) | | | |/ _` |_ / \___ \| __/ _ \| '__/ _ \
\\ | __/| |_| | (_| |/ / ___) | || (_) | | | __/
\\ |_| \__,_|\__, /___| |____/ \__\___/|_| \___|
\\ |___/
\\
\\ Server running at http://localhost:{d}
\\
\\ Routes:
\\ GET / - Home page
\\ GET /products - Products page
\\ GET /products/:id - Product detail
\\ GET /cart - Shopping cart
\\ GET /about - About page
\\ GET /include-demo - Include directive demo
\\
\\ Press Ctrl+C to stop.
\\
, .{port});
try server.listen();
}

View File

@@ -1,4 +0,0 @@
.info-box
h3 Included Partial
p This content comes from includes/some_partial.pug
p It demonstrates the include directive for reusable template fragments.

View File

@@ -1,28 +0,0 @@
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="stylesheet" href="/css/style.css")
block title
title Pugz Store
body
header.header
.container
.header-content
a.logo(href="/") Pugz Store
nav.nav
a.nav-link(href="/") Home
a.nav-link(href="/products") Products
a.nav-link(href="/about") About
.header-actions
a.cart-link(href="/cart")
| Cart (#{cartCount})
main
block content
footer.footer
.container
.footer-content
p Built with Pugz - A Pug template engine for Zig

View File

@@ -1,12 +0,0 @@
//- Alert/notification mixins
mixin alert(message, type)
- var alertClass = type ? "alert alert-" + type : "alert alert-info"
.alert(class=alertClass)
p= message
mixin alert-dismissible(message, type)
- var alertClass = type ? "alert alert-" + type : "alert alert-info"
.alert.alert-dismissible(class=alertClass)
p= message
button.alert-close(type="button" aria-label="Close") x

View File

@@ -1,15 +0,0 @@
//- Button mixins with various styles
mixin btn(text, type)
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
button(class=btnClass)= text
mixin btn-link(href, text, type)
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
a(href=href class=btnClass)= text
mixin btn-icon(icon, text, type)
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
button(class=btnClass)
span.icon= icon
span= text

View File

@@ -1,17 +0,0 @@
//- Cart item display
mixin cart-item(item)
.cart-item
.cart-item-image
img(src=item.image alt=item.name)
.cart-item-details
h4.cart-item-name #{item.name}
p.cart-item-variant #{item.variant}
span.cart-item-price $#{item.price}
.cart-item-quantity
button.qty-btn.qty-minus -
input.qty-input(type="number" value=item.quantity min="1")
button.qty-btn.qty-plus +
.cart-item-total
span $#{item.total}
button.cart-item-remove(aria-label="Remove item") x

View File

@@ -1,25 +0,0 @@
//- Form input mixins
mixin input(name, label, type, placeholder)
.form-group
label(for=name)= label
input.form-control(type=type id=name name=name placeholder=placeholder)
mixin input-required(name, label, type, placeholder)
.form-group
label(for=name)
= label
span.required *
input.form-control(type=type id=name name=name placeholder=placeholder required)
mixin select(name, label, options)
.form-group
label(for=name)= label
select.form-control(id=name name=name)
each opt in options
option(value=opt.value)= opt.label
mixin textarea(name, label, placeholder, rows)
.form-group
label(for=name)= label
textarea.form-control(id=name name=name placeholder=placeholder rows=rows)

View File

@@ -1,38 +0,0 @@
//- Product card mixin - displays a product in grid/list view
//- Parameters:
//- product: { id, name, price, image, rating, category }
mixin product-card(product)
article.product-card
a.product-image(href="/products/" + product.id)
img(src=product.image alt=product.name)
if product.sale
span.badge.badge-sale Sale
.product-info
span.product-category #{product.category}
h3.product-name
a(href="/products/" + product.id) #{product.name}
.product-rating
+rating(product.rating)
.product-footer
span.product-price $#{product.price}
button.btn.btn-primary.btn-sm(data-product=product.id) Add to Cart
//- Featured product card with larger display
mixin product-featured(product)
article.product-card.product-featured
.product-image-large
img(src=product.image alt=product.name)
if product.sale
span.badge.badge-sale Sale
.product-details
span.product-category #{product.category}
h2.product-name #{product.name}
p.product-description #{product.description}
.product-rating
+rating(product.rating)
span.review-count (#{product.reviewCount} reviews)
.product-price-large $#{product.price}
.product-actions
button.btn.btn-primary.btn-lg Add to Cart
button.btn.btn-outline Wishlist

View File

@@ -1,13 +0,0 @@
//- Star rating display
//- Parameters:
//- stars: number of stars (1-5)
mixin rating(stars)
.stars
- var i = 1
while i <= 5
if i <= stars
span.star.star-filled
else
span.star.star-empty
- i = i + 1

View File

@@ -1,15 +0,0 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.error-page
.container
.error-content
h1.error-code 404
h2 Page Not Found
p The page you are looking for does not exist or has been moved.
.error-actions
a.btn.btn-primary(href="/") Go Home
a.btn.btn-outline(href="/products") View Products

View File

@@ -1,53 +0,0 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.page-header
.container
h1 About Pugz
p A Pug template engine written in Zig
section.section
.container
.about-grid
.about-main
h2 What is Pugz?
p Pugz is a high-performance Pug template engine implemented in Zig. It provides both runtime interpretation and build-time compilation for maximum flexibility.
h3 Key Features
ul.feature-list
li Template inheritance with extends and blocks
li Partial includes for modular templates
li Mixins for reusable components
li Conditionals (if/else/unless)
li Iteration with each loops
li Variable interpolation
li Pretty-printed output
li LRU caching with TTL
h3 Performance
p Compiled templates run approximately 3x faster than Pug.js, with zero runtime parsing overhead.
.about-sidebar
.info-card
h3 This Demo Shows
ul
li Template inheritance (extends)
li Named blocks
li Conditional rendering
li Variable interpolation
li Simple iteration
.info-card
h3 Links
ul
li
a(href="https://github.com/ankitpatial/pugz") GitHub Repository
li
a(href="/products") View Products
li
a(href="/include-demo") Include Demo
li
a(href="/") Back to Home

View File

@@ -1,47 +0,0 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.page-header
.container
h1 Shopping Cart
p Review your items before checkout
section.section
.container
.cart-layout
.cart-main
.cart-items
each item in cartItems
.cart-item
.cart-item-info
h3 #{name}
p.text-muted #{variant}
span.cart-item-price $#{price}
.cart-item-qty
button.qty-btn -
input.qty-input(type="text" value=quantity)
button.qty-btn +
.cart-item-total $#{total}
button.cart-item-remove x
.cart-actions
a.btn.btn-outline(href="/products") Continue Shopping
.cart-summary
h3 Order Summary
.summary-row
span Subtotal
span $#{subtotal}
.summary-row
span Shipping
span.text-success Free
.summary-row
span Tax
span $#{tax}
.summary-row.summary-total
span Total
span $#{total}
a.btn.btn-primary.btn-block(href="/checkout") Proceed to Checkout

View File

@@ -1,144 +0,0 @@
extends layouts/base.pug
include mixins/forms.pug
include mixins/alerts.pug
include mixins/buttons.pug
block content
h1 Checkout
if errors
+alert("Please correct the errors below", "error")
.checkout-layout
form.checkout-form(action="/checkout" method="POST")
//- Shipping Information
section.checkout-section
h2 Shipping Information
.form-row
+input-required("firstName", "First Name", "text", "John")
+input-required("lastName", "Last Name", "text", "Doe")
+input-required("email", "Email Address", "email", "john@example.com")
+input-required("phone", "Phone Number", "tel", "+1 (555) 123-4567")
+input-required("address", "Street Address", "text", "123 Main St")
+input("address2", "Apartment, suite, etc.", "text", "Apt 4B")
.form-row
+input-required("city", "City", "text", "New York")
.form-group
label(for="state")
| State
span.required *
select.form-control#state(name="state" required)
option(value="") Select State
each state in states
option(value=state.code)= state.name
+input-required("zip", "ZIP Code", "text", "10001")
.form-group
label(for="country")
| Country
span.required *
select.form-control#country(name="country" required)
option(value="US" selected) United States
option(value="CA") Canada
//- Shipping Method
section.checkout-section
h2 Shipping Method
.shipping-options
each method in shippingMethods
label.shipping-option
input(type="radio" name="shipping" value=method.id checked=method.id == "standard")
.shipping-info
span.shipping-name #{method.name}
span.shipping-time #{method.time}
span.shipping-price
if method.price > 0
| $#{method.price}
else
| Free
//- Payment Information
section.checkout-section
h2 Payment Information
.payment-methods-select
label.payment-method
input(type="radio" name="paymentMethod" value="card" checked)
span Credit/Debit Card
label.payment-method
input(type="radio" name="paymentMethod" value="paypal")
span PayPal
.card-details(id="card-details")
+input-required("cardNumber", "Card Number", "text", "1234 5678 9012 3456")
.form-row
+input-required("expiry", "Expiration Date", "text", "MM/YY")
+input-required("cvv", "CVV", "text", "123")
+input-required("cardName", "Name on Card", "text", "John Doe")
.form-group
label.checkbox-label
input(type="checkbox" name="saveCard")
span Save card for future purchases
//- Billing Address
section.checkout-section
.form-group
label.checkbox-label
input(type="checkbox" name="sameAsShipping" checked)
span Billing address same as shipping
.billing-address(id="billing-address" style="display: none")
+input-required("billingAddress", "Street Address", "text", "")
.form-row
+input-required("billingCity", "City", "text", "")
+input-required("billingState", "State", "text", "")
+input-required("billingZip", "ZIP Code", "text", "")
button.btn.btn-primary.btn-lg(type="submit") Place Order
//- Order Summary Sidebar
aside.order-summary
h3 Order Summary
.summary-items
each item in cart.items
.summary-item
img(src=item.image alt=item.name)
.item-info
span.item-name #{item.name}
span.item-qty x#{item.quantity}
span.item-price $#{item.total}
.summary-details
.summary-row
span Subtotal
span $#{cart.subtotal}
if cart.discount
.summary-row.discount
span Discount
span -$#{cart.discount}
.summary-row
span Shipping
span#shipping-cost $#{selectedShipping.price}
.summary-row
span Tax
span $#{cart.tax}
.summary-row.total
span Total
span $#{cart.total}
.secure-checkout
span Secure Checkout
p Your information is protected with 256-bit SSL encryption

View File

@@ -1,56 +0,0 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.hero
.container
h1 Welcome to Pugz Store
p Discover amazing products powered by Zig
.hero-actions
a.btn.btn-primary(href="/products") Shop Now
a.btn.btn-outline(href="/about") Learn More
section.section
.container
h2 Template Features
.feature-grid
.feature-card
h3 Conditionals
if authenticated
p.text-success You are logged in!
else
p.text-muted Please log in to continue.
.feature-card
h3 Variables
p Title: #{title}
p Cart Items: #{cartCount}
.feature-card
h3 Iteration
ul
each item in items
li= item
.feature-card
h3 Clean Syntax
p Pug templates compile to HTML with minimal overhead.
section.section.section-alt
.container
h2 Shop by Category
.category-grid
a.category-card(href="/products?cat=electronics")
.category-icon E
h3 Electronics
span 24 products
a.category-card(href="/products?cat=accessories")
.category-icon A
h3 Accessories
span 18 products
a.category-card(href="/products?cat=home")
.category-icon H
h3 Home Office
span 12 products

View File

@@ -1,20 +0,0 @@
extends layouts/base.pug
block title
title Include Demo | Pugz Store
block content
section.page-header
.container
h1 Include Demo
p Demonstrating the include directive
section.section
.container
h2 Content from this page
p The box below is included from a separate partial file.
include includes/some_partial.pug
h2 After the include
p This content comes after the included partial.

View File

@@ -1,65 +0,0 @@
extends layouts/base.pug
block title
title #{productName} | Pugz Store
block content
section.page-header
.container
.breadcrumb
a(href="/") Home
span /
a(href="/products") Products
span /
span #{productName}
section.section
.container
.product-detail
.product-detail-image
.product-image-placeholder
.product-detail-info
span.product-category #{category}
h1 #{productName}
.product-price-large $#{price}
p.product-description #{description}
.product-actions
.quantity-selector
label Quantity:
button.qty-btn -
input.qty-input(type="text" value="1")
button.qty-btn +
a.btn.btn-primary.btn-lg(href="/cart") Add to Cart
.product-meta
p SKU: #{sku}
p Category: #{category}
section.section.section-alt
.container
h2 You May Also Like
.product-grid
.product-card
.product-image
.product-info
span.product-category Electronics
h3.product-name Smart Watch Pro
.product-price $199.99
a.btn.btn-sm(href="/products/2") View Details
.product-card
.product-image
.product-info
span.product-category Accessories
h3.product-name Laptop Stand
.product-price $49.99
a.btn.btn-sm(href="/products/3") View Details
.product-card
.product-image
.product-info
span.product-category Accessories
h3.product-name USB-C Hub
.product-price $39.99
a.btn.btn-sm(href="/products/4") View Details

View File

@@ -1,79 +0,0 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.page-header
.container
h1 All Products
p Browse our selection of quality products
section.section
.container
.products-toolbar
span.results-count #{productCount} products
.sort-options
label Sort by:
select
option(value="featured") Featured
option(value="price-low") Price: Low to High
option(value="price-high") Price: High to Low
.product-grid
.product-card
.product-image
.product-badge Sale
.product-info
span.product-category Electronics
h3.product-name Wireless Headphones
.product-price $79.99
a.btn.btn-sm(href="/products/1") View Details
.product-card
.product-image
.product-info
span.product-category Electronics
h3.product-name Smart Watch Pro
.product-price $199.99
a.btn.btn-sm(href="/products/2") View Details
.product-card
.product-image
.product-info
span.product-category Accessories
h3.product-name Laptop Stand
.product-price $49.99
a.btn.btn-sm(href="/products/3") View Details
.product-card
.product-image
.product-badge Sale
.product-info
span.product-category Accessories
h3.product-name USB-C Hub
.product-price $39.99
a.btn.btn-sm(href="/products/4") View Details
.product-card
.product-image
.product-info
span.product-category Electronics
h3.product-name Mechanical Keyboard
.product-price $129.99
a.btn.btn-sm(href="/products/5") View Details
.product-card
.product-image
.product-info
span.product-category Home Office
h3.product-name Desk Lamp
.product-price $34.99
a.btn.btn-sm(href="/products/6") View Details
.pagination
a.page-link(href="#") Prev
a.page-link.active(href="#") 1
a.page-link(href="#") 2
a.page-link(href="#") 3
a.page-link(href="#") Next

View File

@@ -1,4 +0,0 @@
footer.footer
.container
.footer-content
p Built with Pugz - A Pug template engine for Zig

View File

@@ -1,3 +0,0 @@
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="stylesheet" href="/css/style.css")

View File

@@ -1,11 +0,0 @@
header.header
.container
.header-content
a.logo(href="/") Pugz Store
nav.nav
a.nav-link(href="/") Home
a.nav-link(href="/products") Products
a.nav-link(href="/about") About
.header-actions
a.cart-link(href="/cart")
| Cart (#{cartCount})

View File

@@ -2525,9 +2525,8 @@ pub const Lexer = struct {
pub fn getTokens(self: *Lexer) ![]Token {
while (!self.ended) {
const advanced = self.advance();
// Check for errors after every advance, regardless of return value
if (self.last_error) |err| {
std.debug.print("Lexer error at {d}:{d}: {s}\n", .{ err.line, err.column, err.message });
// Check for errors after every advance
if (self.last_error != null) {
return error.LexerError;
}
if (!advanced) {

View File

@@ -1,66 +0,0 @@
// benchmark.zig - Benchmark for pugz (Zig Pug implementation)
//
// This benchmark matches the JavaScript pug benchmark for comparison
// Uses exact same templates as packages/pug/support/benchmark.js
const std = @import("std");
const pug = @import("../pug.zig");
const MIN_ITERATIONS: usize = 200;
const MIN_TIME_NS: u64 = 200_000_000; // 200ms minimum
fn benchmark(comptime name: []const u8, template: []const u8, iterations: *usize, elapsed_ns: *u64) !void {
const allocator = std.heap.page_allocator;
// Warmup
for (0..10) |_| {
var result = try pug.compile(allocator, template, .{});
result.deinit(allocator);
}
var timer = try std.time.Timer.start();
var count: usize = 0;
while (count < MIN_ITERATIONS or timer.read() < MIN_TIME_NS) {
var result = try pug.compile(allocator, template, .{});
result.deinit(allocator);
count += 1;
}
const elapsed = timer.read();
iterations.* = count;
elapsed_ns.* = elapsed;
const ops_per_sec = @as(f64, @floatFromInt(count)) * 1_000_000_000.0 / @as(f64, @floatFromInt(elapsed));
std.debug.print("{s}: {d:.0}\n", .{ name, ops_per_sec });
}
pub fn main() !void {
var iterations: usize = 0;
var elapsed_ns: u64 = 0;
// Tiny template - exact match to JS: 'html\n body\n h1 Title'
const tiny = "html\n body\n h1 Title";
try benchmark("tiny", tiny, &iterations, &elapsed_ns);
// Small template - exact match to JS (note trailing \n on each line)
const small =
"html\n" ++
" body\n" ++
" h1 Title\n" ++
" ul#menu\n" ++
" li: a(href=\"#\") Home\n" ++
" li: a(href=\"#\") About Us\n" ++
" li: a(href=\"#\") Store\n" ++
" li: a(href=\"#\") FAQ\n" ++
" li: a(href=\"#\") Contact\n";
try benchmark("small", small, &iterations, &elapsed_ns);
// Medium template - Array(30).join(str) creates 29 copies in JS
const medium = small ** 29;
try benchmark("medium", medium, &iterations, &elapsed_ns);
// Large template - Array(100).join(str) creates 99 copies in JS
const large = small ** 99;
try benchmark("large", large, &iterations, &elapsed_ns);
}

View File

@@ -1,274 +0,0 @@
// benchmark_examples.zig - Benchmark pug example files
//
// Tests the same example files as the JS benchmark
const std = @import("std");
const pug = @import("../pug.zig");
const Example = struct {
name: []const u8,
source: []const u8,
};
// Example templates (matching JS pug examples that don't use includes/extends)
const examples = [_]Example{
.{
.name = "attributes.pug",
.source =
\\div#id.left.container(class='user user-' + name)
\\ h1.title= name
\\ form
\\ //- unbuffered comment :)
\\ // An example of attributes.
\\ input(type='text' name='user[name]' value=name)
\\ input(checked, type='checkbox', name='user[blocked]')
\\ input(type='submit', value='Update')
,
},
.{
.name = "code.pug",
.source =
\\- var title = "Things"
\\
\\-
\\ var subtitle = ["Really", "long",
\\ "list", "of",
\\ "words"]
\\h1= title
\\h2= subtitle.join(" ")
\\
\\ul#users
\\ each user, name in users
\\ // expands to if (user.isA == 'ferret')
\\ if user.isA == 'ferret'
\\ li(class='user-' + name) #{name} is just a ferret
\\ else
\\ li(class='user-' + name) #{name} #{user.email}
,
},
.{
.name = "dynamicscript.pug",
.source =
\\html
\\ head
\\ title Dynamic Inline JavaScript
\\ script.
\\ var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")}
,
},
.{
.name = "each.pug",
.source =
\\ul#users
\\ each user, name in users
\\ li(class='user-' + name) #{name} #{user.email}
,
},
.{
.name = "extend-layout.pug",
.source =
\\html
\\ head
\\ h1 My Site - #{title}
\\ block scripts
\\ script(src='/jquery.js')
\\ body
\\ block content
\\ block foot
\\ #footer
\\ p some footer content
,
},
.{
.name = "form.pug",
.source =
\\form(method="post")
\\ fieldset
\\ legend General
\\ p
\\ label(for="user[name]") Username:
\\ input(type="text", name="user[name]", value=user.name)
\\ p
\\ label(for="user[email]") Email:
\\ input(type="text", name="user[email]", value=user.email)
\\ .tip.
\\ Enter a valid
\\ email address
\\ such as <em>tj@vision-media.ca</em>.
\\ fieldset
\\ legend Location
\\ p
\\ label(for="user[city]") City:
\\ input(type="text", name="user[city]", value=user.city)
\\ p
\\ select(name="user[province]")
\\ option(value="") -- Select Province --
\\ option(value="AB") Alberta
\\ option(value="BC") British Columbia
\\ option(value="SK") Saskatchewan
\\ option(value="MB") Manitoba
\\ option(value="ON") Ontario
\\ option(value="QC") Quebec
\\ p.buttons
\\ input(type="submit", value="Save")
,
},
.{
.name = "layout.pug",
.source =
\\doctype html
\\html(lang="en")
\\ head
\\ title Example
\\ script.
\\ if (foo) {
\\ bar();
\\ }
\\ body
\\ h1 Pug - node template engine
\\ #container
,
},
.{
.name = "pet.pug",
.source =
\\.pet
\\ h2= pet.name
\\ p #{pet.name} is <em>#{pet.age}</em> year(s) old.
,
},
.{
.name = "rss.pug",
.source =
\\doctype xml
\\rss(version='2.0')
\\channel
\\ title RSS Title
\\ description Some description here
\\ link http://google.com
\\ lastBuildDate Mon, 06 Sep 2010 00:01:00 +0000
\\ pubDate Mon, 06 Sep 2009 16:45:00 +0000
\\
\\ each item in items
\\ item
\\ title= item.title
\\ description= item.description
\\ link= item.link
,
},
.{
.name = "text.pug",
.source =
\\| An example of an
\\a(href='#') inline
\\| link.
\\
\\form
\\ label Username:
\\ input(type='text', name='user[name]')
\\ p
\\ | Just an example of some text usage.
\\ | You can have <em>inline</em> html,
\\ | as well as
\\ strong tags
\\ | .
\\
\\ | Interpolation is also supported. The
\\ | username is currently "#{name}".
\\
\\ label Email:
\\ input(type='text', name='user[email]')
\\ p
\\ | Email is currently
\\ em= email
\\ | .
,
},
.{
.name = "whitespace.pug",
.source =
\\- var js = '<script></script>'
\\doctype html
\\html
\\
\\ head
\\ title= "Some " + "JavaScript"
\\ != js
\\
\\
\\
\\ body
,
},
};
pub fn main() !void {
const allocator = std.heap.page_allocator;
std.debug.print("=== Zig Pugz Example Benchmark ===\n\n", .{});
var passed: usize = 0;
var failed: usize = 0;
var total_time_ns: u64 = 0;
var html_outputs: [examples.len]?[]const u8 = undefined;
for (&html_outputs) |*h| h.* = null;
for (examples, 0..) |example, idx| {
const iterations: usize = 100;
var success = false;
var time_ns: u64 = 0;
// Warmup
for (0..5) |_| {
var result = pug.compile(allocator, example.source, .{}) catch continue;
result.deinit(allocator);
}
// Benchmark
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
var result = pug.compile(allocator, example.source, .{}) catch break;
if (i == iterations - 1) {
// Keep last HTML for output
html_outputs[idx] = result.html;
} else {
result.deinit(allocator);
}
success = true;
}
time_ns = timer.read();
if (success and i == iterations) {
const time_ms = @as(f64, @floatFromInt(time_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations));
std.debug.print("{s}: OK ({d:.3} ms)\n", .{ example.name, time_ms });
passed += 1;
total_time_ns += time_ns;
} else {
std.debug.print("{s}: FAILED\n", .{example.name});
failed += 1;
}
}
std.debug.print("\n=== Summary ===\n", .{});
std.debug.print("Passed: {d}/{d}\n", .{ passed, examples.len });
std.debug.print("Failed: {d}/{d}\n", .{ failed, examples.len });
if (passed > 0) {
const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0;
std.debug.print("Total time (successful): {d:.3} ms\n", .{total_ms});
std.debug.print("Average time: {d:.3} ms\n", .{total_ms / @as(f64, @floatFromInt(passed))});
}
// Output HTML for comparison
std.debug.print("\n=== HTML Output ===\n", .{});
for (examples, 0..) |example, idx| {
if (html_outputs[idx]) |html| {
std.debug.print("\n--- {s} ---\n", .{example.name});
const max_len = @min(html.len, 500);
std.debug.print("{s}", .{html[0..max_len]});
if (html.len > 500) std.debug.print("...", .{});
std.debug.print("\n", .{});
}
}
}

View File

@@ -1,8 +0,0 @@
div#id.left.container(class='user user-' + name)
h1.title= name
form
//- unbuffered comment :)
// An example of attributes.
input(type='text' name='user[name]' value=name)
input(checked, type='checkbox', name='user[blocked]')
input(type='submit', value='Update')

View File

@@ -1,17 +0,0 @@
- var title = "Things"
-
var subtitle = ["Really", "long",
"list", "of",
"words"]
h1= title
h2= subtitle.join(" ")
ul#users
each user, name in users
// expands to if (user.isA == 'ferret')
if user.isA == 'ferret'
li(class='user-' + name) #{name} is just a ferret
else
li(class='user-' + name) #{name} #{user.email}

View File

@@ -1,5 +0,0 @@
html
head
title Dynamic Inline JavaScript
script.
var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")}

View File

@@ -1,3 +0,0 @@
ul#users
each user, name in users
li(class='user-' + name) #{name} #{user.email}

View File

@@ -1,10 +0,0 @@
html
head
h1 My Site - #{title}
block scripts
script(src='/jquery.js')
body
block content
block foot
#footer
p some footer content

View File

@@ -1,11 +0,0 @@
extends extend-layout.pug
block scripts
script(src='/jquery.js')
script(src='/pets.js')
block content
h1= title
each pet in pets
include pet.pug

View File

@@ -1,29 +0,0 @@
form(method="post")
fieldset
legend General
p
label(for="user[name]") Username:
input(type="text", name="user[name]", value=user.name)
p
label(for="user[email]") Email:
input(type="text", name="user[email]", value=user.email)
.tip.
Enter a valid
email address
such as <em>tj@vision-media.ca</em>.
fieldset
legend Location
p
label(for="user[city]") City:
input(type="text", name="user[city]", value=user.city)
p
select(name="user[province]")
option(value="") -- Select Province --
option(value="AB") Alberta
option(value="BC") British Columbia
option(value="SK") Saskatchewan
option(value="MB") Manitoba
option(value="ON") Ontario
option(value="QC") Quebec
p.buttons
input(type="submit", value="Save")

View File

@@ -1,7 +0,0 @@
html
include includes/head.pug
body
h1 My Site
p Welcome to my super lame site.
include includes/foot.pug

View File

@@ -1,14 +0,0 @@
doctype html
html(lang="en")
head
title Example
script.
if (foo) {
bar();
}
body
h1 Pug - node template engine
#container
:markdown-it
Pug is a _high performance_ template engine for [node](http://nodejs.org),
inspired by [haml](http://haml-lang.com/), and written by [TJ Holowaychuk](http://github.com/visionmedia).

View File

@@ -1,14 +0,0 @@
include mixins/dialog.pug
include mixins/profile.pug
.one
+dialog
.two
+dialog-title('Whoop')
.three
+dialog-title-desc('Whoop', 'Just a mixin')
#profile
+profile(user)

View File

@@ -1,3 +0,0 @@
.pet
h2= pet.name
p #{pet.name} is <em>#{pet.age}</em> year(s) old.

View File

@@ -1,14 +0,0 @@
doctype xml
rss(version='2.0')
channel
title RSS Title
description Some description here
link http://google.com
lastBuildDate Mon, 06 Sep 2010 00:01:00 +0000
pubDate Mon, 06 Sep 2009 16:45:00 +0000
each item in items
item
title= item.title
description= item.description
link= item.link

View File

@@ -1,36 +0,0 @@
| An example of an
a(href='#') inline
| link.
form
label Username:
input(type='text', name='user[name]')
p
| Just an example of some text usage.
| You can have <em>inline</em> html,
| as well as
strong tags
| .
| Interpolation is also supported. The
| username is currently "#{name}".
label Email:
input(type='text', name='user[email]')
p
| Email is currently
em= email
| .
// alternatively, if we plan on having only
// text or inline-html, we can use a trailing
// "." to let pug know we want to omit pipes
label Username:
input(type='text')
p.
Just an example, like before
however now we can omit those
annoying pipes!.
Wahoo.

View File

@@ -1,11 +0,0 @@
- var js = '<script></script>'
doctype html
html
head
title= "Some " + "JavaScript"
!= js
body

View File

@@ -1,70 +0,0 @@
/**
* JS Pug - Process all .pug files in playground folder
*/
const fs = require('fs');
const path = require('path');
const pug = require('../../pug');
const dir = path.join(__dirname, 'examples');
// Get all .pug files
const pugFiles = fs.readdirSync(dir)
.filter(f => f.endsWith('.pug'))
.sort();
console.log('=== JS Pug Playground ===\n');
console.log(`Found ${pugFiles.length} .pug files\n`);
let passed = 0;
let failed = 0;
let totalTimeMs = 0;
for (const file of pugFiles) {
const filePath = path.join(dir, file);
const source = fs.readFileSync(filePath, 'utf8');
const iterations = 100;
let success = false;
let html = '';
let error = '';
let timeMs = 0;
try {
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
html = pug.render(source, {
filename: filePath,
basedir: dir
});
}
const end = process.hrtime.bigint();
timeMs = Number(end - start) / 1_000_000 / iterations;
success = true;
passed++;
totalTimeMs += timeMs;
} catch (e) {
error = e.message.split('\n')[0];
failed++;
}
if (success) {
console.log(`${file} (${timeMs.toFixed(3)} ms)`);
// Show first 200 chars of output
const preview = html.replace(/\s+/g, ' ').substring(0, 200);
console.log(`${preview}${html.length > 200 ? '...' : ''}\n`);
} else {
console.log(`${file}`);
console.log(`${error}\n`);
}
}
console.log('=== Summary ===');
console.log(`Passed: ${passed}/${pugFiles.length}`);
console.log(`Failed: ${failed}/${pugFiles.length}`);
if (passed > 0) {
console.log(`Total time: ${totalTimeMs.toFixed(3)} ms`);
console.log(`Average: ${(totalTimeMs / passed).toFixed(3)} ms per file`);
}

View File

@@ -1,120 +0,0 @@
// Zig Pugz - Process all .pug files in playground/examples folder
const std = @import("std");
const pug = @import("../pug.zig");
const fs = std.fs;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
std.debug.print("=== Zig Pugz Playground ===\n\n", .{});
// Open the examples directory relative to cwd
var dir = fs.cwd().openDir("playground/examples", .{ .iterate = true }) catch |err| {
// Try from playground directory
dir = fs.cwd().openDir("examples", .{ .iterate = true }) catch {
std.debug.print("Error opening examples directory: {}\n", .{err});
return;
};
};
defer dir.close();
// Collect .pug files
var files = std.ArrayList([]const u8).init(allocator);
defer {
for (files.items) |f| allocator.free(f);
files.deinit();
}
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".pug")) {
const name = try allocator.dupe(u8, entry.name);
try files.append(name);
}
}
// Sort files
std.mem.sort([]const u8, files.items, {}, struct {
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
return std.mem.lessThan(u8, a, b);
}
}.lessThan);
std.debug.print("Found {d} .pug files\n\n", .{files.items.len});
var passed: usize = 0;
var failed: usize = 0;
var total_time_ns: u64 = 0;
for (files.items) |filename| {
// Read file
const file = dir.openFile(filename, .{}) catch {
std.debug.print("✗ {s}\n → Could not open file\n\n", .{filename});
failed += 1;
continue;
};
defer file.close();
const source = file.readToEndAlloc(allocator, 1024 * 1024) catch {
std.debug.print("✗ {s}\n → Could not read file\n\n", .{filename});
failed += 1;
continue;
};
defer allocator.free(source);
// Benchmark
const iterations: usize = 100;
var success = false;
var last_html: ?[]const u8 = null;
// Warmup
for (0..5) |_| {
var result = pug.compile(allocator, source, .{}) catch continue;
result.deinit(allocator);
}
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
var result = pug.compile(allocator, source, .{}) catch break;
if (i == iterations - 1) {
last_html = result.html;
} else {
result.deinit(allocator);
}
success = true;
}
const elapsed_ns = timer.read();
if (success and i == iterations) {
const time_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations));
std.debug.print("✓ {s} ({d:.3} ms)\n", .{ filename, time_ms });
// Show preview
if (last_html) |html| {
const max_len = @min(html.len, 200);
std.debug.print(" → {s}{s}\n\n", .{ html[0..max_len], if (html.len > 200) "..." else "" });
allocator.free(html);
}
passed += 1;
total_time_ns += elapsed_ns;
} else {
std.debug.print("✗ {s}\n → Compilation failed\n\n", .{filename});
failed += 1;
}
}
std.debug.print("=== Summary ===\n", .{});
std.debug.print("Passed: {d}/{d}\n", .{ passed, files.items.len });
std.debug.print("Failed: {d}/{d}\n", .{ failed, files.items.len });
if (passed > 0) {
const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0;
std.debug.print("Total time: {d:.3} ms\n", .{total_ms});
std.debug.print("Average: {d:.3} ms per file\n", .{total_ms / @as(f64, @floatFromInt(passed))});
}
}

View File

@@ -13,6 +13,8 @@ const runtime = @import("runtime.zig");
const mixin_mod = @import("mixin.zig");
pub const MixinRegistry = mixin_mod.MixinRegistry;
const log = std.log.scoped(.pugz);
pub const TemplateError = error{
OutOfMemory,
LexerError,
@@ -146,7 +148,12 @@ pub fn parseWithSource(allocator: Allocator, source: []const u8) !ParseResult {
var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory;
errdefer lex.deinit();
const tokens = lex.getTokens() catch return error.LexerError;
const tokens = lex.getTokens() catch {
if (lex.last_error) |err| {
log.err("{s} at line {d}, column {d}: {s}", .{ @tagName(err.code), err.line, err.column, err.message });
}
return error.LexerError;
};
// Strip comments
var stripped = pug.strip_comments.stripComments(allocator, tokens, .{}) catch return error.OutOfMemory;
@@ -157,6 +164,9 @@ pub fn parseWithSource(allocator: Allocator, source: []const u8) !ParseResult {
defer pug_parser.deinit();
const ast = pug_parser.parse() catch {
if (pug_parser.getError()) |err| {
log.err("{s} at line {d}, column {d}: {s}", .{ @tagName(err.code), err.line, err.column, err.message });
}
return error.ParserError;
};

View File

@@ -1,6 +0,0 @@
<foo data-user="{&quot;name&quot;:&quot;tobi&quot;}"></foo>
<foo data-items="[1,2,3]"></foo>
<foo data-username="tobi"></foo>
<foo data-escaped="{&quot;message&quot;:&quot;Let's rock!&quot;}"></foo>
<foo data-ampersand="{&quot;message&quot;:&quot;a quote: &amp;quot; this &amp; that&quot;}"></foo>
<foo data-epoc="1970-01-01T00:00:00.000Z"></foo>

View File

@@ -1,7 +0,0 @@
- var user = { name: 'tobi' }
foo(data-user=user)
foo(data-items=[1,2,3])
foo(data-username='tobi')
foo(data-escaped={message: "Let's rock!"})
foo(data-ampersand={message: "a quote: &quot; this & that"})
foo(data-epoc=new Date(0))

View File

@@ -1,4 +0,0 @@
<div :my-var="model"></div>
<span v-for="item in items" :key="item.id" :value="item.name"></span>
<span v-for="item in items" :key="item.id" :value="item.name"></span>
<a :link="goHere" value="static" :my-value="dynamic" @click="onClick()" :another="more">Click Me!</a>

View File

@@ -1,9 +0,0 @@
//- Tests for using a colon-prefexed attribute (typical when using short-cut for Vue.js `v-bind`)
div(:my-var="model")
span(v-for="item in items" :key="item.id" :value="item.name")
span(
v-for="item in items"
:key="item.id"
:value="item.name"
)
a(:link="goHere" value="static" :my-value="dynamic" @click="onClick()" :another="more") Click Me!

View File

@@ -1,20 +0,0 @@
<a href="/contact">contact</a><a class="button" href="/save">save</a><a foo="foo" bar="bar" baz="baz"></a><a foo="foo, bar, baz" bar="1"></a><a foo="((foo))" bar="1"></a>
<select>
<option value="foo" selected="selected">Foo</option>
<option selected="selected" value="bar">Bar</option>
</select><a foo="class:"></a>
<input pattern="\S+"/><a href="/contact">contact</a><a class="button" href="/save">save</a><a foo="foo" bar="bar" baz="baz"></a><a foo="foo, bar, baz" bar="1"></a><a foo="((foo))" bar="1"></a>
<select>
<option value="foo" selected="selected">Foo</option>
<option selected="selected" value="bar">Bar</option>
</select><a foo="class:"></a>
<input pattern="\S+"/>
<foo terse="true"></foo>
<foo date="1970-01-01T00:00:00.000Z"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<foo abc="abc" def="def"></foo>
<div foo="bar" bar="<baz>"></div><a foo="foo" bar="bar"></a><a foo="foo" bar="bar"></a>

View File

@@ -1,5 +0,0 @@
<a class="button" href="/user/5"></a><a class="button" href="/user/5"></a>
<meta key="answer" value="42"/><a class="class1 class2"></a><a class="tag-class class1 class2"></a><a class="button" href="/user/5"></a><a class="button" href="/user/5"></a>
<meta key="answer" value="42"/><a class="class1 class2"></a><a class="tag-class class1 class2"></a>
<div id="5" foo="bar"></div>
<div baz="baz"></div>

View File

@@ -1,17 +0,0 @@
- var id = 5
- function answer() { return 42; }
a(href='/user/' + id, class='button')
a(href = '/user/' + id, class = 'button')
meta(key='answer', value=answer())
a(class = ['class1', 'class2'])
a.tag-class(class = ['class1', 'class2'])
a(href='/user/' + id class='button')
a(href = '/user/' + id class = 'button')
meta(key='answer' value=answer())
a(class = ['class1', 'class2'])
a.tag-class(class = ['class1', 'class2'])
div(id=id)&attributes({foo: 'bar'})
- var bar = null
div(foo=null bar=bar)&attributes({baz: 'baz'})

View File

@@ -1,43 +0,0 @@
a(href='/contact') contact
a(href='/save').button save
a(foo, bar, baz)
a(foo='foo, bar, baz', bar=1)
a(foo='((foo))', bar= (1) ? 1 : 0 )
select
option(value='foo', selected) Foo
option(selected, value='bar') Bar
a(foo="class:")
input(pattern='\\S+')
a(href='/contact') contact
a(href='/save').button save
a(foo bar baz)
a(foo='foo, bar, baz' bar=1)
a(foo='((foo))' bar= (1) ? 1 : 0 )
select
option(value='foo' selected) Foo
option(selected value='bar') Bar
a(foo="class:")
input(pattern='\\S+')
foo(terse="true")
foo(date=new Date(0))
foo(abc
,def)
foo(abc,
def)
foo(abc,
def)
foo(abc
,def)
foo(abc
def)
foo(abc
def)
- var attrs = {foo: 'bar', bar: '<baz>'}
div&attributes(attrs)
a(foo='foo' "bar"="bar")
a(foo='foo' 'bar'='bar')

View File

@@ -1,5 +0,0 @@
<script type="text/x-template">
<div id="user-<%= user.id %>">
<h1><%= user.title %></h1>
</div>
</script>

View File

@@ -1,3 +0,0 @@
script(type='text/x-template')
div(id!='user-<%= user.id %>')
h1 <%= user.title %>

View File

@@ -1 +0,0 @@
block content

View File

@@ -1,4 +0,0 @@
mixin test()
.test&attributes(attributes)
+test()

View File

@@ -1,8 +0,0 @@
doctype html
html
head
title Default title
body
block body
.container
block content

View File

@@ -1,6 +0,0 @@
extends window.pug
block window-content
.dialog
block content

View File

@@ -1,2 +0,0 @@
block test

View File

@@ -1,3 +0,0 @@
<script>
console.log("foo\nbar")
</script>

View File

@@ -1,5 +0,0 @@
extends empty-block.pug
block test
div test1

View File

@@ -1,5 +0,0 @@
extends empty-block.pug
block test
div test2

View File

@@ -1,4 +0,0 @@
extends /auxiliary/layout.pug
block content
include /auxiliary/include-from-root.pug

View File

@@ -1,4 +0,0 @@
extends ../../cases/auxiliary/layout
block content
include ../../cases/auxiliary/include-from-root

View File

@@ -1,8 +0,0 @@
html
head
style(type="text/css")
:less
@pad: 15px;
body {
padding: @pad;
}

View File

@@ -1,8 +0,0 @@
var STRING_SUBSTITUTIONS = {
// table of character substitutions
'\t': '\\t',
'\r': '\\r',
'\n': '\\n',
'"': '\\"',
'\\': '\\\\',
};

View File

@@ -1 +0,0 @@
h1 hello

View File

@@ -1,11 +0,0 @@
mixin article()
article
block
html
head
title My Application
block head
body
+article
block content

View File

@@ -1,2 +0,0 @@
h1 grand-grandparent
block grand-grandparent

View File

@@ -1,6 +0,0 @@
extends inheritance.extend.recursive-grand-grandparent.pug
block grand-grandparent
h2 grandparent
block grandparent

View File

@@ -1,5 +0,0 @@
extends inheritance.extend.recursive-grandparent.pug
block grandparent
h3 parent
block parent

View File

@@ -1,7 +0,0 @@
html
head
title My Application
block head
body
block content
include window.pug

View File

@@ -1,6 +0,0 @@
html
head
title My Application
block head
body
block content

View File

@@ -1,3 +0,0 @@
mixin slide
section.slide
block

View File

@@ -1,3 +0,0 @@
mixin foo()
p bar

View File

@@ -1,3 +0,0 @@
.pet
h1 {{name}}
p {{name}} is a {{species}} that is {{age}} old

View File

@@ -1 +0,0 @@
<p>:)</p>

View File

@@ -1,4 +0,0 @@
.window
a(href='#').close Close
block window-content

View File

@@ -1,10 +0,0 @@
html
head
title
body
h1 Page
#content
#content-wrapper
yield
#footer
stuff

View File

@@ -1,5 +0,0 @@
<html>
<body>
<h1>Title</h1>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More