update: add extends, block, super, filter_block, autoescape and spaceless

This commit is contained in:
Lucas F. 2026-01-03 18:25:09 -03:00
parent 261c02f59b
commit 0192ad0b64
3 changed files with 822 additions and 375 deletions

View file

@ -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ê tem uma melhor, use ela
// // Por enquanto, 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, // 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;

View file

@ -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 <b>negrito</b>{% 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 <b>negrito</b>", 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 <b>negrito</b>{% 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 <b>negrito</b>", ae.body[0].text.?.content);
}
test "parse autoescape off" {
const allocator = testing.allocator;
const template = "{% autoescape off %}Texto <b>negrito</b>{% 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 %}<p> Texto com espaços </p>{% 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("<p> Texto com espaços </p>", 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);
}

15
todo.md
View file

@ -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