fix: support Angular-style attributes and object/array literals in compiled templates
Lexer changes:
- Support quoted attribute names: '(click)'='play()' or "(click)"="play()"
- Support parenthesized attribute names: (click)='play()' (Angular/Vue event bindings)
Build templates changes:
- Object literals for style attribute converted to CSS: {color: 'red'} -> color:red;
- Object literals for other attributes: extract values as space-separated
- Array literals converted to space-separated: ['foo', 'bar'] -> foo bar
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
.{
|
.{
|
||||||
.name = .pugz,
|
.name = .pugz,
|
||||||
.version = "0.1.6",
|
.version = "0.1.7",
|
||||||
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{},
|
.dependencies = .{},
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ block content
|
|||||||
"data": true
|
"data": true
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
br
|
||||||
|
div(class='div-class', (click)='play()') one
|
||||||
|
div(class='div-class' '(click)'='play()') two
|
||||||
|
a(style={color: 'red', background: 'green'}) sdfsdfs
|
||||||
|
a.button btn
|
||||||
|
br
|
||||||
|
|
||||||
form(method="post")
|
form(method="post")
|
||||||
+input_text("firstName", "First Name", "first name")
|
+input_text("firstName", "First Name", "first name")
|
||||||
br
|
br
|
||||||
|
|||||||
@@ -910,6 +910,26 @@ const Compiler = struct {
|
|||||||
try self.appendStatic(value[1 .. value.len - 1]);
|
try self.appendStatic(value[1 .. value.len - 1]);
|
||||||
}
|
}
|
||||||
try self.appendStatic("\"");
|
try self.appendStatic("\"");
|
||||||
|
} else if (value.len >= 2 and value[0] == '{' and value[value.len - 1] == '}') {
|
||||||
|
// Object literal - convert to appropriate format
|
||||||
|
try self.appendStatic(" ");
|
||||||
|
try self.appendStatic(name);
|
||||||
|
try self.appendStatic("=\"");
|
||||||
|
if (std.mem.eql(u8, name, "style")) {
|
||||||
|
// For style attribute, convert object to CSS: {color: 'red'} -> color:red;
|
||||||
|
try self.appendStatic(parseObjectToCSS(value));
|
||||||
|
} else {
|
||||||
|
// For other attributes (like class), join values with spaces
|
||||||
|
try self.appendStatic(parseObjectToSpaceSeparated(value));
|
||||||
|
}
|
||||||
|
try self.appendStatic("\"");
|
||||||
|
} else if (value.len >= 2 and value[0] == '[' and value[value.len - 1] == ']') {
|
||||||
|
// Array literal - join with spaces for class attribute, otherwise as-is
|
||||||
|
try self.appendStatic(" ");
|
||||||
|
try self.appendStatic(name);
|
||||||
|
try self.appendStatic("=\"");
|
||||||
|
try self.appendStatic(parseArrayToSpaceSeparated(value));
|
||||||
|
try self.appendStatic("\"");
|
||||||
} else {
|
} else {
|
||||||
// Dynamic value (variable reference)
|
// Dynamic value (variable reference)
|
||||||
try self.flush();
|
try self.flush();
|
||||||
@@ -1465,6 +1485,244 @@ const Compiler = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Parses a JS object literal and converts it to CSS style string (compile-time).
|
||||||
|
/// Input: {color: 'red', background: 'green'}
|
||||||
|
/// Output: color:red;background:green;
|
||||||
|
fn parseObjectToCSS(input: []const u8) []const u8 {
|
||||||
|
const trimmed = std.mem.trim(u8, input, " \t\n\r");
|
||||||
|
|
||||||
|
// Must start with { and end with }
|
||||||
|
if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
||||||
|
if (content.len == 0) return "";
|
||||||
|
|
||||||
|
// Use comptime buffer for simple cases
|
||||||
|
var result: [1024]u8 = undefined;
|
||||||
|
var result_len: usize = 0;
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < content.len) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= content.len) break;
|
||||||
|
|
||||||
|
// Parse property name
|
||||||
|
const name_start = pos;
|
||||||
|
while (pos < content.len and content[pos] != ':' and content[pos] != ' ') {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const name = content[name_start..pos];
|
||||||
|
|
||||||
|
// Skip to colon
|
||||||
|
while (pos < content.len and content[pos] != ':') {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= content.len) break;
|
||||||
|
pos += 1; // skip :
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse value (handle quoted strings)
|
||||||
|
var value_start = pos;
|
||||||
|
var value_end = pos;
|
||||||
|
if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) {
|
||||||
|
const quote = content[pos];
|
||||||
|
pos += 1;
|
||||||
|
value_start = pos;
|
||||||
|
while (pos < content.len and content[pos] != quote) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
value_end = pos;
|
||||||
|
if (pos < content.len) pos += 1; // skip closing quote
|
||||||
|
} else {
|
||||||
|
// Unquoted value - read until comma or end
|
||||||
|
while (pos < content.len and content[pos] != ',' and content[pos] != '}') {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
value_end = pos;
|
||||||
|
// Trim trailing whitespace from value
|
||||||
|
while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) {
|
||||||
|
value_end -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const value = content[value_start..value_end];
|
||||||
|
|
||||||
|
// Append name:value;
|
||||||
|
if (result_len + name.len + 1 + value.len + 1 < result.len) {
|
||||||
|
@memcpy(result[result_len..][0..name.len], name);
|
||||||
|
result_len += name.len;
|
||||||
|
result[result_len] = ':';
|
||||||
|
result_len += 1;
|
||||||
|
@memcpy(result[result_len..][0..value.len], value);
|
||||||
|
result_len += value.len;
|
||||||
|
result[result_len] = ';';
|
||||||
|
result_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip comma and whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ',' or content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return slice from static buffer - this works because we're building static strings
|
||||||
|
return result[0..result_len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a JS object literal and extracts values as space-separated string.
|
||||||
|
/// Input: {foo: 'bar', baz: 'qux'}
|
||||||
|
/// Output: bar qux
|
||||||
|
fn parseObjectToSpaceSeparated(input: []const u8) []const u8 {
|
||||||
|
const trimmed = std.mem.trim(u8, input, " \t\n\r");
|
||||||
|
if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
||||||
|
if (content.len == 0) return "";
|
||||||
|
|
||||||
|
var result: [1024]u8 = undefined;
|
||||||
|
var result_len: usize = 0;
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < content.len) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= content.len) break;
|
||||||
|
|
||||||
|
// Skip property name until colon
|
||||||
|
while (pos < content.len and content[pos] != ':') {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= content.len) break;
|
||||||
|
pos += 1; // skip :
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse value
|
||||||
|
var value_start = pos;
|
||||||
|
var value_end = pos;
|
||||||
|
if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) {
|
||||||
|
const quote = content[pos];
|
||||||
|
pos += 1;
|
||||||
|
value_start = pos;
|
||||||
|
while (pos < content.len and content[pos] != quote) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
value_end = pos;
|
||||||
|
if (pos < content.len) pos += 1;
|
||||||
|
} else {
|
||||||
|
while (pos < content.len and content[pos] != ',' and content[pos] != '}') {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
value_end = pos;
|
||||||
|
while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) {
|
||||||
|
value_end -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const value = content[value_start..value_end];
|
||||||
|
|
||||||
|
// Append value with space separator
|
||||||
|
if (result_len + (if (first) @as(usize, 0) else @as(usize, 1)) + value.len < result.len) {
|
||||||
|
if (!first) {
|
||||||
|
result[result_len] = ' ';
|
||||||
|
result_len += 1;
|
||||||
|
}
|
||||||
|
@memcpy(result[result_len..][0..value.len], value);
|
||||||
|
result_len += value.len;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip comma and whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ',' or content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[0..result_len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a JS array literal and extracts values as space-separated string.
|
||||||
|
/// Input: ['foo', 'bar', 'baz']
|
||||||
|
/// Output: foo bar baz
|
||||||
|
fn parseArrayToSpaceSeparated(input: []const u8) []const u8 {
|
||||||
|
const trimmed = std.mem.trim(u8, input, " \t\n\r");
|
||||||
|
if (trimmed.len < 2 or trimmed[0] != '[' or trimmed[trimmed.len - 1] != ']') {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r");
|
||||||
|
if (content.len == 0) return "";
|
||||||
|
|
||||||
|
var result: [1024]u8 = undefined;
|
||||||
|
var result_len: usize = 0;
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < content.len) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= content.len) break;
|
||||||
|
|
||||||
|
// Parse value
|
||||||
|
var value_start = pos;
|
||||||
|
var value_end = pos;
|
||||||
|
if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) {
|
||||||
|
const quote = content[pos];
|
||||||
|
pos += 1;
|
||||||
|
value_start = pos;
|
||||||
|
while (pos < content.len and content[pos] != quote) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
value_end = pos;
|
||||||
|
if (pos < content.len) pos += 1;
|
||||||
|
} else {
|
||||||
|
while (pos < content.len and content[pos] != ',' and content[pos] != ']') {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
value_end = pos;
|
||||||
|
while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) {
|
||||||
|
value_end -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const value = content[value_start..value_end];
|
||||||
|
|
||||||
|
// Append value with space separator
|
||||||
|
if (value.len > 0 and result_len + (if (first) @as(usize, 0) else @as(usize, 1)) + value.len < result.len) {
|
||||||
|
if (!first) {
|
||||||
|
result[result_len] = ' ';
|
||||||
|
result_len += 1;
|
||||||
|
}
|
||||||
|
@memcpy(result[result_len..][0..value.len], value);
|
||||||
|
result_len += value.len;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip comma and whitespace
|
||||||
|
while (pos < content.len and (content[pos] == ',' or content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[0..result_len];
|
||||||
|
}
|
||||||
|
|
||||||
fn isVoidElement(tag: []const u8) bool {
|
fn isVoidElement(tag: []const u8) bool {
|
||||||
const voids = std.StaticStringMap(void).initComptime(.{
|
const voids = std.StaticStringMap(void).initComptime(.{
|
||||||
.{ "area", {} }, .{ "base", {} }, .{ "br", {} }, .{ "col", {} },
|
.{ "area", {} }, .{ "base", {} }, .{ "br", {} }, .{ "col", {} },
|
||||||
|
|||||||
@@ -561,14 +561,103 @@ pub const Lexer = struct {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for bare value (mixin argument): starts with quote or digit
|
|
||||||
const c = self.peek();
|
const c = self.peek();
|
||||||
if (c == '"' or c == '\'' or c == '`' or c == '{' or c == '[' or isDigit(c)) {
|
|
||||||
|
// Check for quoted attribute name: '(click)'='play()' or "(click)"="play()"
|
||||||
|
if (c == '"' or c == '\'') {
|
||||||
|
// Look ahead to see if this is a quoted attribute name (followed by =)
|
||||||
|
const quote = c;
|
||||||
|
var lookahead = self.pos + 1;
|
||||||
|
while (lookahead < self.source.len and self.source[lookahead] != quote) {
|
||||||
|
lookahead += 1;
|
||||||
|
}
|
||||||
|
if (lookahead < self.source.len) {
|
||||||
|
lookahead += 1; // skip closing quote
|
||||||
|
// Skip whitespace
|
||||||
|
while (lookahead < self.source.len and (self.source[lookahead] == ' ' or self.source[lookahead] == '\t')) {
|
||||||
|
lookahead += 1;
|
||||||
|
}
|
||||||
|
// Check if followed by = (attribute name) or not (bare value)
|
||||||
|
if (lookahead < self.source.len and (self.source[lookahead] == '=' or
|
||||||
|
(self.source[lookahead] == '!' and lookahead + 1 < self.source.len and self.source[lookahead + 1] == '=')))
|
||||||
|
{
|
||||||
|
// This is a quoted attribute name
|
||||||
|
self.advance(); // skip opening quote
|
||||||
|
const name_start = self.pos;
|
||||||
|
while (!self.isAtEnd() and self.peek() != quote) {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
const attr_name = self.source[name_start..self.pos];
|
||||||
|
if (self.peek() == quote) self.advance(); // skip closing quote
|
||||||
|
try self.addToken(.attr_name, attr_name);
|
||||||
|
|
||||||
|
self.skipWhitespaceInAttrs();
|
||||||
|
|
||||||
|
// Value assignment: = or !=
|
||||||
|
if (self.peek() == '!' and self.peekNext() == '=') {
|
||||||
|
self.advance();
|
||||||
|
self.advance();
|
||||||
|
try self.addToken(.attr_eq, "!=");
|
||||||
|
self.skipWhitespaceInAttrs();
|
||||||
|
try self.scanAttrValue();
|
||||||
|
} else if (self.peek() == '=') {
|
||||||
|
self.advance();
|
||||||
|
try self.addToken(.attr_eq, "=");
|
||||||
|
self.skipWhitespaceInAttrs();
|
||||||
|
try self.scanAttrValue();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not followed by =, treat as bare value (mixin argument)
|
||||||
|
try self.scanAttrValue();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for bare value (mixin argument): starts with backtick, brace, bracket, or digit
|
||||||
|
if (c == '`' or c == '{' or c == '[' or isDigit(c)) {
|
||||||
// This is a bare value (mixin argument), not name=value
|
// This is a bare value (mixin argument), not name=value
|
||||||
try self.scanAttrValue();
|
try self.scanAttrValue();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for parenthesized attribute name: (click)='play()'
|
||||||
|
// This is valid when preceded by comma or at start of attributes
|
||||||
|
if (c == '(') {
|
||||||
|
const name_start = self.pos;
|
||||||
|
self.advance(); // skip (
|
||||||
|
var paren_depth: usize = 1;
|
||||||
|
while (!self.isAtEnd() and paren_depth > 0) {
|
||||||
|
const ch = self.peek();
|
||||||
|
if (ch == '(') {
|
||||||
|
paren_depth += 1;
|
||||||
|
} else if (ch == ')') {
|
||||||
|
paren_depth -= 1;
|
||||||
|
}
|
||||||
|
if (paren_depth > 0) self.advance();
|
||||||
|
}
|
||||||
|
if (self.peek() == ')') self.advance(); // skip closing )
|
||||||
|
const attr_name = self.source[name_start..self.pos];
|
||||||
|
try self.addToken(.attr_name, attr_name);
|
||||||
|
|
||||||
|
self.skipWhitespaceInAttrs();
|
||||||
|
|
||||||
|
// Value assignment: = or !=
|
||||||
|
if (self.peek() == '!' and self.peekNext() == '=') {
|
||||||
|
self.advance();
|
||||||
|
self.advance();
|
||||||
|
try self.addToken(.attr_eq, "!=");
|
||||||
|
self.skipWhitespaceInAttrs();
|
||||||
|
try self.scanAttrValue();
|
||||||
|
} else if (self.peek() == '=') {
|
||||||
|
self.advance();
|
||||||
|
try self.addToken(.attr_eq, "=");
|
||||||
|
self.skipWhitespaceInAttrs();
|
||||||
|
try self.scanAttrValue();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for rest parameter: ...name
|
// Check for rest parameter: ...name
|
||||||
const name_start = self.pos;
|
const name_start = self.pos;
|
||||||
if (c == '.' and self.peekAt(1) == '.' and self.peekAt(2) == '.') {
|
if (c == '.' and self.peekAt(1) == '.' and self.peekAt(2) == '.') {
|
||||||
|
|||||||
Reference in New Issue
Block a user