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:
18
build.zig
18
build.zig
@@ -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.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user