Fix expression concatenation in attribute values
- Lexer now emits a single token for the entire expression (e.g., "btn btn-" + type) - Runtime evaluateExpression now handles + operator for string concatenation - Added findConcatOperator to safely find operators outside quotes/brackets
This commit is contained in:
@@ -80,7 +80,7 @@ pub fn main() !void {
|
|||||||
|
|
||||||
/// Handler for GET /
|
/// Handler for GET /
|
||||||
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
const html = app.view.render(app.allocator, "layout", .{
|
const html = app.view.render(app.allocator, "index", .{
|
||||||
.title = "Home",
|
.title = "Home",
|
||||||
}) catch |err| {
|
}) catch |err| {
|
||||||
res.status = 500;
|
res.status = 500;
|
||||||
|
|||||||
9
src/examples/demo/views/index.pug
Normal file
9
src/examples/demo/views/index.pug
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title hello
|
||||||
|
body
|
||||||
|
p some thing
|
||||||
|
| ballah
|
||||||
|
| ballah
|
||||||
|
+btn("click me ", "secondary")
|
||||||
150
src/lexer.zig
150
src/lexer.zig
@@ -635,117 +635,109 @@ 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.
|
/// Handles expression continuation with operators like + for string concatenation.
|
||||||
/// Note: Quotes are preserved in the token value so evaluateExpression can detect string literals.
|
/// Emits a single token for the entire expression (e.g., "btn btn-" + type).
|
||||||
fn scanAttrValue(self: *Lexer) !void {
|
fn scanAttrValue(self: *Lexer) !void {
|
||||||
const start = self.pos;
|
const start = self.pos;
|
||||||
try self.scanAttrValuePart();
|
|
||||||
|
|
||||||
// Check for expression continuation (e.g., "string" + variable)
|
// Scan the complete expression including operators
|
||||||
while (!self.isAtEnd()) {
|
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
|
||||||
const quote = c;
|
const quote = c;
|
||||||
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) {
|
||||||
if (self.peek() == '\\' and self.peekNext() == quote) {
|
if (self.peek() == '\\' and self.peekNext() == quote) {
|
||||||
self.advance(); // skip backslash
|
self.advance(); // skip backslash
|
||||||
}
|
}
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
|
if (self.peek() == quote) self.advance();
|
||||||
if (self.peek() == quote) self.advance(); // Include closing quote
|
|
||||||
try self.addToken(.attr_value, self.source[part_start..self.pos]);
|
|
||||||
} else if (c == '`') {
|
} else if (c == '`') {
|
||||||
// Template literal - preserve backticks
|
// Template literal
|
||||||
const part_start = self.pos;
|
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
while (!self.isAtEnd() and self.peek() != '`') {
|
while (!self.isAtEnd() and self.peek() != '`') {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.peek() == '`') self.advance();
|
if (self.peek() == '`') self.advance();
|
||||||
try self.addToken(.attr_value, self.source[part_start..self.pos]);
|
|
||||||
} else if (c == '{') {
|
} else if (c == '{') {
|
||||||
// Object literal
|
// Object literal - scan matching braces
|
||||||
try self.scanObjectLiteral();
|
var depth: usize = 0;
|
||||||
} else if (c == '[') {
|
|
||||||
// Array literal
|
|
||||||
try self.scanArrayLiteral();
|
|
||||||
} else {
|
|
||||||
// Unquoted expression (e.g., variable, function call)
|
|
||||||
const part_start = self.pos;
|
|
||||||
var paren_depth: usize = 0;
|
|
||||||
var bracket_depth: usize = 0;
|
|
||||||
|
|
||||||
while (!self.isAtEnd()) {
|
while (!self.isAtEnd()) {
|
||||||
const ch = self.peek();
|
const ch = self.peek();
|
||||||
if (ch == '(') {
|
if (ch == '{') depth += 1;
|
||||||
paren_depth += 1;
|
if (ch == '}') {
|
||||||
} else if (ch == ')') {
|
depth -= 1;
|
||||||
if (paren_depth == 0) break;
|
self.advance();
|
||||||
paren_depth -= 1;
|
if (depth == 0) break;
|
||||||
} else if (ch == '[') {
|
continue;
|
||||||
bracket_depth += 1;
|
|
||||||
} else if (ch == ']') {
|
|
||||||
if (bracket_depth == 0) break;
|
|
||||||
bracket_depth -= 1;
|
|
||||||
} else if (ch == ',' and paren_depth == 0 and bracket_depth == 0) {
|
|
||||||
break;
|
|
||||||
} else if ((ch == ' ' or ch == '\t' or ch == '\n') and paren_depth == 0 and bracket_depth == 0) {
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
} else if (c == '[') {
|
||||||
|
// Array literal - scan matching brackets
|
||||||
|
var depth: usize = 0;
|
||||||
|
while (!self.isAtEnd()) {
|
||||||
|
const ch = self.peek();
|
||||||
|
if (ch == '[') depth += 1;
|
||||||
|
if (ch == ']') {
|
||||||
|
depth -= 1;
|
||||||
|
self.advance();
|
||||||
|
if (depth == 0) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
} else if (c == '(') {
|
||||||
|
// Function call - scan matching parens
|
||||||
|
var depth: usize = 0;
|
||||||
|
while (!self.isAtEnd()) {
|
||||||
|
const ch = self.peek();
|
||||||
|
if (ch == '(') depth += 1;
|
||||||
|
if (ch == ')') {
|
||||||
|
depth -= 1;
|
||||||
|
self.advance();
|
||||||
|
if (depth == 0) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
} else if (c == ')' or c == ',') {
|
||||||
|
// End of attribute value
|
||||||
|
break;
|
||||||
|
} else if (c == ' ' or c == '\t') {
|
||||||
|
// Whitespace - check if followed by operator (continue) or not (end)
|
||||||
|
const ws_start = self.pos;
|
||||||
|
while (self.peek() == ' ' or self.peek() == '\t') {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
const next = self.peek();
|
||||||
|
if (next == '+' or next == '-' or next == '*' or next == '/') {
|
||||||
|
// Operator follows - continue scanning (include whitespace)
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Not an operator - rewind and end
|
||||||
|
self.pos = ws_start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (c == '+' or c == '-' or c == '*' or c == '/') {
|
||||||
|
// Operator - include it and continue
|
||||||
|
self.advance();
|
||||||
|
} else if (c == '\n' or c == '\r') {
|
||||||
|
// Newline ends the value
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Regular character (alphanumeric, etc.)
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const value = std.mem.trim(u8, self.source[part_start..self.pos], " \t");
|
const value = std.mem.trim(u8, self.source[start..self.pos], " \t");
|
||||||
if (value.len > 0) {
|
if (value.len > 0) {
|
||||||
try self.addToken(.attr_value, value);
|
try self.addToken(.attr_value, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Scans an object literal {...} handling nested braces.
|
/// Scans an object literal {...} handling nested braces.
|
||||||
/// Returns error if braces are unmatched.
|
/// Returns error if braces are unmatched.
|
||||||
|
|||||||
@@ -880,6 +880,22 @@ pub const Runtime = struct {
|
|||||||
fn evaluateExpression(self: *Runtime, expr: []const u8) Value {
|
fn evaluateExpression(self: *Runtime, expr: []const u8) Value {
|
||||||
const trimmed = std.mem.trim(u8, expr, " \t");
|
const trimmed = std.mem.trim(u8, expr, " \t");
|
||||||
|
|
||||||
|
// Check for string concatenation with + operator
|
||||||
|
// e.g., "btn btn-" + type or "hello " + name + "!"
|
||||||
|
if (self.findConcatOperator(trimmed)) |op_pos| {
|
||||||
|
const left = std.mem.trim(u8, trimmed[0..op_pos], " \t");
|
||||||
|
const right = std.mem.trim(u8, trimmed[op_pos + 1 ..], " \t");
|
||||||
|
|
||||||
|
const left_val = self.evaluateExpression(left);
|
||||||
|
const right_val = self.evaluateExpression(right);
|
||||||
|
|
||||||
|
const left_str = left_val.toString(self.allocator) catch return Value.null;
|
||||||
|
const right_str = right_val.toString(self.allocator) catch return Value.null;
|
||||||
|
|
||||||
|
const result = std.fmt.allocPrint(self.allocator, "{s}{s}", .{ left_str, right_str }) catch return Value.null;
|
||||||
|
return Value.str(result);
|
||||||
|
}
|
||||||
|
|
||||||
// Check for string literal
|
// Check for string literal
|
||||||
if (trimmed.len >= 2) {
|
if (trimmed.len >= 2) {
|
||||||
if ((trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') or
|
if ((trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') or
|
||||||
@@ -903,6 +919,44 @@ pub const Runtime = struct {
|
|||||||
return self.lookupVariable(trimmed);
|
return self.lookupVariable(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the position of a + operator that's not inside quotes or brackets.
|
||||||
|
/// Returns null if no such operator exists.
|
||||||
|
fn findConcatOperator(_: *Runtime, expr: []const u8) ?usize {
|
||||||
|
var in_string: u8 = 0; // 0 = not in string, '"' or '\'' = in that type of string
|
||||||
|
var bracket_depth: usize = 0;
|
||||||
|
var paren_depth: usize = 0;
|
||||||
|
var brace_depth: usize = 0;
|
||||||
|
|
||||||
|
for (expr, 0..) |c, i| {
|
||||||
|
if (in_string != 0) {
|
||||||
|
if (c == in_string) {
|
||||||
|
in_string = 0;
|
||||||
|
} else if (c == '\\' and i + 1 < expr.len) {
|
||||||
|
// Skip escaped character - we'll handle it in next iteration
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (c) {
|
||||||
|
'"', '\'' => in_string = c,
|
||||||
|
'[' => bracket_depth += 1,
|
||||||
|
']' => bracket_depth -|= 1,
|
||||||
|
'(' => paren_depth += 1,
|
||||||
|
')' => paren_depth -|= 1,
|
||||||
|
'{' => brace_depth += 1,
|
||||||
|
'}' => brace_depth -|= 1,
|
||||||
|
'+' => {
|
||||||
|
if (bracket_depth == 0 and paren_depth == 0 and brace_depth == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Looks up a variable with dot notation support.
|
/// Looks up a variable with dot notation support.
|
||||||
fn lookupVariable(self: *Runtime, path: []const u8) Value {
|
fn lookupVariable(self: *Runtime, path: []const u8) Value {
|
||||||
var parts = std.mem.splitScalar(u8, path, '.');
|
var parts = std.mem.splitScalar(u8, path, '.');
|
||||||
|
|||||||
Reference in New Issue
Block a user