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

66
playground/benchmark.zig Normal file
View File

@@ -0,0 +1,66 @@
// 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

@@ -0,0 +1,274 @@
// 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

@@ -0,0 +1,8 @@
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

@@ -0,0 +1,17 @@
- 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

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,29 @@
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

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

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,14 @@
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

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

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,36 @@
| 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

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

70
playground/run_js.js Normal file
View File

@@ -0,0 +1,70 @@
/**
* 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`);
}

120
playground/run_zig.zig Normal file
View File

@@ -0,0 +1,120 @@
// 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))});
}
}