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("\\alert(\\'oi\\')\\", 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, " + \\
  • item1
  • + \\
  • item2
  • + \\ + ; + std.debug.print("lista gerada: {any}\n", .{ul.string}); + try testing.expectEqualStrings(expected, ul.string); +} diff --git a/todo.md b/todo.md index c7cd2be..2ef6247 100644 --- a/todo.md +++ b/todo.md @@ -31,63 +31,63 @@ # Filters -- [ ] add -- [ ] addslashes -- [ ] capfirst -- [ ] center -- [ ] cut +- [x] add +- [x] addslashes +- [x] capfirst +- [x] center +- [x] cut - [ ] date -- [ ] default -- [ ] default_if_none -- [ ] dictsort -- [ ] dictsortreversed -- [ ] divisibleby -- [ ] escape -- [ ] escapejs -- [ ] escapeseq -- [ ] filesizeformat -- [ ] first -- [ ] floatformat -- [ ] force_escape -- [ ] get_digit -- [ ] iriencode -- [ ] join -- [ ] json_script -- [ ] last -- [ ] length -- [ ] linebreaks -- [ ] linebreaksbr -- [ ] linenumbers -- [ ] ljust -- [ ] lower -- [ ] make_list -- [ ] phone2numeric -- [ ] pluralize -- [ ] pprint -- [ ] random -- [ ] rjust -- [ ] safe -- [ ] safeseq -- [ ] slice -- [ ] slugify -- [ ] stringformat -- [ ] striptags +- [x] default +- [x] default_if_none +- [x] dictsort +- [x] dictsortreversed +- [x] divisibleby +- [x] escape +- [x] escapejs +- [x] escapeseq +- [x] filesizeformat +- [x] first +- [x] floatformat +- [x] force_escape +- [x] get_digit +- [x] iriencode +- [x] join +- [x] json_script +- [x] last +- [x] length +- [x] linebreaks +- [x] linebreaksbr +- [x] linenumbers +- [x] ljust +- [x] lower +- [x] make_list +- [x] phone2numeric +- [x] pluralize +- [x] pprint +- [x] random +- [x] rjust +- [x] safe +- [x] safeseq +- [x] slice +- [x] slugify +- [x] stringformat +- [x] striptags - [ ] time - [ ] timesince - [ ] timeuntil -- [ ] title -- [ ] truncatechars -- [ ] truncatechars_html -- [ ] truncatewords -- [ ] truncatewords_html -- [ ] unordered_list -- [ ] upper -- [ ] urlencode -- [ ] urlize -- [ ] urlizetrunc -- [ ] wordcount -- [ ] wordwrap -- [ ] yesno +- [x] title +- [x] truncatechars +- [-] truncatechars_html +- [x] truncatewords +- [-] truncatewords_html +- [x] unordered_list +- [x] upper +- [x] urlencode +- [x] urlize +- [x] urlizetrunc +- [x] wordcount +- [x] wordwrap +- [x] yesno ___ @@ -131,3 +131,87 @@ ___ - Tudo testado 4 -Filtros + + + + + +na verdade o pushScope não tava não, a versão final que vc me mandou depois que se inspirou no tokamak foi essa: +``` +const std = @import("std"); + +pub const Value = union(enum) { + null, + bool: bool, + int: i64, + float: f64, + string: []const u8, + list: []const Value, + dict: std.StringHashMapUnmanaged(Value), + + pub fn deinit(self: Value) void { + _ = self; // nada — a arena libera tudo + } +}; + +pub const Context = struct { + arena: std.heap.ArenaAllocator, + map: std.StringHashMapUnmanaged(Value), + + pub fn init(child_allocator: std.mem.Allocator) Context { + const arena = std.heap.ArenaAllocator.init(child_allocator); + return .{ + .arena = arena, + .map = .{}, + }; + } + + pub fn allocator(self: *Context) std.mem.Allocator { + return self.arena.allocator(); + } + + pub fn deinit(self: *Context) void { + self.arena.deinit(); + } + + pub fn set(self: *Context, key: []const u8, value: Value) !void { + const gop = try self.map.getOrPut(self.allocator(), key); + if (gop.found_existing) { + // opcional: deinit value antigo se necessário + // mas como arena libera tudo, não precisa + } else { + gop.key_ptr.* = try self.allocator().dupe(u8, key); + } + gop.value_ptr.* = value; + } + + pub fn get(self: *const Context, key: []const u8) ?Value { + return self.map.get(key); + } +}; + +``` +ao colocar isso ordered_dict: []struct { key: []const u8, val: Value }, vc ferrou em todos os switches de Value que agora tem que prever o que fazer com orderdered_dict + +Conclusão sobre o getA melhor abordagem, considerando Zig, é: +Manter o get(comptime T: type, key) — é seguro, performático, e em uso real fica natural + +```zig +const idade: i64 = ctx.get("idade") orelse 0; +const nome: []const u8 = ctx.get("nome") orelse "Desconhecido"; +``` + +É verboso no teste, mas em código real é claro e seguro.Ou podemos fazer uma versão com optional: + +```zig +pub fn get(self: *const Context, comptime T: type, key: []const u8) ?T { + const value = self.map.get(key) orelse return null; + return value.as(T) catch null; +} +``` + +Aí fica: + +```zig +const idade = ctx.get(i64, "idade") orelse 0; +```