This commit is contained in:
Lucas F. 2026-01-03 12:27:27 -03:00
commit da8c1563c6
7 changed files with 935 additions and 0 deletions

411
src/parser.zig Normal file
View file

@ -0,0 +1,411 @@
const std = @import("std");
pub const NodeType = enum {
text,
variable,
tag,
if_block,
for_block,
};
pub const TextNode = struct {
content: []const u8,
};
pub const VariableNode = struct {
content: []const u8,
};
pub const TagNode = struct {
name: []const u8,
args: []const u8,
raw: []const u8,
};
pub const IfNode = struct {
condition: []const u8, // dupe esse sim
true_body: []Node,
false_body: []Node,
raw_open: []const u8, // slice original, NÃO free
raw_close: []const u8, // slice original, NÃO free
};
pub const ForNode = struct {
loop_var: []const u8,
iterable: []const u8,
body: []Node,
empty_body: []Node,
raw_open: []const u8,
raw_close: []const u8,
};
pub const Node = struct {
type: NodeType,
text: ?TextNode = null,
variable: ?VariableNode = null,
tag: ?TagNode = null,
@"if": ?IfNode = null,
@"for": ?ForNode = null, // <--- novo
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),
.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);
allocator.free(ib.true_body);
for (ib.false_body) |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);
allocator.free(fb.body);
for (fb.empty_body) |n| n.deinit(allocator);
allocator.free(fb.empty_body);
// raw_open e raw_close são slices originais não free
},
}
}
};
pub const Parser = struct {
template: []const u8,
pos: usize = 0,
pub fn init(template: []const u8) Parser {
return .{ .template = template };
}
fn advance(self: *Parser, n: usize) void {
self.pos += n;
if (self.pos > self.template.len) self.pos = self.template.len;
}
fn peek(self: Parser, comptime n: usize) ?[]const u8 {
if (self.pos + n > self.template.len) return null;
return self.template[self.pos .. self.pos + n];
}
fn skipWhitespace(self: *Parser) void {
while (self.pos < self.template.len and std.ascii.isWhitespace(self.template[self.pos])) : (self.advance(1)) {}
}
fn parseText(self: *Parser, allocator: std.mem.Allocator) !?Node {
const start = self.pos;
while (self.pos < self.template.len) {
if (self.peek(2)) |p| {
if (std.mem.eql(u8, p, "{{") or std.mem.eql(u8, p, "{%")) {
break;
}
}
self.advance(1);
}
if (self.pos == start) return null;
const content = try allocator.dupe(u8, self.template[start..self.pos]);
return Node{
.type = .text,
.text = .{ .content = content },
};
}
fn parseVariable(self: *Parser, allocator: std.mem.Allocator) !?Node {
if (self.peek(2)) |p| {
if (!std.mem.eql(u8, p, "{{")) return null;
} else return null;
self.advance(2);
self.skipWhitespace();
const content_start = self.pos;
while (self.pos < self.template.len) : (self.advance(1)) {
if (self.peek(2)) |p| {
if (std.mem.eql(u8, p, "}}")) break;
}
}
if (self.pos + 2 > self.template.len or !std.mem.eql(u8, self.template[self.pos .. self.pos + 2], "}}")) {
return error.UnclosedVariable;
}
const raw_content = self.template[content_start..self.pos];
const content = std.mem.trim(u8, raw_content, " \t\r\n");
const duped = try allocator.dupe(u8, content);
self.advance(2);
return Node{
.type = .variable,
.variable = .{ .content = duped },
};
}
fn parseTag(self: *Parser, allocator: std.mem.Allocator) !?Node {
if (self.peek(2)) |p| {
if (!std.mem.eql(u8, p, "{%")) return null;
} else return null;
const raw_start = self.pos;
self.advance(2);
self.skipWhitespace();
const content_start = self.pos;
while (self.pos < self.template.len) : (self.advance(1)) {
if (self.peek(2)) |p| {
if (std.mem.eql(u8, p, "%}")) break;
}
}
if (self.pos + 2 > self.template.len or !std.mem.eql(u8, self.template[self.pos .. self.pos + 2], "%}")) {
return error.UnclosedTag;
}
const raw_slice = self.template[raw_start .. self.pos + 2];
const inner = std.mem.trim(u8, self.template[content_start..self.pos], " \t\r\n");
const space_idx = std.mem.indexOfScalar(u8, inner, ' ') orelse inner.len;
const name_raw = inner[0..space_idx];
const args_raw = if (space_idx < inner.len) std.mem.trim(u8, inner[space_idx + 1 ..], " \t\r\n") else "";
const name = try allocator.dupe(u8, name_raw);
const args = try allocator.dupe(u8, args_raw);
self.advance(2);
return Node{
.type = .tag,
.tag = .{
.name = name,
.args = args,
.raw = raw_slice, // slice original, sem dupe
},
};
}
fn parseIfBlock(self: *Parser, allocator: std.mem.Allocator, condition: []const u8, raw_open: []const u8) !Node {
var true_body = std.ArrayList(Node){};
defer true_body.deinit(allocator);
var false_body = std.ArrayList(Node){};
defer false_body.deinit(allocator);
var current_body = &true_body;
var depth: usize = 1;
while (self.pos < self.template.len and depth > 0) {
if (try self.parseText(allocator)) |node| {
try current_body.append(allocator, node);
continue;
}
if (try self.parseVariable(allocator)) |node| {
try current_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, "if")) {
depth += 1;
try current_body.append(allocator, tag_node);
continue;
}
if (std.mem.eql(u8, tag_name, "endif")) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
// Libera name e args da tag endif
allocator.free(tag_node.tag.?.name);
allocator.free(tag_node.tag.?.args);
if (depth == 0) {
return Node{
.type = .if_block,
.@"if" = .{
.condition = condition,
.true_body = try true_body.toOwnedSlice(allocator),
.false_body = try false_body.toOwnedSlice(allocator),
.raw_open = raw_open,
.raw_close = raw_close,
},
};
}
// Se depth > 0, é endif aninhado adiciona como tag normal
try current_body.append(allocator, tag_node);
continue;
}
if (std.mem.eql(u8, tag_name, "else") and depth == 1) {
current_body = &false_body;
allocator.free(tag_node.tag.?.name);
allocator.free(tag_node.tag.?.args);
continue;
}
// Qualquer outra tag
try current_body.append(allocator, tag_node);
} else {
self.advance(1);
}
}
return error.UnclosedBlock;
}
fn parseForBlock(self: *Parser, allocator: std.mem.Allocator, loop_var: []const u8, iterable: []const u8, raw_open: []const u8) !Node {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
var empty_body = std.ArrayList(Node){};
defer empty_body.deinit(allocator);
var current_body = &body;
var depth: usize = 1;
while (self.pos < self.template.len and depth > 0) {
if (try self.parseText(allocator)) |node| {
try current_body.append(allocator, node);
continue;
}
if (try self.parseVariable(allocator)) |node| {
try current_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, "for")) {
depth += 1;
try current_body.append(allocator, tag_node);
continue;
}
if (std.mem.eql(u8, tag_name, "endfor")) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
if (depth == 0) {
// Libera name e args essa é a tag de fechamento final
allocator.free(tag_node.tag.?.name);
allocator.free(tag_node.tag.?.args);
return Node{
.type = .for_block,
.@"for" = .{
.loop_var = loop_var,
.iterable = iterable,
.body = try body.toOwnedSlice(allocator),
.empty_body = try empty_body.toOwnedSlice(allocator),
.raw_open = raw_open,
.raw_close = raw_close,
},
};
}
// depth > 0: endfor aninhado adiciona como tag normal
try current_body.append(allocator, tag_node);
continue;
}
if (std.mem.eql(u8, tag_name, "empty") and depth == 1) {
current_body = &empty_body;
allocator.free(tag_node.tag.?.name);
allocator.free(tag_node.tag.?.args);
continue;
}
try current_body.append(allocator, tag_node);
} else {
self.advance(1);
}
}
return error.UnclosedBlock;
}
pub fn parse(self: *Parser, allocator: std.mem.Allocator) ![]Node {
var list = std.ArrayList(Node){};
defer list.deinit(allocator);
while (self.pos < self.template.len) {
if (try self.parseTag(allocator)) |node| {
const tag_name = node.tag.?.name;
if (std.mem.eql(u8, tag_name, "if")) {
const condition_raw = node.tag.?.args;
const raw_open = node.tag.?.raw;
const condition = try allocator.dupe(u8, condition_raw);
// Libera apenas name e args da tag open
allocator.free(node.tag.?.name);
allocator.free(node.tag.?.args);
// NÃO chame node.deinit aqui raw_open ainda é usado
const if_node = try self.parseIfBlock(allocator, condition, raw_open);
try list.append(allocator, if_node);
continue;
}
if (std.mem.eql(u8, tag_name, "for")) {
const args = node.tag.?.args;
const raw_open = node.tag.?.raw;
const in_pos = std.mem.indexOf(u8, args, " in ") orelse return error.InvalidForSyntax;
const loop_var_raw = std.mem.trim(u8, args[0..in_pos], " \t");
const iterable_raw = std.mem.trim(u8, args[in_pos + 4 ..], " \t");
// DUPE ANTES DE LIBERAR!
const loop_var = try allocator.dupe(u8, loop_var_raw);
const iterable = try allocator.dupe(u8, iterable_raw);
// Agora sim, libera a tag open
allocator.free(node.tag.?.name);
allocator.free(node.tag.?.args);
const for_node = try self.parseForBlock(allocator, loop_var, iterable, raw_open);
try list.append(allocator, for_node);
continue;
}
// Para tags normais
try list.append(allocator, node);
continue;
}
if (try self.parseVariable(allocator)) |node| {
try list.append(allocator, node);
continue;
}
if (try self.parseText(allocator)) |node| {
try list.append(allocator, node);
continue;
}
self.advance(1);
}
return try list.toOwnedSlice(allocator);
}
};
pub fn parse(allocator: std.mem.Allocator, template: []const u8) ![]Node {
var p = Parser.init(template);
return try p.parse(allocator);
}