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 = - \\
- \\

Bem-vindo

- \\
- ; - - const main = - \\{% include "header.html" %} - \\
- \\

Conteúdo principal

- \\ Olá {{ nome }}! - \\
- ; - - const expected = - \\
- \\

Bem-vindo

- \\
- \\
- \\

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 = +// \\
+// \\

Bem-vindo

+// \\
+// ; +// +// const main = +// \\{% include "header.html" %} +// \\
+// \\

Conteúdo principal

+// \\ Olá {{ nome }}! +// \\
+// ; +// +// const expected = +// \\
+// \\

Bem-vindo

+// \\
+// \\
+// \\

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, "
Bem-vindo
") != 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); +}