Add string concatenation in attributes, lazy mixin loading, and benchmarks

Features:
- Fix string concatenation in attribute values (e.g., class="btn btn-" + type)
- Lexer now properly captures full expressions with operators
- Runtime evaluates expressions for class attributes

ViewEngine improvements:
- Change mixin loading from eager to lazy (on-demand)
- Mixins are now loaded from mixins directory only when first called
- Template-defined mixins take precedence over directory mixins

Benchmarks:
- Add src/benchmark.zig with three template complexity levels
- Simple: ~150k renders/sec, 6KB memory
- Medium: ~70k renders/sec, 45KB memory
- Complex: ~32k renders/sec, 94KB memory
- Memory leak detection confirms no leaks

Documentation:
- Update CLAUDE.md with lazy mixin loading details
- Document mixin resolution order
This commit is contained in:
2026-01-17 20:01:37 +05:30
parent 05bbad64a4
commit 1fff91d7d9
7 changed files with 606 additions and 117 deletions

View File

@@ -638,6 +638,7 @@ pub const Lexer = struct {
/// Emits a single token for the entire expression (e.g., "btn btn-" + type).
fn scanAttrValue(self: *Lexer) !void {
const start = self.pos;
var after_operator = false; // Track if we just passed an operator
// Scan the complete expression including operators
while (!self.isAtEnd()) {
@@ -654,6 +655,7 @@ pub const Lexer = struct {
self.advance();
}
if (self.peek() == quote) self.advance();
after_operator = false;
} else if (c == '`') {
// Template literal
self.advance();
@@ -661,6 +663,7 @@ pub const Lexer = struct {
self.advance();
}
if (self.peek() == '`') self.advance();
after_operator = false;
} else if (c == '{') {
// Object literal - scan matching braces
var depth: usize = 0;
@@ -675,6 +678,7 @@ pub const Lexer = struct {
}
self.advance();
}
after_operator = false;
} else if (c == '[') {
// Array literal - scan matching brackets
var depth: usize = 0;
@@ -689,6 +693,7 @@ pub const Lexer = struct {
}
self.advance();
}
after_operator = false;
} else if (c == '(') {
// Function call - scan matching parens
var depth: usize = 0;
@@ -703,33 +708,46 @@ pub const Lexer = struct {
}
self.advance();
}
after_operator = false;
} 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)
// Whitespace handling depends on context
if (after_operator) {
// After an operator, skip whitespace and continue to get the operand
while (self.peek() == ' ' or self.peek() == '\t') {
self.advance();
}
after_operator = false;
continue;
} else {
// Not an operator - rewind and end
self.pos = ws_start;
break;
// Not after operator - 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
// Operator - include it and mark that we need to continue for the operand
self.advance();
after_operator = true;
} else if (c == '\n' or c == '\r') {
// Newline ends the value
break;
} else {
// Regular character (alphanumeric, etc.)
self.advance();
after_operator = false;
}
}