zdt-prov/src/filters.zig
2026-01-14 09:47:27 -03:00

1518 lines
46 KiB
Zig

const std = @import("std");
const Value = @import("context.zig").Value;
const std_time = std.time;
pub const FilterError = error{
InvalidArgument,
OutOfMemory,
UnknownFilter,
};
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, "&amp;"),
'<' => try result.appendSlice(alloc, "&lt;"),
'>' => try result.appendSlice(alloc, "&gt;"),
'"' => try result.appendSlice(alloc, "&quot;"),
'\'' => try result.appendSlice(alloc, "&#x27;"),
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 <script>
const str = switch (value) {
.string => |s| s,
else => try std.fmt.allocPrint(alloc, "{}", .{value}),
};
errdefer if (@typeInfo(@TypeOf(str)) == .pointer) alloc.free(str);
var result = std.ArrayList(u8){};
for (str) |c| {
switch (c) {
'<' => try result.appendSlice(alloc, "\\u003C"),
'>' => try result.appendSlice(alloc, "\\u003E"),
'&' => try result.appendSlice(alloc, "\\u0026"),
else => try result.append(alloc, c),
}
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_last(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
_ = arg;
return switch (value) {
.list => |l| if (l.len > 0) l[l.len - 1] else .null,
.string => |s| if (s.len > 0) Value{ .string = s[s.len - 1 ..] } else .null,
else => .null,
};
}
fn filter_length(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
_ = arg;
return Value{ .int = switch (value) {
.string => |s| @intCast(s.len),
.list => |l| @intCast(l.len),
.dict => |d| @intCast(d.count()),
else => 0,
} };
}
fn filter_linebreaks(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);
var lines = std.mem.splitScalar(u8, s, '\n');
var empty_line_count: usize = 0;
while (lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r");
if (trimmed.len == 0) {
empty_line_count += 1;
if (empty_line_count == 1) {
try result.appendSlice(alloc, "<p>");
} else if (empty_line_count == 2) {
try result.appendSlice(alloc, "</p><p>");
empty_line_count = 1;
}
} else {
if (empty_line_count >= 1) {
try result.appendSlice(alloc, "<p>");
empty_line_count = 0;
}
try result.appendSlice(alloc, line);
try result.appendSlice(alloc, "<br>");
}
}
if (empty_line_count >= 1) try result.appendSlice(alloc, "</p>");
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_linebreaksbr(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| {
if (c == '\n') {
try result.appendSlice(alloc, "<br>");
} else {
try result.append(alloc, c);
}
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_linenumbers(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){};
var lines = std.mem.splitScalar(u8, s, '\n');
var line_num: usize = 1;
while (lines.next()) |line| {
try std.fmt.format(result.writer(alloc), "{d}. ", .{line_num});
try result.appendSlice(alloc, line);
try result.append(alloc, '\n');
line_num += 1;
}
// Remove última \n se existir
if (result.items.len > 0 and result.items[result.items.len - 1] == '\n') {
_ = result.pop();
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_ljust(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
const width = if (arg) |a| switch (a) {
.int => |i| @as(usize, @max(i, 0)),
else => s.len,
} else s.len;
if (s.len >= width) return value;
var result = std.ArrayList(u8){};
try result.appendSlice(alloc, s);
for (s.len..width) |_| try result.append(alloc, ' ');
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_lower(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const s = switch (value) {
.string => |str| str,
else => return value,
};
const lowered = toLower(alloc, s) catch s;
return Value{ .string = lowered };
}
fn filter_make_list(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const s = switch (value) {
.string => |str| str,
else => return value,
};
var list = std.ArrayList(Value){};
for (s) |c| {
var char_str: [4]u8 = undefined;
const len = std.unicode.utf8Encode(c, &char_str) catch 1;
const char_slice = char_str[0..len];
const copied = try alloc.dupe(u8, char_slice);
try list.append(alloc, Value{ .string = copied });
}
return Value{ .list = try list.toOwnedSlice(alloc) };
}
fn filter_phone2numeric(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| {
const lower = std.ascii.toLower(c);
const digit = switch (lower) {
'a', 'b', 'c' => '2',
'd', 'e', 'f' => '3',
'g', 'h', 'i' => '4',
'j', 'k', 'l' => '5',
'm', 'n', 'o' => '6',
'p', 'q', 'r', 's' => '7',
't', 'u', 'v' => '8',
'w', 'x', 'y', 'z' => '9',
else => c,
};
try result.append(alloc, digit);
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
//
// TODO: refactor
fn filter_pluralize(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
var singular: []const u8 = "";
var plural: []const u8 = "s";
if (arg) |a| {
switch (a) {
.string => |str| {
if (std.mem.indexOf(u8, str, ",")) |pos| {
singular = str[0..pos];
plural = str[pos + 1 ..];
} else {
plural = str;
}
},
else => {}, // ignora outros tipos
}
}
const count: i64 = switch (value) {
.int => |i| i,
.float => |f| @as(i64, @intFromFloat(f)),
.string => |s| if (s.len == 0) 0 else 1,
.list => |l| @as(i64, @intCast(l.len)),
.dict => |d| @as(i64, @intCast(d.count())),
.null => 0,
else => 1,
};
const ending = if (count == 1 or count == -1) singular else plural;
const copied = try alloc.dupe(u8, ending);
return Value{ .string = copied };
}
fn filter_pprint(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const str = try valueToSafeString(alloc, value);
return Value{ .string = str };
}
fn filter_random(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
_ = arg;
const list = switch (value) {
.list => |l| l,
else => return value,
};
if (list.len == 0) return Value.null;
var prng = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp()));
const rand = prng.random();
const index = rand.uintLessThan(usize, list.len);
return list[index];
}
fn filter_rjust(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
const width = if (arg) |a| switch (a) {
.int => |i| @as(usize, @max(i, 0)),
else => s.len,
} else s.len;
if (s.len >= width) return value;
var result = std.ArrayList(u8){};
for (s.len..width) |_| try result.append(alloc, ' ');
try result.appendSlice(alloc, s);
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_safe(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
_ = arg;
return value; // safe: o renderer não escapa
}
fn filter_safeseq(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
_ = arg;
const list = switch (value) {
.list => |l| l,
else => return value,
};
// safeseq marca a lista como segura (não escapa itens)
// Por enquanto, retorna a mesma lista (o renderer vai tratar como safe)
return Value{ .list = list };
}
fn filter_slice(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const slice_str = switch (arg orelse return value) {
.string => |s| s,
else => return value,
};
var start: ?usize = null;
var end: ?usize = null;
if (std.mem.indexOf(u8, slice_str, ":")) |colon| {
const before = slice_str[0..colon];
const after = slice_str[colon + 1 ..];
if (before.len > 0) {
start = std.fmt.parseInt(usize, before, 10) catch 0;
}
if (after.len > 0) {
end = std.fmt.parseInt(usize, after, 10) catch null;
}
} else {
return value;
}
return switch (value) {
.string => |s| blk: {
const effective_start = start orelse 0;
const effective_end = end orelse s.len;
if (effective_start >= s.len) return Value{ .string = "" };
const len = @min(effective_end - effective_start, s.len - effective_start);
const sliced = try alloc.dupe(u8, s[effective_start .. effective_start + len]);
break :blk Value{ .string = sliced };
},
.list => |l| blk: {
const effective_start = start orelse 0;
const effective_end = end orelse l.len;
if (effective_start >= l.len) return Value{ .list = &.{} };
const len = @min(effective_end - effective_start, l.len - effective_start);
const sliced = try alloc.dupe(Value, l[effective_start .. effective_start + len]);
break :blk Value{ .list = sliced };
},
else => value,
};
}
fn filter_slugify(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){};
var last_was_hyphen = false;
var i: usize = 0;
while (i < s.len) {
var replaced: u8 = s[i];
// Se for caractere acentuado de 2 bytes (UTF-8 Latin-1: 0xC3 xx)
if (i + 1 < s.len and s[i] == 0xC3) {
const next = s[i + 1];
replaced = switch (next) {
// Minúsculos
0xA1, 0xA0, 0xA2, 0xA3, 0xA4 => 'a', // á à â ã ä
0xA9, 0xA8, 0xAA, 0xAB => 'e', // é è ê ë
0xAD, 0xAC, 0xAE, 0xAF => 'i', // í ì î ï
0xB3, 0xB2, 0xB4, 0xB5, 0xB6 => 'o', // ó ò ô õ ö
0xBA, 0xB9, 0xBB, 0xBC => 'u', // ú ù û ü
0xA7 => 'c', // ç
0xB1 => 'n', // ñ
// Maiúsculos (convertidos para minúsculo)
0x81, 0x80, 0x82, 0x83, 0x84 => 'a', // Á À Â Ã Ä
0x89, 0x88, 0x8A, 0x8B => 'e', // É È Ê Ë
0x8D, 0x8C, 0x8E, 0x8F => 'i', // Í Ì Î Ï
0x93, 0x92, 0x94, 0x95, 0x96 => 'o', // Ó Ò Ô Õ Ö
0x9A, 0x99, 0x9B, 0x9C => 'u', // Ú Ù Û Ü
0x87 => 'c', // Ç
0x91 => 'n', // Ñ
else => std.ascii.toLower(s[i]),
};
i += 2;
} else {
replaced = std.ascii.toLower(s[i]);
i += 1;
}
if (std.ascii.isAlphanumeric(replaced)) {
try result.append(alloc, replaced);
last_was_hyphen = false;
} else if (replaced == ' ' or replaced == '-' or replaced == '_') {
if (!last_was_hyphen and result.items.len > 0) {
try result.append(alloc, '-');
}
last_was_hyphen = true;
} else {
last_was_hyphen = false;
}
}
// Remove hífen final
if (result.items.len > 0 and result.items[result.items.len - 1] == '-') {
_ = result.pop();
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_stringformat(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const format_str = switch (arg orelse return value) {
.string => |s| s,
else => return value,
};
var result = std.ArrayList(u8){};
var i: usize = 0;
while (i < format_str.len) {
if (format_str[i] == '%' and i + 1 < format_str.len) {
i += 1;
const specifier = format_str[i];
const str = switch (specifier) {
's' => switch (value) {
.string => |s| s,
else => try std.fmt.allocPrint(alloc, "{}", .{value}),
},
'd' => switch (value) {
.int => |n| try std.fmt.allocPrint(alloc, "{d}", .{n}),
else => try std.fmt.allocPrint(alloc, "{}", .{value}),
},
'f' => switch (value) {
.float => |f| try std.fmt.allocPrint(alloc, "{d}", .{f}),
.int => |n| try std.fmt.allocPrint(alloc, "{d}", .{@as(f64, @floatFromInt(n))}),
else => try std.fmt.allocPrint(alloc, "{}", .{value}),
},
'%' => "%",
else => blk: {
try result.append(alloc, '%');
try result.append(alloc, specifier);
break :blk "";
},
};
try result.appendSlice(alloc, str);
if (@typeInfo(@TypeOf(str)) == .pointer) alloc.free(str);
} else {
try result.append(alloc, format_str[i]);
}
i += 1;
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_striptags(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){};
var in_tag = false;
for (s) |c| {
if (c == '<') {
in_tag = true;
} else if (c == '>' and in_tag) {
in_tag = false;
} else if (!in_tag) {
try result.append(alloc, c);
}
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_timesince(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const then = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
const now = std_time.timestamp();
var diff = now - then;
if (diff < 0) diff = -diff;
if (diff < 60) {
return Value{ .string = try alloc.dupe(u8, "menos de um minuto") };
} else if (diff < 3600) {
const mins = diff / 60;
const str = if (mins == 1) "1 minuto" else try std.fmt.allocPrint(alloc, "{d} minutos", .{mins});
return Value{ .string = str };
} else if (diff < 86400) {
const hours = diff / 3600;
const str = if (hours == 1) "1 hora" else try std.fmt.allocPrint(alloc, "{d} horas", .{hours});
return Value{ .string = str };
} else {
const days = diff / 86400;
const str = if (days == 1) "1 dia" else try std.fmt.allocPrint(alloc, "{d} dias", .{days});
return Value{ .string = str };
}
}
fn filter_timeuntil(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
// Reutiliza timesince, mas com sinal invertido
const future = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
const fake_past = Value{ .int = std_time.timestamp() };
const since = try filter_timesince(alloc, fake_past, Value{ .int = future });
return since;
}
fn filter_title(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){};
var word_start = true;
for (s) |c| {
if (std.ascii.isAlphanumeric(c)) {
try result.append(alloc, if (word_start) std.ascii.toUpper(c) else std.ascii.toLower(c));
word_start = false;
} else {
try result.append(alloc, c);
word_start = true;
}
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_truncatechars(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
const n = switch (arg orelse Value{ .int = 100 }) {
.int => |i| @as(usize, @max(i, 0)),
else => 100,
};
const ellipsis_len = 3;
if (n <= ellipsis_len) return Value{ .string = "" };
if (s.len <= n) return value;
const truncate_to = n - ellipsis_len;
const truncated = try alloc.alloc(u8, n);
@memcpy(truncated[0..truncate_to], s[0..truncate_to]);
@memcpy(truncated[truncate_to..], "...");
return Value{ .string = truncated };
}
fn filter_truncatechars_html(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
// Similar, mas poderia verificar tags HTML abertas — implementação avançada depois
return filter_truncatechars(alloc, value, arg);
}
fn filter_truncatewords(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
const n = switch (arg orelse Value{ .int = 20 }) {
.int => |i| @as(usize, @max(i, 0)),
else => 20,
};
if (n == 0) return Value{ .string = "..." };
var words = std.mem.splitScalar(u8, s, ' ');
var result = std.ArrayList(u8){};
defer result.deinit(alloc);
var count: usize = 0;
while (words.next()) |word| : (count += 1) {
if (count == n) {
try result.appendSlice(alloc, "...");
break;
}
if (count > 0) try result.append(alloc, ' ');
try result.appendSlice(alloc, word);
}
// Se não estourou, não adiciona "..."
if (count < n) {
return Value{ .string = try alloc.dupe(u8, s) };
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_truncatewords_html(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
// Similar, mas poderia fechar tags HTML — implementação avançada depois
return filter_truncatewords(alloc, value, arg);
}
fn filter_unordered_list(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const list = switch (value) {
.list => |l| l,
else => return value,
};
var result = std.ArrayList(u8){};
try result.appendSlice(alloc, "<ul>\n");
for (list) |item| {
try result.appendSlice(alloc, "<li>");
const str = try valueToSafeString(alloc, item);
try result.appendSlice(alloc, str);
try result.appendSlice(alloc, "</li>\n");
}
try result.appendSlice(alloc, "</ul>");
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_upper(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const s = switch (value) {
.string => |str| str,
else => return value,
};
const uppered = toUpper(alloc, s) catch s;
return Value{ .string = uppered };
}
fn filter_urlencode(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_urlize(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const s = try valueToSafeString(alloc, value);
var result = std.ArrayList(u8){};
var i: usize = 0;
while (i < s.len) {
const remaining = s[i..];
if (std.mem.startsWith(u8, remaining, "http://") or
std.mem.startsWith(u8, remaining, "https://") or
std.mem.startsWith(u8, remaining, "www."))
{
var url_end = i;
while (url_end < s.len and
!std.ascii.isWhitespace(s[url_end]) and
s[url_end] != '<' and s[url_end] != '>' and
s[url_end] != '"' and s[url_end] != '\'') : (url_end += 1)
{}
const url = s[i..url_end];
const protocol = if (std.mem.startsWith(u8, url, "www.")) "http://" else "";
try result.appendSlice(alloc, "<a href=\"");
try result.appendSlice(alloc, protocol);
try result.appendSlice(alloc, url);
try result.appendSlice(alloc, "\">");
try result.appendSlice(alloc, url);
try result.appendSlice(alloc, "</a>");
i = url_end;
} else {
try result.append(alloc, s[i]);
i += 1;
}
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_urlizetrunc(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const limit = switch (arg orelse Value{ .int = 40 }) {
.int => |i| @as(usize, @max(i, 0)),
else => 40,
};
const s = try valueToSafeString(alloc, value);
std.debug.print("{s}\n", .{value.string});
var result = std.ArrayList(u8){};
var i: usize = 0;
while (i < s.len) {
if (std.mem.startsWith(u8, s[i..], "http://") or std.mem.startsWith(u8, s[i..], "https://")) {
var url_end = i;
while (url_end < s.len and !std.ascii.isWhitespace(s[url_end])) : (url_end += 1) {}
const url = s[i..url_end];
try result.appendSlice(alloc, "<a href=\"");
try result.appendSlice(alloc, url);
try result.appendSlice(alloc, "\">");
if (url.len > limit) {
try result.appendSlice(alloc, url[0..limit]);
try result.appendSlice(alloc, "...");
} else {
try result.appendSlice(alloc, url);
}
try result.appendSlice(alloc, "</a>");
i = url_end;
} else {
try result.append(alloc, s[i]);
i += 1;
}
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_wordcount(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
_ = arg;
const s = switch (value) {
.string => |str| str,
else => return Value{ .int = 0 },
};
var count: usize = 0;
var in_word = false;
for (s) |c| {
if (std.ascii.isAlphanumeric(c)) {
if (!in_word) {
count += 1;
in_word = true;
}
} else {
in_word = false;
}
}
return Value{ .int = @intCast(count) };
}
fn filter_wordwrap(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const width = switch (arg orelse Value{ .int = 80 }) {
.int => |i| @as(usize, @max(i, 0)),
else => 80,
};
const s = switch (value) {
.string => |str| str,
else => return value,
};
var result = std.ArrayList(u8){};
var line_len: usize = 0;
var words = std.mem.splitScalar(u8, s, ' ');
while (words.next()) |word| {
const space_len: usize = if (line_len > 0) 1 else 0;
const needed = line_len + space_len + word.len;
if (needed > width and line_len > 0) {
try result.append(alloc, '\n');
line_len = 0;
}
if (line_len > 0) {
try result.append(alloc, ' ');
line_len += 1;
}
try result.appendSlice(alloc, word);
line_len += word.len;
}
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_yesno(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const arg_str = switch (arg orelse return error.InvalidArgument) {
.string => |s| s,
else => return error.InvalidArgument,
};
var parts = std.mem.splitScalar(u8, arg_str, ',');
var true_val: []const u8 = "yes";
var false_val: []const u8 = "no";
var none_val: []const u8 = "no";
if (parts.next()) |t| true_val = t;
if (parts.next()) |f| false_val = f;
if (parts.next()) |n| none_val = n;
const is_truthy = switch (value) {
.bool => |b| b,
.null => null,
.string => |s| s.len > 0,
.int => |i| i != 0,
.float => |f| f != 0.0,
.list => |l| l.len > 0,
.dict => |d| d.count() > 0,
};
const result_str = if (is_truthy) |b|
if (b) true_val else false_val
else
none_val;
const copied = try alloc.dupe(u8, result_str);
return Value{ .string = copied };
}
// ==================== AUX FUNCTIONS ====================
pub fn capFirst(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
if (input.len == 0) return "";
const first_len = try std.unicode.utf8ByteSequenceLength(input[0]);
const first_codepoint = try std.unicode.utf8Decode(input[0..first_len]);
const upper_first = switch (first_codepoint) {
'a'...'z' => first_codepoint - 32,
'á' => 'Á',
'à' => 'À',
'â' => 'Â',
'ã' => 'Ã',
'ä' => 'Ä',
'é' => 'É',
'è' => 'È',
'ê' => 'Ê',
'í' => 'Í',
'ì' => 'Ì',
'î' => 'Î',
'ó' => 'Ó',
'ò' => 'Ò',
'ô' => 'Ô',
'õ' => 'Õ',
'ú' => 'Ú',
'ù' => 'Ù',
'û' => 'Û',
'ç' => 'Ç',
else => first_codepoint,
};
// ArrayList antigo, Zig 0.15 style
var result = std.ArrayList(u8){};
try result.ensureTotalCapacity(allocator, input.len);
// Escreve a primeira letra maiúscula
var buf: [4]u8 = undefined;
const bytes_written = try std.unicode.utf8Encode(upper_first, &buf);
try result.appendSlice(allocator, buf[0..bytes_written]);
// Copia o resto inalterado
try result.appendSlice(allocator, input[first_len..]);
return try result.toOwnedSlice(allocator);
}
fn toUpper(alloc: std.mem.Allocator, input: []const u8) ![]const u8 {
const result = try alloc.alloc(u8, input.len * 4);
var out_idx: usize = 0;
var i: usize = 0;
while (i < input.len) {
const len = try std.unicode.utf8ByteSequenceLength(input[i]);
const codepoint = try std.unicode.utf8Decode(input[i .. i + len]);
const upper = switch (codepoint) {
'a'...'z' => codepoint - 32,
'á' => 'Á',
'é' => 'É',
'í' => 'Í',
'ó' => 'Ó',
'ú' => 'Ú',
'ã' => 'Ã',
'õ' => 'Õ',
'ç' => 'Ç',
'â' => 'Â',
'ê' => 'Ê',
'î' => 'Î',
'ô' => 'Ô',
'û' => 'Û',
'à' => 'À',
'è' => 'È',
'ì' => 'Ì',
'ò' => 'Ò',
'ù' => 'Ù',
else => codepoint,
};
const bytes_written = try std.unicode.utf8Encode(upper, result[out_idx..]);
out_idx += bytes_written;
i += len;
}
return try alloc.dupe(u8, result[0..out_idx]);
}
fn toLower(alloc: std.mem.Allocator, input: []const u8) ![]const u8 {
const result = try alloc.alloc(u8, input.len * 4);
var out_idx: usize = 0;
var i: usize = 0;
while (i < input.len) {
const len = try std.unicode.utf8ByteSequenceLength(input[i]);
const codepoint = try std.unicode.utf8Decode(input[i .. i + len]);
const lower = switch (codepoint) {
'A'...'Z' => codepoint + 32,
'Á' => 'á',
'É' => 'é',
'Í' => 'í',
'Ó' => 'ó',
'Ú' => 'ú',
'Ã' => 'ã',
'Õ' => 'õ',
'Ç' => 'ç',
'Â' => 'â',
'Ê' => 'ê',
'Î' => 'î',
'Ô' => 'ô',
'Û' => 'û',
'À' => 'à',
'È' => 'è',
'Ì' => 'ì',
'Ò' => 'ò',
'Ù' => 'ù',
else => codepoint,
};
const bytes_written = try std.unicode.utf8Encode(lower, result[out_idx..]);
out_idx += bytes_written;
i += len;
}
return try alloc.dupe(u8, result[0..out_idx]);
}
fn valueToSafeString(alloc: std.mem.Allocator, value: Value) FilterError![]const u8 {
return switch (value) {
.string => |s| s,
.int => |i| try std.fmt.allocPrint(alloc, "{d}", .{i}),
.float => |f| try std.fmt.allocPrint(alloc, "{d}", .{f}),
.bool => |b| if (b) "true" else "false",
.null => "null",
else => "[object]", // list, dict → literal, não aloca
};
}
// ==================== MAPA DE FILTROS BUILTIN ====================
pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "add", &filter_add },
.{ "addslashes", &filter_addslashes },
.{ "capfirst", &filter_capfirst },
.{ "center", &filter_center },
.{ "cut", &filter_cut },
// .{ "date", &filter_date },
.{ "default", &filter_default },
.{ "default_if_none", &filter_default_if_none },
.{ "dictsort", &filter_dictsort },
.{ "dictsortreversed", &filter_dictsortreversed },
.{ "divisibleby", &filter_divisibleby },
.{ "escape", &filter_escape },
.{ "escapejs", &filter_escapejs },
.{ "escapeseq", &filter_escapeseq },
.{ "filesizeformat", &filter_filesizeformat },
.{ "first", &filter_first },
.{ "floatformat", &filter_floatformat },
.{ "force_escape", &filter_force_escape },
.{ "get_digit", &filter_get_digit },
.{ "iriencode", &filter_iriencode },
.{ "join", &filter_join },
.{ "json_script", &filter_json_script },
.{ "last", &filter_last },
.{ "length", &filter_length },
.{ "linebreaks", &filter_linebreaks },
.{ "linebreaksbr", &filter_linebreaksbr },
.{ "linenumbers", &filter_linenumbers },
.{ "ljust", &filter_ljust },
.{ "lower", &filter_lower },
.{ "make_list", &filter_make_list },
.{ "phone2numeric", &filter_phone2numeric },
.{ "pluralize", &filter_pluralize },
.{ "pprint", &filter_pprint },
.{ "random", &filter_random },
.{ "rjust", &filter_rjust },
.{ "safe", &filter_safe },
.{ "safeseq", &filter_safeseq },
.{ "slice", &filter_slice },
.{ "slugify", &filter_slugify },
.{ "stringformat", &filter_stringformat },
.{ "striptags", &filter_striptags },
// .{ "time", &filter_time },
// .{ "timesince", &filter_timesince },
// .{ "timeuntil", &filter_timeuntil },
.{ "title", &filter_title },
.{ "truncatechars", &filter_truncatechars },
.{ "truncatechars_html", &filter_truncatechars_html },
.{ "truncatewords", &filter_truncatewords },
.{ "truncatewords_html", &filter_truncatewords_html },
.{ "unordered_list", &filter_unordered_list },
.{ "upper", &filter_upper },
.{ "urlencode", &filter_urlencode },
.{ "urlize", &filter_urlize },
.{ "urlizetrunc", &filter_urlizetrunc },
.{ "wordwrap", &filter_wordwrap },
.{ "wordcount", &filter_wordcount },
.{ "yesno", &filter_yesno },
});