zdt-prov/src/renderer.zig

408 lines
15 KiB
Zig

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