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"); const TemplateCache = @import("cache.zig").TemplateCache; pub const RenderError = error{ InvalidCharacter, InvalidSyntax, OutOfMemory, Overflow, Unexpected, UnsupportedExpression, } || FilterError || parser.ParserError || std.fs.File.OpenError; pub const Renderer = struct { context: *Context, allocator: Allocator, cache: *TemplateCache, pub fn init(context: *Context, cache: *TemplateCache) Renderer { return .{ .context = context, .allocator = context.allocator(), .cache = cache, }; } fn checkForExtends(self: *const Renderer, nodes: []parser.Node) ?parser.Node { _ = self; for (nodes) |n| { if (n.type == .extends) return n; } return null; } fn readTemplateFile(self: *const Renderer, path: []const u8) ![]const u8 { const max_size = 10 * 1024 * 1024; const full_path = try std.fs.path.join(self.allocator, &.{ self.cache.default_path.?, path }); defer self.allocator.free(full_path); const file = try std.fs.cwd().openFile(full_path, .{}); defer file.close(); // return std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) { return file.readToEndAlloc(self.allocator, max_size) catch |err| switch (err) { else => return RenderError.Unexpected, }; } fn renderNodes(self: *const Renderer, alloc: Allocator, nodes: []parser.Node, writer: anytype) RenderError!void { const extends_node = self.checkForExtends(nodes); if (extends_node) |ext| { const base_template = try self.readTemplateFile(ext.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); } else { for (nodes) |node| { try self.renderNode(alloc, nodes, node, writer, null, null); } } } fn renderTemplate(self: *const Renderer, template: []const u8, writer: anytype, cache_key: ?[]const u8) RenderError!void { var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); const alloc = arena.allocator(); if (cache_key) |ck| { if (self.cache.get(ck)) |cached_nodes| { try self.renderNodes(alloc, cached_nodes, writer); return; } } var p = parser.Parser.init(template); const nodes = try p.parse(alloc); defer { for (nodes) |node| node.deinit(alloc); alloc.free(nodes); } if (cache_key) |ck| { var alc = self.cache.allocator(); var cached_nodes = try alc.alloc(parser.Node, nodes.len); errdefer alc.free(cached_nodes); for (nodes, 0..) |node, i| { cached_nodes[i] = try node.clone(self.allocator); std.debug.print("clonou {any}\n", .{cached_nodes[i]}); } try self.cache.add(ck, nodes); } return try self.renderNodes(alloc, nodes, writer); } pub fn render(self: *const Renderer, template_path: []const u8, writer: anytype) RenderError!void { const base_template = try self.readTemplateFile(template_path); defer self.allocator.free(base_template); return try self.renderTemplate(base_template, writer, template_path); } pub fn renderString(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void { return try self.renderTemplate(template, 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 o filho, passando o conteúdo do pai para block.super for (child.body) |child_node| { try self.renderNode(alloc, child_nodes, child_node, writer, null, base_node.block.?.body); } } else { // Renderiza o do pai for (base_node.block.?.body) |child| { try self.renderNode(alloc, child_nodes, child, writer, null, null); } } } else { // Qualquer outra coisa no base try self.renderNode(alloc, child_nodes, base_node, writer, null, null); } } } fn renderNode(self: *const Renderer, alloc: Allocator, nodes: []parser.Node, node: parser.Node, writer: anytype, context: ?*Context, parent_block_nodes: ?[]parser.Node) RenderError!void { 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; var arg: Value = Value.null; if (f.arg) |a| { arg = Value{ .string = a }; const result = try std.fmt.parseInt(i64, a, 10); if (std.math.maxInt(i64) < result) return error.Overflow; arg = Value{ .int = result }; } 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, null); } } else { for (node.@"if".?.false_body) |child| { try self.renderNode(alloc, nodes, child, writer, null, null); } } }, .include => { const included_template = try self.readTemplateFile(node.include.?.template_name); defer alloc.free(included_template); var included_parser = parser.Parser.init(included_template); const included_nodes = try included_parser.parse(alloc); defer { for (included_nodes) |n| n.deinit(alloc); alloc.free(included_nodes); } // Renderiza o include no contexto atual (sem novo contexto) for (included_nodes) |included_node| { try self.renderNode(alloc, nodes, included_node, writer, context, 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, null); } for (node.@"for".?.empty_body) |child| { try self.renderNode(alloc, nodes, child, writer, &ctx, null); } } }, .super => { if (parent_block_nodes) |parent| { for (parent) |child| { try self.renderNode(alloc, nodes, child, writer, null, null); } } }, .block => { for (node.block.?.body) |child| { const parent_content = parent_block_nodes orelse node.block.?.body; try self.renderNode(alloc, nodes, child, writer, null, parent_content); } }, 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, }; } fn evaluateCondition(self: *const Renderer, allocator: Allocator, expr: []const u8) RenderError!bool { const trimmed = std.mem.trim(u8, expr, " \t\r\n"); if (trimmed.len == 0) return false; var parts = std.mem.splitScalar(u8, trimmed, ' '); // Coleta tokens não vazios var tokens = std.ArrayList([]const u8){}; defer tokens.deinit(allocator); while (parts.next()) |part| { const t = std.mem.trim(u8, part, " \t\r\n"); if (t.len > 0) try tokens.append(allocator, t); } // Caso simples: só nome de variável if (tokens.items.len == 1) { const value = self.context.get(tokens.items[0]) orelse Value.null; return isTruthy(value); } // Caso especial: "not variavel" if (tokens.items.len == 2 and std.mem.eql(u8, tokens.items[0], "not")) { const value = self.context.get(tokens.items[1]) orelse Value.null; return !isTruthy(value); } // Caso com operadores de comparação: var op valor if (tokens.items.len == 3) { const left = tokens.items[0]; const op = tokens.items[1]; const right_str = tokens.items[2]; const left_value = self.context.get(left) orelse Value.null; const right_value = parseLiteral(right_str); if (std.mem.eql(u8, op, ">")) return compare(left_value, right_value, .gt); if (std.mem.eql(u8, op, "<")) return compare(left_value, right_value, .lt); if (std.mem.eql(u8, op, ">=")) return compare(left_value, right_value, .ge); if (std.mem.eql(u8, op, "<=")) return compare(left_value, right_value, .le); if (std.mem.eql(u8, op, "==")) return compare(left_value, right_value, .eq); if (std.mem.eql(u8, op, "!=")) return compare(left_value, right_value, .ne); return error.InvalidSyntax; } // Caso mais complexo (and/or/not composto) - por enquanto erro return error.UnsupportedExpression; } // Função auxiliar: converte string literal pra Value fn parseLiteral(str: []const u8) Value { const trimmed = std.mem.trim(u8, str, " \t\r\n\"'"); if (std.mem.eql(u8, trimmed, "true")) return Value{ .bool = true }; if (std.mem.eql(u8, trimmed, "false")) return Value{ .bool = false }; if (std.mem.eql(u8, trimmed, "null")) return Value.null; if (std.fmt.parseInt(i64, trimmed, 10)) |n| return Value{ .int = n } else |_| {} if (std.fmt.parseFloat(f64, trimmed)) |f| return Value{ .float = f } else |_| {} return Value{ .string = trimmed }; } // Função auxiliar: truthy check fn isTruthy(v: Value) bool { return switch (v) { .null => false, .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, }; } // Função auxiliar: comparação fn compare(left: Value, right: Value, op: enum { gt, lt, ge, le, eq, ne, not }) bool { // Implementação básica (expanda conforme necessário) switch (left) { .int => |l| switch (right) { .int => |r| return switch (op) { .gt => l > r, .lt => l < r, .ge => l >= r, .le => l <= r, .eq => l == r, .ne, .not => l != r, }, else => return false, }, .float => |l| switch (right) { .float => |r| return switch (op) { .gt => l > r, .lt => l < r, .ge => l >= r, .le => l <= r, .eq => l == r, .ne, .not => l != r, }, else => return false, }, .string => |l| switch (right) { .string => |r| return switch (op) { .eq => std.mem.eql(u8, l, r), .ne, .not => !std.mem.eql(u8, l, r), else => false, }, else => return false, }, else => return false, } } };