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, }; } };