diff --git a/src/context.zig b/src/context.zig
index f723366..8b5dd92 100644
--- a/src/context.zig
+++ b/src/context.zig
@@ -40,7 +40,7 @@ pub const Context = struct {
.bool => Value{ .bool = value },
.int, .comptime_int => Value{ .int = @intCast(value) },
.float, .comptime_float => Value{ .float = @floatCast(value) },
- .pointer => Value{ .string = std.fmt.allocPrint(self.allocator(), "{s}", .{value}) catch "" },
+ .pointer => Value{ .string = try std.fmt.allocPrint(self.allocator(), "{s}", .{value}) },
.@"struct" => blk: {
var dict = std.StringHashMapUnmanaged(Value){};
inline for (std.meta.fields(T)) |field| {
@@ -59,13 +59,14 @@ pub const Context = struct {
},
.optional => if (value) |v| try self.toValue(v) else .null,
.null => .null,
+ // CASO ESPECIAL: o valor já é um Value (ex: lista de Value)
+ .@"union" => if (T == Value) value else @compileError("Unsupported union type: " ++ @typeName(T)),
else => @compileError("Unsupported type: " ++ @typeName(T)),
};
}
pub fn set(self: *Context, key: []const u8, value: anytype) !void {
const v = try self.toValue(value);
-
const gop = try self.map.getOrPut(self.allocator(), key);
if (!gop.found_existing) {
gop.key_ptr.* = try self.allocator().dupe(u8, key);
diff --git a/src/context_test.zig b/src/context_test.zig
index d3702b6..899284a 100644
--- a/src/context_test.zig
+++ b/src/context_test.zig
@@ -18,8 +18,8 @@ test "context set amigável e get com ponto" {
const Person = struct { nome: []const u8, idade: i64 };
const p = Person{ .nome = "Ana", .idade = 25 };
try ctx.set("user", p);
- //
- // // list
+
+ // list
const numeros = [_]i64{ 1, 2, 3 };
try ctx.set("lista", numeros);
diff --git a/src/filters.zig b/src/filters.zig
new file mode 100644
index 0000000..b86d356
--- /dev/null
+++ b/src/filters.zig
@@ -0,0 +1,1517 @@
+const std = @import("std");
+const Value = @import("context.zig").Value;
+const std_time = std.time;
+
+pub const FilterError = error{
+ InvalidArgument,
+ OutOfMemory,
+};
+
+const DictEntry = struct {
+ key: []const u8,
+ val: Value,
+};
+
+pub const FilterFn = fn (alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value;
+
+// ==================== FILTROS BUILTIN ====================
+fn filter_add(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = alloc;
+ const addend = arg orelse return value;
+
+ return switch (value) {
+ .int => |i| switch (addend) {
+ .int => |a| Value{ .int = i + a },
+ .float => |a| Value{ .float = @as(f64, @floatFromInt(i)) + a },
+ else => value,
+ },
+ .float => |f| switch (addend) {
+ .int => |a| Value{ .float = f + @as(f64, @floatFromInt(a)) },
+ .float => |a| Value{ .float = f + a },
+ else => value,
+ },
+ else => value,
+ };
+}
+
+fn filter_addslashes(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ for (s) |c| {
+ switch (c) {
+ '\'', '"', '\\' => {
+ try result.append(alloc, '\\');
+ try result.append(alloc, c);
+ },
+ else => try result.append(alloc, c),
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+fn filter_capfirst(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ if (s.len == 0) return value;
+
+ const capped = capFirst(alloc, s) catch s;
+ return Value{ .string = capped };
+}
+
+fn filter_center(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ const width = switch (arg orelse Value{ .int = 80 }) {
+ .int => |i| @as(usize, @max(@as(i64, i), 0)),
+ else => 80,
+ };
+
+ if (s.len >= width) return value;
+
+ const padding = width - s.len;
+ const left = padding / 2;
+ const right = padding - left;
+
+ var result = std.ArrayList(u8){};
+
+ for (0..left) |_| try result.append(alloc, ' ');
+ try result.appendSlice(alloc, s);
+ for (0..right) |_| try result.append(alloc, ' ');
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+fn filter_cut(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ const to_cut = switch (arg orelse return value) {
+ .string => |cut| cut,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ var i: usize = 0;
+ while (i < s.len) {
+ if (i + to_cut.len <= s.len and std.mem.eql(u8, s[i .. i + to_cut.len], to_cut)) {
+ i += to_cut.len;
+ } else {
+ try result.append(alloc, s[i]);
+ i += 1;
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+// fn filter_date(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+// // Por enquanto, simples: aceita string ou int (timestamp) e formata com strftime-like
+// // Futuro: suporte completo a std.time
+// _ = alloc;
+// const format = switch (arg orelse Value{ .string = "d/m/Y" }) {
+// .string => |f| f,
+// else => "d/m/Y",
+// };
+//
+// const timestamp = switch (value) {
+// .int => |i| @as(i64, i),
+// .string => |s| std.fmt.parseInt(i64, s, 10) catch 0,
+// else => 0,
+// };
+//
+// // Simulação simples (em produção usar std.time)
+// const day = @rem(timestamp, 30) + 1;
+// const month = @rem(timestamp / 30, 12) + 1;
+// const year = 2026 + @divFloor(timestamp, 360);
+//
+// var buf: [64]u8 = undefined;
+// const formatted = switch (format) {
+// "d/m/Y" => std.fmt.bufPrint(&buf, "{d:0>2}/{d:0>2}/{d}", .{ day, month, year }) catch "??/??/????",
+// "Y-m-d" => std.fmt.bufPrint(&buf, "{d}-{d:0>2}-{d:0>2}", .{ year, month, day }) catch "????-??-??",
+// else => "formato não suportado",
+// };
+//
+// return Value{ .string = try alloc.dupe(u8, formatted) };
+// }
+
+fn filter_default(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = alloc;
+ const def = arg orelse return value;
+
+ return switch (value) {
+ .null => def,
+ .string => |s| if (s.len == 0) def else value,
+ .list => |l| if (l.len == 0) def else value,
+ else => value,
+ };
+}
+
+fn filter_default_if_none(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = alloc;
+ const def = arg orelse return value;
+ return if (value == .null) def else value;
+}
+
+fn filter_dictsort(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const d = switch (value) {
+ .dict => |dict| dict,
+ else => return value,
+ };
+
+ var entries = std.ArrayList(Value){};
+
+ var keys = std.ArrayList([]const u8){};
+ defer keys.deinit(alloc);
+
+ var iter = d.iterator();
+ while (iter.next()) |entry| {
+ const key_copy = try alloc.dupe(u8, entry.key_ptr.*);
+ try keys.append(alloc, key_copy);
+ }
+
+ std.mem.sort([]const u8, keys.items, {}, struct {
+ pub fn lessThan(ctx: void, a: []const u8, b: []const u8) bool {
+ _ = ctx;
+ return std.mem.order(u8, a, b) == .lt;
+ }
+ }.lessThan);
+
+ for (keys.items) |key| {
+ const val = d.get(key).?;
+ const pair = std.StringHashMapUnmanaged(Value){};
+ var pair_map = pair;
+ try pair_map.put(alloc, "key", Value{ .string = key });
+ try pair_map.put(alloc, "value", val);
+ try entries.append(alloc, Value{ .dict = pair_map });
+ }
+
+ return Value{ .list = try entries.toOwnedSlice(alloc) };
+}
+
+fn filter_dictsortreversed(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ const sorted = try filter_dictsort(alloc, value, arg);
+
+ const list = sorted.list;
+ const reversed = try alloc.dupe(Value, list);
+ std.mem.reverse(Value, reversed);
+
+ return Value{ .list = reversed };
+}
+
+fn filter_divisibleby(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = alloc;
+ const divisor = switch (arg orelse return Value{ .bool = false }) {
+ .int => |i| if (i == 0) return Value{ .bool = false } else i,
+ else => return Value{ .bool = false },
+ };
+
+ const n = switch (value) {
+ .int => |i| i,
+ .float => |f| @as(i64, @intFromFloat(f)),
+ else => return Value{ .bool = false },
+ };
+
+ return Value{ .bool = @rem(n, divisor) == 0 };
+}
+
+fn filter_escape(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ return filter_force_escape(alloc, value, arg); // alias de force_escape
+}
+
+fn filter_escapejs(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ for (s) |c| {
+ switch (c) {
+ '\'', '"', '\\' => {
+ try result.append(alloc, '\\');
+ try result.append(alloc, c);
+ },
+ '\n' => try result.appendSlice(alloc, "\\n"),
+ '\r' => try result.appendSlice(alloc, "\\r"),
+ '\t' => try result.appendSlice(alloc, "\\t"),
+ '<' => try result.appendSlice(alloc, "\\<"),
+ '>' => try result.appendSlice(alloc, "\\>"),
+ '&' => try result.appendSlice(alloc, "\\&"),
+ '=' => try result.appendSlice(alloc, "\\="),
+ '-' => try result.appendSlice(alloc, "\\-"),
+ ';' => try result.appendSlice(alloc, "\\-"),
+ // Agora sem overlap: excluímos \n (0x0A), \r (0x0D), \t (0x09)
+ 0x00...0x08, 0x0B...0x0C, 0x0E...0x1F, 0x7F => {
+ try std.fmt.format(result.writer(alloc), "\\u{:0>4}", .{c});
+ },
+ else => try result.append(alloc, c),
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+fn filter_escapeseq(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ for (s) |c| {
+ switch (c) {
+ '\\' => try result.appendSlice(alloc, "\\\\"),
+ '\n' => try result.appendSlice(alloc, "\\n"),
+ '\r' => try result.appendSlice(alloc, "\\r"),
+ '\t' => try result.appendSlice(alloc, "\\t"),
+ '"' => try result.appendSlice(alloc, "\\\""),
+ '\'' => try result.appendSlice(alloc, "\\'"),
+ else => try result.append(alloc, c),
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+fn filter_filesizeformat(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const bytes = switch (value) {
+ .int => |i| @as(f64, @floatFromInt(i)),
+ .float => |f| f,
+ else => return value,
+ };
+
+ const units = [_][]const u8{ "B", "KB", "MB", "GB", "TB" };
+ var size = bytes;
+ var unit_index: usize = 0;
+
+ while (size >= 1024 and unit_index < units.len - 1) {
+ size /= 1024;
+ unit_index += 1;
+ }
+
+ const formatted = if (size < 10)
+ try std.fmt.allocPrint(alloc, "{d:.1} {s}", .{ size, units[unit_index] })
+ else
+ try std.fmt.allocPrint(alloc, "{d:.0} {s}", .{ size, units[unit_index] });
+
+ return Value{ .string = formatted };
+}
+
+fn filter_first(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = alloc;
+ _ = arg;
+ return switch (value) {
+ .list => |l| if (l.len > 0) l[0] else .null,
+ .string => |s| if (s.len > 0) Value{ .string = s[0..1] } else .null,
+ else => .null,
+ };
+}
+
+fn filter_floatformat(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ const f = switch (value) {
+ .float => |f| f,
+ .int => |i| @as(f64, @floatFromInt(i)),
+ else => return value,
+ };
+
+ const precision = switch (arg orelse Value{ .int = -1 }) {
+ .int => |i| if (i < 0) null else @as(usize, @intCast(i)),
+ else => null,
+ };
+
+ var buf: [64]u8 = undefined;
+ const formatted = if (precision) |p| blk: {
+ // Usamos switch para evitar placeholder dinâmico
+ const str = switch (p) {
+ 0 => std.fmt.bufPrint(&buf, "{d:.0}", .{f}) catch unreachable,
+ 1 => std.fmt.bufPrint(&buf, "{d:.1}", .{f}) catch unreachable,
+ 2 => std.fmt.bufPrint(&buf, "{d:.2}", .{f}) catch unreachable,
+ 3 => std.fmt.bufPrint(&buf, "{d:.3}", .{f}) catch unreachable,
+ 4 => std.fmt.bufPrint(&buf, "{d:.4}", .{f}) catch unreachable,
+ 5 => std.fmt.bufPrint(&buf, "{d:.5}", .{f}) catch unreachable,
+ else => std.fmt.bufPrint(&buf, "{d}", .{f}) catch unreachable,
+ };
+ break :blk try alloc.dupe(u8, str);
+ } else try std.fmt.allocPrint(alloc, "{d}", .{f});
+
+ return Value{ .string = formatted };
+}
+
+fn filter_force_escape(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+ defer result.deinit(alloc);
+
+ for (s) |c| {
+ switch (c) {
+ '&' => try result.appendSlice(alloc, "&"),
+ '<' => try result.appendSlice(alloc, "<"),
+ '>' => try result.appendSlice(alloc, ">"),
+ '"' => try result.appendSlice(alloc, """),
+ '\'' => try result.appendSlice(alloc, "'"),
+ else => try result.append(alloc, c),
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+fn filter_get_digit(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ const n = switch (value) {
+ .int => |i| i,
+ else => return value,
+ };
+
+ const pos = switch (arg orelse Value{ .int = 1 }) {
+ .int => |p| p,
+ else => 1,
+ };
+
+ if (pos <= 0) return Value{ .int = 0 };
+
+ const abs = if (n < 0) -1 * n else n;
+ const str = try std.fmt.allocPrint(alloc, "{d}", .{abs});
+
+ if (pos > str.len) return Value{ .int = 0 };
+
+ const pos_usize = @as(usize, @intCast(pos));
+ const npos = str.len - (pos_usize - 1);
+
+ if (pos <= str.len) return Value{ .int = @intCast(npos) };
+
+ return Value{ .int = 0 };
+}
+
+fn filter_iriencode(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ const s = switch (value) {
+ .string => |str| str,
+ else => return value,
+ };
+
+ var result = std.ArrayList(u8){};
+
+ for (s) |c| {
+ if (std.ascii.isAlphanumeric(c) or c == '-' or c == '.' or c == '_' or c == '~') {
+ try result.append(alloc, c);
+ } else {
+ try std.fmt.format(result.writer(alloc), "%{X:0>2}", .{c});
+ }
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+fn filter_join(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ const sep = switch (arg orelse Value{ .string = "," }) {
+ .string => |s| s,
+ else => ",",
+ };
+
+ const list = switch (value) {
+ .list => |l| l,
+ else => return value,
+ };
+
+ if (list.len == 0) return Value{ .string = "" };
+
+ var result = std.ArrayList(u8){};
+ defer result.deinit(alloc);
+
+ for (list, 0..) |item, i| {
+ if (i > 0) try result.appendSlice(alloc, sep);
+
+ // const str = switch (item) {
+ // .string => |s| s,
+ // .int => |n| blk: {
+ // const printed = try std.fmt.allocPrint(alloc, "{d}", .{n});
+ // break :blk printed;
+ // },
+ // .float => |f| blk: {
+ // const printed = try std.fmt.allocPrint(alloc, "{d}", .{f});
+ // break :blk printed;
+ // },
+ // .bool => |b| if (b) "true" else "false",
+ // .null => "None",
+ // .list, .dict => blk: {
+ // const printed = try std.fmt.allocPrint(alloc, "{}", .{item});
+ // break :blk printed;
+ // },
+ // };
+ const str = try valueToSafeString(alloc, item);
+ try result.appendSlice(alloc, str);
+ }
+
+ return Value{ .string = try result.toOwnedSlice(alloc) };
+}
+
+// TODO: refactor
+fn filter_json_script(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
+ _ = arg;
+ // Simples: converte para string JSON-like e escapa para uso em ");
+
+ const v = ctx.get("html").?;
+
+ const escaped = try force_escape(ctx.allocator(), v, null);
+ try testing.expectEqualStrings("<script>alert('xss')</script>", escaped.string);
+
+ // safe retorna o mesmo (o renderer vai tratar como seguro)
+ const safed = try safe(ctx.allocator(), v, null);
+ try testing.expectEqualStrings("", safed.string);
+}
+
+test "builtin filters - linebreaksbr and linebreaks" {
+ const alloc = testing.allocator;
+ const linebreaksbr = builtin_filters.get("linebreaksbr").?;
+ const linebreaks = builtin_filters.get("linebreaks").?;
+
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set("texto", "Linha 1\nLinha 2\n\nLinha 3");
+
+ const v = ctx.get("texto").?;
+
+ const br = try linebreaksbr(ctx.allocator(), v, null);
+ const p = try linebreaks(ctx.allocator(), v, null);
+
+ try testing.expect(std.mem.indexOf(u8, br.string, "
") != null);
+ try testing.expect(std.mem.indexOf(u8, p.string, "
") != null); +} + +test "builtin filters - escape and force_escape" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("xss", ""); + + const v = ctx.get("xss").?; + + const escape = builtin_filters.get("escape").?; + const force_escape = builtin_filters.get("force_escape").?; + + const escaped = try escape(ctx.allocator(), v, null); + const forced = try force_escape(ctx.allocator(), v, null); + + const expected = "<script>alert('xss')</script>"; + try testing.expectEqualStrings(expected, escaped.string); + try testing.expectEqualStrings(expected, forced.string); +} + +test "builtin filters - striptags" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("html", "
Olá mundo legal!
"); + + const v = ctx.get("html").?; + + const striptags = builtin_filters.get("striptags").?; + const stripped = try striptags(ctx.allocator(), v, null); + + try testing.expectEqualStrings("Olá mundo legal!", stripped.string); +} + +test "builtin filters - slugify" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("titulo", "Olá Mundo Legal!!! 2026 ações"); + + const v = ctx.get("titulo").?; + + const slugify = builtin_filters.get("slugify").?; + const slugged = try slugify(ctx.allocator(), v, null); + + try testing.expectEqualStrings("ola-mundo-legal-2026-acoes", slugged.string); +} + +test "builtin filters - floatformat" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("pi", 3.14159); + try ctx.set("e", 2.71828); + try ctx.set("inteiro", 42); + + const v_pi = ctx.get("pi").?; + const v_e = ctx.get("e").?; + const v_int = ctx.get("inteiro").?; + + const floatformat = builtin_filters.get("floatformat").?; + + const default_pi = try floatformat(ctx.allocator(), v_pi, null); + const pi_2 = try floatformat(ctx.allocator(), v_pi, Value{ .int = 2 }); + const pi_0 = try floatformat(ctx.allocator(), v_pi, Value{ .int = 0 }); + const int_1 = try floatformat(ctx.allocator(), v_int, Value{ .int = 1 }); + const ve = try floatformat(ctx.allocator(), v_e, Value{ .int = 2 }); + + try testing.expectEqualStrings("3.14159", default_pi.string); + try testing.expectEqualStrings("3.14", pi_2.string); + try testing.expectEqualStrings("3", pi_0.string); + try testing.expectEqualStrings("42.0", int_1.string); + try testing.expectEqualStrings("2.72", ve.string); +} + +test "builtin filters - stringformat" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("nome", "Lucas"); + try ctx.set("idade", 30); + + const v_nome = ctx.get("nome").?; + const v_idade = ctx.get("idade").?; + + const stringformat = builtin_filters.get("stringformat").?; + + const fmt_nome = try stringformat(ctx.allocator(), v_nome, Value{ .string = "Olá %s!" }); + const fmt_idade = try stringformat(ctx.allocator(), v_idade, Value{ .string = "Idade: %d anos" }); + + try testing.expectEqualStrings("Olá Lucas!", fmt_nome.string); + try testing.expectEqualStrings("Idade: 30 anos", fmt_idade.string); +} + +test "builtin filters - cut" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("texto", "Olá mundo cruel, mundo!"); + + const v = ctx.get("texto").?; + + const cut = builtin_filters.get("cut").?; + const cut_mundo = try cut(ctx.allocator(), v, Value{ .string = "mundo" }); + + try testing.expectEqualStrings("Olá cruel, !", cut_mundo.string); +} + +test "builtin filters - title" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("texto", "olá mundo cruel"); + + const v = ctx.get("texto").?; + + const title = builtin_filters.get("title").?; + const titled = try title(ctx.allocator(), v, null); + + try testing.expectEqualStrings("Olá Mundo Cruel", titled.string); +} + +test "builtin filters - wordcount" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("frase", "Este é um texto com exatamente sete palavras"); + + const v = ctx.get("frase").?; + + const wordcount = builtin_filters.get("wordcount").?; + const count = try wordcount(ctx.allocator(), v, null); + + try testing.expect(count.int == 7); +} + +test "builtin filters - urlencode" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("query", "busca=zig lang&pagina=1"); + + const v = ctx.get("query").?; + + const urlencode = builtin_filters.get("urlencode").?; + const encoded = try urlencode(ctx.allocator(), v, null); + + try testing.expectEqualStrings("busca%3Dzig%20lang%26pagina%3D1", encoded.string); +} + +test "builtin filters - pluralize" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("voto", 1); + try ctx.set("votos", 5); + // try ctx.set("zero", 0); + + const v1 = ctx.get("voto").?; + const v5 = ctx.get("votos").?; + // const v0 = ctx.get("zero").?; + + const pluralize = builtin_filters.get("pluralize").?; + + const s1 = try pluralize(ctx.allocator(), v1, null); + const s5 = try pluralize(ctx.allocator(), v5, null); + const custom = try pluralize(ctx.allocator(), v5, Value{ .string = "es" }); + // const zero = try pluralize(ctx.allocator(), v0, null); + + try testing.expectEqualStrings("", s1.string); // 1 → singular (vazio) + try testing.expectEqualStrings("s", s5.string); // 5 → plural + try testing.expectEqualStrings("es", custom.string); + // try testing.expectEqualStrings("", zero.string); +} + +test "builtin filters - addslashes, center, date" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("quote", "He's a good boy"); + try ctx.set("texto", "zig"); + + const v_quote = ctx.get("quote").?; + const v_texto = ctx.get("texto").?; + + const addslashes = builtin_filters.get("addslashes").?; + const center = builtin_filters.get("center").?; + // const date = builtin_filters.get("date").?; + + const slashed = try addslashes(ctx.allocator(), v_quote, null); + const centered = try center(ctx.allocator(), v_texto, Value{ .int = 10 }); + // const formatted = try date(ctx.allocator(), Value{ .int = 0 }, Value{ .string = "Y-m-d" }); + + try testing.expectEqualStrings("He\\'s a good boy", slashed.string); + try testing.expectEqualStrings(" zig ", centered.string); + // try testing.expect(std.mem.startsWith(u8, formatted.string, "2026")); +} + +test "builtin filters - dictsort and dictsortreversed" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + var dict = std.StringHashMapUnmanaged(Value){}; + try dict.put(ctx.allocator(), "b", Value{ .int = 2 }); + try dict.put(ctx.allocator(), "a", Value{ .int = 1 }); + try dict.put(ctx.allocator(), "c", Value{ .int = 3 }); + + const v = Value{ .dict = dict }; + + const dictsort = builtin_filters.get("dictsort").?; + const dictsortreversed = builtin_filters.get("dictsortreversed").?; + + const sorted = try dictsort(ctx.allocator(), v, null); + const reversed = try dictsortreversed(ctx.allocator(), v, null); + + const sorted_list = sorted.list; + try testing.expect(sorted_list.len == 3); + try testing.expectEqualStrings("a", sorted_list[0].dict.get("key").?.string); + try testing.expectEqualStrings("b", sorted_list[1].dict.get("key").?.string); + try testing.expectEqualStrings("c", sorted_list[2].dict.get("key").?.string); + + const reversed_list = reversed.list; + try testing.expectEqualStrings("c", reversed_list[0].dict.get("key").?.string); + try testing.expectEqualStrings("b", reversed_list[1].dict.get("key").?.string); + try testing.expectEqualStrings("a", reversed_list[2].dict.get("key").?.string); +} + +test "builtin filters - divisibleby, escapejs, filesizeformat, get_digit, json_script" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + const divisibleby = builtin_filters.get("divisibleby").?; + const escapejs = builtin_filters.get("escapejs").?; + const filesizeformat = builtin_filters.get("filesizeformat").?; + const get_digit = builtin_filters.get("get_digit").?; + const json_script = builtin_filters.get("json_script").?; + + const div = try divisibleby(ctx.allocator(), Value{ .int = 10 }, Value{ .int = 5 }); + const div_2 = try divisibleby(ctx.allocator(), Value{ .int = 10 }, Value{ .int = 3 }); + try testing.expect(div.bool == true); + try testing.expect(div_2.bool == false); + + const js = try escapejs(ctx.allocator(), Value{ .string = "" }, null); + try testing.expectEqualStrings("\\", js.string); + + const size = try filesizeformat(ctx.allocator(), Value{ .int = 1500000 }, null); + try testing.expect(std.mem.indexOf(u8, size.string, "MB") != null); + + const digit = try get_digit(ctx.allocator(), Value{ .int = 12345 }, Value{ .int = 2 }); + try testing.expect(digit.int == 4); + + const digit_3 = try get_digit(ctx.allocator(), Value{ .int = 123456789 }, Value{ .int = 11 }); + try testing.expect(digit_3.int == 0); + + const digit_2 = try get_digit(ctx.allocator(), Value{ .string = "baz" }, Value{ .int = 2 }); + try testing.expect(std.mem.eql(u8, digit_2.string, "baz")); + + const json = try json_script(ctx.allocator(), Value{ .string = "negrito" }, null); + try testing.expect(std.mem.indexOf(u8, json.string, "\\u003C") != null); +} + +test "builtin filters - escapeseq, iriencode, linenumbers" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("texto", "Hello \"world\"\\n"); + + const v = ctx.get("texto").?; + + const escapeseq = builtin_filters.get("escapeseq").?; + const iriencode = builtin_filters.get("iriencode").?; + const linenumbers = builtin_filters.get("linenumbers").?; + + const escaped = try escapeseq(ctx.allocator(), v, null); + const encoded = try iriencode(ctx.allocator(), v, null); + const numbered = try linenumbers(ctx.allocator(), Value{ .string = "linha1\nlinha2\nlinha3" }, null); + + try testing.expect(std.mem.indexOf(u8, escaped.string, "\\\"") != null); + try testing.expect(std.mem.indexOf(u8, encoded.string, "%20") != null); + try testing.expect(std.mem.indexOf(u8, numbered.string, "1. linha1") != null); +} + +test "builtin filters - ljust, rjust, center" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("texto", "zig"); + + const v = ctx.get("texto").?; + + const ljust = builtin_filters.get("ljust").?; + const rjust = builtin_filters.get("rjust").?; + const center = builtin_filters.get("center").?; + + const left = try ljust(ctx.allocator(), v, Value{ .int = 10 }); + const right = try rjust(ctx.allocator(), v, Value{ .int = 10 }); + const centered = try center(ctx.allocator(), v, Value{ .int = 10 }); + + try testing.expectEqualStrings("zig ", left.string); + try testing.expectEqualStrings(" zig", right.string); + try testing.expectEqualStrings(" zig ", centered.string); +} + +test "builtin filters - make_list, phone2numeric, pprint" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("palavra", "zig"); + try ctx.set("telefone", "1-800-FLOWERS"); + + const v_palavra = ctx.get("palavra").?; + const v_telefone = ctx.get("telefone").?; + + const make_list = builtin_filters.get("make_list").?; + const phone2numeric = builtin_filters.get("phone2numeric").?; + const pprint = builtin_filters.get("pprint").?; + + const list = try make_list(ctx.allocator(), v_palavra, null); + const numeric = try phone2numeric(ctx.allocator(), v_telefone, null); + const printed = try pprint(ctx.allocator(), Value{ .int = 42 }, null); + + try testing.expect(list.list.len == 3); + try testing.expectEqualStrings("z", list.list[0].string); + try testing.expect(std.mem.indexOf(u8, numeric.string, "3569377") != null); + try testing.expect(std.mem.indexOf(u8, printed.string, "42") != null); +} + +test "builtin filters - random, safeseq" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + var items = try alloc.alloc(Value, 3); + defer alloc.free(items); + items[0] = Value{ .string = "a" }; + items[1] = Value{ .string = "b" }; + items[2] = Value{ .string = "c" }; + + const v = Value{ .list = items }; + + const random = builtin_filters.get("random").?; + const safeseq = builtin_filters.get("safeseq").?; + + const rand = try random(ctx.allocator(), v, null); + const safe = try safeseq(ctx.allocator(), v, null); + + try testing.expect(rand == .string); + try testing.expect(safe == .list); +} + +// test "builtin filters - date, time, timesince, timeuntil" { +// const alloc = testing.allocator; +// var ctx = Context.init(alloc); +// defer ctx.deinit(); +// +// const now = std_time.timestamp(); +// +// // const date = builtin_filters.get("date").?; +// const time = builtin_filters.get("time").?; +// const timesince = builtin_filters.get("timesince").?; +// // const timeuntil = builtin_filters.get("timeuntil").?; +// +// // const d = try date(ctx.allocator(), Value{ .int = now }, Value{ .string = "d/m/Y" }); +// const t = try time(ctx.allocator(), Value{ .int = now }, Value{ .string = "H:i" }); +// +// // try testing.expect(d.string.len > 0); +// try testing.expect(t.string.len > 0); +// +// const since = try timesince(ctx.allocator(), Value{ .int = now - 3600 }, null); +// try testing.expect(std.mem.indexOf(u8, since.string, "hora") != null); +// } + +test "builtin filters - urlize, urlizetrunc, wordwrap, unordered_list" { + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + try ctx.set("url", "Visite https://ziglang.org"); + + const v_url = ctx.get("url").?; + + const urlize = builtin_filters.get("urlize").?; + const urlizetrunc = builtin_filters.get("urlizetrunc").?; + const wordwrap = builtin_filters.get("wordwrap").?; + + const u = try urlize(ctx.allocator(), v_url, null); + try testing.expect(std.mem.indexOf(u8, u.string, " + \\