diff --git a/build.zig.zon b/build.zig.zon index b40c5f8..61ab054 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .pugz, - .version = "0.1.6", + .version = "0.1.7", .fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, diff --git a/examples/demo/views/page-a.pug b/examples/demo/views/page-a.pug index 12dd0f0..4d37b66 100644 --- a/examples/demo/views/page-a.pug +++ b/examples/demo/views/page-a.pug @@ -19,6 +19,14 @@ block content "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") +input_text("firstName", "First Name", "first name") br diff --git a/src/build_templates.zig b/src/build_templates.zig index f276ed7..c182f61 100644 --- a/src/build_templates.zig +++ b/src/build_templates.zig @@ -910,6 +910,26 @@ const Compiler = struct { try self.appendStatic(value[1 .. value.len - 1]); } 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 { // Dynamic value (variable reference) 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 { const voids = std.StaticStringMap(void).initComptime(.{ .{ "area", {} }, .{ "base", {} }, .{ "br", {} }, .{ "col", {} }, diff --git a/src/lexer.zig b/src/lexer.zig index c47683c..d97f2b1 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -561,14 +561,103 @@ pub const Lexer = struct { continue; } - // Check for bare value (mixin argument): starts with quote or digit 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 try self.scanAttrValue(); 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 const name_start = self.pos; if (c == '.' and self.peekAt(1) == '.' and self.peekAt(2) == '.') {