udate: add include, with_block and now into parser

This commit is contained in:
Lucas F. 2026-01-03 15:08:48 -03:00
parent da8c1563c6
commit 3019009325
2 changed files with 473 additions and 1 deletions

View file

@ -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ê tem uma melhor, use ela
// // Por enquanto, 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 %}
// estamos após o %}, então 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);
}
};

View file

@ -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);
}