diff --git a/src/parser.zig b/src/parser.zig index f05eed6..c7d5ad2 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -9,9 +9,42 @@ pub const NodeType = enum { include, with_block, now, - extends, // <--- novo - block, // <--- novo - super, // <--- novo (para {{ block.super }}) + extends, + block, + super, + filter_block, + autoescape, + spaceless, +}; + +pub const AutoescapeNode = struct { + body: []Node, + raw_open: []const u8, + raw_close: []const u8, + enabled: bool, +}; + +pub const SpacelessNode = struct { + body: []Node, + raw_open: []const u8, + raw_close: []const u8, +}; + +pub const FilterBlockNode = struct { + filters: []const u8, // "upper|escape|truncatewords:30" + body: []Node, + raw_open: []const u8, + raw_close: []const u8, +}; + +pub const Filter = struct { + name: []const u8, + arg: ?[]const u8 = null, // null ou string com argumento +}; + +pub const VariableNode = struct { + expr: []const u8, + filters: []const Filter, }; pub const NowNode = struct { @@ -50,10 +83,6 @@ pub const TextNode = struct { content: []const u8, }; -pub const VariableNode = struct { - content: []const u8, -}; - pub const TagNode = struct { name: []const u8, args: []const u8, @@ -90,35 +119,43 @@ pub const Node = struct { extends: ?ExtendsNode = null, block: ?BlockNode = null, super: bool = false, // para {{ block.super }} + filter_block: ?FilterBlockNode = null, + autoescape: ?AutoescapeNode = null, + spaceless: ?SpacelessNode = null, pub fn deinit(self: Node, allocator: std.mem.Allocator) void { switch (self.type) { .text => if (self.text) |t| allocator.free(t.content), - .variable => if (self.variable) |v| allocator.free(v.content), + .variable => if (self.variable) |v| { + allocator.free(v.expr); + for (v.filters) |f| { + allocator.free(f.name); + if (f.arg) |a| allocator.free(a); + } + allocator.free(v.filters); + }, .tag => if (self.tag) |t| { allocator.free(t.name); allocator.free(t.args); }, .if_block => if (self.@"if") |ib| { allocator.free(ib.condition); - // NÃO free ib.raw_open - // NÃO free ib.raw_close - for (ib.true_body) |n| n.deinit(allocator); + const true_copy = ib.true_body; + for (true_copy) |n| n.deinit(allocator); allocator.free(ib.true_body); - for (ib.false_body) |n| n.deinit(allocator); + const false_copy = ib.false_body; + for (false_copy) |n| n.deinit(allocator); allocator.free(ib.false_body); }, .for_block => if (self.@"for") |fb| { allocator.free(fb.loop_var); allocator.free(fb.iterable); - for (fb.body) |n| n.deinit(allocator); + const body_copy = fb.body; + for (body_copy) |n| n.deinit(allocator); allocator.free(fb.body); - for (fb.empty_body) |n| n.deinit(allocator); + const empty_copy = fb.empty_body; + for (empty_copy) |n| n.deinit(allocator); 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| { @@ -126,22 +163,35 @@ pub const Node = struct { allocator.free(a.value_expr); } allocator.free(w.assignments); - for (w.body) |n| n.deinit(allocator); + const body_copy = w.body; + for (body_copy) |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); - }, - .extends => if (self.extends) |e| { - allocator.free(e.parent_name); }, .block => if (self.block) |b| { allocator.free(b.name); - for (b.body) |n| n.deinit(allocator); + const body_copy = b.body; + for (body_copy) |n| n.deinit(allocator); allocator.free(b.body); - // raw_open e raw_close são slices originais — não free }, + .filter_block => if (self.filter_block) |fb| { + allocator.free(fb.filters); + const body_copy = fb.body; + for (body_copy) |n| n.deinit(allocator); + allocator.free(fb.body); + }, + .autoescape => if (self.autoescape) |ae| { + const body_copy = ae.body; + for (body_copy) |n| n.deinit(allocator); + allocator.free(ae.body); + }, + .spaceless => if (self.spaceless) |sl| { + const body_copy = sl.body; + for (body_copy) |n| n.deinit(allocator); + allocator.free(sl.body); + }, + .include => if (self.include) |inc| allocator.free(inc.template_name), + .now => if (self.now) |n| allocator.free(n.format), + .extends => if (self.extends) |e| allocator.free(e.parent_name), .super => {}, } } @@ -169,16 +219,6 @@ 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, @@ -242,6 +282,185 @@ pub const Parser = struct { return try list.toOwnedSlice(allocator); } + fn parseAutoescapeBlock(self: *Parser, allocator: std.mem.Allocator, enabled: bool, raw_open: []const u8) !Node { + 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| { + try body.append(allocator, node); + continue; + } + + if (try self.parseVariable(allocator)) |node| { + 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, "autoescape")) { + depth += 1; + try body.append(allocator, tag_node); + continue; + } + + if (std.mem.eql(u8, tag_name, "endautoescape")) { + depth -= 1; + const raw_close = tag_node.tag.?.raw; + + allocator.free(tag_node.tag.?.name); + allocator.free(tag_node.tag.?.args); + + if (depth == 0) { + return Node{ + .type = .autoescape, + .autoescape = .{ + .enabled = enabled, + .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 parseSpacelessBlock(self: *Parser, allocator: std.mem.Allocator, raw_open: []const u8) !Node { + 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| { + // Adiciona texto (mesmo vazio — o renderer vai limpar depois) + try body.append(allocator, node); + continue; + } + + if (try self.parseVariable(allocator)) |node| { + 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, "spaceless")) { + depth += 1; + // Ignora a tag open aninhada + allocator.free(tag_node.tag.?.name); + allocator.free(tag_node.tag.?.args); + continue; + } + + if (std.mem.eql(u8, tag_name, "endspaceless")) { + depth -= 1; + const raw_close = tag_node.tag.?.raw; + + allocator.free(tag_node.tag.?.name); + allocator.free(tag_node.tag.?.args); + + if (depth == 0) { + return Node{ + .type = .spaceless, + .spaceless = .{ + .body = try body.toOwnedSlice(allocator), + .raw_open = raw_open, + .raw_close = raw_close, + }, + }; + } + + // depth > 0: endspaceless aninhado — IGNORA a tag + // NÃO adicione ao body + continue; + } + + try body.append(allocator, tag_node); + } else { + self.advance(1); + } + } + + return error.UnclosedBlock; + } + + fn parseFilterBlock(self: *Parser, allocator: std.mem.Allocator, filters_raw: []const u8, raw_open: []const u8) !Node { + 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| { + try body.append(allocator, node); + continue; + } + + if (try self.parseVariable(allocator)) |node| { + 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, "filter")) { + depth += 1; + try body.append(allocator, tag_node); + continue; + } + + if (std.mem.eql(u8, tag_name, "endfilter")) { + depth -= 1; + const raw_close = tag_node.tag.?.raw; + + allocator.free(tag_node.tag.?.name); + allocator.free(tag_node.tag.?.args); + + if (depth == 0) { + // const filters = try allocator.dupe(u8, filters_raw); + + return Node{ + .type = .filter_block, + .filter_block = .{ + .filters = filters_raw, // já duped + .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 parseBlockBlock(self: *Parser, allocator: std.mem.Allocator, name: []const u8, raw_open: []const u8) !Node { var body = std.ArrayList(Node){}; defer body.deinit(allocator); @@ -256,9 +475,9 @@ pub const Parser = struct { if (try self.parseVariable(allocator)) |node| { if (node.variable) |v| { - if (std.mem.eql(u8, v.content, "block.super")) { + if (std.mem.eql(u8, v.expr, "block.super")) { try body.append(allocator, Node{ .type = .super, .super = true }); - allocator.free(v.content); + allocator.free(v.expr); continue; } } @@ -321,7 +540,7 @@ pub const Parser = struct { } if (try self.parseVariable(allocator)) |node| { - std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.content}); + std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.expr}); try body.append(allocator, node); continue; } @@ -448,18 +667,20 @@ 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); self.skipWhitespace(); - const content_start = self.pos; + const expr_start = self.pos; while (self.pos < self.template.len) : (self.advance(1)) { if (self.peek(2)) |p| { if (std.mem.eql(u8, p, "}}")) break; + + std.debug.print("2.1 - fiz o peek de 2 em 2 até que achei {{{{\n", .{}); } } @@ -469,16 +690,45 @@ pub const Parser = struct { 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 full_expr = std.mem.trim(u8, self.template[expr_start..self.pos], " \t\r\n"); - const duped = try allocator.dupe(u8, content); self.advance(2); + // Separar expr e filtros + var filters = std.ArrayList(Filter){}; + defer filters.deinit(allocator); + + var expr_end = full_expr.len; + var pipe_pos = std.mem.lastIndexOfScalar(u8, full_expr, '|'); + while (pipe_pos) |pos| { + const filter_part = std.mem.trim(u8, full_expr[pos + 1 .. expr_end], " \t\r\n"); + expr_end = pos; + + const colon_pos = std.mem.indexOfScalar(u8, filter_part, ':'); + const filter_name = if (colon_pos) |cp| std.mem.trim(u8, filter_part[0..cp], " \t\r\n") else filter_part; + const filter_arg = if (colon_pos) |cp| std.mem.trim(u8, filter_part[cp + 1 ..], " \"") else null; + + try filters.append(allocator, .{ + .name = try allocator.dupe(u8, filter_name), + .arg = if (filter_arg) |a| try allocator.dupe(u8, a) else null, + }); + + pipe_pos = std.mem.lastIndexOfScalar(u8, full_expr[0..expr_end], '|'); + } + + const base_expr = std.mem.trim(u8, full_expr[0..expr_end], " \t\r\n"); + const duped_expr = try allocator.dupe(u8, base_expr); + + // Inverte os filters (porque usamos lastIndexOf) + std.mem.reverse(Filter, filters.items); + + std.debug.print("2.3 - meu conteúdo:\n - expr: \'{s}\' \n - filters: {any}\n", .{ duped_expr, filters }); return Node{ .type = .variable, - .variable = .{ .content = duped }, + .variable = .{ + .expr = duped_expr, + .filters = try filters.toOwnedSlice(allocator), + }, }; } @@ -547,7 +797,7 @@ pub const Parser = struct { } if (try self.parseVariable(allocator)) |node| { - std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.content}); + std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.expr}); try current_body.append(allocator, node); continue; } @@ -628,7 +878,7 @@ pub const Parser = struct { } if (try self.parseVariable(allocator)) |node| { - std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.content}); + std.debug.print("2.3 - Encontrei uma variável: {s}\n", .{node.variable.?.expr}); try current_body.append(allocator, node); continue; } @@ -706,6 +956,42 @@ pub const Parser = struct { std.debug.print("3.1 - meu tag name é: {s}\n", .{tag_name}); + if (std.mem.eql(u8, tag_name, "autoescape")) { + const args = std.mem.trim(u8, node.tag.?.args, " \t\r\n"); + const raw_open = node.tag.?.raw; + + const enabled = if (std.mem.eql(u8, args, "on")) + true + else if (std.mem.eql(u8, args, "off")) + false + else + return error.InvalidAutoescapeArgument; + + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + + std.debug.print("3.0 - na real sou um autoescape\n", .{}); + std.debug.print("===================================\n", .{}); + + const ae_node = try self.parseAutoescapeBlock(allocator, enabled, raw_open); + try list.append(allocator, ae_node); + continue; + } + + if (std.mem.eql(u8, tag_name, "spaceless")) { + const raw_open = node.tag.?.raw; + + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + + std.debug.print("3.0 - na real sou um spaceless\n", .{}); + std.debug.print("===================================\n", .{}); + + const spaceless_node = try self.parseSpacelessBlock(allocator, raw_open); + try list.append(allocator, spaceless_node); + continue; + } + if (std.mem.eql(u8, tag_name, "comment")) { std.debug.print("3.0 - na real sou um comentário\n", .{}); std.debug.print("===================================\n", .{}); @@ -715,6 +1001,24 @@ pub const Parser = struct { continue; } + if (std.mem.eql(u8, tag_name, "filter")) { + const filters_raw = node.tag.?.args; + const raw_open = node.tag.?.raw; + + // DUPE O FILTERS IMEDIATAMENTE + const duped_filters = try allocator.dupe(u8, filters_raw); + + // Agora libera a tag open + allocator.free(node.tag.?.name); + allocator.free(node.tag.?.args); + std.debug.print("3.0 - na real sou um filter\n", .{}); + std.debug.print("===================================\n", .{}); + + const filter_node = try self.parseFilterBlock(allocator, duped_filters, raw_open); + try list.append(allocator, filter_node); + continue; + } + if (std.mem.eql(u8, tag_name, "extends")) { const parent = std.mem.trim(u8, node.tag.?.args, " \t\""); @@ -868,7 +1172,8 @@ pub const Parser = struct { 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("4.0 - content: \'{s}\'\n", .{node.variable.?.expr}); + std.debug.print("4.1 - filters: \'{any}\'\n", .{node.variable.?.filters}); std.debug.print("===================================\n", .{}); try list.append(allocator, node); continue; diff --git a/src/parser_test.zig b/src/parser_test.zig index aa5c88d..2b00150 100644 --- a/src/parser_test.zig +++ b/src/parser_test.zig @@ -2,329 +2,329 @@ const std = @import("std"); const testing = std.testing; const parser = @import("parser.zig"); -// test "parse texto simples" { -// const allocator = testing.allocator; -// const template = "Olá mundo!"; -// 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("Olá mundo!", nodes[0].text.?.content); -// } -// -// test "parse variável simples" { -// const allocator = testing.allocator; -// const template = "Olá {{ nome }}!"; -// const nodes = try parser.parse(allocator, template); -// defer { -// for (nodes) |node| { -// node.deinit(allocator); -// } -// allocator.free(nodes); -// } -// -// try testing.expectEqual(@as(usize, 3), nodes.len); -// -// try testing.expect(nodes[0].type == .text); -// try testing.expectEqualStrings("Olá ", nodes[0].text.?.content); -// -// try testing.expect(nodes[1].type == .variable); -// try testing.expectEqualStrings("nome", nodes[1].variable.?.content); -// -// try testing.expect(nodes[2].type == .text); -// try testing.expectEqualStrings("!", nodes[2].text.?.content); -// } -// -// test "parse variável com espaços" { -// const allocator = testing.allocator; -// const template = "{{ espacos }}"; -// 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 == .variable); -// try testing.expectEqualStrings("espacos", nodes[0].variable.?.content); -// } -// -// test "parse tag simples" { -// const allocator = testing.allocator; -// const template = "Antes {% minha_tag %} Depois"; -// const nodes = try parser.parse(allocator, template); -// defer { -// for (nodes) |node| { -// node.deinit(allocator); -// } -// allocator.free(nodes); -// } -// -// try testing.expectEqual(@as(usize, 3), nodes.len); -// -// try testing.expect(nodes[0].type == .text); -// try testing.expectEqualStrings("Antes ", nodes[0].text.?.content); -// -// try testing.expect(nodes[1].type == .tag); -// try testing.expectEqualStrings("minha_tag", nodes[1].tag.?.name); -// try testing.expectEqualStrings("", nodes[1].tag.?.args); -// -// try testing.expect(nodes[2].type == .text); -// try testing.expectEqualStrings(" Depois", nodes[2].text.?.content); -// } -// -// test "parse if block básico" { -// const allocator = testing.allocator; -// const template = "{% if usuario.logado %}Bem-vindo!{% endif %}"; -// 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 == .if_block); -// -// const ib = nodes[0].@"if".?; -// try testing.expectEqualStrings("usuario.logado", ib.condition); -// try testing.expectEqual(@as(usize, 1), ib.true_body.len); -// try testing.expect(nodes[0].@"if".?.true_body[0].type == .text); -// try testing.expectEqualStrings("Bem-vindo!", ib.true_body[0].text.?.content); -// try testing.expectEqual(@as(usize, 0), ib.false_body.len); -// } -// -// test "parse if block sem else" { -// const allocator = testing.allocator; -// const template = "{% if cond %}Verdadeiro{% endif %}"; -// 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 == .if_block); -// const ib = nodes[0].@"if".?; -// try testing.expectEqualStrings("cond", ib.condition); -// try testing.expectEqual(@as(usize, 1), ib.true_body.len); -// try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content); -// try testing.expectEqual(@as(usize, 0), ib.false_body.len); -// } -// -// test "parse if block com else" { -// const allocator = testing.allocator; -// const template = "{% if cond %}Verdadeiro{% else %}Falso{% endif %}"; -// 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 == .if_block); -// const ib = nodes[0].@"if".?; -// try testing.expectEqualStrings("cond", ib.condition); -// try testing.expectEqual(@as(usize, 1), ib.true_body.len); -// try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content); -// try testing.expectEqual(@as(usize, 1), ib.false_body.len); -// try testing.expectEqualStrings("Falso", ib.false_body[0].text.?.content); -// } -// -// test "parse for block sem empty" { -// const allocator = testing.allocator; -// const template = "{% for item in lista %}Item: {{ item }}{% endfor %}"; -// 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 == .for_block); -// const fb = nodes[0].@"for".?; -// try testing.expectEqualStrings("item", fb.loop_var); -// try testing.expectEqualStrings("lista", fb.iterable); -// try testing.expectEqual(@as(usize, 2), fb.body.len); // <--- corrigido: 2 nós -// try testing.expectEqual(@as(usize, 0), fb.empty_body.len); -// -// try testing.expect(fb.body[0].type == .text); -// try testing.expectEqualStrings("Item: ", fb.body[0].text.?.content); -// try testing.expect(fb.body[1].type == .variable); -// try testing.expectEqualStrings("item", fb.body[1].variable.?.content); -// } -// -// test "parse for block com empty" { -// const allocator = testing.allocator; -// const template = "{% for item in lista %}Tem{% empty %}Vazio{% endfor %}"; -// 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 == .for_block); -// const fb = nodes[0].@"for".?; -// try testing.expectEqual(@as(usize, 1), fb.body.len); -// try testing.expectEqualStrings("Tem", fb.body[0].text.?.content); -// 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); -// } -// -// test "parse extends" { -// const allocator = testing.allocator; -// const template = "{% extends \"base.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 == .extends); -// try testing.expectEqualStrings("base.zdt", nodes[0].extends.?.parent_name); -// } -// -// test "parse block simples" { -// const allocator = testing.allocator; -// const template = "{% block titulo %}Meu Título{% endblock %}"; -// 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 == .block); -// const b = nodes[0].block.?; -// try testing.expectEqualStrings("titulo", b.name); -// try testing.expectEqual(@as(usize, 1), b.body.len); -// try testing.expectEqualStrings("Meu Título", b.body[0].text.?.content); -// } -// +test "parse texto simples" { + const allocator = testing.allocator; + const template = "Olá mundo!"; + 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("Olá mundo!", nodes[0].text.?.content); +} + +test "parse variável simples" { + const allocator = testing.allocator; + const template = "Olá {{ nome }}!"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| { + node.deinit(allocator); + } + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 3), nodes.len); + + try testing.expect(nodes[0].type == .text); + try testing.expectEqualStrings("Olá ", nodes[0].text.?.content); + + try testing.expect(nodes[1].type == .variable); + try testing.expectEqualStrings("nome", nodes[1].variable.?.expr); + + try testing.expect(nodes[2].type == .text); + try testing.expectEqualStrings("!", nodes[2].text.?.content); +} + +test "parse variável com espaços" { + const allocator = testing.allocator; + const template = "{{ espacos }}"; + 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 == .variable); + try testing.expectEqualStrings("espacos", nodes[0].variable.?.expr); +} + +test "parse tag simples" { + const allocator = testing.allocator; + const template = "Antes {% minha_tag %} Depois"; + const nodes = try parser.parse(allocator, template); + defer { + for (nodes) |node| { + node.deinit(allocator); + } + allocator.free(nodes); + } + + try testing.expectEqual(@as(usize, 3), nodes.len); + + try testing.expect(nodes[0].type == .text); + try testing.expectEqualStrings("Antes ", nodes[0].text.?.content); + + try testing.expect(nodes[1].type == .tag); + try testing.expectEqualStrings("minha_tag", nodes[1].tag.?.name); + try testing.expectEqualStrings("", nodes[1].tag.?.args); + + try testing.expect(nodes[2].type == .text); + try testing.expectEqualStrings(" Depois", nodes[2].text.?.content); +} + +test "parse if block básico" { + const allocator = testing.allocator; + const template = "{% if usuario.logado %}Bem-vindo!{% endif %}"; + 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 == .if_block); + + const ib = nodes[0].@"if".?; + try testing.expectEqualStrings("usuario.logado", ib.condition); + try testing.expectEqual(@as(usize, 1), ib.true_body.len); + try testing.expect(nodes[0].@"if".?.true_body[0].type == .text); + try testing.expectEqualStrings("Bem-vindo!", ib.true_body[0].text.?.content); + try testing.expectEqual(@as(usize, 0), ib.false_body.len); +} + +test "parse if block sem else" { + const allocator = testing.allocator; + const template = "{% if cond %}Verdadeiro{% endif %}"; + 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 == .if_block); + const ib = nodes[0].@"if".?; + try testing.expectEqualStrings("cond", ib.condition); + try testing.expectEqual(@as(usize, 1), ib.true_body.len); + try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content); + try testing.expectEqual(@as(usize, 0), ib.false_body.len); +} + +test "parse if block com else" { + const allocator = testing.allocator; + const template = "{% if cond %}Verdadeiro{% else %}Falso{% endif %}"; + 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 == .if_block); + const ib = nodes[0].@"if".?; + try testing.expectEqualStrings("cond", ib.condition); + try testing.expectEqual(@as(usize, 1), ib.true_body.len); + try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content); + try testing.expectEqual(@as(usize, 1), ib.false_body.len); + try testing.expectEqualStrings("Falso", ib.false_body[0].text.?.content); +} + +test "parse for block sem empty" { + const allocator = testing.allocator; + const template = "{% for item in lista %}Item: {{ item }}{% endfor %}"; + 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 == .for_block); + const fb = nodes[0].@"for".?; + try testing.expectEqualStrings("item", fb.loop_var); + try testing.expectEqualStrings("lista", fb.iterable); + try testing.expectEqual(@as(usize, 2), fb.body.len); // <--- corrigido: 2 nós + try testing.expectEqual(@as(usize, 0), fb.empty_body.len); + + try testing.expect(fb.body[0].type == .text); + try testing.expectEqualStrings("Item: ", fb.body[0].text.?.content); + try testing.expect(fb.body[1].type == .variable); + try testing.expectEqualStrings("item", fb.body[1].variable.?.expr); +} + +test "parse for block com empty" { + const allocator = testing.allocator; + const template = "{% for item in lista %}Tem{% empty %}Vazio{% endfor %}"; + 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 == .for_block); + const fb = nodes[0].@"for".?; + try testing.expectEqual(@as(usize, 1), fb.body.len); + try testing.expectEqualStrings("Tem", fb.body[0].text.?.content); + 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); +} + +test "parse extends" { + const allocator = testing.allocator; + const template = "{% extends \"base.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 == .extends); + try testing.expectEqualStrings("base.zdt", nodes[0].extends.?.parent_name); +} + +test "parse block simples" { + const allocator = testing.allocator; + const template = "{% block titulo %}Meu Título{% endblock %}"; + 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 == .block); + const b = nodes[0].block.?; + try testing.expectEqualStrings("titulo", b.name); + try testing.expectEqual(@as(usize, 1), b.body.len); + try testing.expectEqualStrings("Meu Título", b.body[0].text.?.content); +} + test "parse block com super" { const allocator = testing.allocator; const template = "{% block conteudo %}{{ block.super }} Conteúdo filho{% endblock %}"; @@ -342,3 +342,132 @@ test "parse block com super" { // try testing.expectEqualStrings("conteudo", b.name); try testing.expectEqualStrings(" Conteúdo filho", b.body[1].text.?.content); } + + +test "parse filter block simples" { + const allocator = testing.allocator; + const template = "{% filter upper %}olá mundo{% endfilter %}"; + 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 == .filter_block); + const fb = nodes[0].filter_block.?; + try testing.expectEqualStrings("upper", fb.filters); // correto + try testing.expectEqual(@as(usize, 1), fb.body.len); + try testing.expectEqualStrings("olá mundo", fb.body[0].text.?.content); +} + +test "parse filter block com múltiplos filtros" { + const allocator = testing.allocator; + const template = "{% filter upper|escape %}Conteúdo negrito{% endfilter %}"; + 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 == .filter_block); + const fb = nodes[0].filter_block.?; + try testing.expectEqualStrings("upper|escape", fb.filters); + try testing.expectEqual(@as(usize, 1), fb.body.len); + try testing.expectEqualStrings("Conteúdo negrito", fb.body[0].text.?.content); +} + +test "parse variable com filtros" { + const allocator = testing.allocator; + const template = "{{ nome|upper|default:\"Visitante\" }}"; + 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 == .variable); + const v = nodes[0].variable.?; + try testing.expectEqualStrings("nome", v.expr); + try testing.expectEqual(@as(usize, 2), v.filters.len); + try testing.expectEqualStrings("upper", v.filters[0].name); + try testing.expect(v.filters[0].arg == null); + try testing.expectEqualStrings("default", v.filters[1].name); + try testing.expectEqualStrings("Visitante", v.filters[1].arg.?); +} + +test "parse autoescape on" { + const allocator = testing.allocator; + const template = "{% autoescape on %}Texto negrito{% endautoescape %}"; + 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 == .autoescape); + const ae = nodes[0].autoescape.?; + try testing.expect(ae.enabled == true); + try testing.expectEqual(@as(usize, 1), ae.body.len); + try testing.expectEqualStrings("Texto negrito", ae.body[0].text.?.content); +} + +test "parse autoescape off" { + const allocator = testing.allocator; + const template = "{% autoescape off %}Texto negrito{% endautoescape %}"; + 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 == .autoescape); + const ae = nodes[0].autoescape.?; + try testing.expect(ae.enabled == false); + try testing.expectEqual(@as(usize, 1), ae.body.len); +} + +test "parse spaceless simples" { + const allocator = testing.allocator; + const template = "{% spaceless %}

Texto com espaços

{% endspaceless %}"; + 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 == .spaceless); + const sl = nodes[0].spaceless.?; + try testing.expectEqual(@as(usize, 1), sl.body.len); + try testing.expectEqualStrings("

Texto com espaços

", sl.body[0].text.?.content); +} + +test "parse spaceless aninhado" { + const allocator = testing.allocator; + const template = "{% spaceless %}Outer {% spaceless %}Inner{% endspaceless %} Outer{% endspaceless %}"; + 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 == .spaceless); + const sl = nodes[0].spaceless.?; + for (sl.body) |b| { + std.debug.print("type: {s}\n", .{@tagName(b.type)}); + if (b.type == .spaceless) { + std.debug.print(" - tag -> spaceless\n", .{}); + } + if(b.type == .text) { + std.debug.print(" - text -> {s}\n", .{b.text.?.content}); + } + std.debug.print("----------\n", .{}); + } + try testing.expectEqual(@as(usize, 3), sl.body.len); +} diff --git a/todo.md b/todo.md index 390b434..f28dbda 100644 --- a/todo.md +++ b/todo.md @@ -7,7 +7,7 @@ - [ ] cycle - [ ] debug - [x] extends -- [ ] filter +- [x] filter - [ ] firstof - [x] for - [x] if @@ -89,3 +89,16 @@ - [ ] wordwrap - [ ] yesno +___ + +## Doing + +- [x] filter — super útil (ex.: {{ var|upper }}) +- [x] autoescape — segurança importante +- [x] spaceless — remove espaços em branco +- [ ] verbatim — como raw +- [ ] url — reverse de URLs (quando tiver routing) +- [ ] cycle — alternar valores em loop +- [ ] firstof — fallback de variáveis +- [ ] load — para custom tags/filters (futuro) +- [ ] csrf_token — quando tiver web