diff --git a/src/parser.zig b/src/parser.zig index 28fa677..65c16c8 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -6,6 +6,33 @@ pub const NodeType = enum { tag, if_block, for_block, + include, + with_block, + now, + // extends, // <--- novo + // block, // <--- novo + // super, // <--- novo (para {{ block.super }}) +}; + +pub const NowNode = struct { + format: []const u8, +}; + +pub const Assignment = struct { + key: []const u8, + value_expr: []const u8, + is_literal: bool, +}; + +pub const WithNode = struct { + assignments: []const Assignment, + body: []Node, + raw_open: []const u8, + raw_close: []const u8, +}; + +pub const IncludeNode = struct { + template_name: []const u8, }; pub const TextNode = struct { @@ -45,7 +72,10 @@ pub const Node = struct { variable: ?VariableNode = null, tag: ?TagNode = null, @"if": ?IfNode = null, - @"for": ?ForNode = null, // <--- novo + @"for": ?ForNode = null, + include: ?IncludeNode = null, + with: ?WithNode = null, + now: ?NowNode = null, pub fn deinit(self: Node, allocator: std.mem.Allocator) void { switch (self.type) { @@ -73,6 +103,22 @@ pub const Node = struct { allocator.free(fb.empty_body); // raw_open e raw_close são slices originais — não free }, + .include => if (self.include) |inc| { + allocator.free(inc.template_name); + }, + .with_block => if (self.with) |w| { + for (w.assignments) |a| { + allocator.free(a.key); + allocator.free(a.value_expr); + } + allocator.free(w.assignments); + for (w.body) |n| n.deinit(allocator); + allocator.free(w.body); + // raw_open e raw_close são slices originais — não free + }, + .now => if (self.now) |n| { + allocator.free(n.format); + }, } } }; @@ -99,12 +145,202 @@ pub const Parser = struct { while (self.pos < self.template.len and std.ascii.isWhitespace(self.template[self.pos])) : (self.advance(1)) {} } + // fn parseAssignments(allocator: std.mem.Allocator, args: []const u8) ![]const Assignment { + // var list = std.ArrayList(Assignment){}; + // defer list.deinit(allocator); + // + // // Implementação básica — você já tem uma melhor, use ela + // // Por enquanto, só para passar teste + // _ = args; + // return try list.toOwnedSlice(allocator); + // } + + fn parseAssignments( + self: *Parser, + allocator: std.mem.Allocator, + args: []const u8, + ) ![]const Assignment { + var list = std.ArrayList(Assignment){}; + defer list.deinit(allocator); + + _ = self; + var i: usize = 0; + while (i < args.len) { + // Pula whitespaces iniciais + while (i < args.len and std.mem.indexOfScalar(u8, " \t\r\n", args[i]) != null) : (i += 1) {} + + if (i >= args.len) break; + + // Parse key (até '=') + const key_start = i; + while (i < args.len and args[i] != '=') : (i += 1) {} + if (i >= args.len or args[i] != '=') return error.InvalidAssignmentSyntax; + const key = std.mem.trim(u8, args[key_start..i], " \t\r\n"); + if (key.len == 0) return error.InvalidAssignmentSyntax; + i += 1; // Pula '=' + + // Pula whitespaces após '=' + while (i < args.len and std.mem.indexOfScalar(u8, " \t\r\n", args[i]) != null) : (i += 1) {} + + // Parse value: se começa com ", parse até próximo " não escapado; senão, até próximo espaço + const value_start = i; + var in_quote = false; + if (i < args.len and args[i] == '"') { + in_quote = true; + i += 1; // Pula aspa inicial + } + while (i < args.len) { + if (in_quote) { + if (args[i] == '"' and (i == 0 or args[i - 1] != '\\')) break; // Fecha aspa não escapada + } else { + if (std.mem.indexOfScalar(u8, " \t\r\n", args[i]) != null) break; // Fim sem quote + } + i += 1; + } + const value_end = i; + var value = args[value_start..value_end]; + if (in_quote) { + if (i >= args.len or args[i] != '"') return error.UnclosedQuoteInAssignment; + i += 1; // Pula aspa final + value = args[value_start + 1 .. value_end]; // Remove aspas + // TODO: Se precisar, handle escapes como \" aqui (remova \\ antes de ") + } else { + value = std.mem.trim(u8, value, " \t\r\n"); + } + + try list.append(allocator, .{ + .key = try allocator.dupe(u8, key), + .value_expr = try allocator.dupe(u8, value), + .is_literal = in_quote, + }); + } + + return try list.toOwnedSlice(allocator); + } + + fn parseWithBlock(self: *Parser, allocator: std.mem.Allocator, assignments: []const Assignment, raw_open: []const u8) !Node { + std.debug.print("Vou verificar se sou bloco with\n", .{}); + var body = std.ArrayList(Node){}; + defer body.deinit(allocator); + + var depth: usize = 1; + + while (self.pos < self.template.len and depth > 0) { + if (try self.parseText(allocator)) |node| { + std.debug.print("2.3 - Encontrei um texto: {s}\n", .{node.text.?.content}); + try body.append(allocator, node); + continue; + } + + if (try self.parseVariable(allocator)) |node| { + std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.content}); + try body.append(allocator, node); + continue; + } + + if (try self.parseTag(allocator)) |tag_node| { + const tag_name = tag_node.tag.?.name; + + if (std.mem.eql(u8, tag_name, "with")) { + depth += 1; + try body.append(allocator, tag_node); + continue; + } + + if (std.mem.eql(u8, tag_name, "endwith")) { + depth -= 1; + const raw_close = tag_node.tag.?.raw; + + allocator.free(tag_node.tag.?.name); + allocator.free(tag_node.tag.?.args); + + if (depth == 0) { + // para fins de debug + std.debug.print("2.4 - Encontrei um bloco with:\n - assignments: {any}\n - body: {any}\n - raw_open: {s}\n - raw_close: {s}\n", .{ + assignments, + body.items, + raw_open, + raw_close, + }); + // fim para fins de debug + return Node{ + .type = .with_block, + .with = .{ + .assignments = assignments, + .body = try body.toOwnedSlice(allocator), + .raw_open = raw_open, + .raw_close = raw_close, + }, + }; + } + + try body.append(allocator, tag_node); + continue; + } + + try body.append(allocator, tag_node); + } else { + self.advance(1); + } + } + + return error.UnclosedBlock; + } + + fn parseComment(self: *Parser) !void { + // Consome a tag open {% comment %} + // Já estamos após o %}, então só avançamos + var depth: usize = 1; + + while (self.pos < self.template.len and depth > 0) { + if (self.peek(2)) |p| { + if (std.mem.eql(u8, p, "{%")) { + self.advance(2); + self.skipWhitespace(); + + const content_start = self.pos; + while (self.pos < self.template.len) : (self.advance(1)) { + if (self.peek(2)) |closing| { + if (std.mem.eql(u8, closing, "%}")) break; + } + } + + if (self.pos + 2 > self.template.len or !std.mem.eql(u8, self.template[self.pos .. self.pos + 2], "%}")) { + return error.UnclosedTag; + } + + const inner = std.mem.trim(u8, self.template[content_start..self.pos], " \t\r\n"); + + if (std.mem.eql(u8, inner, "comment")) { + depth += 1; + } else if (std.mem.eql(u8, inner, "endcomment")) { + depth -= 1; + if (depth == 0) { + self.advance(2); // consome %} + return; + } + } + + self.advance(2); + continue; + } + } + self.advance(1); + } + + return error.UnclosedComment; + } + fn parseText(self: *Parser, allocator: std.mem.Allocator) !?Node { + std.debug.print("2.0 - Vou verificar se sou texto\n", .{}); const start = self.pos; + std.debug.print("2.1 - meu start é {d}\n", .{start}); + while (self.pos < self.template.len) { if (self.peek(2)) |p| { if (std.mem.eql(u8, p, "{{") or std.mem.eql(u8, p, "{%")) { + std.debug.print("2.2 - fiz o peek de 2 em 2 até que achei {{{{ ou {{%, então parei\n", .{}); break; } } @@ -114,6 +350,7 @@ pub const Parser = struct { if (self.pos == start) return null; const content = try allocator.dupe(u8, self.template[start..self.pos]); + std.debug.print("2.2 - meu content é \'{s}\'\n", .{content}); return Node{ .type = .text, .text = .{ .content = content }, @@ -121,8 +358,11 @@ pub const Parser = struct { } fn parseVariable(self: *Parser, allocator: std.mem.Allocator) !?Node { + std.debug.print("2.0 - Vou verificar se sou variável\n", .{}); + std.debug.print("2.1 - meu start é {d}\n", .{self.pos}); if (self.peek(2)) |p| { if (!std.mem.eql(u8, p, "{{")) return null; + std.debug.print("2.1 - fiz o peek de 2 em 2 até que achei {{{{\n", .{}); } else return null; self.advance(2); @@ -135,12 +375,15 @@ pub const Parser = struct { } } + std.debug.print("2.2 - fiz o peek de 2 em 2 até que achei }}}}\n", .{}); if (self.pos + 2 > self.template.len or !std.mem.eql(u8, self.template[self.pos .. self.pos + 2], "}}")) { + std.debug.print("2.3 - deu ruim achei uma variável que não fecha!\n", .{}); return error.UnclosedVariable; } const raw_content = self.template[content_start..self.pos]; const content = std.mem.trim(u8, raw_content, " \t\r\n"); + std.debug.print("2.3 - meu content é \'{s}\'\n", .{content}); const duped = try allocator.dupe(u8, content); self.advance(2); @@ -152,8 +395,11 @@ pub const Parser = struct { } fn parseTag(self: *Parser, allocator: std.mem.Allocator) !?Node { + std.debug.print("2.0 - Vou verificar se sou uma tag\n", .{}); + std.debug.print("2.1 - meu start é {d}\n", .{self.pos}); if (self.peek(2)) |p| { if (!std.mem.eql(u8, p, "{%")) return null; + std.debug.print("2.1 - fiz o peek de 2 em 2 até que achei {{%\n", .{}); } else return null; const raw_start = self.pos; @@ -167,6 +413,7 @@ pub const Parser = struct { } } + std.debug.print("2.2 - fiz o peek de 2 em 2 até que achei %}}\n", .{}); if (self.pos + 2 > self.template.len or !std.mem.eql(u8, self.template[self.pos .. self.pos + 2], "%}")) { return error.UnclosedTag; } @@ -181,6 +428,7 @@ pub const Parser = struct { const name = try allocator.dupe(u8, name_raw); const args = try allocator.dupe(u8, args_raw); + std.debug.print("2.3 - meu node:\n - nome: {s}\n - args: {s}\n - raw: {s}\n", .{ name, args, raw_slice }); self.advance(2); return Node{ @@ -194,6 +442,7 @@ pub const Parser = struct { } fn parseIfBlock(self: *Parser, allocator: std.mem.Allocator, condition: []const u8, raw_open: []const u8) !Node { + std.debug.print("Vou verificar se sou bloco\n", .{}); var true_body = std.ArrayList(Node){}; defer true_body.deinit(allocator); var false_body = std.ArrayList(Node){}; @@ -204,11 +453,13 @@ pub const Parser = struct { while (self.pos < self.template.len and depth > 0) { if (try self.parseText(allocator)) |node| { + std.debug.print("2.3 - Encontrei um texto: {s}\n", .{node.text.?.content}); try current_body.append(allocator, node); continue; } if (try self.parseVariable(allocator)) |node| { + std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.content}); try current_body.append(allocator, node); continue; } @@ -231,6 +482,13 @@ pub const Parser = struct { allocator.free(tag_node.tag.?.args); if (depth == 0) { + std.debug.print("2.4 - Encontrei um bloco if:\n - condition: {s}\n - true_body: {any}\n - false_body: {any}\n - raw_open: {s}\n - raw_close: {s}\n", .{ + condition, + true_body.items, + false_body.items, + raw_open, + raw_close, + }); return Node{ .type = .if_block, .@"if" = .{ @@ -276,11 +534,13 @@ pub const Parser = struct { while (self.pos < self.template.len and depth > 0) { if (try self.parseText(allocator)) |node| { + std.debug.print("2.3 - Encontrei um texto: {s}\n", .{node.text.?.content}); try current_body.append(allocator, node); continue; } if (try self.parseVariable(allocator)) |node| { + std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.content}); try current_body.append(allocator, node); continue; } @@ -303,6 +563,14 @@ pub const Parser = struct { allocator.free(tag_node.tag.?.name); allocator.free(tag_node.tag.?.args); + std.debug.print("2.4 - Encontrei um bloco for:\n - loop_var: {s}\n - iterable: {s}\n - body: {any}\n - empty_body: {any}\n - raw_open: {s}\n - raw_close: {s}\n", .{ + loop_var, + iterable, + body.items, + empty_body.items, + raw_open, + raw_close, + }); return Node{ .type = .for_block, .@"for" = .{ @@ -341,10 +609,89 @@ pub const Parser = struct { var list = std.ArrayList(Node){}; defer list.deinit(allocator); + std.debug.print("O template recebido é:\n\n{s}\n\n", .{self.template}); while (self.pos < self.template.len) { + std.debug.print("1.0 - minha posição ainda é menor que o tamanho do template\n", .{}); if (try self.parseTag(allocator)) |node| { + std.debug.print("3.0 - na real sou uma tag\n", .{}); const tag_name = node.tag.?.name; + std.debug.print("3.1 - meu tag name é: {s}\n", .{tag_name}); + + if (std.mem.eql(u8, tag_name, "comment")) { + std.debug.print("3.0 - na real sou um comentário\n", .{}); + std.debug.print("===================================\n", .{}); + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + try self.parseComment(); + continue; + } + + if (std.mem.eql(u8, tag_name, "now")) { + const args = node.tag.?.args; + + // Remove aspas se existirem + var format_str = args; + if (format_str.len >= 2 and format_str[0] == '"' and format_str[format_str.len - 1] == '"') { + format_str = format_str[1 .. format_str.len - 1]; + } + + const duped_format = try allocator.dupe(u8, format_str); + + // Libera a tag now + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + + std.debug.print("3.0 - na real sou uma tag now\n", .{}); + std.debug.print("===================================\n", .{}); + + try list.append(allocator, Node{ + .type = .now, + .now = .{ .format = duped_format }, + }); + continue; + } + + if (std.mem.eql(u8, tag_name, "with")) { + const args = node.tag.?.args; + const raw_open = node.tag.?.raw; + + const assignments = try self.parseAssignments(allocator, args); + + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + + const with_node = try self.parseWithBlock(allocator, assignments, raw_open); + std.debug.print("3.0 - na real sou um bloco with\n", .{}); + std.debug.print("===================================\n", .{}); + try list.append(allocator, with_node); + continue; + } + + if (std.mem.eql(u8, tag_name, "include")) { + const args = node.tag.?.args; + + // Remove aspas se existirem + var template_name = args; + if (template_name.len >= 2 and template_name[0] == '"' and template_name[template_name.len - 1] == '"') { + template_name = template_name[1 .. template_name.len - 1]; + } + + const duped_name = try allocator.dupe(u8, template_name); + + // Libera a tag include + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + + std.debug.print("3.0 - na real sou um include\n", .{}); + std.debug.print("===================================\n", .{}); + try list.append(allocator, Node{ + .type = .include, + .include = .{ .template_name = duped_name }, + }); + continue; + } + if (std.mem.eql(u8, tag_name, "if")) { const condition_raw = node.tag.?.args; const raw_open = node.tag.?.raw; @@ -359,6 +706,8 @@ pub const Parser = struct { const if_node = try self.parseIfBlock(allocator, condition, raw_open); try list.append(allocator, if_node); + + std.debug.print("===================================\n", .{}); continue; } @@ -379,21 +728,30 @@ pub const Parser = struct { allocator.free(node.tag.?.args); const for_node = try self.parseForBlock(allocator, loop_var, iterable, raw_open); + + std.debug.print("===================================\n", .{}); try list.append(allocator, for_node); continue; } // Para tags normais + std.debug.print("===================================\n", .{}); try list.append(allocator, node); continue; } if (try self.parseVariable(allocator)) |node| { + std.debug.print("3.0 - na real sou variável\n", .{}); + std.debug.print("4.0 - content: \'{s}\'\n", .{node.variable.?.content}); + std.debug.print("===================================\n", .{}); try list.append(allocator, node); continue; } if (try self.parseText(allocator)) |node| { + std.debug.print("3.0 - na real sou texto\n", .{}); + std.debug.print("4.0 - content: \'{s}\'\n", .{node.text.?.content}); + std.debug.print("===================================\n", .{}); try list.append(allocator, node); continue; } @@ -401,6 +759,11 @@ pub const Parser = struct { self.advance(1); } + std.debug.print("\nO resultado disso foi esse:\n", .{}); + for (list.items) |item| { + std.debug.print(" -> type: {s}\n", .{@tagName(item.type)}); + } + std.debug.print("\n", .{}); return try list.toOwnedSlice(allocator); } }; diff --git a/src/parser_test.zig b/src/parser_test.zig index 745b2fb..e160020 100644 --- a/src/parser_test.zig +++ b/src/parser_test.zig @@ -184,3 +184,112 @@ test "parse for block com empty" { try testing.expectEqual(@as(usize, 1), fb.empty_body.len); try testing.expectEqualStrings("Vazio", fb.empty_body[0].text.?.content); } + +test "parse comment" { + const allocator = testing.allocator; + const template = "Bazinga! {% comment %}{% for item in lista %}Tem{% empty %}Vazio{% endfor %}{% endcomment %}"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 1), nodes.len); + try testing.expect(nodes[0].type == .text); + try testing.expectEqualStrings("Bazinga! ", nodes[0].text.?.content); +} + +test "parse include simples" { + const allocator = testing.allocator; + const template = "Cabeçalho {% include \"header.zdt\" %} Conteúdo {% include \"footer.zdt\" %}"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 4), nodes.len); + + try testing.expect(nodes[0].type == .text); + try testing.expectEqualStrings("Cabeçalho ", nodes[0].text.?.content); + + try testing.expect(nodes[1].type == .include); + try testing.expectEqualStrings("header.zdt", nodes[1].include.?.template_name); + + try testing.expect(nodes[2].type == .text); + try testing.expectEqualStrings(" Conteúdo ", nodes[2].text.?.content); + + try testing.expect(nodes[3].type == .include); + try testing.expectEqualStrings("footer.zdt", nodes[3].include.?.template_name); + +} + +test "parse include sem aspas (erro esperado no futuro, mas por enquanto aceita)" { + const allocator = testing.allocator; + const template = "{% include header.zdt %}"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 1), nodes.len); + try testing.expect(nodes[0].type == .include); + try testing.expectEqualStrings("header.zdt", nodes[0].include.?.template_name); +} + +test "parse with simples" { + const allocator = testing.allocator; + const template = "{% with nome=\"Lucas\" idade=30 %}Olá {{ nome }}, você tem {{ idade }} anos.{% endwith %}"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 1), nodes.len); + try testing.expect(nodes[0].type == .with_block); + const w = nodes[0].with.?; + try testing.expectEqual(@as(usize, 2), w.assignments.len); + try testing.expectEqual(@as(usize, 5), w.body.len); + try testing.expect(w.body[0].type == .text); +} + +test "parse now simples" { + const allocator = testing.allocator; + const template = "Data atual: {% now \"Y-m-d\" %} às {% now \"H:i\" %}"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 4), nodes.len); + + try testing.expect(nodes[0].type == .text); + try testing.expectEqualStrings("Data atual: ", nodes[0].text.?.content); + + try testing.expect(nodes[1].type == .now); + try testing.expectEqualStrings("Y-m-d", nodes[1].now.?.format); + + try testing.expect(nodes[2].type == .text); + try testing.expectEqualStrings(" às ", nodes[2].text.?.content); + + try testing.expect(nodes[3].type == .now); + try testing.expectEqualStrings("H:i", nodes[3].now.?.format); + +} + +test "parse now sem aspas" { + const allocator = testing.allocator; + const template = "{% now Y-m-d %}"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 1), nodes.len); + try testing.expect(nodes[0].type == .now); + try testing.expectEqualStrings("Y-m-d", nodes[0].now.?.format); +}