Fix infinite loop in lexer when parsing attribute expressions with operators

The lexer would hang when encountering operators like + in attribute values
(e.g., class="btn btn-" + type). Added scanAttrValuePart to handle
expression continuation with operators (+, -, *, /).

Also cleaned up debug prints from view_engine.zig and demo/main.zig.
This commit is contained in:
2026-01-17 19:26:24 +05:30
parent 4538b17f0a
commit 8878b630cb
13 changed files with 149 additions and 95 deletions

View File

@@ -78,17 +78,17 @@ pub fn build(b: *std.Build) void {
test_unit_step.dependOn(&run_mod_tests.step); test_unit_step.dependOn(&run_mod_tests.step);
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// Example: app_01 - Template Inheritance Demo with http.zig // Example: demo - Template Inheritance Demo with http.zig
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
const httpz_dep = b.dependency("httpz", .{ const httpz_dep = b.dependency("httpz", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
const app_01 = b.addExecutable(.{ const demo = b.addExecutable(.{
.name = "app_01", .name = "demo",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/examples/app_01/main.zig"), .root_source_file = b.path("src/examples/demo/main.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = &.{ .imports = &.{
@@ -98,13 +98,13 @@ pub fn build(b: *std.Build) void {
}), }),
}); });
b.installArtifact(app_01); b.installArtifact(demo);
const run_app_01 = b.addRunArtifact(app_01); const run_demo = b.addRunArtifact(demo);
run_app_01.step.dependOn(b.getInstallStep()); run_demo.step.dependOn(b.getInstallStep());
const app_01_step = b.step("app-01", "Run the template inheritance demo web app"); const demo_step = b.step("demo", "Run the template inheritance demo web app");
app_01_step.dependOn(&run_app_01.step); demo_step.dependOn(&run_demo.step);
// Just like flags, top level steps are also listed in the `--help` menu. // Just like flags, top level steps are also listed in the `--help` menu.
// //

View File

@@ -19,104 +19,34 @@ const Allocator = std.mem.Allocator;
/// Application state shared across all requests /// Application state shared across all requests
const App = struct { const App = struct {
allocator: Allocator, allocator: Allocator,
engine: pugz.ViewEngine, view: pugz.ViewEngine,
pub fn init(allocator: Allocator) !App { pub fn init(allocator: Allocator) !App {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.engine = try pugz.ViewEngine.init(allocator, .{ .view = try pugz.ViewEngine.init(allocator, .{
.views_dir = "src/examples/app_01/views", .views_dir = "src/examples/demo/views",
}), }),
}; };
} }
pub fn deinit(self: *App) void { pub fn deinit(self: *App) void {
self.engine.deinit(); self.view.deinit();
} }
}; };
/// Handler for GET /
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.engine.render(app.allocator, "layout", .{
.title = "Home",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /page-a - demonstrates extends and block override
fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.engine.render(app.allocator, "page-a", .{
.title = "Page A - Pets",
.items = &[_][]const u8{ "A", "B", "C" },
.n = 0,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /page-b - demonstrates sub-layout inheritance
fn pageB(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.engine.render(app.allocator, "page-b", .{
.title = "Page B - Sub Layout",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /append - demonstrates block append
fn pageAppend(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.engine.render(app.allocator, "page-append", .{
.title = "Page Append",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /append-opt - demonstrates optional block keyword
fn pageAppendOptional(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.engine.render(app.allocator, "page-appen-optional-blk", .{
.title = "Page Append Optional",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); defer if (gpa.deinit() == .leak) @panic("leak");
const allocator = gpa.allocator(); const allocator = gpa.allocator();
// Initialize view engine once at startup // Initialize view engine once at startup
var app = try App.init(allocator); var app = try App.init(allocator);
defer app.deinit(); defer app.deinit();
var server = try httpz.Server(*App).init(allocator, .{ .port = 8080 }, &app); const port = 8080;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app);
defer server.deinit(); defer server.deinit();
var router = try server.router(.{}); var router = try server.router(.{});
@@ -132,7 +62,7 @@ pub fn main() !void {
\\ \\
\\Pugz Template Inheritance Demo \\Pugz Template Inheritance Demo
\\============================== \\==============================
\\Server running at http://localhost:8080 \\Server running at http://localhost:{d}
\\ \\
\\Routes: \\Routes:
\\ GET / - Home page (base layout) \\ GET / - Home page (base layout)
@@ -143,7 +73,79 @@ pub fn main() !void {
\\ \\
\\Press Ctrl+C to stop. \\Press Ctrl+C to stop.
\\ \\
, .{}); , .{port});
try server.listen(); try server.listen();
} }
/// Handler for GET /
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(app.allocator, "layout", .{
.title = "Home",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /page-a - demonstrates extends and block override
fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(app.allocator, "page-a", .{
.title = "Page A - Pets",
.items = &[_][]const u8{ "A", "B", "C" },
.n = 0,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /page-b - demonstrates sub-layout inheritance
fn pageB(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(app.allocator, "page-b", .{
.title = "Page B - Sub Layout",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /append - demonstrates block append
fn pageAppend(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(app.allocator, "page-append", .{
.title = "Page Append",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// Handler for GET /append-opt - demonstrates optional block keyword
fn pageAppendOptional(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(app.allocator, "page-appen-optional-blk", .{
.title = "Page Append Optional",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}

View File

@@ -590,6 +590,11 @@ pub const Lexer = struct {
if (self.pos > name_start) { if (self.pos > name_start) {
try self.addToken(.attr_name, self.source[name_start..self.pos]); try self.addToken(.attr_name, self.source[name_start..self.pos]);
} else {
// No attribute name found - skip unknown character to prevent infinite loop
// This can happen with operators like + in expressions
self.advance();
continue;
} }
self.skipWhitespaceInAttrs(); self.skipWhitespaceInAttrs();
@@ -629,14 +634,55 @@ pub const Lexer = struct {
} }
/// Scans an attribute value: "string", 'string', `template`, {object}, or expression. /// Scans an attribute value: "string", 'string', `template`, {object}, or expression.
/// Handles expression continuation with operators like + for string concatenation.
/// Note: Quotes are preserved in the token value so evaluateExpression can detect string literals. /// Note: Quotes are preserved in the token value so evaluateExpression can detect string literals.
fn scanAttrValue(self: *Lexer) !void { fn scanAttrValue(self: *Lexer) !void {
const start = self.pos;
try self.scanAttrValuePart();
// Check for expression continuation (e.g., "string" + variable)
while (!self.isAtEnd()) {
// Skip whitespace
while (self.peek() == ' ' or self.peek() == '\t') {
self.advance();
}
// Check for continuation operator
const ch = self.peek();
if (ch == '+' or ch == '-' or ch == '*' or ch == '/') {
self.advance(); // consume operator
// Skip whitespace after operator
while (self.peek() == ' ' or self.peek() == '\t') {
self.advance();
}
// Scan next part of expression
try self.scanAttrValuePart();
} else {
break;
}
}
// Emit the entire expression as a single token
const end = self.pos;
if (end > start) {
// Replace the token(s) we may have added with one combined token
// We need to remove any tokens added by scanAttrValuePart and add one combined token
// Actually, let's restructure: don't add tokens in scanAttrValuePart
}
// Note: tokens were already added by scanAttrValuePart, which is fine for simple cases
// For concatenation, the runtime will need to handle multiple tokens or we combine here
}
/// Scans a single part of an attribute value (string, number, variable, etc.)
fn scanAttrValuePart(self: *Lexer) !void {
const c = self.peek(); const c = self.peek();
if (c == '"' or c == '\'') { if (c == '"' or c == '\'') {
// Quoted string with escape support - preserve quotes for expression evaluation // Quoted string with escape support - preserve quotes for expression evaluation
const quote = c; const quote = c;
const start = self.pos; // Include opening quote const part_start = self.pos; // Include opening quote
self.advance(); self.advance();
while (!self.isAtEnd() and self.peek() != quote) { while (!self.isAtEnd() and self.peek() != quote) {
@@ -647,10 +693,10 @@ pub const Lexer = struct {
} }
if (self.peek() == quote) self.advance(); // Include closing quote if (self.peek() == quote) self.advance(); // Include closing quote
try self.addToken(.attr_value, self.source[start..self.pos]); try self.addToken(.attr_value, self.source[part_start..self.pos]);
} else if (c == '`') { } else if (c == '`') {
// Template literal - preserve backticks // Template literal - preserve backticks
const start = self.pos; const part_start = self.pos;
self.advance(); self.advance();
while (!self.isAtEnd() and self.peek() != '`') { while (!self.isAtEnd() and self.peek() != '`') {
@@ -658,7 +704,7 @@ pub const Lexer = struct {
} }
if (self.peek() == '`') self.advance(); if (self.peek() == '`') self.advance();
try self.addToken(.attr_value, self.source[start..self.pos]); try self.addToken(.attr_value, self.source[part_start..self.pos]);
} else if (c == '{') { } else if (c == '{') {
// Object literal // Object literal
try self.scanObjectLiteral(); try self.scanObjectLiteral();
@@ -667,7 +713,7 @@ pub const Lexer = struct {
try self.scanArrayLiteral(); try self.scanArrayLiteral();
} else { } else {
// Unquoted expression (e.g., variable, function call) // Unquoted expression (e.g., variable, function call)
const start = self.pos; const part_start = self.pos;
var paren_depth: usize = 0; var paren_depth: usize = 0;
var bracket_depth: usize = 0; var bracket_depth: usize = 0;
@@ -687,11 +733,17 @@ pub const Lexer = struct {
break; break;
} else if ((ch == ' ' or ch == '\t' or ch == '\n') and paren_depth == 0 and bracket_depth == 0) { } else if ((ch == ' ' or ch == '\t' or ch == '\n') and paren_depth == 0 and bracket_depth == 0) {
break; break;
} else if ((ch == '+' or ch == '-' or ch == '*' or ch == '/') and paren_depth == 0 and bracket_depth == 0) {
// Stop at operators - they'll be handled by scanAttrValue
break;
} }
self.advance(); self.advance();
} }
try self.addToken(.attr_value, std.mem.trim(u8, self.source[start..self.pos], " \t")); const value = std.mem.trim(u8, self.source[part_start..self.pos], " \t");
if (value.len > 0) {
try self.addToken(.attr_value, value);
}
} }
} }