diff --git a/src/renderer.zig b/src/renderer.zig new file mode 100644 index 0000000..e761c47 --- /dev/null +++ b/src/renderer.zig @@ -0,0 +1,105 @@ +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(), + }; + } + + pub fn renderString(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void { + var p = parser.Parser.init(template); + const nodes = try p.parse(self.allocator); + for (nodes) |node| { + try self.renderNode(node, writer); + } + } + + fn renderNode(self: *const Renderer, node: parser.Node, writer: anytype) RenderError!void { + switch (node.type) { + .text => try writer.writeAll(node.text.?.content), + .variable => { + const var_name = node.variable.?.expr; + var value: Value = self.context.get(var_name) orelse Value.null; + var buf = ArrayListUnmanaged(u8){}; + defer buf.deinit(self.allocator); + + var is_safe = false; + + for (node.variable.?.filters) |f| { + const filter_fn = builtin_filters.get(f.name) orelse continue; + value = try filter_fn(self.allocator, value, Value{.string = f.arg orelse ""}); + 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(self.allocator, value, null); + } + } + + try self.valueToString(&buf, value); + try writer.writeAll(buf.items); + }, + else => unreachable, + } + } + + fn valueToString(self: *const Renderer, buf: *ArrayListUnmanaged(u8), value: Value) !void { + var w = buf.writer(self.allocator); + 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}"), + } + } +}; diff --git a/src/renderer_test.zig b/src/renderer_test.zig new file mode 100644 index 0000000..977ac46 --- /dev/null +++ b/src/renderer_test.zig @@ -0,0 +1,120 @@ +const std = @import("std"); +const testing = std.testing; + +const Renderer = @import("renderer.zig").Renderer; +const RenderError = @import("renderer.zig").RenderError; + +const Context = @import("context.zig").Context; +const Value = @import("context.zig").Value; // ajuste o caminho se estiver diferente + +test "renderer: literal + variável simples" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("nome", Value{ .string = "Mariana" }); + + const renderer = Renderer.init(&ctx); + + 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(); + + try ctx.set("html", Value{ .string = "" }); + try ctx.set("texto", Value{ .string = "maiusculo e slug" }); + + const renderer = Renderer.init(&ctx); + + 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(); + + const renderer = Renderer.init(&ctx); + + 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(); + + const renderer = Renderer.init(&ctx); + + 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(); + + const renderer = Renderer.init(&ctx); + + 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); +}