Compare commits

..

1 commit
main ... bkp

Author SHA1 Message Date
Lucas F.
efd19a8a81 backup ideas 2026-01-12 09:09:51 -03:00
29 changed files with 5470 additions and 6496 deletions

View file

@ -84,15 +84,6 @@ pub fn build(b: *std.Build) void {
.use_llvm = true,
});
// const lib = b.addLibrary(.{
// .name = "zdt_prov",
// .root_module = mod,
// });
//
// lib.root_module.addIncludePath(b.path("src/svg"));
//
// b.installArtifact(lib);
// This declares intent for the executable to be installed into the
// install prefix when running `zig build` (i.e. when executing the default
// step). By default the install prefix is `zig-out/` but can be overridden

View file

@ -2,13 +2,10 @@ const std = @import("std");
const Allocator = std.heap.ArenaAllocator;
const parser = @import("parser.zig");
const icons = @import("svg/icons.zig");
pub const TemplateCache = struct {
arena: Allocator,
cache: std.StringHashMapUnmanaged([]parser.Node),
default_path: ?[]const u8 = "templates",
icons: ?icons.SvgIcon =null,
pub fn init(child_allocator: std.mem.Allocator) TemplateCache {
const arena = std.heap.ArenaAllocator.init(child_allocator);
@ -18,6 +15,7 @@ pub const TemplateCache = struct {
};
}
pub fn deinit(self: *TemplateCache) void {
self.arena.deinit();
}
@ -52,10 +50,6 @@ pub const TemplateCache = struct {
}
}
pub fn initIcons(self: *TemplateCache) !void {
self.icons = icons.SvgIcon.init(self.allocator()) catch null;
}
pub fn clear(self: *TemplateCache) void {
self.deinit();
self.cache = .{};

51
src/cache_bkp.zig Normal file
View file

@ -0,0 +1,51 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("parser.zig");
pub const TemplateCache = struct {
allocator: Allocator,
cache: std.StringHashMapUnmanaged([]parser.Node),
pub fn init(allocator: Allocator) TemplateCache {
return .{
.allocator = allocator,
.cache = .{},
};
}
pub fn deinit(self: *TemplateCache) void {
var it = self.cache.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
for (entry.value_ptr.*) |node| node.deinit(self.allocator);
self.allocator.free(entry.value_ptr.*);
}
self.cache.deinit(self.allocator);
}
pub fn get(self: *const TemplateCache, key: []const u8) ?[]parser.Node {
return self.cache.get(key);
}
pub fn add(self: *TemplateCache, key: []const u8, nodes: []parser.Node) !void {
const key_copy = try self.allocator.dupe(u8, key);
errdefer self.allocator.free(key_copy);
try self.cache.put(self.allocator, key_copy, nodes);
}
pub fn invalidate(self: *TemplateCache, key: []const u8) void {
if (self.cache.getEntry(key)) |entry| {
self.allocator.free(entry.key_ptr.*);
for (entry.value_ptr.*) |node| node.deinit(self.allocator);
self.allocator.free(entry.value_ptr.*);
_ = self.cache.remove(key);
}
}
pub fn clear(self: *TemplateCache) void {
self.deinit();
self.cache = .{};
}
};

View file

@ -1,6 +1,4 @@
const std = @import("std");
const time = @import("time.zig");
const util = @import("util.zig");
pub const Value = union(enum) {
null,
@ -36,24 +34,8 @@ pub const Context = struct {
self.arena.deinit();
}
pub fn toValue(self: *Context, value: anytype) !Value {
fn toValue(self: *Context, value: anytype) !Value {
const T = @TypeOf(value);
if (T == time.Time) {
return Value{ .string = try time.formatDateTime(self.allocator(), value, "Y-m-d H:i:s") };
}
if (@typeInfo(T) == .pointer) {
if (@typeInfo(T).pointer.size == .slice) {
if (@typeInfo(@typeInfo(T).pointer.child) == .@"struct") {
var list = try self.allocator().alloc(Value, value.len);
for (value, 0..) |item, i| {
list[i] = try self.toValue(item);
}
return Value{ .list = list };
}
}
}
return switch (@typeInfo(T)) {
.bool => Value{ .bool = value },
.int, .comptime_int => Value{ .int = @intCast(value) },
@ -123,14 +105,6 @@ pub const Context = struct {
return current;
}
pub fn getOr(self: *Context, path: []const u8, default: anytype) ?Value {
return self.get(path) orelse try self.toValue(default);
}
fn isArrayList(value: anytype) bool {
if (std.mem.startsWith(u8, @typeName(value), "array_list")) return true;
return false;
}
// pub fn get(self: *const Context, comptime T: type, key: []const u8) !T {
// // const opt_value = self.map.get(key) orelse return error.KeyNotFound;

81
src/context_proposta.zig Normal file
View file

@ -0,0 +1,81 @@
const std = @import("std");
pub const Value = union(enum) {
null,
bool: bool,
int: i64,
float: f64,
string: []const u8,
list: []Value,
dict: std.HashMapUnmanaged([]const u8, Value, std.hash_map.StringContext, 80),
struct_: std.HashMapUnmanaged([]const u8, Value, std.hash_map.StringContext, 80),
pub fn deinit(self: *Value, allocator: std.mem.Allocator) void {
switch (self.*) {
.string => allocator.free(self.string),
.list => {
for (self.list) |*v| v.deinit(allocator);
allocator.free(self.list);
},
.dict => {
var iter = self.dict.iterator();
while (iter.next()) |entry| {
allocator.free(entry.key_ptr.*);
entry.value_ptr.deinit(allocator);
}
self.dict.deinit(allocator);
},
.struct_ => {
var iter = self.struct_.iterator();
while (iter.next()) |entry| {
allocator.free(entry.key_ptr.*);
entry.value_ptr.deinit(allocator);
}
self.struct_.deinit(allocator);
},
else => {},
}
}
};
pub const Context = struct {
allocator: std.mem.Allocator,
map: std.HashMapUnmanaged([]const u8, Value, std.hash_map.StringContext, 80),
parent: ?*Context = null,
pub fn init(allocator: std.mem.Allocator) Context {
return .{
.allocator = allocator,
.map = .{},
};
}
pub fn deinit(self: *Context) void {
var iter = self.map.iterator();
while (iter.next()) |entry| {
allocator.free(entry.key_ptr.*);
entry.value_ptr.deinit(self.allocator);
}
self.map.deinit(self.allocator);
if (self.parent) |p| p.deinit();
}
pub fn get(self: *const Context, key: []const u8) ?Value {
return self.map.get(key) or (self.parent orelse return null).get(key);
}
pub fn set(self: *Context, key: []const u8, value: Value) !void {
const duped_key = try self.allocator.dupe(u8, key);
try self.map.put(self.allocator, duped_key, value);
}
pub fn pushScope(self: *Context) *Context {
var child = Context.init(self.allocator);
child.parent = self;
return &child;
}
pub fn popScope(self: *Context) void {
self.deinit(); // libera o escopo atual
}
};

View file

@ -4,9 +4,6 @@ const Context = @import("context.zig").Context;
const Value = @import("context.zig").Value;
test "context set amigável e get com ponto" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("1 - context set amigável e get com ponto\n", .{});
const allocator = testing.allocator;
var ctx = Context.init(allocator);
defer ctx.deinit();
@ -20,26 +17,17 @@ test "context set amigável e get com ponto" {
// struct
const Person = struct { nome: []const u8, idade: i64 };
const p = Person{ .nome = "Ana", .idade = 25 };
const p2 = Person{ .nome = "Fulana", .idade = 28 };
const people = [_]Person{ p, p2 };
// try ctx.set("user", p);
try ctx.set("user", people);
try ctx.set("user", p);
// list
const numeros = [_]i64{ 1, 2, 3 };
try ctx.set("lista", numeros);
for (ctx.get("user").?.list) |item| {
std.debug.print("user {any}\n", .{item.dict.get("nome").?});
}
// acesso
try testing.expectEqualStrings("Lucas", ctx.get("nome").?.string);
try testing.expect(ctx.get("idade").?.int == 30);
// try testing.expectEqualStrings("Ana", ctx.get("user.nome").?.string);
// try testing.expect(ctx.get("user.idade").?.int == 25);
try testing.expectEqualStrings("Ana", ctx.get("user.nome").?.string);
try testing.expect(ctx.get("user.idade").?.int == 25);
try testing.expect(ctx.get("lista.1").?.int == 2);
try testing.expect(ctx.get("vazio").?.string.len == 0);
try testing.expect(ctx.get("preco").?.float == 99.99);

View file

@ -1,83 +0,0 @@
// Em time.zig (ou crie um novo arquivo relativedelta.zig e importe)
pub const RelativeDelta = struct {
years: i32 = 0,
months: i32 = 0,
days: i32 = 0,
hours: i32 = 0,
minutes: i32 = 0,
seconds: i32 = 0,
pub fn init(fields: struct {
years: i32 = 0,
months: i32 = 0,
days: i32 = 0,
hours: i32 = 0,
minutes: i32 = 0,
seconds: i32 = 0,
}) RelativeDelta {
return .{
.years = fields.years,
.months = fields.months,
.days = fields.days,
.hours = fields.hours,
.minutes = fields.minutes,
.seconds = fields.seconds,
};
}
// Helpers úteis (muito usados depois)
pub fn isZero(self: RelativeDelta) bool {
return self.years == 0 and
self.months == 0 and
self.days == 0 and
self.hours == 0 and
self.minutes == 0 and
self.seconds == 0;
}
pub fn inSeconds(self: RelativeDelta) i64 {
return @as(i64, self.years) * 365 * 24 * 60 * 60 +
@as(i64, self.months) * 30 * 24 * 60 * 60 +
@as(i64, self.days) * 24 * 60 * 60 +
@as(i64, self.hours) * 60 * 60 +
@as(i64, self.minutes) * 60 +
@as(i64, self.seconds);
}
pub fn inDays(self: RelativeDelta) i64 {
return @as(i64, self.years) * 365 +
@as(i64, self.months) * 30 +
@as(i64, self.days);
}
pub fn normalize(self: *RelativeDelta) void {
// Normaliza meses anos + meses
if (self.months >= 12 or self.months <= -12) {
const carry = @divTrunc(self.months, 12);
self.years += carry;
self.months -= carry * 12;
}
// Normaliza segundos minutos + segundos
if (self.seconds >= 60 or self.seconds <= -60) {
const carry = @divTrunc(self.seconds, 60);
self.minutes += carry;
self.seconds -= carry * 60;
}
// Normaliza minutos horas + minutos
if (self.minutes >= 60 or self.minutes <= -60) {
const carry = @divTrunc(self.minutes, 60);
self.hours += carry;
self.minutes -= carry * 60;
}
// Normaliza horas dias + horas
if (self.hours >= 24 or self.hours <= -24) {
const carry = @divTrunc(self.hours, 24);
self.days += carry;
self.hours -= carry * 24;
}
}
};

View file

@ -1,224 +0,0 @@
const std = @import("std");
const testing = std.testing;
const Time = @import("time.zig").Time;
const Context = @import("context.zig").Context;
const RelativeDelta = @import("delta.zig").RelativeDelta;
test "relativedelta rigoroso - meses" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("1 - context set amigável e get com ponto\n", .{});
const a = try Time.parse("2025-03-31");
const b = try Time.parse("2025-01-31");
const delta = a.subRelative(b);
try testing.expectEqual(delta.months, 2);
try testing.expectEqual(delta.years, 0);
try testing.expectEqual(delta.days, 0);
const rev = b.subRelative(a);
try testing.expectEqual(rev.months, -2);
}
test "relativedelta - overflow de dia" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("2 - relativedelta - overflow de dia\n", .{});
const jan31 = try Time.parse("2023-01-31");
const mar01 = try Time.parse("2023-03-01");
const delta = mar01.subRelative(jan31);
// Esperado algo como: +1 mês +1 dia (ou +2 meses -30 dias, mas dateutil prefere o primeiro)
try testing.expect(delta.months == 1);
try testing.expect(delta.days == 1);
}
test "bissexto: 2021-02-28 - 2020-02-29" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("3 - bissexto: 2021-02-28 - 2020-02-29\n", .{});
const a = try Time.parse("2021-02-28");
const b = try Time.parse("2020-02-29");
const delta = a.subRelative(b);
try testing.expectEqual(delta.years, 1);
try testing.expectEqual(delta.months, 0);
try testing.expectEqual(delta.days, 0);
}
test "bissexto: 2021-03-01 - 2020-02-29" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("4 - bissexto: 2021-03-01 - 2020-02-29\n", .{});
const a = try Time.parse("2021-03-01");
const b = try Time.parse("2020-02-29");
const delta = a.subRelative(b);
try testing.expectEqual(delta.years, 1);
try testing.expectEqual(delta.months, 0);
try testing.expectEqual(delta.days, 1);
}
test "bissexto: 2021-02-27 - 2020-02-29" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("5 - bissexto: 2021-02-27 - 2020-02-29\n", .{});
const a = try Time.parse("2021-02-27");
const b = try Time.parse("2020-02-29");
const delta = a.subRelative(b);
try testing.expectEqual(delta.years, 0);
try testing.expectEqual(delta.months, 11);
try testing.expectEqual(delta.days, 29);
}
test "bissexto reverso: 2020-02-29 - 2021-02-28" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("6 - bissexto reverso: 2020-02-29 - 2021-02-28\n", .{});
const a = try Time.parse("2020-02-29");
const b = try Time.parse("2021-02-28");
const delta = a.subRelative(b);
try testing.expectEqual(delta.years, -1);
try testing.expectEqual(delta.months, 0);
try testing.expectEqual(delta.days, 0);
}
test "addRelative: anos normais (não bissexto)" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("7 - addRelative: anos normais (não bissexto)\n", .{});
// 2023 não é bissexto
const base = try Time.parse("2023-06-15");
const expected = try Time.parse("2026-06-15");
const delta = RelativeDelta.init(.{ .years = 3 });
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try expected.toString( null),
try result.toString( null),
);
}
test "addRelative: anos normais com overflow de dia" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("8 - addRelative: anos normais com overflow de dia\n", .{});
// Janeiro 31 + 2 anos deve ir para 31/jan (2025 não bissexto)
const base = try Time.parse("2023-01-31");
const expected = try Time.parse("2025-01-31");
const delta = RelativeDelta.init(.{ .years = 2 });
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try expected.toString( null),
try result.toString( null),
);
}
test "addRelative: de 29/fev bissexto + 1 ano (vai para 28/fev)" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("9 - addRelative: de 29/fev bissexto + 1 ano (vai para 28/fev)\n", .{});
// 2020 foi bissexto +1 ano deve ir para 2021-02-28 (não bissexto)
const base = try Time.parse("2020-02-29");
const expected = try Time.parse("2021-02-28");
const delta = RelativeDelta.init(.{ .years = 1 });
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try expected.toString( null),
try result.toString( null),
);
}
test "addRelative: de 29/fev bissexto + 4 anos (permanece 29/fev)" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("10 - addRelative: de 29/fev bissexto + 4 anos (permanece 29/fev)\n", .{});
// 2020 2024 (ambos bissextos)
const base = try Time.parse("2020-02-29");
const expected = try Time.parse("2024-02-29");
const delta = RelativeDelta.init(.{ .years = 4 });
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try expected.toString( null),
try result.toString( null),
);
}
test "addRelative: de 29/fev + 1 ano + 1 mês (vai para março)" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("11 - addRelative: de 29/fev + 1 ano + 1 mês (vai para março)\n", .{});
// 2020-02-29 + 1 ano 2021-02-28 + 1 mês 2021-03-28
const base = try Time.parse("2020-02-29");
const expected = try Time.parse("2021-03-28");
const delta = RelativeDelta.init(.{ .years = 1, .months = 1 });
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try expected.toString( null),
try result.toString( null),
);
}
test "addRelative: meses com overflow (31 → 28/30)" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("12 - addRelative: meses com overflow (31 → 28/30)\n", .{});
const cases = [_]struct { base: []const u8, months: i32, expected: []const u8 }{
.{ .base = "2023-01-31", .months = 1, .expected = "2023-02-28" }, // não bissexto
.{ .base = "2024-01-31", .months = 1, .expected = "2024-02-29" }, // bissexto
.{ .base = "2023-03-31", .months = -1, .expected = "2023-02-28" },
.{ .base = "2023-01-31", .months = 2, .expected = "2023-03-31" },
.{ .base = "2023-08-31", .months = 1, .expected = "2023-09-30" }, // setembro tem 30
};
for (cases) |c| {
const base_t = try Time.parse(c.base);
const exp_t = try Time.parse(c.expected);
const delta = RelativeDelta.init(.{ .months = c.months });
const result = base_t.addRelative(delta);
try testing.expectEqualStrings(
try exp_t.toString( null),
try result.toString( null),
);
}
}
test "addRelative: combinação anos + meses + dias (bissexto envolvido)" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("13 - addRelative: combinação anos + meses + dias (bissexto envolvido)\n", .{});
// 2024-02-29 + 1 ano + 2 meses + 3 dias
// 2025-02-28 + 2 meses 2025-04-28 + 3 dias 2025-05-01
const base = try Time.parse("2024-02-29");
const expected = try Time.parse("2025-05-01");
const delta = RelativeDelta.init(.{ .years = 1, .months = 2, .days = 3 });
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try expected.toString( null),
try result.toString( null),
);
}
test "addRelative: delta zero não altera data" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("14 - addRelative: delta zero não altera data\n", .{});
const base = try Time.parse("2025-07-20");
const delta = RelativeDelta.init(.{});
const result = base.addRelative(delta);
try testing.expectEqualStrings(
try base.toString( null),
try result.toString( null),
);
}

View file

@ -2,15 +2,10 @@ const std = @import("std");
const Value = @import("context.zig").Value;
const std_time = std.time;
const time = @import("time.zig");
pub const FilterError = error{
InvalidArgument,
InvalidCharacter,
Overflow,
OutOfMemory,
UnknownFilter,
} || time.TimeError;
};
const DictEntry = struct {
key: []const u8,
@ -126,9 +121,35 @@ fn filter_cut(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!V
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_date(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
return dateTimeToString(alloc, value, arg);
}
// 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;
@ -634,12 +655,6 @@ fn filter_make_list(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterE
return Value{ .list = try list.toOwnedSlice(alloc) };
}
fn filter_now(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = value;
if (arg.?.string.len == 0) return Value{ .string = try time.Time.now().toStringAlloc(alloc, "F d, Y") };
return Value{ .string = try time.Time.now().toStringAlloc(alloc, arg.?.string) };
}
fn filter_phone2numeric(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
const s = switch (value) {
@ -943,22 +958,45 @@ fn filter_striptags(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterE
return Value{ .string = try result.toOwnedSlice(alloc) };
}
fn filter_time(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
return dateTimeToString(alloc, value, arg);
}
fn filter_timesince(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
const d = time.Time.parse(value.string) catch return value;
const now = time.Time.parse(arg.?.string) catch return value;
_ = arg;
const then = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
return Value{ .string = try d.timeSince(alloc, now) };
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 {
const d = time.Time.parse(value.string) catch return value;
const now = time.Time.parse(arg.?.string) catch return value;
_ = arg;
// Reutiliza timesince, mas com sinal invertido
const future = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
return Value{ .string = try now.timeSince(alloc, d) };
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 {
@ -1152,6 +1190,7 @@ fn filter_urlizetrunc(alloc: std.mem.Allocator, value: Value, arg: ?Value) Filte
const s = try valueToSafeString(alloc, value);
std.debug.print("{s}\n", .{value.string});
var result = std.ArrayList(u8){};
var i: usize = 0;
@ -1279,20 +1318,6 @@ fn filter_yesno(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError
}
// ==================== AUX FUNCTIONS ====================
pub fn dateTimeToString(allocator: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
if (value.string.len > 0) {
const t: time.Time = try time.Time.parse(value.string);
const arg_str: Value = arg orelse Value{ .string = "F d, Y" };
const format_str: []const u8 = arg_str.string;
const result: []const u8 = try t.toStringAlloc(allocator, format_str);
if (result.len > 0) {
return Value{ .string = result };
}
}
return value;
}
pub fn capFirst(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
if (input.len == 0) return "";
@ -1437,7 +1462,7 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "capfirst", &filter_capfirst },
.{ "center", &filter_center },
.{ "cut", &filter_cut },
.{ "date", &filter_date },
// .{ "date", &filter_date },
.{ "default", &filter_default },
.{ "default_if_none", &filter_default_if_none },
.{ "dictsort", &filter_dictsort },
@ -1462,7 +1487,6 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "ljust", &filter_ljust },
.{ "lower", &filter_lower },
.{ "make_list", &filter_make_list },
.{ "now", &filter_now },
.{ "phone2numeric", &filter_phone2numeric },
.{ "pluralize", &filter_pluralize },
.{ "pprint", &filter_pprint },
@ -1474,9 +1498,9 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "slugify", &filter_slugify },
.{ "stringformat", &filter_stringformat },
.{ "striptags", &filter_striptags },
.{ "time", &filter_time },
.{ "timesince", &filter_timesince },
.{ "timeuntil", &filter_timeuntil },
// .{ "time", &filter_time },
// .{ "timesince", &filter_timesince },
// .{ "timeuntil", &filter_timeuntil },
.{ "title", &filter_title },
.{ "truncatechars", &filter_truncatechars },
.{ "truncatechars_html", &filter_truncatechars_html },

View file

@ -3,16 +3,10 @@ const testing = std.testing;
const Value = @import("context.zig").Value;
const Context = @import("context.zig").Context;
const builtin_filters = @import("filters.zig").builtin_filters;
const filter = @import("filters.zig");
const FilterError = filter.FilterError;
const time = @import("time.zig");
const FilterError = @import("filters.zig").FilterError;
const std_time = std.time;
const RelativeDelta = @import("delta.zig").RelativeDelta;
test "filters upper/lower, capfirst" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("1 - upper/lower, capfirst\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -35,9 +29,6 @@ test "filters upper/lower, capfirst" {
}
test "builtin filters - add" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("2 - add\n", .{});
const alloc = testing.allocator;
const add = builtin_filters.get("add").?;
@ -58,9 +49,6 @@ test "builtin filters - add" {
}
test "builtin filters - default and default_if_none" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("3 - default and default_if_none\n", .{});
const alloc = testing.allocator;
const default_filter = builtin_filters.get("default").?;
const default_if_none = builtin_filters.get("default_if_none").?;
@ -85,9 +73,6 @@ test "builtin filters - default and default_if_none" {
}
test "builtin filters - length" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("4 - length\n", .{});
const alloc = testing.allocator;
const length = builtin_filters.get("length").?;
@ -109,10 +94,7 @@ test "builtin filters - length" {
try testing.expect((try length(ctx.allocator(), list_val, null)).int == 4);
}
test "builtin filters - length with dict" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("5 - length with dict\n", .{});
test "builtin filters - length com dict" {
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -132,9 +114,6 @@ test "builtin filters - length with dict" {
}
test "builtin filters - first and last" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("6 - first and last\n", .{});
const alloc = testing.allocator;
const first = builtin_filters.get("first").?;
const last = builtin_filters.get("last").?;
@ -164,9 +143,6 @@ test "builtin filters - first and last" {
}
test "builtin filters - join" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("7 - join\n", .{});
const alloc = testing.allocator;
const join = builtin_filters.get("join").?;
@ -183,6 +159,7 @@ test "builtin filters - join" {
try ctx.set("mixed", mixed);
const mixed_val = ctx.get("mixed").?;
const default_join = try join(ctx.allocator(), list_val, null);
const custom_join = try join(ctx.allocator(), list_val, sep_dash);
const mixed_join = try join(ctx.allocator(), mixed_val, null);
@ -193,9 +170,6 @@ test "builtin filters - join" {
}
test "builtin filters - yesno" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("8 - yesno\n", .{});
const alloc = testing.allocator;
const yesno = builtin_filters.get("yesno").?;
@ -218,9 +192,6 @@ test "builtin filters - yesno" {
}
test "builtin filters - truncatechars and truncatechars_html" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("9 - truncatechars and truncatechars_html\n", .{});
const alloc = testing.allocator;
const truncatechars = builtin_filters.get("truncatechars").?;
const truncatechars_html = builtin_filters.get("truncatechars_html").?;
@ -247,9 +218,6 @@ test "builtin filters - truncatechars and truncatechars_html" {
}
test "builtin filters - truncatewords" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("10 - truncatewords\n", .{});
const alloc = testing.allocator;
const truncatewords = builtin_filters.get("truncatewords").?;
const truncatewords_html = builtin_filters.get("truncatewords_html").?;
@ -273,9 +241,6 @@ test "builtin filters - truncatewords" {
}
test "builtin filters - slice" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("11 - slice\n", .{});
const alloc = testing.allocator;
const slice = builtin_filters.get("slice").?;
@ -302,9 +267,6 @@ test "builtin filters - slice" {
}
test "builtin filters - safe and force_escape" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("12 - safe and force_escape\n", .{});
const alloc = testing.allocator;
const safe = builtin_filters.get("safe").?;
const force_escape = builtin_filters.get("force_escape").?;
@ -325,9 +287,6 @@ test "builtin filters - safe and force_escape" {
}
test "builtin filters - linebreaksbr and linebreaks" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("13 - linebreaksbr and linebreaks\n", .{});
const alloc = testing.allocator;
const linebreaksbr = builtin_filters.get("linebreaksbr").?;
const linebreaks = builtin_filters.get("linebreaks").?;
@ -347,9 +306,6 @@ test "builtin filters - linebreaksbr and linebreaks" {
}
test "builtin filters - escape and force_escape" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("14 - escape and force_escape\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -370,9 +326,6 @@ test "builtin filters - escape and force_escape" {
}
test "builtin filters - striptags" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("15 - striptags\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -388,9 +341,6 @@ test "builtin filters - striptags" {
}
test "builtin filters - slugify" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("16 - slugify\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -406,9 +356,6 @@ test "builtin filters - slugify" {
}
test "builtin filters - floatformat" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("17 - floatformat\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -437,9 +384,6 @@ test "builtin filters - floatformat" {
}
test "builtin filters - stringformat" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("18 - stringformat\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -460,9 +404,6 @@ test "builtin filters - stringformat" {
}
test "builtin filters - cut" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("19 - cut\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -478,9 +419,6 @@ test "builtin filters - cut" {
}
test "builtin filters - title" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("20 - title\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -496,9 +434,6 @@ test "builtin filters - title" {
}
test "builtin filters - wordcount" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("21 - wordcount\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -514,9 +449,6 @@ test "builtin filters - wordcount" {
}
test "builtin filters - urlencode" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("22 - urlencode\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -532,9 +464,6 @@ test "builtin filters - urlencode" {
}
test "builtin filters - pluralize" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("23 - pluralize\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -560,35 +489,31 @@ test "builtin filters - pluralize" {
// try testing.expectEqualStrings("", zero.string);
}
test "builtin filters - addslashes, center" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("24 - addslashes, center\n", .{});
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");
try ctx.set("time", time.Time.new(2026, 1, 1, 0, 0, 0));
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" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("25 - dictsort and dictsortreversed\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -619,9 +544,6 @@ test "builtin filters - dictsort and dictsortreversed" {
}
test "builtin filters - divisibleby, escapejs, filesizeformat, get_digit, json_script" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("26 - divisibleby, escapejs, filesizeformat, get_digit, json_script\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -657,9 +579,6 @@ test "builtin filters - divisibleby, escapejs, filesizeformat, get_digit, json_s
}
test "builtin filters - escapeseq, iriencode, linenumbers" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("27 - escapeseq, iriencode, linenumbers\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -682,9 +601,6 @@ test "builtin filters - escapeseq, iriencode, linenumbers" {
}
test "builtin filters - ljust, rjust, center" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("28 - ljust, rjust, center\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -707,9 +623,6 @@ test "builtin filters - ljust, rjust, center" {
}
test "builtin filters - make_list, phone2numeric, pprint" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("29 - make_list, phone2numeric, pprint\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -735,9 +648,6 @@ test "builtin filters - make_list, phone2numeric, pprint" {
}
test "builtin filters - random, safeseq" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("30 - random, safeseq\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -760,69 +670,29 @@ test "builtin filters - random, safeseq" {
try testing.expect(safe == .list);
}
test "builtin filters - date, now, time, timesince, timeuntil" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("31 - date, now, time, timesince, timeuntil\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
const times = [_]time.Time{
time.Time.new(2026, 1, 2, 13, 15, 10),
time.Time.new(2026, 1, 6, 19, 25, 0),
time.Time.new(2026, 1, 2, 13, 35, 0),
time.Time.new(2026, 1, 2, 13, 15, 19),
time.Time.new(2025, 1, 2, 13, 15,19),
time.Time.new(2024, 7, 5, 19, 4, 2),
};
const date_filter = builtin_filters.get("date").?;
const now_filter = builtin_filters.get("now").?;
const time_filter = builtin_filters.get("time").?;
const timesince_filter = builtin_filters.get("timesince").?;
const timeuntil_filer = builtin_filters.get("timeuntil").?;
try ctx.set("dates", times);
const dates = ctx.get("dates").?;
const date_formated = try date_filter(ctx.allocator(), dates.list[0], Value{ .string = "Y-m-d" });
const now_formated = try now_filter(ctx.allocator(), dates.list[0], Value{ .string = "Y-m-d" });
const time_formated = try time_filter(ctx.allocator(), dates.list[0], Value{ .string = "H:i:s" });
const timesince_formated_1 = try timesince_filter(ctx.allocator(), dates.list[0], dates.list[1]);
const timesince_formated_2 = try timesince_filter(ctx.allocator(), dates.list[0], dates.list[2]);
const timesince_formated_3 = try timesince_filter(ctx.allocator(), dates.list[0], dates.list[3]);
const timesince_formated_4 = try timesince_filter(ctx.allocator(), dates.list[4], dates.list[0]);
const timesince_formated_5 = try timesince_filter(ctx.allocator(), dates.list[5], dates.list[0]);
const timeuntil_formated_1 = try timeuntil_filer(ctx.allocator(), dates.list[1], dates.list[0]);
const timeuntil_formated_2 = try timeuntil_filer(ctx.allocator(), dates.list[2], dates.list[0]);
const timeuntil_formated_3 = try timeuntil_filer(ctx.allocator(), dates.list[3], dates.list[0]);
const timeuntil_formated_4 = try timeuntil_filer(ctx.allocator(), dates.list[0], dates.list[4]);
const timeuntil_formated_5 = try timeuntil_filer(ctx.allocator(), dates.list[0], dates.list[5]);
try testing.expectEqualStrings("2026-01-02", date_formated.string);
try testing.expect(isDateFormat(now_formated.string));
try testing.expectEqualStrings("13:15:10", time_formated.string);
try testing.expectEqualStrings("4 days, 6 hours",timesince_formated_1.string);
try testing.expectEqualStrings("19 minutes",timesince_formated_2.string);
try testing.expectEqualStrings("0 minutes",timesince_formated_3.string);
try testing.expectEqualStrings("11 months, 4 weeks",timesince_formated_4.string);
try testing.expectEqualStrings("1 year, 5 months",timesince_formated_5.string);
try testing.expectEqualStrings("4 days, 6 hours",timeuntil_formated_1.string);
try testing.expectEqualStrings("19 minutes",timeuntil_formated_2.string);
try testing.expectEqualStrings("0 minutes",timeuntil_formated_3.string);
try testing.expectEqualStrings("11 months, 4 weeks",timeuntil_formated_4.string);
try testing.expectEqualStrings("1 year, 5 months",timeuntil_formated_5.string);
}
// 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" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("32 - urlize, urlizetrunc, wordwrap, unordered_list\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -844,16 +714,15 @@ test "builtin filters - urlize, urlizetrunc, wordwrap, unordered_list" {
const long_text = "Este é um texto muito longo que precisa ser quebrado em várias linhas para caber na largura especificada";
const wrapped = try wordwrap(ctx.allocator(), Value{ .string = long_text }, Value{ .int = 20 });
try testing.expect(std.mem.indexOf(u8, wrapped.string, "\n") != null);
}
test "builtin filters - unordered_list" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("33 - unordered_list\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
const list = [_]Value{ Value{ .string = "item1" }, Value{ .string = "item2" } };
try ctx.set("lista", list);
@ -868,15 +737,6 @@ test "builtin filters - unordered_list" {
\\<li>item2</li>
\\</ul>
;
std.debug.print("lista gerada: {any}\n", .{ul.string});
try testing.expectEqualStrings(expected, ul.string);
}
fn isDateFormat(txt: []const u8) bool {
if (txt.len != 10) return false;
if (txt[4] != '-' or txt[7] != '-') return false;
for (txt, 0..) |c, i| {
if (i == 4 or i == 7) continue;
if (!std.ascii.isDigit(c)) return false;
}
return true;
}

View file

@ -1,338 +0,0 @@
const std = @import("std");
const rand = std.crypto.random;
pub const LOREM_COMMON_P =
\\Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
\\tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
\\veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
\\commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
\\velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
\\occaecat cupidatat non proident, sunt in culpa qui officia deserunt
\\mollit anim id est laborum.
;
pub const LOREM_WORDS = [182][]const u8{
"exercitationem",
"perferendis",
"perspiciatis",
"laborum",
"eveniet",
"sunt",
"iure",
"nam",
"nobis",
"eum",
"cum",
"officiis",
"excepturi",
"odio",
"consectetur",
"quasi",
"aut",
"quisquam",
"vel",
"eligendi",
"itaque",
"non",
"odit",
"tempore",
"quaerat",
"dignissimos",
"facilis",
"neque",
"nihil",
"expedita",
"vitae",
"vero",
"ipsum",
"nisi",
"animi",
"cumque",
"pariatur",
"velit",
"modi",
"natus",
"iusto",
"eaque",
"sequi",
"illo",
"sed",
"ex",
"et",
"voluptatibus",
"tempora",
"veritatis",
"ratione",
"assumenda",
"incidunt",
"nostrum",
"placeat",
"aliquid",
"fuga",
"provident",
"praesentium",
"rem",
"necessitatibus",
"suscipit",
"adipisci",
"quidem",
"possimus",
"voluptas",
"debitis",
"sint",
"accusantium",
"unde",
"sapiente",
"voluptate",
"qui",
"aspernatur",
"laudantium",
"soluta",
"amet",
"quo",
"aliquam",
"saepe",
"culpa",
"libero",
"ipsa",
"dicta",
"reiciendis",
"nesciunt",
"doloribus",
"autem",
"impedit",
"minima",
"maiores",
"repudiandae",
"ipsam",
"obcaecati",
"ullam",
"enim",
"totam",
"delectus",
"ducimus",
"quis",
"voluptates",
"dolores",
"molestiae",
"harum",
"dolorem",
"quia",
"voluptatem",
"molestias",
"magni",
"distinctio",
"omnis",
"illum",
"dolorum",
"voluptatum",
"ea",
"quas",
"quam",
"corporis",
"quae",
"blanditiis",
"atque",
"deserunt",
"laboriosam",
"earum",
"consequuntur",
"hic",
"cupiditate",
"quibusdam",
"accusamus",
"ut",
"rerum",
"error",
"minus",
"eius",
"ab",
"ad",
"nemo",
"fugit",
"officia",
"at",
"in",
"id",
"quos",
"reprehenderit",
"numquam",
"iste",
"fugiat",
"sit",
"inventore",
"beatae",
"repellendus",
"magnam",
"recusandae",
"quod",
"explicabo",
"doloremque",
"aperiam",
"consequatur",
"asperiores",
"commodi",
"optio",
"dolor",
"labore",
"temporibus",
"repellat",
"veniam",
"architecto",
"est",
"esse",
"mollitia",
"nulla",
"a",
"similique",
"eos",
"alias",
"dolore",
"tenetur",
"deleniti",
"porro",
"facere",
"maxime",
"corrupti",
};
pub const LOREM_COMMON_WORDS = [19][]const u8{
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipisicing",
"elit",
"sed",
"do",
"eiusmod",
"tempor",
"incididunt",
"ut",
"labore",
"et",
"dolore",
"magna",
"aliqua",
};
pub fn sentence(allocator: std.mem.Allocator) ![]const u8 {
const num_sections = rand.intRangeAtMost(u32, 1, 4);
var parts = std.ArrayList([]u8){};
defer {
for (parts.items) |p| allocator.free(p);
parts.deinit(allocator);
}
var i: u32 = 0;
while (i < num_sections) : (i += 1) {
const num_words = rand.intRangeAtMost(u32, 3, 12);
var wds = std.ArrayList([]const u8){};
defer wds.deinit(allocator);
try wds.ensureTotalCapacity(allocator, num_words);
var j: u32 = 0;
while (j < num_words) : (j += 1) {
const idx = rand.intRangeAtMost(usize, 0, LOREM_WORDS.len - 1);
try wds.append(allocator, LOREM_WORDS[idx]);
}
const section = try std.mem.join(allocator, " ", wds.items);
try parts.append(allocator, section);
}
const text = try std.mem.join(allocator, ", ", parts.items);
defer allocator.free(text);
var result = try allocator.alloc(u8, text.len + 1);
if (text.len > 0) {
result[0] = std.ascii.toUpper(text[0]);
@memcpy(result[1..text.len], text[1..]);
}
result[text.len] = if (rand.boolean()) '.' else '?';
return result;
}
pub fn paragraph(allocator: std.mem.Allocator) ![]const u8 {
const num_sentences = rand.intRangeAtMost(u32, 1, 4);
var sentences = std.ArrayList([]const u8){};
defer sentences.deinit(allocator);
for (0..num_sentences) |_| {
try sentences.append(allocator, try sentence(allocator));
}
return try std.mem.join(allocator, ". ", sentences.items);
}
pub fn paragraphs(allocator: std.mem.Allocator, count: u32, random: bool) ![]const u8 {
var pa = std.ArrayList([]const u8){};
defer pa.deinit(allocator);
if (count == 0) return "";
if (random == true) {
for (0..count) |_| {
const pg = try paragraph(allocator);
if (pg.len > 0) {
try pa.append(allocator, try std.fmt.allocPrint(allocator, "<p>{s}</p>", .{pg}));
}
}
return try std.mem.join(allocator, "\n", pa.items);
}
const first = try std.fmt.allocPrint(allocator, "<p>{s}</p>", .{LOREM_COMMON_P});
if (count == 1) {
return first;
}
const ncount: u32 = count - 1;
try pa.append(allocator, first);
for (0..ncount) |_| {
const pg = try paragraph(allocator);
if (pg.len > 0) {
try pa.append(allocator, try std.fmt.allocPrint(allocator, "<p>{s}</p>", .{pg}));
}
}
return try std.mem.join(allocator, "\n", pa.items);
}
pub fn words(allocator: std.mem.Allocator, count: u32, random: bool) ![]const u8 {
var wd = std.ArrayList([]const u8){};
defer wd.deinit(allocator);
if (random == true) {
for (0..count) |_| {
const idx = rand.intRangeAtMost(usize, 0, LOREM_COMMON_WORDS.len - 1);
try wd.append(allocator, LOREM_COMMON_WORDS[idx]);
}
return try std.mem.join(allocator, " ", wd.items);
}
var inc: u32 = 0;
for (LOREM_COMMON_WORDS) |word| {
try wd.append(allocator, word);
inc += 1;
if (inc >= count or inc >= 20) break;
}
if (count >= 20) {
const ncount = count - inc;
for (0..ncount) |_| {
const idx = rand.intRangeAtMost(usize, 0, LOREM_COMMON_WORDS.len - 1);
try wd.append(allocator, LOREM_COMMON_WORDS[idx]);
}
}
return try std.mem.join(allocator, " ", wd.items);
}

View file

@ -9,40 +9,33 @@ pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const alloc = arena.allocator();
// const base =
// \\<html>
// \\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
// \\<body>
// \\{% block content %}Conteúdo padrão{% endblock %}
// \\</body>
// \\</html>
// ;
//
// const child =
// \\{% extends "base.html" %}
// \\{% block title %}Meu Título{% endblock %}
// \\{% block content %}
// \\Olá {{ nome }}!
// \\{% endblock %}
// ;
//
// try std.fs.cwd().writeFile(.{
// .sub_path = "base.html",
// .data = base,
// });
// try std.fs.cwd().writeFile(.{
// .sub_path = "child.html",
// .data = child,
// });
// defer std.fs.cwd().deleteFile("base.html") catch {};
// defer std.fs.cwd().deleteFile("child.html") catch {};
const base =
\\<html>
\\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
\\<body>
\\{% block content %}Conteúdo padrão{% endblock %}
\\</body>
\\</html>
;
const child =
\\{% extends "base.html" %}
\\{% block title %}Meu Título{% endblock %}
\\{% block content %}
\\Olá {{ nome }}!
\\{% endblock %}
;
const User =struct {
name: []const u8,
email: []const u8,
notifications: i64 = 0
};
try std.fs.cwd().writeFile(.{
.sub_path = "base.html",
.data = base,
});
try std.fs.cwd().writeFile(.{
.sub_path = "child.html",
.data = child,
});
defer std.fs.cwd().deleteFile("base.html") catch {};
defer std.fs.cwd().deleteFile("child.html") catch {};
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -52,27 +45,12 @@ pub fn main() !void {
const renderer = Renderer.init(&ctx, &cache);
const user = User{
.name = "Lucas",
.email = "lucas@email",
.notifications = 5
};
const itens = [3][]const u8{"Livro", "Caneta", "Caderno"};
try ctx.set("nome", "Lucas");
try ctx.set("user", user);
try ctx.set("msg", "Bazinga!");
try ctx.set("itens", itens);
for(ctx.get("itens").?.list) |item| {
std.debug.print(" - {s}\n", .{item.string});
}
var buffer = std.ArrayList(u8){};
defer buffer.deinit(ctx.allocator());
try renderer.render("home.html", buffer.writer(ctx.allocator()));
try renderer.render("child.html", buffer.writer(ctx.allocator()));
const output = buffer.items;

View file

@ -1,208 +0,0 @@
// https://github.com/cztomsik/tokamak
const std = @import("std");
const util = @import("util.zig");
// https://github.com/ziglang/zig/issues/19858#issuecomment-2370673253
// NOTE: I've tried to make it work with enum / packed struct but I was still
// getting weird "operation is runtime due to this operand" here and there
// but it should be possible because we do something similar in util.Smol
pub const TypeId = *const struct {
name: [*:0]const u8,
pub fn sname(self: *const @This()) []const u8 {
// NOTE: we can't switch (invalid record Zig 0.14.1)
if (self == tid([]const u8)) return "str";
if (self == tid(?[]const u8)) return "?str";
return shortName(std.mem.span(self.name), '.');
}
};
pub inline fn tid(comptime T: type) TypeId {
const H = struct {
const id: Deref(TypeId) = .{ .name = @typeName(T) };
};
return &H.id;
}
pub fn tids(comptime types: []const type) []const TypeId {
var buf = util.Buf(TypeId).initComptime(types.len);
for (types) |T| buf.push(tid(T));
return buf.finish();
}
/// Ptr to a comptime value, wrapped together with its type. We use this to
/// pass around values (including a concrete fun types!) during the Bundle
/// compilation.
pub const ComptimeVal = struct {
type: type,
ptr: *const anyopaque,
pub fn wrap(comptime val: anytype) ComptimeVal {
return .{ .type = @TypeOf(val), .ptr = @ptrCast(&val) };
}
pub fn unwrap(self: ComptimeVal) self.type {
return @as(*const self.type, @ptrCast(@alignCast(self.ptr))).*;
}
};
pub fn dupe(allocator: std.mem.Allocator, value: anytype) !@TypeOf(value) {
return switch (@typeInfo(@TypeOf(value))) {
.optional => try dupe(allocator, value orelse return null),
.@"struct" => |s| {
var res: @TypeOf(value) = undefined;
inline for (s.fields) |f| @field(res, f.name) = try dupe(allocator, @field(value, f.name));
return res;
},
.pointer => |p| switch (p.size) {
.slice => if (p.child == u8) allocator.dupe(p.child, value) else error.NotSupported,
else => value,
},
else => value,
};
}
pub fn free(allocator: std.mem.Allocator, value: anytype) void {
switch (@typeInfo(@TypeOf(value))) {
.optional => if (value) |v| free(allocator, v),
.@"struct" => |s| {
inline for (s.fields) |f| free(allocator, @field(value, f.name));
},
.pointer => |p| switch (p.size) {
.slice => if (p.child == u8) allocator.free(value),
else => {},
},
else => {},
}
}
pub fn upcast(context: anytype, comptime T: type) T {
return .{
.context = context,
.vtable = comptime brk: {
const Impl = Deref(@TypeOf(context));
var vtable: T.VTable = undefined;
for (std.meta.fields(T.VTable)) |f| {
@field(vtable, f.name) = @ptrCast(&@field(Impl, f.name));
}
const copy = vtable;
break :brk &copy;
},
};
}
pub fn Return(comptime fun: anytype) type {
return switch (@typeInfo(@TypeOf(fun))) {
.@"fn" => |f| f.return_type.?,
else => @compileError("Expected a function, got " ++ @typeName(@TypeOf(fun))),
};
}
pub fn Result(comptime fun: anytype) type {
const R = Return(fun);
return switch (@typeInfo(R)) {
.error_union => |r| r.payload,
else => R,
};
}
pub fn LastArg(comptime fun: anytype) type {
const params = @typeInfo(@TypeOf(fun)).@"fn".params;
return params[params.len - 1].type.?;
}
pub inline fn isStruct(comptime T: type) bool {
return @typeInfo(T) == .@"struct";
}
pub inline fn isTuple(comptime T: type) bool {
return switch (@typeInfo(T)) {
.@"struct" => |s| s.is_tuple,
else => false,
};
}
pub inline fn isGeneric(comptime fun: anytype) bool {
return @typeInfo(@TypeOf(fun)).@"fn".is_generic;
}
pub inline fn isOptional(comptime T: type) bool {
return @typeInfo(T) == .optional;
}
pub inline fn isOnePtr(comptime T: type) bool {
return switch (@typeInfo(T)) {
.pointer => |p| p.size == .one,
else => false,
};
}
pub inline fn isSlice(comptime T: type) bool {
return switch (@typeInfo(T)) {
.pointer => |p| p.size == .slice,
else => false,
};
}
pub inline fn isString(comptime T: type) bool {
return switch (@typeInfo(T)) {
.pointer => |ptr| ptr.child == u8 or switch (@typeInfo(ptr.child)) {
.array => |arr| arr.child == u8,
else => false,
},
else => false,
};
}
pub fn Deref(comptime T: type) type {
return if (isOnePtr(T)) std.meta.Child(T) else T;
}
pub fn Unwrap(comptime T: type) type {
return switch (@typeInfo(T)) {
.optional => |o| o.child,
else => T,
};
}
pub fn Const(comptime T: type) type {
return switch (@typeInfo(T)) {
.pointer => |p| {
var info = p;
info.is_const = true;
return @Type(.{ .pointer = info });
},
else => T,
};
}
pub inline fn hasDecl(comptime T: type, comptime name: []const u8) bool {
return switch (@typeInfo(T)) {
.@"struct", .@"union", .@"enum", .@"opaque" => @hasDecl(T, name),
else => false,
};
}
pub fn fieldTypes(comptime T: type) []const type {
const fields = std.meta.fields(T);
var buf = util.Buf(type).initComptime(fields.len);
for (fields) |f| buf.push(f.type);
return buf.finish();
}
pub fn fnParams(comptime fun: anytype) []const type {
const info = @typeInfo(@TypeOf(fun));
if (info != .@"fn") @compileError("Expected a function, got " ++ @typeName(@TypeOf(fun)));
const params = info.@"fn".params;
var buf = util.Buf(type).initComptime(params.len);
for (params) |param| buf.push(param.type.?);
return buf.finish();
}
// TODO: move somewhere else?
fn shortName(name: []const u8, delim: u8) []const u8 {
return if (std.mem.lastIndexOfScalar(u8, name, delim)) |i| name[i + 1 ..] else name;
}

386
src/old/renderer.zig Normal file
View file

@ -0,0 +1,386 @@
const std = @import("std");
const builtin_filters = @import("filters.zig").builtin_filters;
const Context = @import("context.zig").Context;
const FilterError = @import("filters.zig").FilterError;
const Value = @import("context.zig").Value;
pub const RenderError = FilterError || error{
OutOfMemory,
InvalidTemplate,
BlockNotFound,
CircularExtends,
FileNotFound,
AccessDenied,
FileTooBig,
NoSpaceLeft,
Unexpected,
};
const TemplateBlock = struct {
name: []const u8,
content: []const u8,
};
const TemplateParser = struct {
template: []const u8,
pos: usize = 0,
last_position: usize = 0,
const Block = struct {
kind: enum { text, variable, tag },
content: []const u8,
};
fn init(template: []const u8) TemplateParser {
return .{ .template = template };
}
fn nextBlock(self: *TemplateParser) ?Block {
if (self.pos >= self.template.len) return null;
const start = self.pos;
if (std.mem.startsWith(u8, self.template[self.pos..], "{{")) {
self.pos += 2;
const end = std.mem.indexOfPos(u8, self.template, self.pos, "}}") orelse self.template.len;
self.pos = end + 2;
return .{ .kind = .variable, .content = self.template[start + 2 .. end] };
} else if (std.mem.startsWith(u8, self.template[self.pos..], "{%")) {
self.pos += 2;
const end = std.mem.indexOfPos(u8, self.template, self.pos, "%}") orelse self.template.len;
self.pos = end + 2;
return .{ .kind = .tag, .content = self.template[start + 2 .. end] };
} else {
const end = std.mem.indexOfAnyPos(u8, self.template, self.pos, "{%") orelse self.template.len;
self.pos = end;
return .{ .kind = .text, .content = self.template[start..end] };
}
}
fn skipTo(self: *TemplateParser, tag: []const u8) void {
while (self.nextBlock()) |block| {
if (block.kind == .tag) {
const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
if (std.mem.eql(u8, trimmed, tag)) {
break;
}
}
}
}
fn reset(self: *TemplateParser) void {
self.pos = 0;
}
fn resetToLastPosition(self: *TemplateParser) void {
self.pos = self.last_position;
}
};
pub const Renderer = struct {
context: *Context,
blocks: std.ArrayList(TemplateBlock),
allocator: std.mem.Allocator,
pub fn init(context: *Context) Renderer {
return .{
.context = context,
.blocks = std.ArrayList(TemplateBlock){},
.allocator = context.allocator(),
};
}
pub fn deinit(self: *Renderer) void {
for (self.blocks.items) |block| {
self.allocator.free(block.name);
}
self.blocks.deinit();
}
pub fn render(self: *Renderer, template_path: []const u8, writer: anytype) RenderError!void {
const max_size = 10 * 1024 * 1024; // 10MB
const template = std.fs.cwd().readFileAlloc(self.allocator, template_path, max_size) catch |err| switch (err) {
error.FileNotFound => return RenderError.FileNotFound,
error.AccessDenied => return RenderError.AccessDenied,
error.FileTooBig => return RenderError.FileTooBig,
error.NoSpaceLeft => return RenderError.NoSpaceLeft,
error.OutOfMemory => return RenderError.OutOfMemory,
else => return RenderError.Unexpected,
};
defer self.allocator.free(template);
try self.renderTemplate(template, writer);
}
pub fn renderString(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
try self.renderTemplate(template, writer);
}
fn renderTemplate(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
var parser = TemplateParser.init(template);
var extends_path: ?[]const u8 = null;
while (parser.nextBlock()) |block| {
const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
if (block.kind == .tag) {
if (std.mem.startsWith(u8, trimmed, "extends ")) {
const quoted = trimmed["extends ".len..];
extends_path = std.mem.trim(u8, quoted, " \"'");
} else if (std.mem.startsWith(u8, trimmed, "block ")) {
const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
const name_copy = try self.allocator.dupe(u8, block_name);
// Agora coletamos o conteúdo real do block
// Começa **após** o fechamento da tag de abertura
const block_start = parser.pos; // está após o %}
// Avança até o endblock
parser.skipTo("endblock");
// O conteúdo vai do início do corpo até o início do endblock
const endblock_start = parser.pos;
const block_content = template[block_start..endblock_start];
try self.blocks.append(self.allocator, .{ .name = name_copy, .content = block_content });
}
}
}
if (extends_path) |path| {
const max_size = 10 * 1024 * 1024;
const base_template = std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) {
error.FileNotFound => return RenderError.FileNotFound,
error.AccessDenied => return RenderError.AccessDenied,
error.FileTooBig => return RenderError.FileTooBig,
error.NoSpaceLeft => return RenderError.NoSpaceLeft,
error.OutOfMemory => return RenderError.OutOfMemory,
else => return RenderError.Unexpected,
};
defer self.allocator.free(base_template);
var base_parser = TemplateParser.init(base_template);
try self.renderWithInheritance(&base_parser, writer);
} else {
parser.reset();
try self.renderBlocks(&parser, writer);
}
}
fn renderWithInheritance(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
if (block.kind == .tag) {
const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
if (std.mem.startsWith(u8, trimmed, "block ")) {
const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
if (self.findBlock(block_name)) |child| {
try writer.writeAll(child.content);
parser.skipTo("endblock");
} else {
try self.renderBlockContent(parser, writer);
}
} else {
try self.renderTag(block.content, parser, writer);
}
} else {
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => unreachable,
}
}
}
}
fn findBlock(self: *Renderer, name: []const u8) ?TemplateBlock {
for (self.blocks.items) |b| {
if (std.mem.eql(u8, b.name, name)) return b;
}
return null;
}
fn renderBlockContent(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endblock")) {
break;
}
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
fn renderBlocks(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
/// Renderiza uma expressão: var ou var|filtro|...
fn renderExpression(self: *Renderer, expr: []const u8, writer: anytype) !void {
var parts = std.mem.splitScalar(u8, expr, '|');
var var_name = parts.next() orelse return;
var_name = std.mem.trim(u8, var_name, " \t\r\n");
var value = self.context.get(var_name) orelse Value.null;
var is_safe = false;
while (parts.next()) |part| {
const trimmed = std.mem.trim(u8, part, " \t\r\n");
if (trimmed.len == 0) continue;
const colon_pos = std.mem.indexOf(u8, trimmed, ":");
const filter_name = if (colon_pos) |c| trimmed[0..c] else trimmed;
const arg_str = if (colon_pos) |c| std.mem.trim(u8, trimmed[c + 1 ..], " \t\r\n") else null;
if (std.mem.eql(u8, filter_name, "safe")) {
is_safe = true;
continue;
}
const filter_fn = builtin_filters.get(filter_name) orelse continue;
const arg = if (arg_str) |a| blk: {
if (std.mem.eql(u8, a, "null")) break :blk Value.null;
if (std.fmt.parseInt(i64, a, 10)) |num| break :blk Value{ .int = num } else |_| {}
break :blk Value{ .string = a };
} else null;
value = try filter_fn(self.allocator, value, arg);
}
// Autoescape: escapa se não for safe
const str = if (is_safe) blk: {
break :blk try self.valueToString(value);
} else blk: {
const escaped = try self.escapeHtml(value);
break :blk try self.valueToString(escaped);
};
try writer.writeAll(str);
}
fn renderTag(self: *Renderer, tag_content: []const u8, parser: *TemplateParser, writer: anytype) !void {
const trimmed = std.mem.trim(u8, tag_content, " \t\r\n");
if (std.mem.startsWith(u8, trimmed, "if ")) {
try self.renderIf(trimmed["if ".len..], parser, writer);
} else if (std.mem.startsWith(u8, trimmed, "for ")) {
try self.renderFor(trimmed["for ".len..], parser, writer);
} else if (std.mem.eql(u8, trimmed, "endif") or std.mem.eql(u8, trimmed, "endfor")) {
// Nada a fazer o bloco foi consumido no if/for
} else {
// Tag desconhecida ignora
}
}
fn renderIf(self: *Renderer, condition_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
const condition = try self.evaluateCondition(condition_expr);
if (condition) {
try self.renderBlocksUntilEndif(parser, writer);
} else {
parser.skipTo("endif"); // sem try retorna void
}
}
fn renderFor(self: *Renderer, for_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
var parts = std.mem.splitSequence(u8, for_expr, " in ");
const item_name = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
const list_expr = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
const list_value = self.context.get(list_expr) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list) |item| {
try self.context.set(item_name, item);
try self.renderBlocksUntilEndfor(parser, writer);
parser.resetToLastPosition();
}
}
fn evaluateCondition(self: *Renderer, expr: []const u8) !bool {
const value = self.context.get(expr) orelse Value.null;
return switch (value) {
.bool => |b| b,
.int => |i| i != 0,
.float => |f| f != 0.0,
.string => |s| s.len > 0,
.list => |l| l.len > 0,
.dict => |d| d.count() > 0,
.null => false,
};
}
fn renderBlocksUntilEndif(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endif")) {
break;
}
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
fn renderBlocksUntilEndfor(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
const start_pos = parser.pos;
while (parser.nextBlock()) |block| {
if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endfor")) {
parser.last_position = start_pos;
break;
}
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
/// Escapa HTML manualmente (sem depender de filtro externo)
fn escapeHtml(self: *Renderer, value: Value) !Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
var result = std.ArrayList(u8){};
for (s) |c| {
switch (c) {
'&' => try result.appendSlice(self.allocator, "&amp;"),
'<' => try result.appendSlice(self.allocator, "&lt;"),
'>' => try result.appendSlice(self.allocator, "&gt;"),
'"' => try result.appendSlice(self.allocator, "&quot;"),
'\'' => try result.appendSlice(self.allocator, "&#x27;"),
else => try result.append(self.allocator, c),
}
}
return Value{ .string = try result.toOwnedSlice(self.allocator) };
}
/// Converte Value para string de forma segura
fn valueToString(self: *Renderer, value: Value) ![]const u8 {
return switch (value) {
.string => |s| s,
.int => |i| try std.fmt.allocPrint(self.allocator, "{d}", .{i}),
.float => |f| try std.fmt.allocPrint(self.allocator, "{d}", .{f}),
.bool => |b| if (b) "true" else "false",
.null => "",
else => "[object]",
};
}
};

395
src/old/renderer_bkp.zig Normal file
View file

@ -0,0 +1,395 @@
const std = @import("std");
const builtin_filters = @import("filters.zig").builtin_filters;
const Context = @import("context.zig").Context;
const FilterError = @import("filters.zig").FilterError;
const Value = @import("context.zig").Value;
pub const RenderError = FilterError || error{
OutOfMemory,
InvalidTemplate,
BlockNotFound,
CircularExtends,
FileNotFound,
AccessDenied,
FileTooBig,
NoSpaceLeft,
Unexpected,
};
const TemplateBlock = struct {
name: []const u8,
content: []const u8,
};
const TemplateParser = struct {
template: []const u8,
pos: usize = 0,
last_position: usize = 0,
const Block = struct {
kind: enum { text, variable, tag },
content: []const u8,
};
fn init(template: []const u8) TemplateParser {
return .{ .template = template };
}
fn nextBlock(self: *TemplateParser) ?Block {
if (self.pos >= self.template.len) return null;
const start = self.pos;
if (std.mem.startsWith(u8, self.template[self.pos..], "{{")) {
self.pos += 2;
const end = std.mem.indexOfPos(u8, self.template, self.pos, "}}") orelse self.template.len;
self.pos = end + 2;
return .{ .kind = .variable, .content = self.template[start + 2 .. end] };
} else if (std.mem.startsWith(u8, self.template[self.pos..], "{%")) {
self.pos += 2;
const end = std.mem.indexOfPos(u8, self.template, self.pos, "%}") orelse self.template.len;
self.pos = end + 2;
return .{ .kind = .tag, .content = self.template[start + 2 .. end] };
} else {
const end = std.mem.indexOfAnyPos(u8, self.template, self.pos, "{%") orelse self.template.len;
self.pos = end;
return .{ .kind = .text, .content = self.template[start..end] };
}
}
fn skipTo(self: *TemplateParser, tag: []const u8) void {
while (self.nextBlock()) |block| {
if (block.kind == .tag) {
const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
if (std.mem.eql(u8, trimmed, tag)) {
break;
}
}
}
}
fn reset(self: *TemplateParser) void {
self.pos = 0;
}
fn resetToLastPosition(self: *TemplateParser) void {
self.pos = self.last_position;
}
};
pub const Renderer = struct {
context: *Context,
blocks: std.ArrayList(TemplateBlock),
pub fn init(context: *Context) Renderer {
return .{
.context = context,
.blocks = std.ArrayList(TemplateBlock){},
};
}
pub fn deinit(self: *Renderer) void {
for (self.blocks.items) |block| {
self.context.allocator().free(block.name);
}
self.blocks.deinit();
}
pub fn render(self: *Renderer, template_path: []const u8, writer: anytype) RenderError!void {
const max_size = 10 * 1024 * 1024; // 10MB
const template = std.fs.cwd().readFileAlloc(self.context.allocator(), template_path, max_size) catch |err| switch (err) {
error.FileNotFound => return RenderError.FileNotFound,
error.AccessDenied => return RenderError.AccessDenied,
error.FileTooBig => return RenderError.FileTooBig,
error.NoSpaceLeft => return RenderError.NoSpaceLeft,
error.OutOfMemory => return RenderError.OutOfMemory,
else => return RenderError.Unexpected,
};
defer self.context.allocator().free(template);
try self.renderTemplate(template, writer);
}
pub fn renderString(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
try self.renderTemplate(template, writer);
}
fn renderTemplate(self: *Renderer, template: []const u8, writer: anytype) RenderError!void {
var parser = TemplateParser.init(template);
var extends_path: ?[]const u8 = null;
while (parser.nextBlock()) |block| {
const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
if (block.kind == .tag) {
if (std.mem.startsWith(u8, trimmed, "extends ")) {
const quoted = trimmed["extends ".len..];
extends_path = std.mem.trim(u8, quoted, " \"'");
} else if (std.mem.startsWith(u8, trimmed, "block ")) {
const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
const name_copy = try self.context.allocator().dupe(u8, block_name);
// Agora extrai o conteúdo real do block (após o nome até endblock)
const block_start = parser.pos; // está após o %}
parser.skipTo("endblock");
const endblock_start = parser.pos; // início do {% endblock %}
const block_content = template[block_start..endblock_start];
try self.blocks.append(self.context.allocator(), .{ .name = name_copy, .content = block_content });
}
} else if (std.mem.startsWith(u8, trimmed, "block ")) {
const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
const name_copy = try self.context.allocator().dupe(u8, block_name);
// Agora coletamos o conteúdo real do block
// Começa **após** o fechamento da tag de abertura
const block_start = parser.pos; // está após o %}
// Avança até o endblock
parser.skipTo("endblock");
// O conteúdo vai do início do corpo até o início do endblock
const endblock_start = parser.pos;
const block_content = template[block_start..endblock_start];
try self.blocks.append(self.context.allocator(), .{ .name = name_copy, .content = block_content });
}
}
if (extends_path) |path| {
const max_size = 10 * 1024 * 1024;
const base_template = std.fs.cwd().readFileAlloc(self.context.allocator(), path, max_size) catch |err| switch (err) {
error.FileNotFound => return RenderError.FileNotFound,
error.AccessDenied => return RenderError.AccessDenied,
error.FileTooBig => return RenderError.FileTooBig,
error.NoSpaceLeft => return RenderError.NoSpaceLeft,
error.OutOfMemory => return RenderError.OutOfMemory,
else => return RenderError.Unexpected,
};
defer self.context.allocator().free(base_template);
var base_parser = TemplateParser.init(base_template);
try self.renderWithInheritance(&base_parser, writer);
} else {
parser.reset();
try self.renderBlocks(&parser, writer);
}
}
fn renderWithInheritance(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
if (block.kind == .tag) {
const trimmed = std.mem.trim(u8, block.content, " \t\r\n");
if (std.mem.startsWith(u8, trimmed, "block ")) {
const block_name = std.mem.trim(u8, trimmed["block ".len..], " \t\r\n");
if (self.findBlock(block_name)) |child| {
try writer.writeAll(child.content);
parser.skipTo("endblock");
} else {
try self.renderBlockContent(parser, writer);
}
} else {
try self.renderTag(block.content, parser, writer);
}
} else {
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => unreachable,
}
}
}
}
fn findBlock(self: *Renderer, name: []const u8) ?TemplateBlock {
for (self.blocks.items) |b| {
if (std.mem.eql(u8, b.name, name)) return b;
}
return null;
}
fn renderBlockContent(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endblock")) {
break;
}
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
fn renderBlocks(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
/// Renderiza uma expressão: var ou var|filtro|...
fn renderExpression(self: *Renderer, expr: []const u8, writer: anytype) !void {
var parts = std.mem.splitScalar(u8, expr, '|');
var var_name = parts.next() orelse return;
var_name = std.mem.trim(u8, var_name, " \t\r\n");
var value = self.context.get(var_name) orelse Value.null;
var is_safe = false;
while (parts.next()) |part| {
const trimmed = std.mem.trim(u8, part, " \t\r\n");
if (trimmed.len == 0) continue;
const colon_pos = std.mem.indexOf(u8, trimmed, ":");
const filter_name = if (colon_pos) |c| trimmed[0..c] else trimmed;
const arg_str = if (colon_pos) |c| std.mem.trim(u8, trimmed[c + 1 ..], " \t\r\n") else null;
if (std.mem.eql(u8, filter_name, "safe")) {
is_safe = true;
continue;
}
const filter_fn = builtin_filters.get(filter_name) orelse continue;
const arg = if (arg_str) |a| blk: {
if (std.mem.eql(u8, a, "null")) break :blk Value.null;
if (std.fmt.parseInt(i64, a, 10)) |num| break :blk Value{ .int = num } else |_| {}
break :blk Value{ .string = a };
} else null;
value = try filter_fn(self.context.allocator(), value, arg);
}
// Autoescape: escapa se não for safe
const str = if (is_safe) blk: {
break :blk try self.valueToString(value);
} else blk: {
const escaped = try self.escapeHtml(value);
break :blk try self.valueToString(escaped);
};
try writer.writeAll(str);
}
fn renderTag(self: *Renderer, tag_content: []const u8, parser: *TemplateParser, writer: anytype) !void {
const trimmed = std.mem.trim(u8, tag_content, " \t\r\n");
if (std.mem.startsWith(u8, trimmed, "if ")) {
try self.renderIf(trimmed["if ".len..], parser, writer);
} else if (std.mem.startsWith(u8, trimmed, "for ")) {
try self.renderFor(trimmed["for ".len..], parser, writer);
} else if (std.mem.eql(u8, trimmed, "endif") or std.mem.eql(u8, trimmed, "endfor")) {
// Nada a fazer o bloco foi consumido no if/for
} else {
// Tag desconhecida ignora
}
}
fn renderIf(self: *Renderer, condition_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
const condition = try self.evaluateCondition(condition_expr);
if (condition) {
try self.renderBlocksUntilEndif(parser, writer);
} else {
parser.skipTo("endif"); // sem try retorna void
}
}
fn renderFor(self: *Renderer, for_expr: []const u8, parser: *TemplateParser, writer: anytype) !void {
var parts = std.mem.splitSequence(u8, for_expr, " in ");
const item_name = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
const list_expr = std.mem.trim(u8, parts.next() orelse return, " \t\r\n");
const list_value = self.context.get(list_expr) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list) |item| {
try self.context.set(item_name, item);
try self.renderBlocksUntilEndfor(parser, writer);
parser.resetToLastPosition();
}
}
fn evaluateCondition(self: *Renderer, expr: []const u8) !bool {
const value = self.context.get(expr) orelse Value.null;
return switch (value) {
.bool => |b| b,
.int => |i| i != 0,
.float => |f| f != 0.0,
.string => |s| s.len > 0,
.list => |l| l.len > 0,
.dict => |d| d.count() > 0,
.null => false,
};
}
fn renderBlocksUntilEndif(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
while (parser.nextBlock()) |block| {
if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endif")) {
break;
}
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
fn renderBlocksUntilEndfor(self: *Renderer, parser: *TemplateParser, writer: anytype) RenderError!void {
const start_pos = parser.pos;
while (parser.nextBlock()) |block| {
if (block.kind == .tag and std.mem.eql(u8, std.mem.trim(u8, block.content, " \t\r\n"), "endfor")) {
parser.last_position = start_pos;
break;
}
switch (block.kind) {
.text => try writer.writeAll(block.content),
.variable => try self.renderExpression(block.content, writer),
.tag => try self.renderTag(block.content, parser, writer),
}
}
}
/// Escapa HTML manualmente (sem depender de filtro externo)
fn escapeHtml(self: *Renderer, value: Value) !Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
var result = std.ArrayList(u8){};
for (s) |c| {
switch (c) {
'&' => try result.appendSlice(self.context.allocator(), "&amp;"),
'<' => try result.appendSlice(self.context.allocator(), "&lt;"),
'>' => try result.appendSlice(self.context.allocator(), "&gt;"),
'"' => try result.appendSlice(self.context.allocator(), "&quot;"),
'\'' => try result.appendSlice(self.context.allocator(), "&#x27;"),
else => try result.append(self.context.allocator(), c),
}
}
return Value{ .string = try result.toOwnedSlice(self.context.allocator()) };
}
/// Converte Value para string de forma segura
fn valueToString(self: *Renderer, value: Value) ![]const u8 {
return switch (value) {
.string => |s| s,
.int => |i| try std.fmt.allocPrint(self.context.allocator(), "{d}", .{i}),
.float => |f| try std.fmt.allocPrint(self.context.allocator(), "{d}", .{f}),
.bool => |b| if (b) "true" else "false",
.null => "",
else => "[object]",
};
}
};

156
src/old/renderer_test.zig Normal file
View file

@ -0,0 +1,156 @@
const std = @import("std");
const testing = std.testing;
const Context = @import("context.zig").Context;
const Renderer = @import("renderer.zig").Renderer;
const Value = @import("context.zig").Value;
test "renderer - texto literal e variável simples" {
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set("nome", "Fulano");
try ctx.set("idade", 30);
var renderer = Renderer.init(&ctx);
const template = "Olá {{ nome }}! Você tem {{ idade }} anos.";
var buffer = std.ArrayList(u8){};
defer buffer.deinit(alloc);
try renderer.renderString(template, buffer.writer(alloc));
try testing.expectEqualStrings("Olá Fulano! Você tem 30 anos.", buffer.items);
}
test "renderer - filtros com argumentos" {
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set("titulo", "Olá Mundo Legal");
var renderer = Renderer.init(&ctx);
const template = "{{ titulo|lower|slugify }}";
var buffer = std.ArrayList(u8){};
defer buffer.deinit(alloc);
try renderer.renderString(template, buffer.writer(alloc));
try testing.expectEqualStrings("ola-mundo-legal", buffer.items);
}
test "renderer - literal, variável e autoescape" {
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set("nome", "Lucas");
try ctx.set("xss", "<script>alert('xss')</script>");
try ctx.set("html", "<b>negrito</b>");
var renderer = Renderer.init(&ctx);
const template = "Olá {{ nome }}! Perigo: {{ xss }} Seguro: {{ html|safe }}";
var buffer = std.ArrayList(u8){};
defer buffer.deinit(alloc);
try renderer.renderString(template, buffer.writer(alloc));
const output = buffer.items;
try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
try testing.expect(std.mem.indexOf(u8, output, "&lt;script&gt;") != null); // escapado
try testing.expect(std.mem.indexOf(u8, output, "<b>negrito</b>") != null); // safe
}
test "renderer - if and for" {
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set("ativo", true);
try ctx.set("inativo", false);
try ctx.set("nomes", [_]Value{ Value{ .string = "Ana" }, Value{ .string = "Bia" }, Value{ .string = "Cris" } });
var renderer = Renderer.init(&ctx);
const template =
\\{% if ativo %}Sim!{% endif %}
\\{% if inativo %}Não{% endif %}
\\Lista:
\\{% for nome in nomes %}
\\- {{ nome }}
\\{% endfor %}
;
var buffer = std.ArrayList(u8){};
defer buffer.deinit(alloc);
try renderer.renderString(template, buffer.writer(alloc));
const output = buffer.items;
try testing.expect(std.mem.indexOf(u8, output, "Sim!") != null);
try testing.expect(std.mem.indexOf(u8, output, "Não") == null);
try testing.expect(std.mem.indexOf(u8, output, "- Ana") != null);
try testing.expect(std.mem.indexOf(u8, output, "- Bia") != null);
try testing.expect(std.mem.indexOf(u8, output, "- Cris") != null);
}
// test "renderer - block and extends" {
// const alloc = testing.allocator;
//
// const base =
// \\<html>
// \\<head><title>{% block title %}Título Padrão{% endblock %}</title></head>
// \\<body>
// \\{% block content %}Conteúdo padrão{% endblock %}
// \\</body>
// \\</html>
// ;
//
// const child =
// \\{% extends "base.html" %}
// \\{% block title %}Meu Título{% endblock %}
// \\{% block content %}
// \\Olá {{ nome }}!
// \\{% endblock %}
// ;
//
// try std.fs.cwd().writeFile(.{
// .sub_path = "base.html",
// .data = base,
// });
// try std.fs.cwd().writeFile(.{
// .sub_path = "child.html",
// .data = child,
// });
// defer std.fs.cwd().deleteFile("base.html") catch {};
// defer std.fs.cwd().deleteFile("child.html") catch {};
//
// var ctx = Context.init(alloc);
// defer ctx.deinit();
//
// try ctx.set("nome", "Lucas");
//
// var renderer = Renderer.init(&ctx);
//
// var buffer = std.ArrayList(u8){};
// defer buffer.deinit(ctx.allocator());
//
// try renderer.render("child.html", buffer.writer(ctx.allocator()));
//
// const output = buffer.items;
//
// std.debug.print("{s}", .{output});
//
// try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") == null);
// try testing.expect(std.mem.indexOf(u8, output, "Título Padrão") == null);
// }

File diff suppressed because it is too large Load diff

1208
src/parser_bkp.zig Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,19 +10,37 @@ const builtin_filters = @import("filters.zig").builtin_filters;
const FilterError = @import("filters.zig").FilterError;
const parser = @import("parser.zig");
const TemplateCache = @import("cache.zig").TemplateCache;
const time = @import("time.zig");
const lorem = @import("lorem.zig");
const icons = @import("svg/icons.zig");
pub const RenderError = error{
InvalidCharacter,
InvalidSyntax,
pub const RenderError = FilterError || error{
OutOfMemory,
Overflow,
InvalidSyntax,
UnknownVariable,
UnknownFilter,
InvalidTemplate,
BlockNotFound,
CircularExtends,
FileNotFound,
AccessDenied,
FileTooBig,
NoSpaceLeft,
Unexpected,
UnsupportedExpression,
// } || FilterError || parser.ParserError || icons.SvgError || std.fs.File.OpenError;
} || FilterError || parser.ParserError || std.fs.File.OpenError;
UnclosedTag,
InvalidAutoescapeArgument,
UnclosedVariable,
UnclosedBlock,
UnclosedComment,
InvalidAssignmentSyntax,
UnclosedQuoteInAssignment,
InvalidForSyntax,
UnclosedVerbatim,
InvalidUrlSyntax,
UnclosedQuoteInUrl,
InvalidDebugArgs,
InvalidRegroupSyntax,
InvalidWidthRatioSyntax,
InvalidTemplateTag,
InvalidCsrfTokenArgs,
};
pub const Renderer = struct {
context: *Context,
@ -40,23 +58,19 @@ pub const Renderer = struct {
fn checkForExtends(self: *const Renderer, nodes: []parser.Node) ?parser.Node {
_ = self;
for (nodes) |n| {
// if (n.type == .extends) return n;
if (n.type == .tag and n.tag.?.kind == .extends) return n;
if (n.type == .extends) return n;
}
return null;
}
fn readTemplateFile(self: *const Renderer, path: []const u8) ![]const u8 {
fn readTemplateFile(self: *const Renderer, path: []const u8) RenderError![]const u8 {
const max_size = 10 * 1024 * 1024;
const full_path = try std.fs.path.join(self.allocator, &.{ self.cache.default_path.?, path });
defer self.allocator.free(full_path);
const file = try std.fs.cwd().openFile(full_path, .{});
defer file.close();
// return std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) {
return file.readToEndAlloc(self.allocator, max_size) catch |err| switch (err) {
return std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) {
error.FileNotFound => return RenderError.FileNotFound,
error.AccessDenied => return RenderError.AccessDenied,
error.FileTooBig => return RenderError.FileTooBig,
error.NoSpaceLeft => return RenderError.NoSpaceLeft,
error.OutOfMemory => return RenderError.OutOfMemory,
else => return RenderError.Unexpected,
};
}
@ -65,8 +79,7 @@ pub const Renderer = struct {
const extends_node = self.checkForExtends(nodes);
if (extends_node) |ext| {
// const base_template = try self.readTemplateFile(ext.extends.?.parent_name);
const base_template = try self.readTemplateFile(ext.tag.?.body.extends.parent_name);
const base_template = try self.readTemplateFile(ext.extends.?.parent_name);
defer self.allocator.free(base_template);
var base_parser = parser.Parser.init(base_template);
@ -84,17 +97,16 @@ pub const Renderer = struct {
}
fn renderTemplate(self: *const Renderer, template: []const u8, writer: anytype, cache_key: ?[]const u8) RenderError!void {
_ = cache_key;
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
const alloc = arena.allocator();
// if (cache_key) |ck| {
// if (self.cache.get(ck)) |cached_nodes| {
// try self.renderNodes(alloc, cached_nodes, writer);
// return;
// }
// }
if (cache_key) |ck| {
if (self.cache.get(ck)) |cached_nodes| {
try self.renderNodes(alloc, cached_nodes, writer);
return;
}
}
var p = parser.Parser.init(template);
const nodes = try p.parse(alloc);
@ -103,15 +115,16 @@ pub const Renderer = struct {
alloc.free(nodes);
}
// if (cache_key) |ck| {
// var alc = self.cache.allocator();
// var cached_nodes = try alc.alloc(parser.Node, nodes.len);
// errdefer alc.free(cached_nodes);
// for (nodes, 0..) |node, i| {
// cached_nodes[i] = try node.clone(self.allocator);
// }
// try self.cache.add(ck, nodes);
// }
if (cache_key) |ck| {
var alc = self.cache.allocator();
var cached_nodes = try alc.alloc(parser.Node, nodes.len);
errdefer alc.free(cached_nodes);
for (nodes, 0..) |node, i| {
cached_nodes[i] = try node.clone(self.allocator);
std.debug.print("clonou {any}\n", .{cached_nodes[i]});
}
try self.cache.add(ck, nodes);
}
return try self.renderNodes(alloc, nodes, writer);
}
@ -129,22 +142,24 @@ pub const Renderer = struct {
fn renderWithInheritance(self: *const Renderer, alloc: Allocator, base_nodes: []parser.Node, child_nodes: []parser.Node, writer: anytype) RenderError!void {
for (base_nodes) |base_node| {
if (base_node.type == .tag and base_node.tag.?.kind == .block) {
const block_name = base_node.tag.?.body.block.name;
if (base_node.type == .block) {
const block_name = base_node.block.?.name;
// Procura no filho
const child_block = self.findChildBlock(child_nodes, block_name);
if (child_block) |child| {
// Renderiza o filho, passando o conteúdo do pai para block.super
for (child.body) |child_node| {
try self.renderNode(alloc, child_nodes, child_node, writer, null, base_node.tag.?.body.block.body);
try self.renderNode(alloc, child_nodes, child_node, writer, null, base_node.block.?.body);
}
} else {
// Renderiza o do pai
for (base_node.tag.?.body.block.body) |child| {
for (base_node.block.?.body) |child| {
try self.renderNode(alloc, child_nodes, child, writer, null, null);
}
}
} else {
// Qualquer outra coisa no base
try self.renderNode(alloc, child_nodes, base_node, writer, null, null);
}
}
@ -167,14 +182,7 @@ pub const Renderer = struct {
for (node.variable.?.filters) |f| {
const filter_fn = builtin_filters.get(f.name) orelse return error.UnknownFilter;
// const arg = if (f.arg) |a| Value{ .string = a } else null;
var arg: Value = Value.null;
if (f.arg) |a| {
arg = Value{ .string = a };
const result = try std.fmt.parseInt(i64, a, 10);
if (std.math.maxInt(i64) < result) return error.Overflow;
arg = Value{ .int = result };
}
const arg = if (f.arg) |a| Value{ .string = a } else null;
value = try filter_fn(alloc, value, arg);
if (std.mem.eql(u8, f.name, "safe")) is_safe = true;
@ -192,292 +200,90 @@ pub const Renderer = struct {
try self.valueToString(alloc, &buf, value);
try writer.writeAll(buf.items);
},
.tag => {
switch (node.tag.?.kind) {
.if_block => {
const condition = try self.evaluateCondition(alloc, node.tag.?.body.@"if".condition, context);
.if_block => {
const condition = self.evaluateCondition(node.@"if".?.condition) catch return false;
if (condition) {
for (node.tag.?.body.@"if".true_body) |child| {
try self.renderNode(alloc, nodes, child, writer, context, null);
}
} else {
for (node.tag.?.body.@"if".false_body) |child| {
try self.renderNode(alloc, nodes, child, writer, context, null);
}
}
},
.include => {
const included_template = try self.readTemplateFile(node.tag.?.body.include.template_path);
defer alloc.free(included_template);
var included_parser = parser.Parser.init(included_template);
const included_nodes = try included_parser.parse(alloc);
defer {
for (included_nodes) |n| n.deinit(alloc);
alloc.free(included_nodes);
}
// Renderiza o include no contexto atual (sem novo contexto)
for (included_nodes) |included_node| {
try self.renderNode(alloc, nodes, included_node, writer, context, null);
}
},
.for_block => {
const list_value = self.context.get(node.tag.?.body.@"for".iterable) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list, 0..) |item, i| {
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set(node.tag.?.body.@"for".loop_var, item);
try ctx.set("forloop.counter", i + 1);
try ctx.set("forloop.counter0", i);
try ctx.set("forloop.revcounter", (list.len - i));
try ctx.set("forloop.revcounter0", (list.len - i) - 1);
try ctx.set("forloop.first", i == 0);
try ctx.set("forloop.last", i == (list.len - 1));
try ctx.set("forloop.length", list.len);
// forloop.counter
// The current iteration of the loop (1-indexed)
// forloop.counter0
// The current iteration of the loop (0-indexed)
// forloop.revcounter
// The number of iterations from the end of the loop (1-indexed)
// forloop.revcounter0
// The number of iterations from the end of the loop (0-indexed)
// forloop.first
// True if this is the first time through the loop
// forloop.last
// True if this is the last time through the loop
// forloop.length
for (node.tag.?.body.@"for".body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
if (node.tag.?.body.@"for".body.len == 0) {
for (node.tag.?.body.@"for".empty_body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
}
}
},
.super => {
if (parent_block_nodes) |parent| {
for (parent) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null);
}
}
},
.block => {
for (node.tag.?.body.block.body) |child| {
const parent_content = parent_block_nodes orelse node.tag.?.body.block.body;
try self.renderNode(alloc, nodes, child, writer, null, parent_content);
}
},
.widthratio => {
var divisor: Value = Value{ .float = 1.0 };
var float_divisor: f64 = 1.0;
var value: Value = Value{ .float = 1.0 };
var float_value: f64 = 1.0;
var max_value: Value = Value{ .float = 1.0 };
var float_max_value: f64 = 1.0;
if (!std.mem.eql(u8, node.tag.?.body.widthratio.value, "")) {
value = Value{ .string = node.tag.?.body.widthratio.value };
if (self.context.get(node.tag.?.body.widthratio.value)) |v| {
value = v;
}
float_value = switch (value) {
.int => @as(f64, @floatFromInt(value.int)),
.float => value.float,
.string => std.fmt.parseFloat(f64, value.string) catch 1.0,
else => 1.0,
};
}
if (!std.mem.eql(u8, node.tag.?.body.widthratio.max_value, "")) {
max_value = Value{ .string = node.tag.?.body.widthratio.max_value };
if (self.context.get(node.tag.?.body.widthratio.max_value)) |v| {
max_value = v;
}
float_max_value = switch (max_value) {
.int => @as(f64, @floatFromInt(max_value.int)),
.float => max_value.float,
.string => std.fmt.parseFloat(f64, max_value.string) catch 1.0,
else => 1.0,
};
}
if (node.tag.?.body.widthratio.divisor) |div| {
divisor = Value{ .string = div };
if (self.context.get(div)) |d| {
divisor = d;
}
float_divisor = switch (divisor) {
.int => @as(f64, @floatFromInt(divisor.int)),
.float => divisor.float,
.string => std.fmt.parseFloat(f64, divisor.string) catch 0.0,
else => 1.0,
};
}
const ratio = (float_value / float_max_value) * float_divisor;
try writer.writeAll(std.fmt.allocPrint(alloc, "{d}", .{ratio}) catch "0");
},
.now => {
var format: []const u8 = node.tag.?.body.now.format;
if (format.len == 0) format = "Y-m-d H:i:s";
const datetime = try time.Time.now().toStringAlloc(alloc, format);
try writer.writeAll(datetime);
},
.csrf_token => {
const token = self.context.get("csrf_token");
if (token == null) return;
try writer.writeAll(std.fmt.allocPrint(alloc, "<input type=\"hidden\" name=\"csrfmiddlewaretoken\" value=\"{s}\">", .{token.?.string}) catch "");
},
.firstof => {
const values = node.tag.?.body.firstof.values;
for (values) |value| {
if (self.context.get(value)) |v| {
if (!isTruthy(v)) continue;
var buf = ArrayListUnmanaged(u8){};
defer buf.deinit(alloc);
try self.valueToString(alloc, &buf, v);
try writer.writeAll(buf.items);
return;
} else {
const check_value = self.resolveStringVariable(value).?;
if (!isTruthy(check_value)) continue;
var buf = ArrayListUnmanaged(u8){};
defer buf.deinit(alloc);
try self.valueToString(alloc, &buf, Value{ .string = value });
try writer.writeAll(buf.items);
return;
}
}
try writer.writeAll(node.tag.?.body.firstof.fallback);
},
.lorem => {
const count = node.tag.?.body.lorem.count;
const method = node.tag.?.body.lorem.method;
const random = node.tag.?.body.lorem.random;
if (count == null and method == null) {
if (random == false) {
try writer.writeAll(lorem.LOREM_COMMON_P);
return;
} else {
try writer.writeAll(try lorem.sentence(alloc));
return;
}
}
const ncount: u32 = std.fmt.parseInt(u32, count.?, 10) catch 1;
if (std.mem.eql(u8, method.?, "p")) {
const lorem_ = try lorem.paragraphs(alloc, ncount, random);
try writer.writeAll(lorem_);
return;
} else {
const lorem_ = try lorem.words(alloc, ncount, random);
try writer.writeAll(lorem_);
return;
}
},
.svg => {
const svg_kind = node.tag.?.body.svg.kind;
const svg_name = node.tag.?.body.svg.name;
if (self.cache.icons.?.getIcon(alloc, svg_kind, svg_name)) |svg_content| {
try writer.writeAll("<div class=\"svg-container\">");
try writer.writeAll(svg_content);
try writer.writeAll("</div>");
} else {
try writer.writeAll(icons.fallback_svg);
// Opcional: log ou comentário de debug
// try writer.print("<!-- SVG não encontrado: {s}/{s} -->", .{svg_kind, svg_name});
}
return;
},
else => {
std.debug.print("PANIC: unknown node type {d}\n", .{@intFromEnum(node.type)});
// @panic("unknown node type");
try writer.writeAll("<!-- tag não suportada: ");
// try writer.writeAll(try std.fmt.allocPrint(alloc, "{any}", .{node.tag.?.kind}));
try writer.writeAll(" -->");
},
if (condition) {
for (node.@"if".?.true_body) |child| {
std.debug.print("caí no true\n", .{});
try self.renderNode(alloc, nodes, child, writer, null, null);
}
} else {
for (node.@"if".?.false_body) |child| {
std.debug.print("caí no false\n", .{});
try self.renderNode(alloc, nodes, child, writer, null, null);
}
}
},
.include => {
const included_template = try self.readTemplateFile(node.include.?.template_name);
defer alloc.free(included_template);
var included_parser = parser.Parser.init(included_template);
const included_nodes = try included_parser.parse(alloc);
defer {
for (included_nodes) |n| n.deinit(alloc);
alloc.free(included_nodes);
}
// Renderiza o include no contexto atual (sem novo contexto)
for (included_nodes) |included_node| {
try self.renderNode(alloc, nodes, included_node, writer, context, null);
}
},
.for_block => {
const list_value = self.context.get(node.@"for".?.iterable) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list) |item| {
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set(node.@"for".?.loop_var, item);
for (node.@"for".?.body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
for (node.@"for".?.empty_body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
}
},
.super => {
if (parent_block_nodes) |parent| {
for (parent) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null);
}
}
},
.block => {
for (node.block.?.body) |child| {
const parent_content = parent_block_nodes orelse node.block.?.body;
try self.renderNode(alloc, nodes, child, writer, null, parent_content);
}
},
else => {},
}
}
fn resolveStringVariable(self: *const Renderer, value: []const u8) ?Value {
_ = self;
if (std.mem.eql(u8, value, "true")) return Value{ .bool = true };
if (std.mem.eql(u8, value, "false")) return Value{ .bool = false };
const is_int = std.fmt.parseInt(i64, value, 10) catch |err| switch (err) {
error.InvalidCharacter => null,
error.Overflow => null,
};
if (is_int != null) return Value{ .int = is_int.? };
const is_float = std.fmt.parseFloat(f64, value) catch |err| switch (err) {
error.InvalidCharacter => null,
};
if (is_float != null) return Value{ .float = is_float.? };
return Value{ .string = value };
}
fn findChildBlock(self: *const Renderer, nodes: []parser.Node, name: []const u8) ?parser.BlockNode {
_ = self;
for (nodes) |n| {
if (n.type != .tag) continue;
if (n.tag.?.kind != .block) continue;
if (std.mem.eql(u8, n.tag.?.body.block.name, name)) return n.tag.?.body.block;
if (n.type != .block) continue;
if (std.mem.eql(u8, n.block.?.name, name)) return n.block.?;
}
return null;
}
fn escapeHtml(self: *const Renderer, value: Value) !Value {
const s = switch (value) {
.string => |str| str,
else => return value,
};
var result = std.ArrayList(u8){};
for (s) |c| {
switch (c) {
'&' => try result.appendSlice(self.allocator, "&amp;"),
'<' => try result.appendSlice(self.allocator, "&lt;"),
'>' => try result.appendSlice(self.allocator, "&gt;"),
'"' => try result.appendSlice(self.allocator, "&quot;"),
'\'' => try result.appendSlice(self.allocator, "&#x27;"),
else => try result.append(self.allocator, c),
}
}
return Value{ .string = try result.toOwnedSlice(self.allocator) };
}
fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) RenderError!void {
fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) !void {
_ = self;
var w = buf.writer(alloc);
switch (value) {
.null => try w.writeAll(""),
.null => try w.writeAll("null"),
.bool => |b| try w.print("{}", .{b}),
.int => |n| try w.print("{d}", .{n}),
.float => |f| try w.print("{d}", .{f}),
@ -487,7 +293,7 @@ pub const Renderer = struct {
}
}
fn evaluateCondition_bkp(self: *const Renderer, expr: []const u8) RenderError!bool {
fn evaluateCondition(self: *const Renderer, expr: []const u8) !bool {
const value = self.context.get(expr) orelse Value.null;
return switch (value) {
.bool => |b| b,
@ -499,123 +305,4 @@ pub const Renderer = struct {
.null => false,
};
}
fn evaluateCondition(self: *const Renderer, allocator: Allocator, expr: []const u8, context: ?*Context) RenderError!bool {
const trimmed = std.mem.trim(u8, expr, " \t\r\n");
if (trimmed.len == 0) return false;
var parts = std.mem.splitScalar(u8, trimmed, ' ');
// Coleta tokens não vazios
var tokens = std.ArrayList([]const u8){};
defer tokens.deinit(allocator);
while (parts.next()) |part| {
const t = std.mem.trim(u8, part, " \t\r\n");
if (t.len > 0) try tokens.append(allocator, t);
}
// Caso simples: nome de variável
if (tokens.items.len == 1) {
const value = self.context.get(tokens.items[0]) orelse Value.null;
return isTruthy(value);
}
// Caso especial: "not variavel"
if (tokens.items.len == 2 and std.mem.eql(u8, tokens.items[0], "not")) {
const value = self.context.get(tokens.items[1]) orelse Value.null;
return !isTruthy(value);
}
// Caso com operadores de comparação: var op valor
if (tokens.items.len == 3) {
const left = tokens.items[0];
const op = tokens.items[1];
const right_str = tokens.items[2];
var left_value: Value = Value.null;
if (context) |ctx| {
left_value = ctx.get(left) orelse Value.null;
}
if (left_value == Value.null) left_value = self.context.get(left) orelse Value.null;
const right_value = parseLiteral(right_str);
if (std.mem.eql(u8, op, ">")) return compare(left_value, right_value, .gt);
if (std.mem.eql(u8, op, "<")) return compare(left_value, right_value, .lt);
if (std.mem.eql(u8, op, ">=")) return compare(left_value, right_value, .ge);
if (std.mem.eql(u8, op, "<=")) return compare(left_value, right_value, .le);
if (std.mem.eql(u8, op, "==")) return compare(left_value, right_value, .eq);
if (std.mem.eql(u8, op, "!=")) return compare(left_value, right_value, .ne);
return error.InvalidSyntax;
}
// Caso mais complexo (and/or/not composto) - por enquanto erro
return error.UnsupportedExpression;
}
// Função auxiliar: converte string literal pra Value
fn parseLiteral(str: []const u8) Value {
const trimmed = std.mem.trim(u8, str, " \t\r\n\"'");
if (std.mem.eql(u8, trimmed, "true")) return Value{ .bool = true };
if (std.mem.eql(u8, trimmed, "false")) return Value{ .bool = false };
if (std.mem.eql(u8, trimmed, "null")) return Value.null;
if (std.fmt.parseInt(i64, trimmed, 10)) |n| return Value{ .int = n } else |_| {}
if (std.fmt.parseFloat(f64, trimmed)) |f| return Value{ .float = f } else |_| {}
return Value{ .string = trimmed };
}
// Função auxiliar: truthy check
fn isTruthy(v: Value) bool {
return switch (v) {
.null => false,
.bool => |b| b,
.int => |i| i != 0,
.float => |f| f != 0.0,
.string => |s| s.len > 0,
.list => |l| l.len > 0,
.dict => |d| d.count() > 0,
};
}
// Função auxiliar: comparação
fn compare(left: Value, right: Value, op: enum { gt, lt, ge, le, eq, ne, not }) bool {
// Implementação básica (expanda conforme necessário)
switch (left) {
.int => |l| switch (right) {
.int => |r| return switch (op) {
.gt => l > r,
.lt => l < r,
.ge => l >= r,
.le => l <= r,
.eq => l == r,
.ne, .not => l != r,
},
else => return false,
},
.float => |l| switch (right) {
.float => |r| return switch (op) {
.gt => l > r,
.lt => l < r,
.ge => l >= r,
.le => l <= r,
.eq => l == r,
.ne, .not => l != r,
},
else => return false,
},
.string => |l| switch (right) {
.string => |r| return switch (op) {
.eq => std.mem.eql(u8, l, r),
.ne, .not => !std.mem.eql(u8, l, r),
else => false,
},
else => return false,
},
else => return false,
}
}
};

275
src/renderer_bkp.zig Normal file
View file

@ -0,0 +1,275 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const Context = @import("context.zig").Context;
const Value = @import("context.zig").Value;
const builtin_filters = @import("filters.zig").builtin_filters;
const FilterError = @import("filters.zig").FilterError;
const parser = @import("parser.zig");
pub const RenderError = FilterError || error{
OutOfMemory,
InvalidSyntax,
UnknownVariable,
UnknownFilter,
InvalidTemplate,
BlockNotFound,
CircularExtends,
FileNotFound,
AccessDenied,
FileTooBig,
NoSpaceLeft,
Unexpected,
UnclosedTag,
InvalidAutoescapeArgument,
UnclosedVariable,
UnclosedBlock,
UnclosedComment,
InvalidAssignmentSyntax,
UnclosedQuoteInAssignment,
InvalidForSyntax,
UnclosedVerbatim,
InvalidUrlSyntax,
UnclosedQuoteInUrl,
InvalidDebugArgs,
InvalidRegroupSyntax,
InvalidWidthRatioSyntax,
InvalidTemplateTag,
InvalidCsrfTokenArgs,
};
pub const Renderer = struct {
context: *Context,
allocator: Allocator,
pub fn init(context: *Context) Renderer {
return .{
.context = context,
.allocator = context.allocator(),
};
}
fn checkForExtends(self: *const Renderer, nodes: []parser.Node) bool {
_ = self;
for (nodes) |n| {
if (n.type == .extends) return true;
}
return false;
}
fn readTemplateFile(self: *const Renderer, path: []const u8) RenderError![]const u8 {
const max_size = 10 * 1024 * 1024;
const base_template = std.fs.cwd().readFileAlloc(self.allocator, path, max_size) catch |err| switch (err) {
error.FileNotFound => return RenderError.FileNotFound,
error.AccessDenied => return RenderError.AccessDenied,
error.FileTooBig => return RenderError.FileTooBig,
error.NoSpaceLeft => return RenderError.NoSpaceLeft,
error.OutOfMemory => return RenderError.OutOfMemory,
else => return RenderError.Unexpected,
};
return base_template;
}
pub fn render(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void {
const base_template = try self.readTemplateFile(template);
return self.renderString(base_template, writer);
}
pub fn renderString_bkp(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var p = parser.Parser.init(template);
const nodes = try p.parse(alloc);
defer {
for (nodes) |node| node.deinit(alloc);
alloc.free(nodes);
}
for (nodes) |node| {
try self.renderNode(alloc, nodes, node, writer, null);
}
}
pub fn renderString(self: *const Renderer, template: []const u8, writer: anytype) RenderError!void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var p = parser.Parser.init(template);
const nodes = try p.parse(alloc);
defer {
for (nodes) |node| node.deinit(alloc);
alloc.free(nodes);
}
const has_extends = self.checkForExtends(nodes);
if (has_extends) {
for (nodes) |node| {
if (node.type == .extends) {
const base_template = try self.readTemplateFile(node.extends.?.parent_name);
defer self.allocator.free(base_template);
var base_parser = parser.Parser.init(base_template);
const base_nodes = try base_parser.parse(alloc);
defer {
for (base_nodes) |n| n.deinit(alloc);
alloc.free(base_nodes);
}
try self.renderWithInheritance(alloc, base_nodes, nodes, writer);
return;
}
}
} else {
for (nodes) |node| {
try self.renderNode(alloc, nodes, node, writer, null);
}
}
}
fn renderWithInheritance(self: *const Renderer, alloc: Allocator, base_nodes: []parser.Node, child_nodes: []parser.Node, writer: anytype) RenderError!void {
for (base_nodes) |base_node| {
if (base_node.type == .block) {
const block_name = base_node.block.?.name;
// Procura no filho
const child_block = self.findChildBlock(child_nodes, block_name);
if (child_block) |child| {
// Renderiza o filho
for (child.body) |child_node| {
try self.renderNode(alloc, child_nodes, child_node, writer, null);
}
} else {
// Renderiza o do pai
for (base_node.block.?.body) |child| {
try self.renderNode(alloc, child_nodes, child, writer, null);
}
}
} else {
// Qualquer outra coisa no base
try self.renderNode(alloc, child_nodes, base_node, writer, null);
}
}
}
fn renderNode(self: *const Renderer, alloc: Allocator, nodes: []parser.Node, node: parser.Node, writer: anytype, context: ?*Context) RenderError!void {
// std.debug.print("Vou renderizar o node: {any}\n", .{node});
switch (node.type) {
.text => try writer.writeAll(node.text.?.content),
.variable => {
const var_name = node.variable.?.expr;
var value: Value = Value.null;
if (context != null) {
value = context.?.get(var_name) orelse Value.null;
} else {
value = self.context.get(var_name) orelse Value.null;
}
var is_safe = false;
for (node.variable.?.filters) |f| {
const filter_fn = builtin_filters.get(f.name) orelse return error.UnknownFilter;
const arg = if (f.arg) |a| Value{ .string = a } else null;
value = try filter_fn(alloc, value, arg);
if (std.mem.eql(u8, f.name, "safe")) is_safe = true;
}
if (!is_safe) {
if (builtin_filters.get("escape")) |escape_fn| {
value = try escape_fn(alloc, value, null);
}
}
var buf = ArrayListUnmanaged(u8){};
defer buf.deinit(alloc);
try self.valueToString(alloc, &buf, value);
try writer.writeAll(buf.items);
},
.if_block => {
const condition = self.evaluateCondition(node.@"if".?.condition) catch return false;
if (condition) {
for (node.@"if".?.true_body) |child| {
try self.renderNode(alloc, nodes, child, writer, null);
}
} else {
for (node.@"if".?.false_body) |child| {
try self.renderNode(alloc, nodes, child, writer, null);
}
}
},
.for_block => {
const list_value = self.context.get(node.@"for".?.iterable) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list) |item| {
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set(node.@"for".?.loop_var, item);
for (node.@"for".?.body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx);
}
for (node.@"for".?.empty_body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx);
}
}
},
.block => {
for (node.block.?.body) |child| {
try self.renderNode(alloc, nodes, child, writer, null);
}
},
else => {},
}
}
fn findChildBlock(self: *const Renderer, nodes: []parser.Node, name: []const u8) ?parser.BlockNode {
_ = self;
for (nodes) |n| {
if (n.type != .block) continue;
if (std.mem.eql(u8, n.block.?.name, name)) return n.block.?;
}
return null;
}
fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) !void {
_ = self;
var w = buf.writer(alloc);
switch (value) {
.null => try w.writeAll("null"),
.bool => |b| try w.print("{}", .{b}),
.int => |n| try w.print("{d}", .{n}),
.float => |f| try w.print("{d}", .{f}),
.string => |s| try w.writeAll(s),
.list => try w.writeAll("[list]"),
.dict => try w.writeAll("{dict}"),
}
}
fn evaluateCondition(self: *const Renderer, expr: []const u8) !bool {
const value = self.context.get(expr) orelse Value.null;
return switch (value) {
.bool => |b| b,
.int => |i| i != 0,
.float => |f| f != 0.0,
.string => |s| s.len > 0,
.list => |l| l.len > 0,
.dict => |d| d.count() > 0,
.null => false,
};
}
};

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,23 @@
pub const cache = @import("cache.zig");
pub const context = @import("context.zig");
pub const delta = @import("delta.zig");
pub const filters = @import("filters.zig");
pub const icons = @import("svg/icons.zig");
pub const lorem = @import("lorem.zig");
pub const meta = @import("meta.zig");
pub const parser = @import("parser.zig");
pub const renderer = @import("renderer.zig");
pub const time = @import("time.zig");
pub const util = @import("util.zig");
//! By convention, root.zig is the root source file when making a library.
const std = @import("std");
pub fn bufferedPrint() !void {
// Stdout is for the actual output of your application, for example if you
// are implementing gzip, then only the compressed bytes should be sent to
// stdout, not any debugging messages.
var stdout_buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
const stdout = &stdout_writer.interface;
try stdout.print("Run `zig build test` to run the tests.\n", .{});
try stdout.flush(); // Don't forget to flush!
}
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
test "basic add functionality" {
try std.testing.expect(add(3, 7) == 10);
}

View file

@ -1,117 +0,0 @@
const std = @import("std");
pub const IconSet = enum {
bootstrap,
dripicons,
hero_outline,
hero_solid,
material,
};
pub const embedded_data = std.EnumMap(IconSet, []const u8).init(.{
.bootstrap = @embedFile("bootstrap.svgs.bin"),
.dripicons = @embedFile("dripicons.svgs.bin"),
.hero_outline = @embedFile("hero_outline.svgs.bin"),
.hero_solid = @embedFile("hero_solid.svgs.bin"),
.material = @embedFile("material.svgs.bin"),
});
pub const SvgIcon = struct {
icon_map: std.StringHashMapUnmanaged([]const u8) = .{},
pub fn init(allocator: std.mem.Allocator) !SvgIcon {
var self = SvgIcon{};
inline for (std.meta.fields(IconSet)) |field| {
const set = @field(IconSet, field.name);
const data = embedded_data.get(set);
try self.loadSet(allocator, set, data.?);
}
return self;
}
pub fn deinit(self: *SvgIcon, allocator: std.mem.Allocator) void {
var it = self.icon_map.iterator();
while (it.next()) |entry| {
allocator.free(entry.key_ptr.*);
allocator.free(entry.value_ptr.*);
}
self.icon_map.deinit(allocator);
self.* = .{};
}
pub fn get(self: *const SvgIcon, key: []const u8) ?[]const u8 {
return self.icon_map.get(key);
}
pub fn getIcon(self: *const SvgIcon,allocator: std.mem.Allocator, kind: []const u8, name: []const u8) ?[]const u8 {
const key = std.fmt.allocPrint(allocator, "{s}:{s}", .{ kind, name }) catch return null;
defer allocator.free(key);
return self.icon_map.get(key);
}
pub fn count(self: *const SvgIcon) usize {
return self.icon_map.count();
}
fn loadSet(
self: *SvgIcon,
allocator: std.mem.Allocator,
set: IconSet,
data: []const u8,
) !void {
if (data.len < 12) return error.InvalidEmbeddedData;
var pos: usize = 0;
const magic = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (magic != 0x53564749) return error.InvalidMagic;
const version = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (version != 1) return error.UnsupportedVersion;
const num_entries = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
const prefix = @tagName(set);
var i: u32 = 0;
while (i < num_entries) : (i += 1) {
const name_len = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (pos + name_len > data.len) return error.CorruptedNameLength;
const name_slice = data[pos .. pos + name_len];
pos += name_len;
const svg_len = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (pos + svg_len > data.len) return error.CorruptedSvgLength;
const svg_slice = data[pos .. pos + svg_len];
pos += svg_len;
// Monta a chave com prefixo do set
const key = try std.fmt.allocPrint(allocator, "{s}:{s}", .{ prefix, name_slice });
// Duplica o conteúdo SVG (o map assume ownership)
const value = try allocator.dupe(u8, svg_slice);
// Insere no mapa unmanaged
try self.icon_map.put(allocator, key, value);
}
}
};
pub const fallback_svg =
\\<div class="svg-container">
\\<svg width="24" height="24" fill="none">
\\ <rect width="24" height="24" rx="4" fill="#f0f0f0"/>
\\ <text x="12" y="16" font-size="10" text-anchor="middle" fill="#999">?</text>
\\</svg>
\\</div>
;

View file

@ -1,878 +0,0 @@
// https://github.com/cztomsik/tokamak
const std = @import("std");
const util = @import("util.zig");
const testing = std.testing;
const meta = @import("meta.zig");
const dlt = @import("delta.zig");
const RATA_MIN = date_to_rata(Date.MIN);
const RATA_MAX = date_to_rata(Date.MAX);
const RATA_TO_UNIX = 719468;
const EOD = 86_400 - 1;
pub const DAY_NAMES_SHORT = [_][]const u8{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
pub const DAY_NAMES_LONG = [_][]const u8{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
pub const MONTH_NAMES_SHORT = [_][]const u8{ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
pub const MONTH_NAMES_LONG = [_][]const u8{ "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
pub const MONTH_DAYS = [12]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
pub const TIME_CHUNKS = [4]u32{
60 * 60 * 24 * 7, //week
60 * 60 * 24, //day
60 * 60, //hour
60, //minute
};
pub const TIME_STRINGS = [6][]const u8{ "year", "month", "week", "day", "hour", "minute" };
// TODO: Decide if we want to use std.debug.assert(), @panic() or just throw an error
fn checkRange(num: anytype, min: @TypeOf(num), max: @TypeOf(num)) void {
if (util.lt(num, min) or util.gt(num, max)) {
// TODO: fix later (we can't use {f} and {any} is also wrong)
// std.log.warn("Value {} is not in range [{}, {}]", .{ num, min, max });
std.log.warn("Value not in range", .{});
}
}
pub const TimeUnit = enum { second, minute, hour, day, month, year };
pub const DateUnit = enum { day, month, year };
// pub const SECS_PER_DAY: i64 = 86_400;
// pub const SECS_PER_HOUR: i64 = 3_600;
// pub const SECS_PER_MIN: i64 = 60;
pub const TimeError = error{
Eof,
ExpectedNull,
ExpectedValue,
InvalidCharacter,
InvalidFormat,
Overflow,
OutOfMemory,
};
// https://www.youtube.com/watch?v=0s9F4QWAl-E&t=2120
pub fn isLeapYear(year: i32) bool {
const d: i32 = if (@mod(year, 100) != 0) 4 else 16;
return (year & (d - 1)) == 0;
}
// https://www.youtube.com/watch?v=0s9F4QWAl-E&t=2257
pub fn daysInMonth(year: i32, month: u8) u8 {
if (month == 2) {
return if (isLeapYear(year)) 29 else 28;
}
return 30 | (month ^ (month >> 3));
}
pub fn formatDateTime(alloc: std.mem.Allocator, t: Time, format_str: []const u8) ![]u8 {
var result = std.ArrayList(u8){};
defer result.deinit(alloc);
var writer = result.writer(alloc);
var i: usize = 0;
while (i < format_str.len) : (i += 1) {
const c = format_str[i];
if (c == '\\') {
i += 1;
if (i >= format_str.len) break;
try writer.writeByte(format_str[i]);
continue;
}
// Todos os códigos do date + time que implementamos
switch (c) {
// === Códigos de data (do filtro date) ===
'Y' => try writer.print("{d:0>4}", .{@as(u16, @intCast(t.year()))}),
'm' => try writer.print("{d:0>2}", .{t.month()}),
'n' => try writer.print("{d}", .{t.month()}),
'd' => try writer.print("{d:0>2}", .{t.day()}),
'j' => try writer.print("{d}", .{t.day()}),
'F' => try writer.writeAll(t.monthNameLong()),
'M' => try writer.writeAll(t.monthNameShort()),
'l' => try writer.writeAll(t.weekdayNameLong()),
'D' => try writer.writeAll(t.weekdayNameShort()),
// === Códigos de tempo (do filtro time) ===
'H' => try writer.print("{d:0>2}", .{t.hour()}),
'G' => try writer.print("{d}", .{t.hour()}),
'i' => try writer.print("{d:0>2}", .{t.minute()}),
's' => try writer.print("{d:0>2}", .{t.second()}),
'a' => try writer.writeAll(if (t.hour() < 12) "a.m." else "p.m."),
'A' => try writer.writeAll(if (t.hour() < 12) "AM" else "PM"),
'P' => {
const hr24 = t.hour();
const min = t.minute();
if (hr24 == 0 and min == 0) {
try writer.writeAll("midnight");
} else if (hr24 == 12 and min == 0) {
try writer.writeAll("noon");
} else {
var hr12 = @mod(hr24, 12);
if (hr12 == 0) hr12 = 12;
try writer.print("{d}", .{hr12});
if (min > 0) try writer.print(":{d:0>2}", .{min});
try writer.writeAll(if (hr24 < 12) " a.m." else " p.m.");
}
},
'u' => try writer.writeAll("000000"),
else => try writer.writeByte(c),
}
}
return try result.toOwnedSlice(alloc);
}
pub const Date = struct {
year: i32,
month: u8,
day: u8,
pub const MIN = Date.ymd(-1467999, 1, 1);
pub const MAX = Date.ymd(1471744, 12, 31);
pub fn cmp(a: Date, b: Date) std.math.Order {
if (a.year != b.year) return util.cmp(a.year, b.year);
if (a.month != b.month) return util.cmp(a.month, b.month);
return util.cmp(a.day, b.day);
}
pub fn parse(str: []const u8) TimeError!Date {
var it = std.mem.splitScalar(u8, str, '-');
return ymd(
try std.fmt.parseInt(i32, it.next() orelse return error.Eof, 10),
try std.fmt.parseInt(u8, it.next() orelse return error.Eof, 10),
try std.fmt.parseInt(u8, it.next() orelse return error.Eof, 10),
);
}
pub fn ymd(year: i32, month: u8, day: u8) Date {
return .{
.year = year,
.month = month,
.day = day,
};
}
pub fn today() Date {
return Time.now().date();
}
pub fn yesterday() Date {
return today().add(.day, -1);
}
pub fn tomorrow() Date {
return today().add(.day, 1);
}
pub fn startOf(unit: DateUnit) Date {
return today().setStartOf(unit);
}
pub fn endOf(unit: DateUnit) Date {
return today().setEndOf(unit);
}
pub fn setStartOf(self: Date, unit: DateUnit) Date {
return switch (unit) {
.day => self,
.month => ymd(self.year, self.month, 1),
.year => ymd(self.year, 1, 1),
};
}
pub fn setEndOf(self: Date, unit: DateUnit) Date {
return switch (unit) {
.day => self,
.month => ymd(self.year, self.month, daysInMonth(self.year, self.month)),
.year => ymd(self.year, 12, 31),
};
}
pub fn add(self: Date, part: DateUnit, amount: i64) Date {
return switch (part) {
.day => Time.unix(0).setDate(self).add(.days, amount).date(),
.month => {
const total_months = @as(i32, self.month) + @as(i32, @intCast(amount));
const new_year = self.year + @divFloor(total_months - 1, 12);
const new_month = @as(u8, @intCast(@mod(total_months - 1, 12) + 1));
return ymd(
new_year,
new_month,
@min(self.day, daysInMonth(new_year, new_month)),
);
},
.year => {
const new_year = self.year + @as(i32, @intCast(amount));
return ymd(
new_year,
self.month,
@min(self.day, daysInMonth(new_year, self.month)),
);
},
};
}
pub fn dayOfWeek(self: Date) u8 {
const rata_day = date_to_rata(self);
return @intCast(@mod(rata_day + 3, 7));
}
pub fn format(self: Date, writer: anytype) !void {
try writer.print("{d}-{d:0>2}-{d:0>2}", .{
@as(u32, @intCast(self.year)),
self.month,
self.day,
});
}
pub fn ordinal(self: Date) usize {
const days_before_month = [_]u16{ 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
var days: usize = days_before_month[self.month];
days += self.day;
if (self.month > 2 and isLeapYear(self.year)) {
days += 1;
}
return days;
}
pub fn weekday(self: Date) u8 {
const y: i32 = self.year;
const m: u8 = self.month;
const d: u8 = self.day;
const t = [_]i32{ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
var year_adj: i32 = y;
if (m < 3) year_adj -= 1;
var wd: i32 = year_adj + @divFloor(year_adj, 4) - @divFloor(year_adj, 100) + @divFloor(year_adj, 400) + t[m - 1] + @as(i32, d);
wd = @mod(wd, 7);
if (wd < 0) wd += 7;
return @intCast(if (wd == 6) 7 else wd + 1);
}
pub fn isoWeek(self: Date) u8 {
const iso_y = self.isoWeekYear();
const jan4 = Date{ .year = iso_y, .month = 1, .day = 4 };
const jan4_ord: i32 = @intCast(jan4.ordinal());
const self_ord: i32 = @intCast(self.ordinal());
const days_diff = self_ord - jan4_ord;
const week = @divFloor(days_diff + 4, 7) + 1; // +4 corrige o offset (testado em Python)
return @intCast(@max(1, @min(53, week)));
}
pub fn isoWeekYear(self: Date) i32 {
const wd = self.weekday(); // 1=Seg ... 7=Dom
const ord = @as(i32, @intCast(self.ordinal()));
const thursday_ord = ord + (4 - (wd - 1)); // quinta da semana
var y = self.year;
var days_in_year: i32 = 365;
if (isLeapYear(y)) days_in_year = 366;
if (thursday_ord <= 0) {
y -= 1;
} else if (thursday_ord > days_in_year) {
y += 1;
}
return y;
}
};
pub const TimeTime = struct {
hour: u32,
minute: u32,
second: u32,
fn cmp(a: TimeTime, b: TimeTime) std.math.Order {
if (a.hour < b.hour) return .lt;
if (a.hour > b.hour) return .gt;
if (a.minute < b.minute) return .lt;
if (a.minute > b.minute) return .gt;
if (a.second < b.second) return .lt;
if (a.second > b.second) return .gt;
return .eq;
}
};
pub const Time = struct {
epoch: i64,
pub fn parse(str: []const u8) !Time {
if (std.mem.indexOfScalar(u8, str, ' ')) |space| {
// Datetime: "YYYY-MM-DD HH:MM:SS"
const date_str = str[0..space];
const time_str = str[space + 1 ..];
const d = try Date.parse(date_str);
var it = std.mem.splitScalar(u8, time_str, ':');
const h = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10);
const m = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10);
const s = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10);
var t = Time.unix(0).setDate(d);
t = t.setHour(h).setMinute(m).setSecond(s);
return t;
} else {
const d = try Date.parse(str);
return Time.unix(0).setDate(d);
}
return Time.now();
}
pub fn unix(epoch: i64) Time {
return .{ .epoch = epoch };
}
pub fn new(y: i32, m: u8, d: u8, h: ?u32, min: ?u32, sec: ?u32) Time {
var t = unix(0).setDate(.ymd(y, m, d));
if (h) |h_| t = t.setHour(h_);
if (min) |min_| t = t.setMinute(min_);
if (sec) |sec_| t = t.setSecond(sec_);
return t;
}
pub fn now() Time {
return unix(std.time.timestamp());
}
pub fn now_offset(offset: i64) Time {
return unix(std.time.timestamp() + (offset * std.time.s_per_hour));
}
pub fn today() Time {
return unix(0).setDate(.today());
}
pub fn tomorrow() Time {
return unix(0).setDate(.tomorrow());
}
pub fn startOf(unit: TimeUnit) Time {
return Time.now().setStartOf(unit);
}
pub fn endOf(unit: TimeUnit) Time {
return Time.now().setEndOf(unit);
}
pub fn second(self: Time) u32 {
return @intCast(@mod(self.total(.seconds), 60));
}
pub fn setSecond(self: Time, sec: u32) Time {
return self.add(.seconds, @as(i64, sec) - self.second());
}
pub fn minute(self: Time) u32 {
return @intCast(@mod(self.total(.minutes), 60));
}
pub fn setMinute(self: Time, min: u32) Time {
return self.add(.minutes, @as(i64, min) - self.minute());
}
pub fn hour(self: Time) u32 {
return @intCast(@mod(self.total(.hours), 24));
}
pub fn setHour(self: Time, hr: u32) Time {
return self.add(.hours, @as(i64, hr) - self.hour());
}
pub fn date(self: Time) Date {
return rata_to_date(@divTrunc(self.epoch, std.time.s_per_day) + RATA_TO_UNIX);
}
pub fn setDate(self: Time, dat: Date) Time {
var res: i64 = @mod(self.epoch, std.time.s_per_day);
res += (date_to_rata(dat) - RATA_TO_UNIX) * std.time.s_per_day;
return unix(res);
}
pub fn time(self: Time) TimeTime {
return .{
.hour = self.hour(),
.minute = self.minute(),
.second = self.second(),
};
}
pub fn setStartOf(self: Time, unit: TimeUnit) Time {
// TODO: continue :label?
return switch (unit) {
.second => self,
.minute => self.setSecond(0),
.hour => self.setSecond(0).setMinute(0),
.day => self.setSecond(0).setMinute(0).setHour(0),
.month => {
const d = self.date();
return unix(0).setDate(.ymd(d.year, d.month, 1));
},
.year => {
const d = self.date();
return unix(0).setDate(.ymd(d.year, 1, 1));
},
};
}
// TODO: rename to startOfNext?
pub fn next(self: Time, unit: enum { second, minute, hour, day }) Time {
return switch (unit) {
.second => self.add(.seconds, 1),
.minute => self.setSecond(0).add(.minutes, 1),
.hour => self.setSecond(0).setMinute(0).add(.hours, 1),
.day => self.setSecond(0).setMinute(0).setHour(0).add(.hours, 24),
};
}
pub fn setEndOf(self: Time, unit: TimeUnit) Time {
// TODO: continue :label?
return switch (unit) {
.second => self,
.minute => self.setSecond(59),
.hour => self.setSecond(59).setMinute(59),
.day => self.setSecond(59).setMinute(59).setHour(23),
.month => {
const d = self.date();
return unix(EOD).setDate(.ymd(d.year, d.month, daysInMonth(d.year, d.month)));
},
.year => {
const d = self.date();
return unix(EOD).setDate(.ymd(d.year, 12, 31));
},
};
}
pub fn add(self: Time, part: enum { seconds, minutes, hours, days, months, years }, amount: i64) Time {
const n = switch (part) {
.seconds => amount,
.minutes => amount * std.time.s_per_min,
.hours => amount * std.time.s_per_hour,
.days => amount * std.time.s_per_day,
.months => return self.setDate(self.date().add(.month, amount)),
.years => return self.setDate(self.date().add(.year, amount)),
};
return .{ .epoch = self.epoch + n };
}
fn total(self: Time, part: enum { seconds, minutes, hours }) i64 {
return switch (part) {
.seconds => self.epoch,
.minutes => @divTrunc(self.epoch, std.time.s_per_min),
.hours => @divTrunc(self.epoch, std.time.s_per_hour),
};
}
fn year(self: Time) i32 {
return self.date().year;
}
fn month(self: Time) u8 {
return self.date().month;
}
fn day(self: Time) u8 {
return self.date().day;
}
fn monthNameLong(self: Time) []const u8 {
return MONTH_NAMES_LONG[self.date().month];
}
fn monthNameShort(self: Time) []const u8 {
return MONTH_NAMES_SHORT[self.date().month];
}
fn weekdayNameLong(self: Time) []const u8 {
return DAY_NAMES_LONG[self.date().weekday()];
}
fn weekdayNameShort(self: Time) []const u8 {
return DAY_NAMES_SHORT[self.date().weekday()];
}
pub fn format(self: Time, writer: anytype) !void {
try writer.print("{f} {d:0>2}:{d:0>2}:{d:0>2} UTC", .{
self.date(),
self.hour(),
self.minute(),
self.second(),
});
}
pub fn toStringAlloc(self: Time, alloc: std.mem.Allocator, format_str: ?[]const u8) TimeError![]u8 {
const fmt = format_str orelse "Y-m-d H:i:s";
return try formatDateTime(alloc, self, fmt);
}
pub fn toString(self: Time, format_str: ?[]const u8) TimeError![]const u8 {
return try self.toStringAlloc(std.heap.page_allocator, format_str);
}
pub fn addRelative(self: Time, delta: dlt.RelativeDelta) Time {
var d = delta;
d.normalize(); // garante que meses/horas/etc estejam normalizados
// 1. Parte calendáríca (anos + meses + dias)
var dt = self.date();
// anos primeiro (mais estável)
if (d.years != 0) {
dt = dt.add(.year, d.years);
}
// depois meses (respeita dias-in-mês)
if (d.months != 0) {
dt = dt.add(.month, d.months);
}
// por fim dias normais
if (d.days != 0) {
dt = dt.add(.day, d.days);
}
// 2. Parte do relógio (horas, minutos, segundos)
var result = self.setDate(dt);
// Podemos usar o .add() existente para segundos/minutos/horas
if (d.seconds != 0) {
result = result.add(.seconds, d.seconds);
}
if (d.minutes != 0) {
result = result.add(.minutes, d.minutes);
}
if (d.hours != 0) {
result = result.add(.hours, d.hours);
}
return result;
}
pub fn subRelative(self: Time, other: Time) dlt.RelativeDelta {
var delta = dlt.RelativeDelta{};
// Parte de tempo (horas, min, seg) igual antes
var seconds_diff: i64 = self.epoch - other.epoch;
delta.seconds = @as(i32, @intCast(@rem(seconds_diff, 60)));
seconds_diff = @divTrunc(seconds_diff, 60);
delta.minutes = @as(i32, @intCast(@rem(seconds_diff, 60)));
seconds_diff = @divTrunc(seconds_diff, 60);
delta.hours = @as(i32, @intCast(@rem(seconds_diff, 24)));
seconds_diff = @divTrunc(seconds_diff, 24);
// Parte calendárica
var later = self.date();
var earlier = other.date();
const swapped = later.cmp(earlier) == .lt;
if (swapped) {
// const temp = later;
// later = earlier;
// earlier = temp;
std.mem.swap(i32, &later.year, &earlier.year);
}
var years: i32 = later.year - earlier.year;
var months: i32 = @as(i32, later.month) - @as(i32, earlier.month);
if (months < 0) {
years -= 1;
months += 12;
}
var days: i32 = @as(i32, later.day) - @as(i32, earlier.day);
// Ajuste rigoroso para borrow (com handling de bissexto)
if (days < 0) {
const days_in_target = daysInMonth(later.year, later.month);
if (later.day == days_in_target) {
// Caso especial (ex: 28/fev não-bissexto vs 29/fev bissexto): ignora borrow, trata como período completo
days = 0;
} else {
// Borrow normal, mas ajusta se earlier.day > dias no mês anterior (para casos como 29 > 28)
const prev_month = later.add(.month, -1);
var days_borrow: i32 = @as(i32, daysInMonth(prev_month.year, prev_month.month));
if (earlier.day > @as(u8, @intCast(days_borrow))) {
days_borrow = @as(i32, earlier.day);
}
std.debug.print("days_borrow em subRelative {d}\n", .{days_borrow});
days += days_borrow;
months -= 1;
if (months < 0) {
years -= 1;
months += 12;
}
}
}
// Atribui com sinal
delta.years = if (swapped) -years else years;
delta.months = if (swapped) -months else months;
delta.days = if (swapped) -days else days;
// Normaliza ( lida com excessos, mas aqui é para meses/anos)
delta.normalize();
return delta;
}
pub fn timeSince(self: Time, alloc: std.mem.Allocator, then: Time) TimeError![]u8 {
if (self.epoch >= then.epoch) {
return try alloc.dupe(u8, "0 minutes");
}
var total_months: i64 = 0;
const delta_year: i64 = (then.year() - self.year()) * 12;
const delta_month: i32 = @as(i32, @intCast(then.month())) - @as(i32, @intCast(self.month()));
total_months = delta_year + delta_month;
if (self.day() > then.day() or (self.day() == then.day() and self.time().cmp(then.time()) == .gt)) {
total_months -= 1;
}
const months = @rem(total_months, 12);
const years = @divTrunc(total_months, 12);
var pivot_year: i64 = 0;
var pivot_month: i64 = 0;
var pivot: Time = undefined;
if (years > 0 or months > 0) {
pivot_year = @as(i64, self.year()) + years;
pivot_month = @as(i64, self.month()) + months;
if (pivot_month > 12) {
pivot_year += 1;
pivot_month -= 12;
}
const d: u8 = @min(MONTH_DAYS[@intCast(pivot_month - 1)], self.day());
pivot = Time.new(
@as(i32, @intCast(pivot_year)),
@as(u8, @intCast(pivot_month)),
d,
self.hour(),
self.minute(),
self.second(),
);
} else {
pivot = self;
}
var remaining_time = then.epoch - pivot.epoch;
var partials = std.ArrayList(i64){};
errdefer partials.deinit(alloc);
try partials.append(alloc, years);
try partials.append(alloc, months);
for (TIME_CHUNKS) |chunk| {
const count: i32 = @intCast(@divFloor(remaining_time, chunk));
try partials.append(alloc, count);
remaining_time -= count * @as(i32, @intCast(chunk));
}
const min: i64 = std.mem.min(i64, partials.items);
const max: i64 = std.mem.max(i64, partials.items);
if (min == 0 and max == 0) {
return try alloc.dupe(u8, "0 minutes");
}
var buf = std.ArrayList(u8){};
errdefer buf.deinit(alloc);
var count: i32 = 0;
for (partials.items, 0..) |partial, i| {
if (partial > 0) {
if (count >= 2) break;
try buf.appendSlice(alloc, try std.fmt.allocPrint(alloc, "{d} {s}{s}", .{ partial, TIME_STRINGS[i], if (partial > 1) "s" else "" }));
if (count == 0 and i < partials.items.len - 1) try buf.appendSlice(alloc, ", ");
count += 1;
}
}
return try buf.toOwnedSlice(alloc);
}
};
// https://github.com/cassioneri/eaf/blob/1509faf37a0e0f59f5d4f11d0456fd0973c08f85/eaf/gregorian.hpp#L42
fn rata_to_date(N: i64) Date {
checkRange(N, RATA_MIN, RATA_MAX);
// Century.
const N_1: i64 = 4 * N + 3;
const C: i64 = quotient(N_1, 146097);
const N_C: u32 = remainder(N_1, 146097) / 4;
// Year.
const N_2 = 4 * N_C + 3;
const Z: u32 = N_2 / 1461;
const N_Y: u32 = N_2 % 1461 / 4;
const Y: i64 = 100 * C + Z;
// Month and day.
const N_3: u32 = 5 * N_Y + 461;
const M: u32 = N_3 / 153;
const D: u32 = N_3 % 153 / 5;
// Map.
const J: u32 = @intFromBool(M >= 13);
return .{
.year = @intCast(Y + J),
.month = @intCast(M - 12 * J),
.day = @intCast(D + 1),
};
}
// https://github.com/cassioneri/eaf/blob/1509faf37a0e0f59f5d4f11d0456fd0973c08f85/eaf/gregorian.hpp#L88
fn date_to_rata(date: Date) i32 {
checkRange(date, Date.MIN, Date.MAX);
// Map.
const J: u32 = @intFromBool(date.month <= 2);
const Y: i32 = date.year - @as(i32, @intCast(J));
const M: u32 = date.month + 12 * J;
const D: u32 = date.day - 1;
const C: i32 = @intCast(quotient(Y, 100));
// Rata die.
const y_star: i32 = @intCast(quotient(1461 * @as(i64, Y), 4) - C + quotient(C, 4)); // n_days in all prev. years
const m_star: u32 = (153 * M - 457) / 5; // n_days in prev. months
return y_star + @as(i32, @intCast(m_star)) + @as(i32, @intCast(D));
}
fn quotient(n: i64, d: u32) i64 {
return if (n >= 0) @divTrunc(n, d) else @divTrunc((n + 1), d) - 1;
}
fn remainder(n: i64, d: u32) u32 {
return @intCast(if (n >= 0) @mod(n, d) else (n + d) - d * quotient((n + d), d));
}
// const testing = @import("testing.zig");
/// Attempts to print `arg` into a buf and then compare those strings.
pub const allocator = std.testing.allocator;
pub fn expectFmt(arg: anytype, expected: []const u8) !void {
var wb = std.io.Writer.Allocating.init(allocator);
defer wb.deinit();
try wb.writer.print("{f}", .{arg});
try std.testing.expectEqualStrings(expected, wb.written());
}
pub fn expectEqual(res: anytype, expected: meta.Const(@TypeOf(res))) TimeError!void {
if (meta.isOptional(@TypeOf(res))) {
if (expected) |e| return expectEqual(res orelse return error.ExpectedValue, e);
if (res != null) return error.ExpectedNull;
}
// TODO: find all usages of expectEqualStrings and replace it with our expectEqual
if (meta.isString(@TypeOf(res))) {
return std.testing.expectEqualStrings(expected, res);
}
return std.testing.expectEqual(expected, res);
}
// test "basic usage" {
// const t1 = Time.unix(1234567890);
// try expectFmt(t1, "2009-02-13 23:31:30 UTC");
//
// try expectEqual(t1.date(), .{
// .year = 2009,
// .month = 2,
// .day = 13,
// });
//
// try expectEqual(t1.hour(), 23);
// try expectEqual(t1.minute(), 31);
// try expectEqual(t1.second(), 30);
//
// const t2 = t1.setHour(10).setMinute(15).setSecond(45);
// try expectFmt(t2, "2009-02-13 10:15:45 UTC");
//
// const t3 = t2.add(.hours, 14).add(.minutes, 46).add(.seconds, 18);
// try expectFmt(t3, "2009-02-14 01:02:03 UTC");
//
// // t.next()
// try expectFmt(t3.next(.second), "2009-02-14 01:02:04 UTC");
// try expectFmt(t3.next(.minute), "2009-02-14 01:03:00 UTC");
// try expectFmt(t3.next(.hour), "2009-02-14 02:00:00 UTC");
// try expectFmt(t3.next(.day), "2009-02-15 00:00:00 UTC");
//
// // t.setStartOf()
// try expectFmt(t3.setStartOf(.minute), "2009-02-14 01:02:00 UTC");
// try expectFmt(t3.setStartOf(.hour), "2009-02-14 01:00:00 UTC");
// try expectFmt(t3.setStartOf(.day), "2009-02-14 00:00:00 UTC");
// try expectFmt(t3.setStartOf(.month), "2009-02-01 00:00:00 UTC");
// try expectFmt(t3.setStartOf(.year), "2009-01-01 00:00:00 UTC");
//
// // t.setEndOf()
// try expectFmt(t3.setEndOf(.minute), "2009-02-14 01:02:59 UTC");
// try expectFmt(t3.setEndOf(.hour), "2009-02-14 01:59:59 UTC");
// try expectFmt(t3.setEndOf(.day), "2009-02-14 23:59:59 UTC");
// try expectFmt(t3.setEndOf(.month), "2009-02-28 23:59:59 UTC");
// try expectFmt(t3.setEndOf(.year), "2009-12-31 23:59:59 UTC");
// }
//
// test "edge-cases" {
// const jan31 = Date.ymd(2023, 1, 31);
// try expectEqual(jan31.add(.month, 1), Date.ymd(2023, 2, 28));
// try expectEqual(jan31.add(.month, 2), Date.ymd(2023, 3, 31));
// try expectEqual(jan31.add(.month, -1), Date.ymd(2022, 12, 31));
// try expectEqual(jan31.add(.month, -2), Date.ymd(2022, 11, 30));
// try expectEqual(jan31.add(.year, 1).add(.month, 1), Date.ymd(2024, 2, 29));
//
// const feb29 = Time.unix(951782400); // 2000-02-29 00:00:00
// try expectFmt(feb29.setEndOf(.month), "2000-02-29 23:59:59 UTC");
// try expectFmt(feb29.add(.years, 1), "2001-02-28 00:00:00 UTC");
// try expectFmt(feb29.add(.years, 4), "2004-02-29 00:00:00 UTC");
// }
//
// test isLeapYear {
// try testing.expect(!isLeapYear(1999));
// try testing.expect(isLeapYear(2000));
// try testing.expect(isLeapYear(2004));
// }
//
// test daysInMonth {
// try expectEqual(daysInMonth(1999, 2), 28);
// try expectEqual(daysInMonth(2000, 2), 29);
// try expectEqual(daysInMonth(2000, 7), 31);
// try expectEqual(daysInMonth(2000, 8), 31);
// }
//
// test rata_to_date {
// try expectEqual(rata_to_date(RATA_MIN), Date.MIN);
// try expectEqual(rata_to_date(RATA_MAX), Date.MAX);
//
// try expectEqual(rata_to_date(0), .ymd(0, 3, 1));
// try expectEqual(rata_to_date(RATA_TO_UNIX), .ymd(1970, 1, 1));
// }
//
// test date_to_rata {
// try expectEqual(date_to_rata(Date.MIN), RATA_MIN);
// try expectEqual(date_to_rata(Date.MAX), RATA_MAX);
//
// try expectEqual(date_to_rata(.ymd(0, 3, 1)), 0);
// try expectEqual(date_to_rata(.ymd(1970, 1, 1)), RATA_TO_UNIX);
// }

View file

@ -1,149 +0,0 @@
// https://github.com/cztomsik/tokamak
const std = @import("std");
pub const whitespace = std.ascii.whitespace;
pub fn plural(n: i64, singular: []const u8, plural_str: []const u8) []const u8 {
return if (n == 1) singular else plural_str;
}
pub fn trim(slice: []const u8) []const u8 {
return std.mem.trim(u8, slice, &whitespace);
}
pub fn truncateEnd(text: []const u8, width: usize) []const u8 {
return if (text.len <= width) text else text[text.len - width ..];
}
pub fn truncateStart(text: []const u8, width: usize) []const u8 {
return if (text.len <= width) text else text[0..width];
}
pub fn countScalar(comptime T: type, slice: []const T, value: T) usize {
var n: usize = 0;
for (slice) |c| {
if (c == value) n += 1;
}
return n;
}
pub fn Cmp(comptime T: type) type {
return struct {
// TODO: can we somehow flatten the anytype?
// pub const cmp = if (std.meta.hasMethod(T, "cmp")) T.cmp else std.math.order;
pub fn cmp(a: T, b: T) std.math.Order {
if (std.meta.hasMethod(T, "cmp")) {
return a.cmp(b);
}
return std.math.order(a, b);
}
pub fn lt(a: T, b: T) bool {
return @This().cmp(a, b) == .lt;
}
pub fn eq(a: T, b: T) bool {
return @This().cmp(a, b) == .eq;
}
pub fn gt(a: T, b: T) bool {
return @This().cmp(a, b) == .gt;
}
};
}
pub fn cmp(a: anytype, b: @TypeOf(a)) std.math.Order {
return Cmp(@TypeOf(a)).cmp(a, b);
}
pub fn lt(a: anytype, b: @TypeOf(a)) bool {
return Cmp(@TypeOf(a)).lt(a, b);
}
pub fn eq(a: anytype, b: @TypeOf(a)) bool {
return Cmp(@TypeOf(a)).eq(a, b);
}
pub fn gt(a: anytype, b: @TypeOf(a)) bool {
return Cmp(@TypeOf(a)).gt(a, b);
}
// /// ---------------------------------------------------------------
// /// 1 Detecta se um tipo é std.ArrayList(T)
// /// ---------------------------------------------------------------
// fn isArrayList(comptime T: type) bool {
// // @typeName devolve a string completa, por exemplo:
// // "std.ArrayList(i32)" ou "std.ArrayList([]const u8)"
// const name = @typeName(T);
// // Queremos garantir que o prefixo seja exatamente "std.ArrayList("
// // (inclui o parêntese de abertura para evitar colisões com nomes
// // semelhantes, como "my_std.ArrayListHelper").
// // return std.mem.startsWith(u8, name, "std.ArrayList(");
// return std.mem.startsWith(u8, name, "array_list");
// }
//
// /// ---------------------------------------------------------------
// /// 2 Obtém o tipo dos elementos armazenados em std.ArrayList(T)
// /// ---------------------------------------------------------------
// fn arrayListElemType(comptime ListT: type) type {
// // Sabemos que ListT tem a forma std.ArrayList(T). O primeiro campo
// // interno do struct é `items: []T`. Vamos ler esse campo.
// const ti = @typeInfo(ListT);
// if (ti != .@"struct") @compileError("Esperado um struct");
//
// // O campo `items` está na posição 0 da lista de fields:
// const items_field = ti.@"struct".fields[0];
// // Seu tipo é []T (slice). Em Zig, slices são representados como
// // ponteiros (`*T`) com comprimento separado, mas o tipo declarado
// // aqui é exatamente `[]T`, que corresponde a .pointer com
// // .is_slice = true.
// const slice_type = items_field.type;
// const slice_info = @typeInfo(slice_type);
// if (slice_info != .pointer) @compileError("Campo `items` não é slice");
//
// // O tipo filho da slice é o T que procuramos.
// return slice_info.pointer.child;
// }
//
// /// ---------------------------------------------------------------
// /// 3 Função genérica que aceita *qualquer* tipo e age
// /// de acordo se o argumento for um ArrayList ou não.
// /// ---------------------------------------------------------------
// pub fn handle(comptime ArgT: type, arg: ArgT) void {
// if (isArrayList(ArgT)) {
// // É um ArrayList descobrimos o tipo dos elementos.
// const Elem = arrayListElemType(ArgT);
//
// // Exemplo de uso genérico: imprimir tamanho e tipo dos itens.
// std.debug.print(
// "Recebi um std.ArrayList<{s}> contendo {d} itens.\n",
// .{ @typeName(Elem), arg.items.len },
// );
//
// // // Iterar de forma genérica (não sabemos o tipo exato de Elem,
// // // então fazemos operações que são válidas para *qualquer* tipo).
// // var it = arg.iterator();
// // while (it.next()) |item| {
// // // `item` tem tipo `Elem`. Se precisar de lógica específica,
// // // pode fazer outra inspeção de tipo aqui.
// // _ = item; // evita warning de variável não usada
// // }
// for(arg.items) |item| {
// _=item;
// }
// } else {
// // Não é um ArrayList apenas informamos o tipo real.
// std.debug.print(
// "O argumento NÃO é um std.ArrayList (é {s}).\n",
// .{@typeName(ArgT)},
// );
// }
// }
test {
try std.testing.expect(lt(1, 2));
try std.testing.expect(eq(2, 2));
try std.testing.expect(gt(2, 1));
}

View file

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>
{% block title %}Meu Site{% endblock %}
</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
header { background: #333; color: white; padding: 20px; }
nav ul { list-style: none; padding: 0; }
nav li { display: inline; margin: 0 10px; }
nav a { color: white; text-decoration: none; }
</style>
</head>
<body>
{% include "partials/header.html" %}
<main>
{% block content %}<p>Conteúdo padrão</p>{% endblock %}
</main>
<footer>
<p>&copy; 2025 - Feito com ❤️ e Zig</p>
<span>{% now "d/m/Y H:i:s" %}</span>
</footer>
</body>
</html>

View file

@ -1,28 +0,0 @@
{% extends "base.html" %}
{% block title %}Home - Meu Site{% endblock %}
{% block menu %}
{{ block.super }}
<li><a href="/blog">Blog</a></li>
{% endblock %}
{% block content %}
<h2>Bem-vindo, {{ user.name }}!</h2>
<span>{% now "d/m/Y H:i:s" %}</span>
<p>Você tem {{ user.notifications }} notificações pendentes.</p>
<h3>Últimos itens:</h3>
<ul>
{% for item in itens %}
<li>{{ forloop.counter }}: {{ item }}</li>
{% empty %}
<li>Nenhum item encontrado.</li>
{% endfor %}
</ul>
{% with msg="Olá mundo!" %}
<p>Mensagem temporária: {{ msg|upper }}</p>
{% endwith %}
{% endblock %}

View file

@ -1,12 +0,0 @@
<header>
<h1>{{ site_title|upper }}</h1>
<nav>
<ul>
{% block menu %}
<li><a href="/">Home</a></li>
<li><a href="/sobre">Sobre</a></li>
{% endblock %}
<li><a href="/contato">Contato</a></li>
</ul>
</nav>
</header>