backup ideas
This commit is contained in:
parent
9bde4370a6
commit
efd19a8a81
9 changed files with 2991 additions and 382 deletions
51
src/cache_bkp.zig
Normal file
51
src/cache_bkp.zig
Normal file
|
|
@ -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 = .{};
|
||||
}
|
||||
};
|
||||
81
src/context_proposta.zig
Normal file
81
src/context_proposta.zig
Normal file
|
|
@ -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
|
||||
}
|
||||
};
|
||||
386
src/old/renderer.zig
Normal file
386
src/old/renderer.zig
Normal file
|
|
@ -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]",
|
||||
};
|
||||
}
|
||||
};
|
||||
395
src/old/renderer_bkp.zig
Normal file
395
src/old/renderer_bkp.zig
Normal file
|
|
@ -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]",
|
||||
};
|
||||
}
|
||||
};
|
||||
156
src/old/renderer_test.zig
Normal file
156
src/old/renderer_test.zig
Normal file
|
|
@ -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", "<script>alert('xss')</script>");
|
||||
try ctx.set("html", "<b>negrito</b>");
|
||||
|
||||
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, "<b>negrito</b>") != 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 =
|
||||
// \\<html>
|
||||
// \\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
|
||||
// \\<body>
|
||||
// \\{% block content %}Conteúdo padrão{% endblock %}
|
||||
// \\</body>
|
||||
// \\</html>
|
||||
// ;
|
||||
//
|
||||
// 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);
|
||||
// }
|
||||
1208
src/parser_bkp.zig
Normal file
1208
src/parser_bkp.zig
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
275
src/renderer_bkp.zig
Normal file
275
src/renderer_bkp.zig
Normal file
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -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 = "<script>alert()</script>" });
|
||||
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: <script>alert()</script>
|
||||
\\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 = "<div>conteúdo</div>" });
|
||||
|
||||
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: <div>conteúdo</div>", 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 =
|
||||
\\<html>
|
||||
\\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
|
||||
\\<body>
|
||||
\\{% block content %}Conteúdo padrão{% endblock %}
|
||||
\\</body>
|
||||
\\</html>
|
||||
;
|
||||
|
||||
const child =
|
||||
\\{% extends "base.html" %}
|
||||
\\{% block title %}Meu Título{% endblock %}
|
||||
\\{% block content %}
|
||||
\\Olá {{ nome }}!
|
||||
\\{% endblock %}
|
||||
;
|
||||
|
||||
const expected =
|
||||
\\<html>
|
||||
\\<head><title>Meu Título</title></head>
|
||||
\\<body>
|
||||
\\
|
||||
\\Olá Lucas!
|
||||
\\
|
||||
\\</body>
|
||||
\\</html>
|
||||
;
|
||||
|
||||
// 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, "<html>") != 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 =
|
||||
\\<html>
|
||||
\\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
|
||||
\\<body>
|
||||
\\{% block content %}Conteúdo padrão{% endblock %}
|
||||
\\</body>
|
||||
\\</html>
|
||||
;
|
||||
|
||||
const child =
|
||||
\\{% extends "base.html" %}
|
||||
\\{% block title %}Meu Título{% endblock %}
|
||||
\\{% block content %}
|
||||
\\{{ block.super }}
|
||||
\\Olá {{ nome }}!
|
||||
\\{% endblock %}
|
||||
;
|
||||
|
||||
const expected =
|
||||
\\<html>
|
||||
\\<head><title>Meu Título</title></head>
|
||||
\\<body>
|
||||
\\
|
||||
\\Conteúdo padrão
|
||||
\\Olá Lucas!
|
||||
\\
|
||||
\\</body>
|
||||
\\</html>
|
||||
;
|
||||
|
||||
// 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, "<html>") != 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 =
|
||||
\\<header>
|
||||
\\ <h1>Bem-vindo</h1>
|
||||
\\</header>
|
||||
;
|
||||
|
||||
const main =
|
||||
\\{% include "header.html" %}
|
||||
\\<main>
|
||||
\\ <p>Conteúdo principal</p>
|
||||
\\ Olá {{ nome }}!
|
||||
\\</main>
|
||||
;
|
||||
|
||||
const expected =
|
||||
\\<header>
|
||||
\\ <h1>Bem-vindo</h1>
|
||||
\\</header>
|
||||
\\<main>
|
||||
\\ <p>Conteúdo principal</p>
|
||||
\\ Olá Lucas!
|
||||
\\</main>
|
||||
;
|
||||
|
||||
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, "<h1>Bem-vindo</h1>") != 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 = "<script>alert()</script>" });
|
||||
// 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: <script>alert()</script>
|
||||
// \\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 = "<div>conteúdo</div>" });
|
||||
//
|
||||
// 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: <div>conteúdo</div>", 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 =
|
||||
// \\<html>
|
||||
// \\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
|
||||
// \\<body>
|
||||
// \\{% block content %}Conteúdo padrão{% endblock %}
|
||||
// \\</body>
|
||||
// \\</html>
|
||||
// ;
|
||||
//
|
||||
// const child =
|
||||
// \\{% extends "base.html" %}
|
||||
// \\{% block title %}Meu Título{% endblock %}
|
||||
// \\{% block content %}
|
||||
// \\Olá {{ nome }}!
|
||||
// \\{% endblock %}
|
||||
// ;
|
||||
//
|
||||
// const expected =
|
||||
// \\<html>
|
||||
// \\<head><title>Meu Título</title></head>
|
||||
// \\<body>
|
||||
// \\
|
||||
// \\Olá Lucas!
|
||||
// \\
|
||||
// \\</body>
|
||||
// \\</html>
|
||||
// ;
|
||||
//
|
||||
// // 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, "<html>") != 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 =
|
||||
// \\<html>
|
||||
// \\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
|
||||
// \\<body>
|
||||
// \\{% block content %}Conteúdo padrão{% endblock %}
|
||||
// \\</body>
|
||||
// \\</html>
|
||||
// ;
|
||||
//
|
||||
// const child =
|
||||
// \\{% extends "base.html" %}
|
||||
// \\{% block title %}Meu Título{% endblock %}
|
||||
// \\{% block content %}
|
||||
// \\{{ block.super }}
|
||||
// \\Olá {{ nome }}!
|
||||
// \\{% endblock %}
|
||||
// ;
|
||||
//
|
||||
// const expected =
|
||||
// \\<html>
|
||||
// \\<head><title>Meu Título</title></head>
|
||||
// \\<body>
|
||||
// \\
|
||||
// \\Conteúdo padrão
|
||||
// \\Olá Lucas!
|
||||
// \\
|
||||
// \\</body>
|
||||
// \\</html>
|
||||
// ;
|
||||
//
|
||||
// // 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, "<html>") != 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 =
|
||||
// \\<header>
|
||||
// \\ <h1>Bem-vindo</h1>
|
||||
// \\</header>
|
||||
// ;
|
||||
//
|
||||
// const main =
|
||||
// \\{% include "header.html" %}
|
||||
// \\<main>
|
||||
// \\ <p>Conteúdo principal</p>
|
||||
// \\ Olá {{ nome }}!
|
||||
// \\</main>
|
||||
// ;
|
||||
//
|
||||
// const expected =
|
||||
// \\<header>
|
||||
// \\ <h1>Bem-vindo</h1>
|
||||
// \\</header>
|
||||
// \\<main>
|
||||
// \\ <p>Conteúdo principal</p>
|
||||
// \\ Olá Lucas!
|
||||
// \\</main>
|
||||
// ;
|
||||
//
|
||||
// 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, "<h1>Bem-vindo</h1>") != 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, "<header>Bem-vindo</header>") != 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 =
|
||||
\\<main>
|
||||
\\ {% block content %}
|
||||
\\ Conteúdo padrão
|
||||
\\ {% endblock %}
|
||||
\\</main>
|
||||
;
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue