diff --git a/src/cache_bkp.zig b/src/cache_bkp.zig
new file mode 100644
index 0000000..c701b6d
--- /dev/null
+++ b/src/cache_bkp.zig
@@ -0,0 +1,51 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const parser = @import("parser.zig");
+
+pub const TemplateCache = struct {
+ allocator: Allocator,
+ cache: std.StringHashMapUnmanaged([]parser.Node),
+
+ pub fn init(allocator: Allocator) TemplateCache {
+ return .{
+ .allocator = allocator,
+ .cache = .{},
+ };
+ }
+
+ pub fn deinit(self: *TemplateCache) void {
+ var it = self.cache.iterator();
+ while (it.next()) |entry| {
+ self.allocator.free(entry.key_ptr.*);
+ for (entry.value_ptr.*) |node| node.deinit(self.allocator);
+ self.allocator.free(entry.value_ptr.*);
+ }
+ self.cache.deinit(self.allocator);
+ }
+
+ pub fn get(self: *const TemplateCache, key: []const u8) ?[]parser.Node {
+ return self.cache.get(key);
+ }
+
+ pub fn add(self: *TemplateCache, key: []const u8, nodes: []parser.Node) !void {
+ const key_copy = try self.allocator.dupe(u8, key);
+ errdefer self.allocator.free(key_copy);
+
+ try self.cache.put(self.allocator, key_copy, nodes);
+ }
+
+ pub fn invalidate(self: *TemplateCache, key: []const u8) void {
+ if (self.cache.getEntry(key)) |entry| {
+ self.allocator.free(entry.key_ptr.*);
+ for (entry.value_ptr.*) |node| node.deinit(self.allocator);
+ self.allocator.free(entry.value_ptr.*);
+ _ = self.cache.remove(key);
+ }
+ }
+
+ pub fn clear(self: *TemplateCache) void {
+ self.deinit();
+ self.cache = .{};
+ }
+};
diff --git a/src/context_proposta.zig b/src/context_proposta.zig
new file mode 100644
index 0000000..9e15f8c
--- /dev/null
+++ b/src/context_proposta.zig
@@ -0,0 +1,81 @@
+const std = @import("std");
+
+pub const Value = union(enum) {
+ null,
+ bool: bool,
+ int: i64,
+ float: f64,
+ string: []const u8,
+ list: []Value,
+ dict: std.HashMapUnmanaged([]const u8, Value, std.hash_map.StringContext, 80),
+ struct_: std.HashMapUnmanaged([]const u8, Value, std.hash_map.StringContext, 80),
+
+ pub fn deinit(self: *Value, allocator: std.mem.Allocator) void {
+ switch (self.*) {
+ .string => allocator.free(self.string),
+ .list => {
+ for (self.list) |*v| v.deinit(allocator);
+ allocator.free(self.list);
+ },
+ .dict => {
+ var iter = self.dict.iterator();
+ while (iter.next()) |entry| {
+ allocator.free(entry.key_ptr.*);
+ entry.value_ptr.deinit(allocator);
+ }
+ self.dict.deinit(allocator);
+ },
+ .struct_ => {
+ var iter = self.struct_.iterator();
+ while (iter.next()) |entry| {
+ allocator.free(entry.key_ptr.*);
+ entry.value_ptr.deinit(allocator);
+ }
+ self.struct_.deinit(allocator);
+ },
+ else => {},
+ }
+ }
+};
+
+pub const Context = struct {
+ allocator: std.mem.Allocator,
+ map: std.HashMapUnmanaged([]const u8, Value, std.hash_map.StringContext, 80),
+ parent: ?*Context = null,
+
+ pub fn init(allocator: std.mem.Allocator) Context {
+ return .{
+ .allocator = allocator,
+ .map = .{},
+ };
+ }
+
+ pub fn deinit(self: *Context) void {
+ var iter = self.map.iterator();
+ while (iter.next()) |entry| {
+ allocator.free(entry.key_ptr.*);
+ entry.value_ptr.deinit(self.allocator);
+ }
+ self.map.deinit(self.allocator);
+ if (self.parent) |p| p.deinit();
+ }
+
+ pub fn get(self: *const Context, key: []const u8) ?Value {
+ return self.map.get(key) or (self.parent orelse return null).get(key);
+ }
+
+ pub fn set(self: *Context, key: []const u8, value: Value) !void {
+ const duped_key = try self.allocator.dupe(u8, key);
+ try self.map.put(self.allocator, duped_key, value);
+ }
+
+ pub fn pushScope(self: *Context) *Context {
+ var child = Context.init(self.allocator);
+ child.parent = self;
+ return &child;
+ }
+
+ pub fn popScope(self: *Context) void {
+ self.deinit(); // libera o escopo atual
+ }
+};
diff --git a/src/old/renderer.zig b/src/old/renderer.zig
new file mode 100644
index 0000000..77a86c9
--- /dev/null
+++ b/src/old/renderer.zig
@@ -0,0 +1,386 @@
+const std = @import("std");
+
+const builtin_filters = @import("filters.zig").builtin_filters;
+const Context = @import("context.zig").Context;
+const FilterError = @import("filters.zig").FilterError;
+const Value = @import("context.zig").Value;
+
+pub const RenderError = FilterError || error{
+ OutOfMemory,
+ InvalidTemplate,
+ BlockNotFound,
+ CircularExtends,
+ FileNotFound,
+ AccessDenied,
+ FileTooBig,
+ NoSpaceLeft,
+ Unexpected,
+};
+
+const TemplateBlock = struct {
+ name: []const u8,
+ content: []const u8,
+};
+
+const TemplateParser = struct {
+ template: []const u8,
+ pos: usize = 0,
+ last_position: usize = 0,
+
+ const Block = struct {
+ kind: enum { text, variable, tag },
+ content: []const u8,
+ };
+
+ fn init(template: []const u8) TemplateParser {
+ return .{ .template = template };
+ }
+
+ fn nextBlock(self: *TemplateParser) ?Block {
+ if (self.pos >= self.template.len) return null;
+
+ const start = self.pos;
+
+ if (std.mem.startsWith(u8, self.template[self.pos..], "{{")) {
+ self.pos += 2;
+ const end = std.mem.indexOfPos(u8, self.template, self.pos, "}}") orelse self.template.len;
+ self.pos = end + 2;
+ return .{ .kind = .variable, .content = self.template[start + 2 .. end] };
+ } else if (std.mem.startsWith(u8, self.template[self.pos..], "{%")) {
+ self.pos += 2;
+ const end = std.mem.indexOfPos(u8, self.template, self.pos, "%}") orelse self.template.len;
+ self.pos = end + 2;
+ return .{ .kind = .tag, .content = self.template[start + 2 .. end] };
+ } else {
+ const end = std.mem.indexOfAnyPos(u8, self.template, self.pos, "{%") orelse self.template.len;
+ self.pos = end;
+ return .{ .kind = .text, .content = self.template[start..end] };
+ }
+ }
+
+ fn skipTo(self: *TemplateParser, tag: []const u8) void {
+ while (self.nextBlock()) |block| {
+ if (block.kind == .tag) {
+ const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
+ if (std.mem.eql(u8, trimmed, tag)) {
+ break;
+ }
+ }
+ }
+ }
+
+ fn reset(self: *TemplateParser) void {
+ self.pos = 0;
+ }
+
+ fn resetToLastPosition(self: *TemplateParser) void {
+ self.pos = self.last_position;
+ }
+};
+
+pub const Renderer = struct {
+ context: *Context,
+ blocks: std.ArrayList(TemplateBlock),
+ allocator: std.mem.Allocator,
+
+ pub fn init(context: *Context) Renderer {
+ return .{
+ .context = context,
+ .blocks = std.ArrayList(TemplateBlock){},
+ .allocator = context.allocator(),
+ };
+ }
+
+ pub fn deinit(self: *Renderer) void {
+ for (self.blocks.items) |block| {
+ self.allocator.free(block.name);
+ }
+ self.blocks.deinit();
+ }
+
+ pub fn render(self: *Renderer, template_path: []const u8, writer: anytype) RenderError!void {
+ const max_size = 10 * 1024 * 1024; // 10MB
+ const template = std.fs.cwd().readFileAlloc(self.allocator, template_path, max_size) catch |err| switch (err) {
+ error.FileNotFound => return RenderError.FileNotFound,
+ error.AccessDenied => return RenderError.AccessDenied,
+ error.FileTooBig => return RenderError.FileTooBig,
+ error.NoSpaceLeft => return RenderError.NoSpaceLeft,
+ error.OutOfMemory => return RenderError.OutOfMemory,
+ else => return RenderError.Unexpected,
+ };
+ defer self.allocator.free(template);
+
+ try self.renderTemplate(template, writer);
+ }
+
+ pub fn renderString(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
+ try self.renderTemplate(template, writer);
+ }
+
+ fn renderTemplate(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
+ var parser = TemplateParser.init(template);
+
+ var extends_path: ?[]const u8 = null;
+
+ while (parser.nextBlock()) |block| {
+ const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
+ if (block.kind == .tag) {
+ if (std.mem.startsWith(u8, trimmed, "extends ")) {
+ const quoted = trimmed["extends ".len..];
+ extends_path = std.mem.trim(u8, quoted, " \"'");
+ } else if (std.mem.startsWith(u8, trimmed, "block ")) {
+ const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
+ const name_copy = try self.allocator.dupe(u8, block_name);
+
+ // Agora coletamos o conteúdo real do block
+ // Começa **após** o fechamento da tag de abertura
+ const block_start = parser.pos; // já está após o %}
+
+ // Avança até o endblock
+ parser.skipTo("endblock");
+
+ // O conteúdo vai do início do corpo até o início do endblock
+ const endblock_start = parser.pos;
+ const block_content = template[block_start..endblock_start];
+
+ try self.blocks.append(self.allocator, .{ .name = name_copy, .content = block_content });
+ }
+ }
+ }
+
+ if (extends_path) |path| {
+ const max_size = 10 * 1024 * 1024;
+ const base_template = std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) {
+ error.FileNotFound => return RenderError.FileNotFound,
+ error.AccessDenied => return RenderError.AccessDenied,
+ error.FileTooBig => return RenderError.FileTooBig,
+ error.NoSpaceLeft => return RenderError.NoSpaceLeft,
+ error.OutOfMemory => return RenderError.OutOfMemory,
+ else => return RenderError.Unexpected,
+ };
+ defer self.allocator.free(base_template);
+
+ var base_parser = TemplateParser.init(base_template);
+ try self.renderWithInheritance(&base_parser, writer);
+ } else {
+ parser.reset();
+ try self.renderBlocks(&parser, writer);
+ }
+ }
+
+ fn renderWithInheritance(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag) {
+ const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
+ if (std.mem.startsWith(u8, trimmed, "block ")) {
+ const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
+ if (self.findBlock(block_name)) |child| {
+ try writer.writeAll(child.content);
+ parser.skipTo("endblock");
+ } else {
+ try self.renderBlockContent(parser, writer);
+ }
+ } else {
+ try self.renderTag(block.content, parser, writer);
+ }
+ } else {
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => unreachable,
+ }
+ }
+ }
+ }
+
+ fn findBlock(self: *Renderer, name: []const u8) ?TemplateBlock {
+ for (self.blocks.items) |b| {
+ if (std.mem.eql(u8, b.name, name)) return b;
+ }
+ return null;
+ }
+
+ fn renderBlockContent(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endblock")) {
+ break;
+ }
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ fn renderBlocks(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ /// Renderiza uma expressão: var ou var|filtro|...
+ fn renderExpression(self: *Renderer, expr: []const u8, writer: anytype) !void {
+ var parts = std.mem.splitScalar(u8, expr, '|');
+ var var_name = parts.next() orelse return;
+ var_name = std.mem.trim(u8, var_name, " \t\r\n");
+
+ var value = self.context.get(var_name) orelse Value.null;
+ var is_safe = false;
+
+ while (parts.next()) |part| {
+ const trimmed = std.mem.trim(u8, part, " \t\r\n");
+ if (trimmed.len == 0) continue;
+
+ const colon_pos = std.mem.indexOf(u8, trimmed, ":");
+ const filter_name = if (colon_pos) |c| trimmed[0..c] else trimmed;
+ const arg_str = if (colon_pos) |c| std.mem.trim(u8, trimmed[c + 1 ..], " \t\r\n") else null;
+
+ if (std.mem.eql(u8, filter_name, "safe")) {
+ is_safe = true;
+ continue;
+ }
+
+ const filter_fn = builtin_filters.get(filter_name) orelse continue;
+
+ const arg = if (arg_str) |a| blk: {
+ if (std.mem.eql(u8, a, "null")) break :blk Value.null;
+ if (std.fmt.parseInt(i64, a, 10)) |num| break :blk Value{ .int = num } else |_| {}
+ break :blk Value{ .string = a };
+ } else null;
+
+ value = try filter_fn(self.allocator, value, arg);
+ }
+
+ // Autoescape: escapa se não for safe
+ const str = if (is_safe) blk: {
+ break :blk try self.valueToString(value);
+ } else blk: {
+ const escaped = try self.escapeHtml(value);
+ break :blk try self.valueToString(escaped);
+ };
+
+ try writer.writeAll(str);
+ }
+
+ fn renderTag(self: *Renderer, tag_content: []const u8, parser: *TemplateParser, writer: anytype) !void {
+ const trimmed = std.mem.trim(u8, tag_content, " \t\r\n");
+
+ if (std.mem.startsWith(u8, trimmed, "if ")) {
+ try self.renderIf(trimmed["if ".len..], parser, writer);
+ } else if (std.mem.startsWith(u8, trimmed, "for ")) {
+ try self.renderFor(trimmed["for ".len..], parser, writer);
+ } else if (std.mem.eql(u8, trimmed, "endif") or std.mem.eql(u8, trimmed, "endfor")) {
+ // Nada a fazer — o bloco já foi consumido no if/for
+ } else {
+ // Tag desconhecida — ignora
+ }
+ }
+
+ fn renderIf(self: *Renderer, condition_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
+ const condition = try self.evaluateCondition(condition_expr);
+ if (condition) {
+ try self.renderBlocksUntilEndif(parser, writer);
+ } else {
+ parser.skipTo("endif"); // sem try — retorna void
+ }
+ }
+
+ fn renderFor(self: *Renderer, for_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
+ var parts = std.mem.splitSequence(u8, for_expr, " in ");
+ const item_name = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
+ const list_expr = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
+
+ const list_value = self.context.get(list_expr) orelse Value.null;
+ const list = switch (list_value) {
+ .list => |l| l,
+ else => return,
+ };
+
+ for (list) |item| {
+ try self.context.set(item_name, item);
+ try self.renderBlocksUntilEndfor(parser, writer);
+ parser.resetToLastPosition();
+ }
+ }
+
+ fn evaluateCondition(self: *Renderer, expr: []const u8) !bool {
+ const value = self.context.get(expr) orelse Value.null;
+ return switch (value) {
+ .bool => |b| b,
+ .int => |i| i != 0,
+ .float => |f| f != 0.0,
+ .string => |s| s.len > 0,
+ .list => |l| l.len > 0,
+ .dict => |d| d.count() > 0,
+ .null => false,
+ };
+ }
+
+ fn renderBlocksUntilEndif(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endif")) {
+ break;
+ }
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ fn renderBlocksUntilEndfor(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ const start_pos = parser.pos;
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endfor")) {
+ parser.last_position = start_pos;
+ break;
+ }
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ /// Escapa HTML manualmente (sem depender de filtro externo)
+ fn escapeHtml(self: *Renderer, value: Value) !Value {
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ for (s) |c| {
+ switch (c) {
+ '&' => try result.appendSlice(self.allocator, "&"),
+ '<' => try result.appendSlice(self.allocator, "<"),
+ '>' => try result.appendSlice(self.allocator, ">"),
+ '"' => try result.appendSlice(self.allocator, """),
+ '\'' => try result.appendSlice(self.allocator, "'"),
+ else => try result.append(self.allocator, c),
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(self.allocator) };
+ }
+
+ /// Converte Value para string de forma segura
+ fn valueToString(self: *Renderer, value: Value) ![]const u8 {
+ return switch (value) {
+ .string => |s| s,
+ .int => |i| try std.fmt.allocPrint(self.allocator, "{d}", .{i}),
+ .float => |f| try std.fmt.allocPrint(self.allocator, "{d}", .{f}),
+ .bool => |b| if (b) "true" else "false",
+ .null => "",
+ else => "[object]",
+ };
+ }
+};
diff --git a/src/old/renderer_bkp.zig b/src/old/renderer_bkp.zig
new file mode 100644
index 0000000..0f147e3
--- /dev/null
+++ b/src/old/renderer_bkp.zig
@@ -0,0 +1,395 @@
+const std = @import("std");
+
+const builtin_filters = @import("filters.zig").builtin_filters;
+const Context = @import("context.zig").Context;
+const FilterError = @import("filters.zig").FilterError;
+const Value = @import("context.zig").Value;
+
+pub const RenderError = FilterError || error{
+ OutOfMemory,
+ InvalidTemplate,
+ BlockNotFound,
+ CircularExtends,
+ FileNotFound,
+ AccessDenied,
+ FileTooBig,
+ NoSpaceLeft,
+ Unexpected,
+};
+
+const TemplateBlock = struct {
+ name: []const u8,
+ content: []const u8,
+};
+
+const TemplateParser = struct {
+ template: []const u8,
+ pos: usize = 0,
+ last_position: usize = 0,
+
+ const Block = struct {
+ kind: enum { text, variable, tag },
+ content: []const u8,
+ };
+
+ fn init(template: []const u8) TemplateParser {
+ return .{ .template = template };
+ }
+
+ fn nextBlock(self: *TemplateParser) ?Block {
+ if (self.pos >= self.template.len) return null;
+
+ const start = self.pos;
+
+ if (std.mem.startsWith(u8, self.template[self.pos..], "{{")) {
+ self.pos += 2;
+ const end = std.mem.indexOfPos(u8, self.template, self.pos, "}}") orelse self.template.len;
+ self.pos = end + 2;
+ return .{ .kind = .variable, .content = self.template[start + 2 .. end] };
+ } else if (std.mem.startsWith(u8, self.template[self.pos..], "{%")) {
+ self.pos += 2;
+ const end = std.mem.indexOfPos(u8, self.template, self.pos, "%}") orelse self.template.len;
+ self.pos = end + 2;
+ return .{ .kind = .tag, .content = self.template[start + 2 .. end] };
+ } else {
+ const end = std.mem.indexOfAnyPos(u8, self.template, self.pos, "{%") orelse self.template.len;
+ self.pos = end;
+ return .{ .kind = .text, .content = self.template[start..end] };
+ }
+ }
+
+ fn skipTo(self: *TemplateParser, tag: []const u8) void {
+ while (self.nextBlock()) |block| {
+ if (block.kind == .tag) {
+ const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
+ if (std.mem.eql(u8, trimmed, tag)) {
+ break;
+ }
+ }
+ }
+ }
+
+ fn reset(self: *TemplateParser) void {
+ self.pos = 0;
+ }
+
+ fn resetToLastPosition(self: *TemplateParser) void {
+ self.pos = self.last_position;
+ }
+};
+
+pub const Renderer = struct {
+ context: *Context,
+ blocks: std.ArrayList(TemplateBlock),
+
+ pub fn init(context: *Context) Renderer {
+ return .{
+ .context = context,
+ .blocks = std.ArrayList(TemplateBlock){},
+ };
+ }
+
+ pub fn deinit(self: *Renderer) void {
+ for (self.blocks.items) |block| {
+ self.context.allocator().free(block.name);
+ }
+ self.blocks.deinit();
+ }
+
+ pub fn render(self: *Renderer, template_path: []const u8, writer: anytype) RenderError!void {
+ const max_size = 10 * 1024 * 1024; // 10MB
+ const template = std.fs.cwd().readFileAlloc(self.context.allocator(), template_path, max_size) catch |err| switch (err) {
+ error.FileNotFound => return RenderError.FileNotFound,
+ error.AccessDenied => return RenderError.AccessDenied,
+ error.FileTooBig => return RenderError.FileTooBig,
+ error.NoSpaceLeft => return RenderError.NoSpaceLeft,
+ error.OutOfMemory => return RenderError.OutOfMemory,
+ else => return RenderError.Unexpected,
+ };
+ defer self.context.allocator().free(template);
+
+ try self.renderTemplate(template, writer);
+ }
+
+ pub fn renderString(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
+ try self.renderTemplate(template, writer);
+ }
+
+ fn renderTemplate(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
+ var parser = TemplateParser.init(template);
+
+ var extends_path: ?[]const u8 = null;
+
+ while (parser.nextBlock()) |block| {
+ const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
+ if (block.kind == .tag) {
+ if (std.mem.startsWith(u8, trimmed, "extends ")) {
+ const quoted = trimmed["extends ".len..];
+ extends_path = std.mem.trim(u8, quoted, " \"'");
+ } else if (std.mem.startsWith(u8, trimmed, "block ")) {
+ const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
+ const name_copy = try self.context.allocator().dupe(u8, block_name);
+
+ // Agora extrai o conteúdo real do block (após o nome até endblock)
+ const block_start = parser.pos; // já está após o %}
+ parser.skipTo("endblock");
+ const endblock_start = parser.pos; // início do {% endblock %}
+ const block_content = template[block_start..endblock_start];
+
+ try self.blocks.append(self.context.allocator(), .{ .name = name_copy, .content = block_content });
+ }
+ } else if (std.mem.startsWith(u8, trimmed, "block ")) {
+ const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
+ const name_copy = try self.context.allocator().dupe(u8, block_name);
+
+ // Agora coletamos o conteúdo real do block
+ // Começa **após** o fechamento da tag de abertura
+ const block_start = parser.pos; // já está após o %}
+
+ // Avança até o endblock
+ parser.skipTo("endblock");
+
+ // O conteúdo vai do início do corpo até o início do endblock
+ const endblock_start = parser.pos;
+ const block_content = template[block_start..endblock_start];
+
+ try self.blocks.append(self.context.allocator(), .{ .name = name_copy, .content = block_content });
+ }
+ }
+
+ if (extends_path) |path| {
+ const max_size = 10 * 1024 * 1024;
+ const base_template = std.fs.cwd().readFileAlloc(self.context.allocator(), path, max_size) catch |err| switch (err) {
+ error.FileNotFound => return RenderError.FileNotFound,
+ error.AccessDenied => return RenderError.AccessDenied,
+ error.FileTooBig => return RenderError.FileTooBig,
+ error.NoSpaceLeft => return RenderError.NoSpaceLeft,
+ error.OutOfMemory => return RenderError.OutOfMemory,
+ else => return RenderError.Unexpected,
+ };
+ defer self.context.allocator().free(base_template);
+
+ var base_parser = TemplateParser.init(base_template);
+ try self.renderWithInheritance(&base_parser, writer);
+ } else {
+ parser.reset();
+ try self.renderBlocks(&parser, writer);
+ }
+ }
+
+ fn renderWithInheritance(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag) {
+ const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
+ if (std.mem.startsWith(u8, trimmed, "block ")) {
+ const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
+ if (self.findBlock(block_name)) |child| {
+ try writer.writeAll(child.content);
+ parser.skipTo("endblock");
+ } else {
+ try self.renderBlockContent(parser, writer);
+ }
+ } else {
+ try self.renderTag(block.content, parser, writer);
+ }
+ } else {
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => unreachable,
+ }
+ }
+ }
+ }
+
+ fn findBlock(self: *Renderer, name: []const u8) ?TemplateBlock {
+ for (self.blocks.items) |b| {
+ if (std.mem.eql(u8, b.name, name)) return b;
+ }
+ return null;
+ }
+
+ fn renderBlockContent(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endblock")) {
+ break;
+ }
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ fn renderBlocks(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ /// Renderiza uma expressão: var ou var|filtro|...
+ fn renderExpression(self: *Renderer, expr: []const u8, writer: anytype) !void {
+ var parts = std.mem.splitScalar(u8, expr, '|');
+ var var_name = parts.next() orelse return;
+ var_name = std.mem.trim(u8, var_name, " \t\r\n");
+
+ var value = self.context.get(var_name) orelse Value.null;
+ var is_safe = false;
+
+ while (parts.next()) |part| {
+ const trimmed = std.mem.trim(u8, part, " \t\r\n");
+ if (trimmed.len == 0) continue;
+
+ const colon_pos = std.mem.indexOf(u8, trimmed, ":");
+ const filter_name = if (colon_pos) |c| trimmed[0..c] else trimmed;
+ const arg_str = if (colon_pos) |c| std.mem.trim(u8, trimmed[c + 1 ..], " \t\r\n") else null;
+
+ if (std.mem.eql(u8, filter_name, "safe")) {
+ is_safe = true;
+ continue;
+ }
+
+ const filter_fn = builtin_filters.get(filter_name) orelse continue;
+
+ const arg = if (arg_str) |a| blk: {
+ if (std.mem.eql(u8, a, "null")) break :blk Value.null;
+ if (std.fmt.parseInt(i64, a, 10)) |num| break :blk Value{ .int = num } else |_| {}
+ break :blk Value{ .string = a };
+ } else null;
+
+ value = try filter_fn(self.context.allocator(), value, arg);
+ }
+
+ // Autoescape: escapa se não for safe
+ const str = if (is_safe) blk: {
+ break :blk try self.valueToString(value);
+ } else blk: {
+ const escaped = try self.escapeHtml(value);
+ break :blk try self.valueToString(escaped);
+ };
+
+ try writer.writeAll(str);
+ }
+
+ fn renderTag(self: *Renderer, tag_content: []const u8, parser: *TemplateParser, writer: anytype) !void {
+ const trimmed = std.mem.trim(u8, tag_content, " \t\r\n");
+
+ if (std.mem.startsWith(u8, trimmed, "if ")) {
+ try self.renderIf(trimmed["if ".len..], parser, writer);
+ } else if (std.mem.startsWith(u8, trimmed, "for ")) {
+ try self.renderFor(trimmed["for ".len..], parser, writer);
+ } else if (std.mem.eql(u8, trimmed, "endif") or std.mem.eql(u8, trimmed, "endfor")) {
+ // Nada a fazer — o bloco já foi consumido no if/for
+ } else {
+ // Tag desconhecida — ignora
+ }
+ }
+
+ fn renderIf(self: *Renderer, condition_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
+ const condition = try self.evaluateCondition(condition_expr);
+ if (condition) {
+ try self.renderBlocksUntilEndif(parser, writer);
+ } else {
+ parser.skipTo("endif"); // sem try — retorna void
+ }
+ }
+
+ fn renderFor(self: *Renderer, for_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
+ var parts = std.mem.splitSequence(u8, for_expr, " in ");
+ const item_name = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
+ const list_expr = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
+
+ const list_value = self.context.get(list_expr) orelse Value.null;
+ const list = switch (list_value) {
+ .list => |l| l,
+ else => return,
+ };
+
+ for (list) |item| {
+ try self.context.set(item_name, item);
+ try self.renderBlocksUntilEndfor(parser, writer);
+ parser.resetToLastPosition();
+ }
+ }
+
+ fn evaluateCondition(self: *Renderer, expr: []const u8) !bool {
+ const value = self.context.get(expr) orelse Value.null;
+ return switch (value) {
+ .bool => |b| b,
+ .int => |i| i != 0,
+ .float => |f| f != 0.0,
+ .string => |s| s.len > 0,
+ .list => |l| l.len > 0,
+ .dict => |d| d.count() > 0,
+ .null => false,
+ };
+ }
+
+ fn renderBlocksUntilEndif(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endif")) {
+ break;
+ }
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ fn renderBlocksUntilEndfor(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
+ const start_pos = parser.pos;
+ while (parser.nextBlock()) |block| {
+ if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endfor")) {
+ parser.last_position = start_pos;
+ break;
+ }
+ switch (block.kind) {
+ .text => try writer.writeAll(block.content),
+ .variable => try self.renderExpression(block.content, writer),
+ .tag => try self.renderTag(block.content, parser, writer),
+ }
+ }
+ }
+
+ /// Escapa HTML manualmente (sem depender de filtro externo)
+ fn escapeHtml(self: *Renderer, value: Value) !Value {
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ for (s) |c| {
+ switch (c) {
+ '&' => try result.appendSlice(self.context.allocator(), "&"),
+ '<' => try result.appendSlice(self.context.allocator(), "<"),
+ '>' => try result.appendSlice(self.context.allocator(), ">"),
+ '"' => try result.appendSlice(self.context.allocator(), """),
+ '\'' => try result.appendSlice(self.context.allocator(), "'"),
+ else => try result.append(self.context.allocator(), c),
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(self.context.allocator()) };
+ }
+
+ /// Converte Value para string de forma segura
+ fn valueToString(self: *Renderer, value: Value) ![]const u8 {
+ return switch (value) {
+ .string => |s| s,
+ .int => |i| try std.fmt.allocPrint(self.context.allocator(), "{d}", .{i}),
+ .float => |f| try std.fmt.allocPrint(self.context.allocator(), "{d}", .{f}),
+ .bool => |b| if (b) "true" else "false",
+ .null => "",
+ else => "[object]",
+ };
+ }
+};
diff --git a/src/old/renderer_test.zig b/src/old/renderer_test.zig
new file mode 100644
index 0000000..1d1b504
--- /dev/null
+++ b/src/old/renderer_test.zig
@@ -0,0 +1,156 @@
+const std = @import("std");
+const testing = std.testing;
+const Context = @import("context.zig").Context;
+const Renderer = @import("renderer.zig").Renderer;
+const Value = @import("context.zig").Value;
+
+test "renderer - texto literal e variável simples" {
+ const alloc = testing.allocator;
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set("nome", "Fulano");
+ try ctx.set("idade", 30);
+
+ var renderer = Renderer.init(&ctx);
+
+ const template = "Olá {{ nome }}! Você tem {{ idade }} anos.";
+
+ var buffer = std.ArrayList(u8){};
+ defer buffer.deinit(alloc);
+
+ try renderer.renderString(template, buffer.writer(alloc));
+
+ try testing.expectEqualStrings("Olá Fulano! Você tem 30 anos.", buffer.items);
+}
+
+test "renderer - filtros com argumentos" {
+ const alloc = testing.allocator;
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set("titulo", "Olá Mundo Legal");
+
+ var renderer = Renderer.init(&ctx);
+
+ const template = "{{ titulo|lower|slugify }}";
+
+ var buffer = std.ArrayList(u8){};
+ defer buffer.deinit(alloc);
+
+ try renderer.renderString(template, buffer.writer(alloc));
+
+ try testing.expectEqualStrings("ola-mundo-legal", buffer.items);
+}
+
+test "renderer - literal, variável e autoescape" {
+ const alloc = testing.allocator;
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set("nome", "Lucas");
+ try ctx.set("xss", "");
+ try ctx.set("html", "negrito");
+
+ var renderer = Renderer.init(&ctx);
+
+ const template = "Olá {{ nome }}! Perigo: {{ xss }} Seguro: {{ html|safe }}";
+
+ var buffer = std.ArrayList(u8){};
+ defer buffer.deinit(alloc);
+
+ try renderer.renderString(template, buffer.writer(alloc));
+
+ const output = buffer.items;
+
+ try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "<script>") != null); // escapado
+ try testing.expect(std.mem.indexOf(u8, output, "negrito") != null); // safe
+}
+
+test "renderer - if and for" {
+ const alloc = testing.allocator;
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set("ativo", true);
+ try ctx.set("inativo", false);
+ try ctx.set("nomes", [_]Value{ Value{ .string = "Ana" }, Value{ .string = "Bia" }, Value{ .string = "Cris" } });
+
+ var renderer = Renderer.init(&ctx);
+
+ const template =
+ \\{% if ativo %}Sim!{% endif %}
+ \\{% if inativo %}Não{% endif %}
+ \\Lista:
+ \\{% for nome in nomes %}
+ \\- {{ nome }}
+ \\{% endfor %}
+ ;
+
+ var buffer = std.ArrayList(u8){};
+ defer buffer.deinit(alloc);
+
+ try renderer.renderString(template, buffer.writer(alloc));
+
+ const output = buffer.items;
+
+ try testing.expect(std.mem.indexOf(u8, output, "Sim!") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "Não") == null);
+ try testing.expect(std.mem.indexOf(u8, output, "- Ana") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "- Bia") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "- Cris") != null);
+}
+
+// test "renderer - block and extends" {
+// const alloc = testing.allocator;
+//
+// const base =
+// \\
+// \\
{% block title %}Título Padrão{% endblock %}
+// \\
+// \\{% block content %}Conteúdo padrão{% endblock %}
+// \\
+// \\
+// ;
+//
+// const child =
+// \\{% extends "base.html" %}
+// \\{% block title %}Meu Título{% endblock %}
+// \\{% block content %}
+// \\Olá {{ nome }}!
+// \\{% endblock %}
+// ;
+//
+// try std.fs.cwd().writeFile(.{
+// .sub_path = "base.html",
+// .data = base,
+// });
+// try std.fs.cwd().writeFile(.{
+// .sub_path = "child.html",
+// .data = child,
+// });
+// defer std.fs.cwd().deleteFile("base.html") catch {};
+// defer std.fs.cwd().deleteFile("child.html") catch {};
+//
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// try ctx.set("nome", "Lucas");
+//
+// var renderer = Renderer.init(&ctx);
+//
+// var buffer = std.ArrayList(u8){};
+// defer buffer.deinit(ctx.allocator());
+//
+// try renderer.render("child.html", buffer.writer(ctx.allocator()));
+//
+// const output = buffer.items;
+//
+// std.debug.print("{s}", .{output});
+//
+// try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") == null);
+// try testing.expect(std.mem.indexOf(u8, output, "Título Padrão") == null);
+// }
diff --git a/src/parser_bkp.zig b/src/parser_bkp.zig
new file mode 100644
index 0000000..97a2bb7
--- /dev/null
+++ b/src/parser_bkp.zig
@@ -0,0 +1,1208 @@
+const std = @import("std");
+
+pub const NodeType = enum {
+ text,
+ variable,
+ tag,
+ if_block,
+ for_block,
+ include,
+ with_block,
+ now,
+ 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 {
+ format: []const u8,
+};
+
+pub const ExtendsNode = struct {
+ parent_name: []const u8,
+};
+
+pub const BlockNode = struct {
+ name: []const u8,
+ body: []Node,
+ raw_open: []const u8,
+ raw_close: []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 {
+ 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,
+ include: ?IncludeNode = null,
+ with: ?WithNode = null,
+ now: ?NowNode = null,
+ 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);
+ 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
+ },
+ .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);
+ },
+ .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);
+ allocator.free(b.body);
+ // raw_open e raw_close são slices originais — não free
+ },
+ .super => {},
+ .filter_block => if (self.filter_block) |fb| {
+ allocator.free(fb.filters);
+ for (fb.body) |n| n.deinit(allocator);
+ allocator.free(fb.body);
+ },
+ .autoescape => if (self.autoescape) |ae| {
+ for (ae.body) |n| n.deinit(allocator);
+ allocator.free(ae.body);
+ // raw_open e raw_close são slices originais — não free
+ },
+ .spaceless => if (self.spaceless) |sl| {
+ const body_copy = sl.body;
+ for (body_copy) |n| {
+ n.deinit(allocator);
+ }
+ allocator.free(sl.body);
+ },
+ }
+ }
+};
+
+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 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 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| {
+ 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;
+ try body.append(allocator, tag_node);
+ 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,
+ },
+ };
+ }
+
+ try body.append(allocator, tag_node);
+ 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);
+
+ 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| {
+ if (node.variable) |v| {
+ if (std.mem.eql(u8, v.expr, "block.super")) {
+ try body.append(allocator, Node{ .type = .super, .super = true });
+ allocator.free(v.expr);
+ continue;
+ }
+ }
+ 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, "block")) {
+ depth += 1;
+ try body.append(allocator, tag_node);
+ continue;
+ }
+
+ if (std.mem.eql(u8, tag_name, "endblock")) {
+ 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 = .block,
+ .block = .{
+ .name = name,
+ .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 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.?.expr});
+ try body.append(allocator, node);
+ continue;
+ }
+
+ if (try self.parseTag(allocator)) |tag_node| {
+ const tag_name = tag_node.tag.?.name;
+
+ if (std.mem.eql(u8, tag_name, "with")) {
+ depth += 1;
+ try body.append(allocator, tag_node);
+ continue;
+ }
+
+ if (std.mem.eql(u8, tag_name, "endwith")) {
+ depth -= 1;
+ const raw_close = tag_node.tag.?.raw;
+
+ allocator.free(tag_node.tag.?.name);
+ allocator.free(tag_node.tag.?.args);
+
+ if (depth == 0) {
+ // para fins de debug
+ std.debug.print("2.4 - Encontrei um bloco with:\n - assignments: {any}\n - body: {any}\n - raw_open: {s}\n - raw_close: {s}\n", .{
+ assignments,
+ body.items,
+ raw_open,
+ raw_close,
+ });
+ // fim para fins de debug
+ return Node{
+ .type = .with_block,
+ .with = .{
+ .assignments = assignments,
+ .body = try body.toOwnedSlice(allocator),
+ .raw_open = raw_open,
+ .raw_close = raw_close,
+ },
+ };
+ }
+
+ try body.append(allocator, tag_node);
+ continue;
+ }
+
+ try body.append(allocator, tag_node);
+ } else {
+ self.advance(1);
+ }
+ }
+
+ return error.UnclosedBlock;
+ }
+
+ fn parseComment(self: *Parser) !void {
+ // Consome a tag open {% comment %}
+ // Já estamos após o %}, então só avançamos
+ var depth: usize = 1;
+
+ while (self.pos < self.template.len and depth > 0) {
+ if (self.peek(2)) |p| {
+ if (std.mem.eql(u8, p, "{%")) {
+ self.advance(2);
+ self.skipWhitespace();
+
+ const content_start = self.pos;
+ while (self.pos < self.template.len) : (self.advance(1)) {
+ if (self.peek(2)) |closing| {
+ if (std.mem.eql(u8, closing, "%}")) break;
+ }
+ }
+
+ if (self.pos + 2 > self.template.len or !std.mem.eql(u8, self.template[self.pos .. self.pos + 2], "%}")) {
+ return error.UnclosedTag;
+ }
+
+ const inner = std.mem.trim(u8, self.template[content_start..self.pos], " \t\r\n");
+
+ if (std.mem.eql(u8, inner, "comment")) {
+ depth += 1;
+ } else if (std.mem.eql(u8, inner, "endcomment")) {
+ depth -= 1;
+ if (depth == 0) {
+ self.advance(2); // consome %}
+ return;
+ }
+ }
+
+ self.advance(2);
+ continue;
+ }
+ }
+ self.advance(1);
+ }
+
+ return error.UnclosedComment;
+ }
+
+ fn parseText(self: *Parser, allocator: std.mem.Allocator) !?Node {
+ std.debug.print("2.0 - Vou verificar se sou texto\n", .{});
+ const start = self.pos;
+
+ std.debug.print("2.1 - meu start é {d}\n", .{start});
+
+ while (self.pos < self.template.len) {
+ if (self.peek(2)) |p| {
+ if (std.mem.eql(u8, p, "{{") or std.mem.eql(u8, p, "{%")) {
+ std.debug.print("2.2 - fiz o peek de 2 em 2 até que achei {{{{ ou {{%, então parei\n", .{});
+ break;
+ }
+ }
+ self.advance(1);
+ }
+
+ 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 },
+ };
+ }
+
+ 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;
+ } else return null;
+
+ self.advance(2);
+ self.skipWhitespace();
+
+ 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", .{});
+ }
+ }
+
+ 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 full_expr = std.mem.trim(u8, self.template[expr_start..self.pos], " \t\r\n");
+
+ 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 = .{
+ .expr = duped_expr,
+ .filters = try filters.toOwnedSlice(allocator),
+ },
+ };
+ }
+
+ 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;
+ 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;
+ }
+ }
+
+ 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;
+ }
+
+ 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);
+
+ 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{
+ .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 {
+ 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){};
+ 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| {
+ 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.?.expr});
+ 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) {
+ 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" = .{
+ .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| {
+ 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.?.expr});
+ 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);
+
+ 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" = .{
+ .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);
+
+ 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, "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", .{});
+ allocator.free(node.tag.?.name);
+ allocator.free(node.tag.?.args);
+ try self.parseComment();
+ 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\"");
+
+ const duped = try allocator.dupe(u8, parent);
+
+ allocator.free(node.tag.?.name);
+ allocator.free(node.tag.?.args);
+
+ std.debug.print("3.0 - na real sou um extends\n", .{});
+ std.debug.print("===================================\n", .{});
+ try list.append(allocator, Node{
+ .type = .extends,
+ .extends = .{ .parent_name = duped },
+ });
+ continue;
+ }
+
+ if (std.mem.eql(u8, tag_name, "block")) {
+ const block_name_raw = node.tag.?.args;
+ const raw_open = node.tag.?.raw;
+
+ const block_name = std.mem.trim(u8, block_name_raw, " \t\r\n");
+
+ // DUPE O NOME ANTES DE LIBERAR A TAG
+ const duped_name = try allocator.dupe(u8, block_name);
+
+ // Agora libera a tag open
+ allocator.free(node.tag.?.name);
+ allocator.free(node.tag.?.args);
+
+ std.debug.print("3.0 - na real sou um block\n", .{});
+ std.debug.print("===================================\n", .{});
+
+ const block_node = try self.parseBlockBlock(allocator, duped_name, raw_open);
+ try list.append(allocator, block_node);
+ 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;
+
+ 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);
+
+ std.debug.print("===================================\n", .{});
+ 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);
+
+ 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.?.expr});
+ std.debug.print("4.1 - filters: \'{any}\'\n", .{node.variable.?.filters});
+ 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;
+ }
+
+ 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);
+ }
+};
+
+pub fn parse(allocator: std.mem.Allocator, template: []const u8) ![]Node {
+ var p = Parser.init(template);
+ return try p.parse(allocator);
+}
diff --git a/src/renderer.zig b/src/renderer.zig
index 9550710..415de94 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -205,10 +205,12 @@ pub const Renderer = struct {
if (condition) {
for (node.@"if".?.true_body) |child| {
+ std.debug.print("caí no true\n", .{});
try self.renderNode(alloc, nodes, child, writer, null, null);
}
} else {
for (node.@"if".?.false_body) |child| {
+ std.debug.print("caí no false\n", .{});
try self.renderNode(alloc, nodes, child, writer, null, null);
}
}
diff --git a/src/renderer_bkp.zig b/src/renderer_bkp.zig
new file mode 100644
index 0000000..6e2f0eb
--- /dev/null
+++ b/src/renderer_bkp.zig
@@ -0,0 +1,275 @@
+const std = @import("std");
+
+const Allocator = std.mem.Allocator;
+const ArrayListUnmanaged = std.ArrayListUnmanaged;
+
+const Context = @import("context.zig").Context;
+const Value = @import("context.zig").Value;
+
+const builtin_filters = @import("filters.zig").builtin_filters;
+const FilterError = @import("filters.zig").FilterError;
+const parser = @import("parser.zig");
+
+pub const RenderError = FilterError || error{
+ OutOfMemory,
+ InvalidSyntax,
+ UnknownVariable,
+ UnknownFilter,
+ InvalidTemplate,
+ BlockNotFound,
+ CircularExtends,
+ FileNotFound,
+ AccessDenied,
+ FileTooBig,
+ NoSpaceLeft,
+ Unexpected,
+ UnclosedTag,
+ InvalidAutoescapeArgument,
+ UnclosedVariable,
+ UnclosedBlock,
+ UnclosedComment,
+ InvalidAssignmentSyntax,
+ UnclosedQuoteInAssignment,
+ InvalidForSyntax,
+ UnclosedVerbatim,
+ InvalidUrlSyntax,
+ UnclosedQuoteInUrl,
+ InvalidDebugArgs,
+ InvalidRegroupSyntax,
+ InvalidWidthRatioSyntax,
+ InvalidTemplateTag,
+ InvalidCsrfTokenArgs,
+};
+
+pub const Renderer = struct {
+ context: *Context,
+ allocator: Allocator,
+
+ pub fn init(context: *Context) Renderer {
+ return .{
+ .context = context,
+ .allocator = context.allocator(),
+ };
+ }
+
+ fn checkForExtends(self: *const Renderer, nodes: []parser.Node) bool {
+ _ = self;
+ for (nodes) |n| {
+ if (n.type == .extends) return true;
+ }
+ return false;
+ }
+
+ fn readTemplateFile(self: *const Renderer, path: []const u8) RenderError![]const u8 {
+ const max_size = 10 * 1024 * 1024;
+ const base_template = std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) {
+ error.FileNotFound => return RenderError.FileNotFound,
+ error.AccessDenied => return RenderError.AccessDenied,
+ error.FileTooBig => return RenderError.FileTooBig,
+ error.NoSpaceLeft => return RenderError.NoSpaceLeft,
+ error.OutOfMemory => return RenderError.OutOfMemory,
+ else => return RenderError.Unexpected,
+ };
+ return base_template;
+ }
+
+ pub fn render(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void {
+ const base_template = try self.readTemplateFile(template);
+ return self.renderString(base_template, writer);
+ }
+
+ pub fn renderString_bkp(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void {
+ var arena = std.heap.ArenaAllocator.init(self.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(alloc);
+ defer {
+ for (nodes) |node| node.deinit(alloc);
+ alloc.free(nodes);
+ }
+
+ for (nodes) |node| {
+ try self.renderNode(alloc, nodes, node, writer, null);
+ }
+ }
+
+ pub fn renderString(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void {
+ var arena = std.heap.ArenaAllocator.init(self.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(alloc);
+ defer {
+ for (nodes) |node| node.deinit(alloc);
+ alloc.free(nodes);
+ }
+
+ const has_extends = self.checkForExtends(nodes);
+
+ if (has_extends) {
+ for (nodes) |node| {
+ if (node.type == .extends) {
+ const base_template = try self.readTemplateFile(node.extends.?.parent_name);
+ defer self.allocator.free(base_template);
+
+ var base_parser = parser.Parser.init(base_template);
+ const base_nodes = try base_parser.parse(alloc);
+ defer {
+ for (base_nodes) |n| n.deinit(alloc);
+ alloc.free(base_nodes);
+ }
+
+ try self.renderWithInheritance(alloc, base_nodes, nodes, writer);
+ return;
+ }
+ }
+ } else {
+ for (nodes) |node| {
+ try self.renderNode(alloc, nodes, node, writer, null);
+ }
+ }
+ }
+
+ fn renderWithInheritance(self: *const Renderer, alloc: Allocator, base_nodes: []parser.Node, child_nodes: []parser.Node, writer: anytype) RenderError!void {
+ for (base_nodes) |base_node| {
+ if (base_node.type == .block) {
+ const block_name = base_node.block.?.name;
+
+ // Procura no filho
+ const child_block = self.findChildBlock(child_nodes, block_name);
+ if (child_block) |child| {
+ // Renderiza só o filho
+ for (child.body) |child_node| {
+ try self.renderNode(alloc, child_nodes, child_node, writer, null);
+ }
+ } else {
+ // Renderiza o do pai
+ for (base_node.block.?.body) |child| {
+ try self.renderNode(alloc, child_nodes, child, writer, null);
+ }
+ }
+ } else {
+ // Qualquer outra coisa no base
+ try self.renderNode(alloc, child_nodes, base_node, writer, null);
+ }
+ }
+ }
+
+ fn renderNode(self: *const Renderer, alloc: Allocator, nodes: []parser.Node, node: parser.Node, writer: anytype, context: ?*Context) RenderError!void {
+ // std.debug.print("Vou renderizar o node: {any}\n", .{node});
+ switch (node.type) {
+ .text => try writer.writeAll(node.text.?.content),
+ .variable => {
+ const var_name = node.variable.?.expr;
+ var value: Value = Value.null;
+ if (context != null) {
+ value = context.?.get(var_name) orelse Value.null;
+ } else {
+ value = self.context.get(var_name) orelse Value.null;
+ }
+
+ var is_safe = false;
+
+ for (node.variable.?.filters) |f| {
+ const filter_fn = builtin_filters.get(f.name) orelse return error.UnknownFilter;
+
+ const arg = if (f.arg) |a| Value{ .string = a } else null;
+ value = try filter_fn(alloc, value, arg);
+
+ if (std.mem.eql(u8, f.name, "safe")) is_safe = true;
+ }
+
+ if (!is_safe) {
+ if (builtin_filters.get("escape")) |escape_fn| {
+ value = try escape_fn(alloc, value, null);
+ }
+ }
+
+ var buf = ArrayListUnmanaged(u8){};
+ defer buf.deinit(alloc);
+
+ try self.valueToString(alloc, &buf, value);
+ try writer.writeAll(buf.items);
+ },
+ .if_block => {
+ const condition = self.evaluateCondition(node.@"if".?.condition) catch return false;
+
+ if (condition) {
+ for (node.@"if".?.true_body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, null);
+ }
+ } else {
+ for (node.@"if".?.false_body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, null);
+ }
+ }
+ },
+ .for_block => {
+ const list_value = self.context.get(node.@"for".?.iterable) orelse Value.null;
+ const list = switch (list_value) {
+ .list => |l| l,
+ else => return,
+ };
+
+ for (list) |item| {
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set(node.@"for".?.loop_var, item);
+
+ for (node.@"for".?.body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, &ctx);
+ }
+ for (node.@"for".?.empty_body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, &ctx);
+ }
+ }
+ },
+ .block => {
+ for (node.block.?.body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, null);
+ }
+ },
+ else => {},
+ }
+ }
+
+ fn findChildBlock(self: *const Renderer, nodes: []parser.Node, name: []const u8) ?parser.BlockNode {
+ _ = self;
+ for (nodes) |n| {
+ if (n.type != .block) continue;
+ if (std.mem.eql(u8, n.block.?.name, name)) return n.block.?;
+ }
+ return null;
+ }
+
+ fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) !void {
+ _ = self;
+ var w = buf.writer(alloc);
+ switch (value) {
+ .null => try w.writeAll("null"),
+ .bool => |b| try w.print("{}", .{b}),
+ .int => |n| try w.print("{d}", .{n}),
+ .float => |f| try w.print("{d}", .{f}),
+ .string => |s| try w.writeAll(s),
+ .list => try w.writeAll("[list]"),
+ .dict => try w.writeAll("{dict}"),
+ }
+ }
+
+ fn evaluateCondition(self: *const Renderer, expr: []const u8) !bool {
+ const value = self.context.get(expr) orelse Value.null;
+ return switch (value) {
+ .bool => |b| b,
+ .int => |i| i != 0,
+ .float => |f| f != 0.0,
+ .string => |s| s.len > 0,
+ .list => |l| l.len > 0,
+ .dict => |d| d.count() > 0,
+ .null => false,
+ };
+ }
+};
diff --git a/src/renderer_test.zig b/src/renderer_test.zig
index 74a1b38..9062e05 100644
--- a/src/renderer_test.zig
+++ b/src/renderer_test.zig
@@ -8,390 +8,390 @@ const Context = @import("context.zig").Context;
const Value = @import("context.zig").Value;
const TemplateCache = @import("cache.zig").TemplateCache;
-test "renderer: literal + variável simples" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("nome", Value{ .string = "Mariana" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- const template =
- \\Olá, {{ nome }}! Bem-vinda.
- ;
-
- try renderer.renderString(template, buf.writer(alloc));
-
- try testing.expectEqualStrings("Olá, Mariana! Bem-vinda.", buf.items);
-}
-
-test "renderer: filtros + autoescape" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("html", Value{ .string = "" });
- try ctx.set("texto", Value{ .string = "maiusculo e slug" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- const template =
- \\Normal: {{ html }}
- \\Safe: {{ html|safe }}
- \\Filtrado: {{ texto|upper|slugify }}
- ;
-
- try renderer.renderString(template, buf.writer(alloc));
-
- const expected =
- \\Normal: <script>alert()</script>
- \\Safe:
- \\Filtrado: maiusculo-e-slug
- ;
-
- try testing.expectEqualStrings(expected, buf.items);
-}
-
-test "literal simples" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- const template = "Texto literal com acentos: Olá, mundo!";
-
- try renderer.renderString(template, buf.writer(alloc));
-
- try testing.expectEqualStrings("Texto literal com acentos: Olá, mundo!", buf.items);
-}
-
-test "variável com filtro encadeado e autoescape" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("texto", Value{ .string = "Exemplo de Texto" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- const template = "Resultado: {{ texto|lower|upper }}";
-
- try renderer.renderString(template, buf.writer(alloc));
-
- try testing.expectEqualStrings("Resultado: EXEMPLO DE TEXTO", buf.items); // assume lower then upper
-}
-
-test "autoescape com safe" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("html", Value{ .string = "conteúdo
" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- const template = "Escape: {{ html }} | Safe: {{ html|safe }}";
-
- try renderer.renderString(template, buf.writer(alloc));
-
- try testing.expectEqualStrings("Escape: <div>conteúdo</div> | Safe: conteúdo
", buf.items);
-}
-
-test "renderer - if and for" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("ativo", Value{ .bool = true });
- try ctx.set("nomes", Value{ .list = &[_]Value{
- Value{ .string = "Ana" },
- Value{ .string = "Bia" },
- Value{ .string = "Cris" },
- } });
-
- const template =
- \\{% if ativo %}Sim!{% endif %}
- \\{% if not ativo %}Não{% endif %}
- \\Lista:
- \\{% for nome in nomes %}
- \\- {{ nome }}
- \\{% endfor %}
- ;
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- try renderer.renderString(template, buf.writer(alloc));
-
- try testing.expect(std.mem.indexOf(u8, buf.items, "Sim!") != null);
- try testing.expect(std.mem.indexOf(u8, buf.items, "Não") == null);
- try testing.expect(std.mem.indexOf(u8, buf.items, "- Ana") != null);
- try testing.expect(std.mem.indexOf(u8, buf.items, "- Bia") != null);
- try testing.expect(std.mem.indexOf(u8, buf.items, "- Cris") != null);
-}
-
-test "renderer - block and extends" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- const base =
- \\
- \\{% block title %}Título Padrão{% endblock %}
- \\
- \\{% block content %}Conteúdo padrão{% endblock %}
- \\
- \\
- ;
-
- const child =
- \\{% extends "base.html" %}
- \\{% block title %}Meu Título{% endblock %}
- \\{% block content %}
- \\Olá {{ nome }}!
- \\{% endblock %}
- ;
-
- const expected =
- \\
- \\Meu Título
- \\
- \\
- \\Olá Lucas!
- \\
- \\
- \\
- ;
-
- // Simula arquivos (ou usa renderString com base se quiser simplificar)
- try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
- try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
- defer std.fs.cwd().deleteFile("base.html") catch {};
- defer std.fs.cwd().deleteFile("child.html") catch {};
-
- try ctx.set("nome", Value{ .string = "Lucas" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- try renderer.render("child.html", buf.writer(alloc));
-
- const output = buf.items;
-
- std.debug.print("OUTPUT:\n{s}\n", .{output});
-
- try testing.expect(std.mem.indexOf(u8, output, "") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") == null);
- try testing.expectEqualStrings(expected, output);
-}
-
-test "renderer - block and extends with super" {
- const alloc = testing.allocator;
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- const base =
- \\
- \\{% block title %}Título Padrão{% endblock %}
- \\
- \\{% block content %}Conteúdo padrão{% endblock %}
- \\
- \\
- ;
-
- const child =
- \\{% extends "base.html" %}
- \\{% block title %}Meu Título{% endblock %}
- \\{% block content %}
- \\{{ block.super }}
- \\Olá {{ nome }}!
- \\{% endblock %}
- ;
-
- const expected =
- \\
- \\Meu Título
- \\
- \\
- \\Conteúdo padrão
- \\Olá Lucas!
- \\
- \\
- \\
- ;
-
- // Simula arquivos (ou usa renderString com base se quiser simplificar)
- try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
- try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
- defer std.fs.cwd().deleteFile("base.html") catch {};
- defer std.fs.cwd().deleteFile("child.html") catch {};
-
- try ctx.set("nome", Value{ .string = "Lucas" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- try renderer.render("child.html", buf.writer(alloc));
-
- const output = buf.items;
-
- std.debug.print("{s}\n", .{output});
-
- try testing.expect(std.mem.indexOf(u8, output, "") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
- try testing.expectEqualStrings(expected, output);
-}
-
-test "renderer - include" {
- const alloc = testing.allocator;
-
- const header =
- \\
- ;
-
- const main =
- \\{% include "header.html" %}
- \\
- \\ Conteúdo principal
- \\ Olá {{ nome }}!
- \\
- ;
-
- const expected =
- \\
- \\
- \\ Conteúdo principal
- \\ Olá Lucas!
- \\
- ;
-
- try std.fs.cwd().writeFile(.{ .sub_path = "header.html", .data = header });
- try std.fs.cwd().writeFile(.{ .sub_path = "main.html", .data = main });
- defer std.fs.cwd().deleteFile("header.html") catch {};
- defer std.fs.cwd().deleteFile("main.html") catch {};
-
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("nome", Value{ .string = "Lucas" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- try renderer.render("main.html", buf.writer(alloc));
-
- const output = buf.items;
-
- try testing.expect(std.mem.indexOf(u8, output, "Bem-vindo
") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
- try testing.expectEqualStrings(expected, output);
-}
-
-test "renderer - comment" {
- const alloc = testing.allocator;
-
- const template =
- \\Normal: Olá {{ nome }}
- \\{% comment %}
- \\ Isso é um comentário
- \\ que não deve aparecer
- \\ nem processar variáveis {{ nome }}
- \\{% endcomment %}
- \\Fim: {{ nome }}
- ;
- const expected =
- \\Normal: Olá Lucas
- \\
- \\Fim: Lucas
- ;
-
- var ctx = Context.init(alloc);
- defer ctx.deinit();
-
- var cache = TemplateCache.init(alloc);
- defer cache.deinit();
-
- const renderer = Renderer.init(&ctx, &cache);
-
- try ctx.set("nome", Value{ .string = "Lucas" });
-
- var buf = std.ArrayList(u8){};
- defer buf.deinit(alloc);
-
- try renderer.renderString(template, buf.writer(alloc));
-
- const output = buf.items;
-
- try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Fim: Lucas") != null);
- try testing.expect(std.mem.indexOf(u8, output, "Isso é um comentário") == null);
- try testing.expect(std.mem.indexOf(u8, output, "que não deve aparecer") == null);
- try testing.expect(std.mem.indexOf(u8, output, "nem processar variáveis") == null);
- try testing.expectEqualStrings(expected, output);
-}
+// test "renderer: literal + variável simples" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("nome", Value{ .string = "Mariana" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// const template =
+// \\Olá, {{ nome }}! Bem-vinda.
+// ;
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// try testing.expectEqualStrings("Olá, Mariana! Bem-vinda.", buf.items);
+// }
+//
+// test "renderer: filtros + autoescape" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("html", Value{ .string = "" });
+// try ctx.set("texto", Value{ .string = "maiusculo e slug" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// const template =
+// \\Normal: {{ html }}
+// \\Safe: {{ html|safe }}
+// \\Filtrado: {{ texto|upper|slugify }}
+// ;
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// const expected =
+// \\Normal: <script>alert()</script>
+// \\Safe:
+// \\Filtrado: maiusculo-e-slug
+// ;
+//
+// try testing.expectEqualStrings(expected, buf.items);
+// }
+//
+// test "literal simples" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// const template = "Texto literal com acentos: Olá, mundo!";
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// try testing.expectEqualStrings("Texto literal com acentos: Olá, mundo!", buf.items);
+// }
+//
+// test "variável com filtro encadeado e autoescape" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("texto", Value{ .string = "Exemplo de Texto" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// const template = "Resultado: {{ texto|lower|upper }}";
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// try testing.expectEqualStrings("Resultado: EXEMPLO DE TEXTO", buf.items); // assume lower then upper
+// }
+//
+// test "autoescape com safe" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("html", Value{ .string = "conteúdo
" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// const template = "Escape: {{ html }} | Safe: {{ html|safe }}";
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// try testing.expectEqualStrings("Escape: <div>conteúdo</div> | Safe: conteúdo
", buf.items);
+// }
+//
+// test "renderer - if and for" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("ativo", Value{ .bool = true });
+// try ctx.set("nomes", Value{ .list = &[_]Value{
+// Value{ .string = "Ana" },
+// Value{ .string = "Bia" },
+// Value{ .string = "Cris" },
+// } });
+//
+// const template =
+// \\{% if ativo %}Sim!{% endif %}
+// \\{% if not ativo %}Não{% endif %}
+// \\Lista:
+// \\{% for nome in nomes %}
+// \\- {{ nome }}
+// \\{% endfor %}
+// ;
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// try testing.expect(std.mem.indexOf(u8, buf.items, "Sim!") != null);
+// try testing.expect(std.mem.indexOf(u8, buf.items, "Não") == null);
+// try testing.expect(std.mem.indexOf(u8, buf.items, "- Ana") != null);
+// try testing.expect(std.mem.indexOf(u8, buf.items, "- Bia") != null);
+// try testing.expect(std.mem.indexOf(u8, buf.items, "- Cris") != null);
+// }
+//
+// test "renderer - block and extends" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// const base =
+// \\
+// \\{% block title %}Título Padrão{% endblock %}
+// \\
+// \\{% block content %}Conteúdo padrão{% endblock %}
+// \\
+// \\
+// ;
+//
+// const child =
+// \\{% extends "base.html" %}
+// \\{% block title %}Meu Título{% endblock %}
+// \\{% block content %}
+// \\Olá {{ nome }}!
+// \\{% endblock %}
+// ;
+//
+// const expected =
+// \\
+// \\Meu Título
+// \\
+// \\
+// \\Olá Lucas!
+// \\
+// \\
+// \\
+// ;
+//
+// // Simula arquivos (ou usa renderString com base se quiser simplificar)
+// try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
+// try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
+// defer std.fs.cwd().deleteFile("base.html") catch {};
+// defer std.fs.cwd().deleteFile("child.html") catch {};
+//
+// try ctx.set("nome", Value{ .string = "Lucas" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// try renderer.render("child.html", buf.writer(alloc));
+//
+// const output = buf.items;
+//
+// std.debug.print("OUTPUT:\n{s}\n", .{output});
+//
+// try testing.expect(std.mem.indexOf(u8, output, "") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") == null);
+// try testing.expectEqualStrings(expected, output);
+// }
+//
+// test "renderer - block and extends with super" {
+// const alloc = testing.allocator;
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// const base =
+// \\
+// \\{% block title %}Título Padrão{% endblock %}
+// \\
+// \\{% block content %}Conteúdo padrão{% endblock %}
+// \\
+// \\
+// ;
+//
+// const child =
+// \\{% extends "base.html" %}
+// \\{% block title %}Meu Título{% endblock %}
+// \\{% block content %}
+// \\{{ block.super }}
+// \\Olá {{ nome }}!
+// \\{% endblock %}
+// ;
+//
+// const expected =
+// \\
+// \\Meu Título
+// \\
+// \\
+// \\Conteúdo padrão
+// \\Olá Lucas!
+// \\
+// \\
+// \\
+// ;
+//
+// // Simula arquivos (ou usa renderString com base se quiser simplificar)
+// try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
+// try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
+// defer std.fs.cwd().deleteFile("base.html") catch {};
+// defer std.fs.cwd().deleteFile("child.html") catch {};
+//
+// try ctx.set("nome", Value{ .string = "Lucas" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// try renderer.render("child.html", buf.writer(alloc));
+//
+// const output = buf.items;
+//
+// std.debug.print("{s}\n", .{output});
+//
+// try testing.expect(std.mem.indexOf(u8, output, "") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
+// try testing.expectEqualStrings(expected, output);
+// }
+//
+// test "renderer - include" {
+// const alloc = testing.allocator;
+//
+// const header =
+// \\
+// ;
+//
+// const main =
+// \\{% include "header.html" %}
+// \\
+// \\ Conteúdo principal
+// \\ Olá {{ nome }}!
+// \\
+// ;
+//
+// const expected =
+// \\
+// \\
+// \\ Conteúdo principal
+// \\ Olá Lucas!
+// \\
+// ;
+//
+// try std.fs.cwd().writeFile(.{ .sub_path = "header.html", .data = header });
+// try std.fs.cwd().writeFile(.{ .sub_path = "main.html", .data = main });
+// defer std.fs.cwd().deleteFile("header.html") catch {};
+// defer std.fs.cwd().deleteFile("main.html") catch {};
+//
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("nome", Value{ .string = "Lucas" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// try renderer.render("main.html", buf.writer(alloc));
+//
+// const output = buf.items;
+//
+// try testing.expect(std.mem.indexOf(u8, output, "Bem-vindo
") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
+// try testing.expectEqualStrings(expected, output);
+// }
+//
+// test "renderer - comment" {
+// const alloc = testing.allocator;
+//
+// const template =
+// \\Normal: Olá {{ nome }}
+// \\{% comment %}
+// \\ Isso é um comentário
+// \\ que não deve aparecer
+// \\ nem processar variáveis {{ nome }}
+// \\{% endcomment %}
+// \\Fim: {{ nome }}
+// ;
+// const expected =
+// \\Normal: Olá Lucas
+// \\
+// \\Fim: Lucas
+// ;
+//
+// var ctx = Context.init(alloc);
+// defer ctx.deinit();
+//
+// var cache = TemplateCache.init(alloc);
+// defer cache.deinit();
+//
+// const renderer = Renderer.init(&ctx, &cache);
+//
+// try ctx.set("nome", Value{ .string = "Lucas" });
+//
+// var buf = std.ArrayList(u8){};
+// defer buf.deinit(alloc);
+//
+// try renderer.renderString(template, buf.writer(alloc));
+//
+// const output = buf.items;
+//
+// try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Fim: Lucas") != null);
+// try testing.expect(std.mem.indexOf(u8, output, "Isso é um comentário") == null);
+// try testing.expect(std.mem.indexOf(u8, output, "que não deve aparecer") == null);
+// try testing.expect(std.mem.indexOf(u8, output, "nem processar variáveis") == null);
+// try testing.expectEqualStrings(expected, output);
+// }
// FIX: comment inside block
-//
+
// test "renderer - full template with extends, super, include, comment" {
// const alloc = testing.allocator;
//
@@ -436,8 +436,63 @@ test "renderer - comment" {
//
// const output = buf.items;
//
+// std.debug.print("OUTPUT:\n{s}\n", .{output});
+//
// try testing.expect(std.mem.indexOf(u8, output, "") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Isso não aparece") == null);
// }
+
+test "renderer - if inside block" {
+ const alloc = testing.allocator;
+
+ const base =
+ \\
+ \\ {% block content %}
+ \\ Conteúdo padrão
+ \\ {% endblock %}
+ \\
+ ;
+
+ const child =
+ \\{% extends "base.html" %}
+ \\{% block content %}
+ \\ {{ block.super }}
+ \\ Conteúdo do filho
+ \\{% if idade > 18 %}
+ \\ Idade: {{ idade }}
+ \\{% else %}
+ \\ Oops
+ \\{% endif %}
+ \\{% endblock %}
+ ;
+
+ try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
+ try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
+ defer std.fs.cwd().deleteFile("base.html") catch {};
+ defer std.fs.cwd().deleteFile("child.html") catch {};
+
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ var cache = TemplateCache.init(alloc);
+ defer cache.deinit();
+
+ const renderer = Renderer.init(&ctx, &cache);
+
+ try ctx.set("idade", 23);
+
+ var buf = std.ArrayList(u8){};
+ defer buf.deinit(alloc);
+
+ try renderer.render("child.html", buf.writer(alloc));
+
+ const output = buf.items;
+
+ std.debug.print("OUTPUT:\n{s}\n", .{output});
+
+ try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "Oops") == null);
+}