Compare commits

...
Sign in to create a new pull request.

54 commits
bkp ... main

Author SHA1 Message Date
Lucas F.
f38f43dbc4 update: time with offset 2026-01-24 23:06:28 -03:00
Lucas F.
47f52fea58 update: forloop variables 2026-01-24 22:40:35 -03:00
Lucas F.
20e4bacab0 fix: null value for string is "" 2026-01-23 15:01:33 -03:00
Lucas F.
8bf9902ebf fix: passing context 2026-01-22 12:49:02 -03:00
Lucas F.
777b13ed98 fix: render tag inside ifblock 2026-01-22 10:15:11 -03:00
Lucas F.
fadae5d39c fix: parse tags inside forblock 2026-01-22 10:03:03 -03:00
Lucas F.
ea30958701 update: cleanup 2026-01-22 09:38:38 -03:00
Lucas F.
a6d8795c79 fix: slice of structs 2026-01-22 09:32:30 -03:00
Lucas F.
59e543ca89 update: icons 2026-01-20 19:04:46 -03:00
Lucas F.
7beb8758a5 update: icons 2026-01-20 19:04:28 -03:00
Lucas F.
1181f0fa68 update: svgNode 2026-01-20 19:04:08 -03:00
Lucas F.
f0115a15b9 update: cleanup 2026-01-20 19:03:28 -03:00
Lucas F.
a0645cb0d1 update: cleanup 2026-01-20 19:03:22 -03:00
Lucas F.
8fd9086d98 update: icons 2026-01-20 19:03:14 -03:00
Lucas F.
cce0c6a954 update: getIcon and fallback 2026-01-20 19:02:53 -03:00
Lucas F.
966035dccb update: icons 2026-01-20 18:11:19 -03:00
Lucas F.
56dd881e44 update: root 2026-01-18 18:08:15 -03:00
Lucas F.
4d45072435 Merge branch 'refact_parse' 2026-01-18 17:41:28 -03:00
Lucas F.
24ecea0244 update: lorem 2026-01-18 16:00:11 -03:00
Lucas F.
14e307f3dc fix: firstof 2026-01-18 15:59:48 -03:00
Lucas F.
655bed4c8f update: set toValue as public 2026-01-18 15:57:21 -03:00
Lucas F.
1888c5e07a update: now 2026-01-18 07:26:34 -03:00
Lucas F.
36cc1caac0 update: test for with, extends, super, include and commment 2026-01-17 20:41:55 -03:00
Lucas F.
745b2b29ca update: cleanup 2026-01-17 20:41:16 -03:00
Lucas F.
1ab6291116 update: test for something inside block 2026-01-17 20:32:33 -03:00
Lucas F.
7c53a272a7 update: debugNodes function to display nodes content 2026-01-17 20:32:11 -03:00
Lucas F.
44fb897a41 fix: parse inside block 2026-01-17 20:31:45 -03:00
Lucas F.
d3cff325eb update: misc 2026-01-17 17:38:08 -03:00
Lucas F.
3fa37f9613 update: refactor for new parser style 2026-01-17 17:38:03 -03:00
Lucas F.
f09689d539 fix: clone 2026-01-17 17:37:36 -03:00
Lucas F.
f8e7b01e9c update: time 2026-01-17 16:04:30 -03:00
Lucas F.
e11d3fb034 update: date filters 2026-01-17 16:04:11 -03:00
Lucas F.
002d2b949e update: inSeconds and inDays 2026-01-17 16:03:50 -03:00
Lucas F.
c7d52a0f32 update: add delta, util, meta 2026-01-16 19:36:55 -03:00
Lucas F.
c4319f5da3 update: receive Time 2026-01-16 18:20:15 -03:00
Lucas F.
5ff94ccf17 update: cleanup 2026-01-16 08:32:17 -03:00
Lucas F.
327a44baf0 update: remove debug print 2026-01-16 08:32:10 -03:00
Lucas F.
eb30117bbf update: refactor parser 2026-01-15 19:23:06 -03:00
Lucas F.
d4538db83a update: parseTagContent 2026-01-14 14:29:01 -03:00
Lucas F.
ac122a513b update: toValue method 2026-01-14 14:22:39 -03:00
Lucas F.
bb76e6cd44 update: render widthratio 2026-01-14 14:21:40 -03:00
Lucas F.
b93d6f5c81 update: passing allocator to evaluateCondition 2026-01-14 14:20:56 -03:00
Lucas F.
c14a92e7ab update: cleanup 2026-01-14 14:20:25 -03:00
Lucas F.
1e94763ba1 update: evaluateCondition now needs allocator 2026-01-14 14:19:47 -03:00
Lucas F.
9e1e3c2039 update: include int option for arg in renderNode 2026-01-14 14:17:57 -03:00
Lucas F.
8b36704652 update: add full_path to readTemplateFile 2026-01-14 14:14:59 -03:00
Lucas F.
7f47cf440b update: errors list 2026-01-14 14:14:04 -03:00
Lucas F.
cf24e968ca update: sample templates 2026-01-14 14:10:44 -03:00
Lucas F.
eaff212b63 update: full template 2026-01-14 14:09:09 -03:00
Lucas F.
0116e84c09 update: full template test 2026-01-14 14:08:49 -03:00
Lucas F.
c0e15a0179 update: RenderError 2026-01-14 10:01:51 -03:00
Lucas F.
0911b47396 update: UnkownFilter error 2026-01-14 09:47:27 -03:00
Lucas F.
a4ec89b743 update: default path for search templates 2026-01-14 09:47:15 -03:00
Lucas F.
024386eaf5 update: getOr 2026-01-12 15:04:37 -03:00
22 changed files with 6132 additions and 2497 deletions

View file

@ -84,6 +84,15 @@ 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,10 +2,13 @@ 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);
@ -15,7 +18,6 @@ pub const TemplateCache = struct {
};
}
pub fn deinit(self: *TemplateCache) void {
self.arena.deinit();
}
@ -50,6 +52,10 @@ 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 = .{};

View file

@ -1,4 +1,6 @@
const std = @import("std");
const time = @import("time.zig");
const util = @import("util.zig");
pub const Value = union(enum) {
null,
@ -34,8 +36,24 @@ pub const Context = struct {
self.arena.deinit();
}
fn toValue(self: *Context, value: anytype) !Value {
pub 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) },
@ -105,6 +123,14 @@ 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;

View file

@ -4,6 +4,9 @@ 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();
@ -17,17 +20,26 @@ 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 };
try ctx.set("user", p);
const p2 = Person{ .nome = "Fulana", .idade = 28 };
const people = [_]Person{ p, p2 };
// try ctx.set("user", p);
try ctx.set("user", people);
// 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);

83
src/delta.zig Normal file
View file

@ -0,0 +1,83 @@
// 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;
}
}
};

224
src/delta_test.zig Normal file
View file

@ -0,0 +1,224 @@
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,10 +2,15 @@ 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,
@ -121,35 +126,9 @@ 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 {
// // 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_date(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
return dateTimeToString(alloc, value, arg);
}
fn filter_default(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc;
@ -655,6 +634,12 @@ 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) {
@ -958,45 +943,22 @@ 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 {
_ = arg;
const then = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
const d = time.Time.parse(value.string) catch return value;
const now = time.Time.parse(arg.?.string) catch return value;
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 };
}
return Value{ .string = try d.timeSince(alloc, now) };
}
fn filter_timeuntil(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg;
// Reutiliza timesince, mas com sinal invertido
const future = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
const d = time.Time.parse(value.string) catch return value;
const now = time.Time.parse(arg.?.string) catch return value;
const fake_past = Value{ .int = std_time.timestamp() };
const since = try filter_timesince(alloc, fake_past, Value{ .int = future });
return since;
return Value{ .string = try now.timeSince(alloc, d) };
}
fn filter_title(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
@ -1190,7 +1152,6 @@ 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;
@ -1318,6 +1279,20 @@ 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 "";
@ -1462,7 +1437,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 },
@ -1487,6 +1462,7 @@ 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 },
@ -1498,9 +1474,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,10 +3,16 @@ 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 FilterError = @import("filters.zig").FilterError;
const filter = @import("filters.zig");
const FilterError = filter.FilterError;
const time = @import("time.zig");
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();
@ -29,6 +35,9 @@ 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").?;
@ -49,6 +58,9 @@ 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").?;
@ -73,6 +85,9 @@ 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").?;
@ -94,7 +109,10 @@ test "builtin filters - length" {
try testing.expect((try length(ctx.allocator(), list_val, null)).int == 4);
}
test "builtin filters - length com dict" {
test "builtin filters - length with dict" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("5 - length with dict\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -114,6 +132,9 @@ test "builtin filters - length com 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").?;
@ -143,6 +164,9 @@ 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").?;
@ -159,7 +183,6 @@ 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);
@ -170,6 +193,9 @@ 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").?;
@ -192,6 +218,9 @@ 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").?;
@ -218,6 +247,9 @@ 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").?;
@ -241,6 +273,9 @@ 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").?;
@ -267,6 +302,9 @@ 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").?;
@ -287,6 +325,9 @@ 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").?;
@ -306,6 +347,9 @@ 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();
@ -326,6 +370,9 @@ 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();
@ -341,6 +388,9 @@ 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();
@ -356,6 +406,9 @@ 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();
@ -384,6 +437,9 @@ 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();
@ -404,6 +460,9 @@ 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();
@ -419,6 +478,9 @@ 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();
@ -434,6 +496,9 @@ 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();
@ -449,6 +514,9 @@ 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();
@ -464,6 +532,9 @@ 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();
@ -489,31 +560,35 @@ test "builtin filters - pluralize" {
// try testing.expectEqualStrings("", zero.string);
}
test "builtin filters - addslashes, center, date" {
test "builtin filters - addslashes, center" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("24 - addslashes, center\n", .{});
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();
@ -544,6 +619,9 @@ 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();
@ -579,6 +657,9 @@ 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();
@ -601,6 +682,9 @@ 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();
@ -623,6 +707,9 @@ 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();
@ -648,6 +735,9 @@ 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();
@ -670,29 +760,69 @@ test "builtin filters - random, safeseq" {
try testing.expect(safe == .list);
}
// test "builtin filters - date, time, timesince, timeuntil" {
// const alloc = testing.allocator;
// var ctx = Context.init(alloc);
// defer ctx.deinit();
//
// const now = std_time.timestamp();
//
// // const date = builtin_filters.get("date").?;
// const time = builtin_filters.get("time").?;
// const timesince = builtin_filters.get("timesince").?;
// // const timeuntil = builtin_filters.get("timeuntil").?;
//
// // const d = try date(ctx.allocator(), Value{ .int = now }, Value{ .string = "d/m/Y" });
// const t = try time(ctx.allocator(), Value{ .int = now }, Value{ .string = "H:i" });
//
// // try testing.expect(d.string.len > 0);
// try testing.expect(t.string.len > 0);
//
// const since = try timesince(ctx.allocator(), Value{ .int = now - 3600 }, null);
// try testing.expect(std.mem.indexOf(u8, since.string, "hora") != null);
// }
test "builtin filters - 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 - 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();
@ -714,15 +844,16 @@ 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);
@ -737,6 +868,15 @@ 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;
}

338
src/lorem.zig Normal file
View file

@ -0,0 +1,338 @@
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,33 +9,40 @@ 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 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 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 User =struct {
name: []const u8,
email: []const u8,
notifications: i64 = 0
};
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -45,12 +52,27 @@ 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("child.html", buffer.writer(ctx.allocator()));
try renderer.render("home.html", buffer.writer(ctx.allocator()));
const output = buffer.items;

208
src/meta.zig Normal file
View file

@ -0,0 +1,208 @@
// 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;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,37 +10,19 @@ 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 = FilterError || error{
OutOfMemory,
pub const RenderError = error{
InvalidCharacter,
InvalidSyntax,
UnknownVariable,
UnknownFilter,
InvalidTemplate,
BlockNotFound,
CircularExtends,
FileNotFound,
AccessDenied,
FileTooBig,
NoSpaceLeft,
OutOfMemory,
Overflow,
Unexpected,
UnclosedTag,
InvalidAutoescapeArgument,
UnclosedVariable,
UnclosedBlock,
UnclosedComment,
InvalidAssignmentSyntax,
UnclosedQuoteInAssignment,
InvalidForSyntax,
UnclosedVerbatim,
InvalidUrlSyntax,
UnclosedQuoteInUrl,
InvalidDebugArgs,
InvalidRegroupSyntax,
InvalidWidthRatioSyntax,
InvalidTemplateTag,
InvalidCsrfTokenArgs,
};
UnsupportedExpression,
// } || FilterError || parser.ParserError || icons.SvgError || std.fs.File.OpenError;
} || FilterError || parser.ParserError || std.fs.File.OpenError;
pub const Renderer = struct {
context: *Context,
@ -58,19 +40,23 @@ 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 == .extends) return n;
if (n.type == .tag and n.tag.?.kind == .extends) return n;
}
return null;
}
fn readTemplateFile(self: *const Renderer, path: []const u8) RenderError![]const u8 {
fn readTemplateFile(self: *const Renderer, path: []const u8) ![]const u8 {
const max_size = 10 * 1024 * 1024;
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,
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) {
else => return RenderError.Unexpected,
};
}
@ -79,7 +65,8 @@ 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.extends.?.parent_name);
const base_template = try self.readTemplateFile(ext.tag.?.body.extends.parent_name);
defer self.allocator.free(base_template);
var base_parser = parser.Parser.init(base_template);
@ -97,16 +84,17 @@ 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);
@ -115,16 +103,15 @@ 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);
std.debug.print("clonou {any}\n", .{cached_nodes[i]});
}
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);
// }
// try self.cache.add(ck, nodes);
// }
return try self.renderNodes(alloc, nodes, writer);
}
@ -142,24 +129,22 @@ 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 == .block) {
const block_name = base_node.block.?.name;
if (base_node.type == .tag and base_node.tag.?.kind == .block) {
const block_name = base_node.tag.?.body.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.block.?.body);
try self.renderNode(alloc, child_nodes, child_node, writer, null, base_node.tag.?.body.block.body);
}
} else {
// Renderiza o do pai
for (base_node.block.?.body) |child| {
for (base_node.tag.?.body.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);
}
}
@ -182,7 +167,14 @@ 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;
// 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 };
}
value = try filter_fn(alloc, value, arg);
if (std.mem.eql(u8, f.name, "safe")) is_safe = true;
@ -200,88 +192,292 @@ pub const Renderer = struct {
try self.valueToString(alloc, &buf, value);
try writer.writeAll(buf.items);
},
.if_block => {
const condition = self.evaluateCondition(node.@"if".?.condition) catch return false;
.tag => {
switch (node.tag.?.kind) {
.if_block => {
const condition = try self.evaluateCondition(alloc, node.tag.?.body.@"if".condition, context);
if (condition) {
for (node.@"if".?.true_body) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null);
}
} else {
for (node.@"if".?.false_body) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null);
}
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(" -->");
},
}
},
.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 != .block) continue;
if (std.mem.eql(u8, n.block.?.name, name)) return n.block.?;
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;
}
return null;
}
fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) !void {
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 {
_ = self;
var w = buf.writer(alloc);
switch (value) {
.null => try w.writeAll("null"),
.null => try w.writeAll(""),
.bool => |b| try w.print("{}", .{b}),
.int => |n| try w.print("{d}", .{n}),
.float => |f| try w.print("{d}", .{f}),
@ -291,7 +487,7 @@ pub const Renderer = struct {
}
}
fn evaluateCondition(self: *const Renderer, expr: []const u8) !bool {
fn evaluateCondition_bkp(self: *const Renderer, expr: []const u8) RenderError!bool {
const value = self.context.get(expr) orelse Value.null;
return switch (value) {
.bool => |b| b,
@ -303,4 +499,123 @@ 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,
}
}
};

View file

@ -9,6 +9,8 @@ const Value = @import("context.zig").Value;
const TemplateCache = @import("cache.zig").TemplateCache;
test "renderer: literal + variável simples" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("1 - renderer: literal + variável simples\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -18,7 +20,7 @@ test "renderer: literal + variável simples" {
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("nome", Value{ .string = "Mariana" });
try ctx.set("nome", Value{ .string = "Fulana" });
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
@ -29,10 +31,14 @@ test "renderer: literal + variável simples" {
try renderer.renderString(template, buf.writer(alloc));
try testing.expectEqualStrings("Olá, Mariana! Bem-vinda.", buf.items);
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("Olá, Fulana! Bem-vinda.", buf.items);
}
test "renderer: filtros + autoescape" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("2 - renderer: filtros + autoescape\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -62,10 +68,14 @@ test "renderer: filtros + autoescape" {
\\Filtrado: maiusculo-e-slug
;
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "literal simples" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("3 - literal simples\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -82,10 +92,14 @@ test "literal simples" {
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("Texto literal com acentos: Olá, mundo!", buf.items);
}
test "variável com filtro encadeado e autoescape" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("4 - variável com filtro encadeado e autoescape\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -104,10 +118,14 @@ test "variável com filtro encadeado e autoescape" {
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("Resultado: EXEMPLO DE TEXTO", buf.items); // assume lower then upper
}
test "autoescape com safe" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("5 - autoescape com safe\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -126,10 +144,15 @@ test "autoescape com safe" {
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("Escape: &lt;div&gt;conteúdo&lt;/div&gt; | Safe: <div>conteúdo</div>", buf.items);
}
// // TODO: evaluationConditions more complex
test "renderer - if and for" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("6 - renderer - if and for\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@ -160,6 +183,8 @@ test "renderer - if and for" {
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, buf.items, "Sim!") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "Não") == null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- Ana") != null);
@ -168,12 +193,15 @@ test "renderer - if and for" {
}
test "renderer - block and extends" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("7 - renderer - block and extends\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
@ -220,7 +248,7 @@ test "renderer - block and extends" {
const output = buf.items;
std.debug.print("OUTPUT:\n{s}\n", .{output});
// std.debug.print("OUTPUT:\n\n{s}\n", .{output});
try testing.expect(std.mem.indexOf(u8, output, "<html>") != null);
try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
@ -230,12 +258,15 @@ test "renderer - block and extends" {
}
test "renderer - block and extends with super" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("8 - renderer - block and extends with super\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
@ -284,7 +315,7 @@ test "renderer - block and extends with super" {
const output = buf.items;
std.debug.print("{s}\n", .{output});
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, output, "<html>") != null);
try testing.expect(std.mem.indexOf(u8, output, "Meu Título") != null);
@ -294,6 +325,8 @@ test "renderer - block and extends with super" {
}
test "renderer - include" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("9 - renderer - include\n", .{});
const alloc = testing.allocator;
const header =
@ -330,6 +363,7 @@ test "renderer - include" {
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
@ -342,12 +376,16 @@ test "renderer - include" {
const output = buf.items;
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, output, "<h1>Bem-vindo</h1>") != null);
try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas!") != null);
try testing.expectEqualStrings(expected, output);
}
test "renderer - comment" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("10 - renderer - comment\n", .{});
const alloc = testing.allocator;
const template =
@ -370,6 +408,7 @@ test "renderer - comment" {
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
@ -382,6 +421,8 @@ test "renderer - comment" {
const output = buf.items;
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, output, "Olá Lucas") != null);
try testing.expect(std.mem.indexOf(u8, output, "Fim: Lucas") != null);
try testing.expect(std.mem.indexOf(u8, output, "Isso é um comentário") == null);
@ -390,54 +431,670 @@ test "renderer - comment" {
try testing.expectEqualStrings(expected, output);
}
// FIX: comment inside block
//
// test "renderer - full template with extends, super, include, comment" {
// const alloc = testing.allocator;
//
// const header = "<header>Bem-vindo</header>";
// const base =
// \\{% include "header.html" %}
// \\<main>
// \\ {% block content %}
// \\ Conteúdo padrão
// \\ {% endblock %}
// \\</main>
// ;
//
// const child =
// \\{% extends "base.html" %}
// \\{% block content %}
// \\ {{ block.super }}
// \\ Conteúdo do filho
// \\ {% comment %} Isso não aparece {% endcomment %}
// \\{% endblock %}
// ;
//
// try std.fs.cwd().writeFile(.{ .sub_path = "header.html", .data = header });
// 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("header.html") catch {};
// defer std.fs.cwd().deleteFile("base.html") catch {};
// defer std.fs.cwd().deleteFile("child.html") catch {};
//
// var ctx = Context.init(alloc);
// defer ctx.deinit();
//
// var cache = TemplateCache.init(alloc);
// defer cache.deinit();
//
// const renderer = Renderer.init(&ctx, &cache);
//
// var buf = std.ArrayList(u8){};
// defer buf.deinit(alloc);
//
// try renderer.render("child.html", buf.writer(alloc));
//
// const output = buf.items;
//
// try testing.expect(std.mem.indexOf(u8, output, "<header>Bem-vindo</header>") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
// try testing.expect(std.mem.indexOf(u8, output, "Isso não aparece") == null);
// }
test "renderer - full template with extends, super, include, comment" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("11 - renderer - full template with extends, super, include, comment\n", .{});
const alloc = testing.allocator;
const header = "<header>Bem-vindo</header>";
const base =
\\{% include "header.html" %}
\\<main>
\\ {% block content %}
\\ Conteúdo padrão
\\ {% endblock %}
\\</main>
;
const child =
\\{% extends "base.html" %}
\\{% block content %}
\\ {{ block.super }}
\\ Conteúdo do filho
\\ {% comment %} Isso não aparece {% endcomment %}
\\{% endblock %}
\\{% comment %} bazinga {% endcomment %}
;
try std.fs.cwd().writeFile(.{ .sub_path = "header.html", .data = header });
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("header.html") catch {};
defer std.fs.cwd().deleteFile("base.html") catch {};
defer std.fs.cwd().deleteFile("child.html") catch {};
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.render("child.html", buf.writer(alloc));
const output = buf.items;
// std.debug.print("OUTPUT:\n\n{s}\n", .{output});
try testing.expect(std.mem.indexOf(u8, output, "<header>Bem-vindo</header>") != null);
try testing.expect(std.mem.indexOf(u8, output, "<main>") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
try testing.expect(std.mem.indexOf(u8, output, "Isso não aparece") == null);
try testing.expect(std.mem.indexOf(u8, output, "bazinga") == null);
}
test "renderer - if inside block" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("12 - render - if inside block\n", .{});
const alloc = testing.allocator;
const base =
\\<main>
\\ {% block content %}
\\ Conteúdo padrão
\\ {% endblock %}
\\</main>
;
const child =
\\{% extends "base.html" %}
\\{% block content %}
\\ {{ block.super }}
\\ Conteúdo do filho
\\{% if idade > 18 %}
\\ Idade: {{ idade }}
\\{% else %}
\\ Oops
\\{% endif %}
\\{% endblock %}
;
try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
defer std.fs.cwd().deleteFile("base.html") catch {};
defer std.fs.cwd().deleteFile("child.html") catch {};
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("idade", 23);
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.render("child.html", buf.writer(alloc));
const output = buf.items;
// std.debug.print("OUTPUT:\n\n{s}\n", .{output});
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
try testing.expect(std.mem.indexOf(u8, output, "Oops") == null);
try testing.expect(std.mem.indexOf(u8, output, "Idade: 23") != null);
}
test "renderer - if with operators" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("13 - render - if inside block\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("idade", Value{ .int = 20 });
try ctx.set("nome", Value{ .string = "Lucas" });
try ctx.set("ativo", Value{ .bool = true });
try ctx.set("order", 1);
const template =
\\{% if idade > 18 %}Maior{% endif %}
\\{% if idade < 18 %}Menor{% endif %}
\\{% if nome == "Lucas" %}Olá Lucas{% endif %}
\\{% if ativo %}Ativo{% endif %}
\\{% if order >= 2 %}High Order{% else %}Low Order{% endif %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, buf.items, "Maior") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "Olá Lucas") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "Ativo") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "Menor") == null);
try testing.expect(std.mem.indexOf(u8, buf.items, "Low Order") != null);
}
test "renderer - widthratio inside block" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("14 - render - widthratio inside block\n", .{});
const alloc = testing.allocator;
const base =
\\<main>
\\ {% block content %}
\\ Conteúdo padrão
\\ {% endblock %}
\\</main>
;
const child =
\\{% extends "base.html" %}
\\{% block content %}
\\ Conteúdo do filho
\\<img src="bar.png" alt="Bar" height="10" width="{% widthratio value 400 %}">
\\ {{ block.super }}
\\{% endblock %}
;
try std.fs.cwd().writeFile(.{ .sub_path = "base.html", .data = base });
try std.fs.cwd().writeFile(.{ .sub_path = "child.html", .data = child });
defer std.fs.cwd().deleteFile("base.html") catch {};
defer std.fs.cwd().deleteFile("child.html") catch {};
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
cache.default_path = ".";
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("value", 50);
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.render("child.html", buf.writer(alloc));
const output = buf.items;
// std.debug.print("OUTPUT:\n\n{s}\n", .{output});
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
}
test "renderer - now" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("15 - render now\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("idade", Value{ .int = 20 });
const template =
\\{% now %}
\\{% now \"Y-m-d H:i:s\" %}
\\{% now \"Y" %}
\\{% now \"m\" %}
\\{% now \"n\" %}
\\{% now \"d\" %}
\\{% now \"j\" %}
\\{% now \"F\" %}
\\{% now \"M\" %}
\\{% now \"l\" %}
\\{% now \"D\" %}
\\{% now \"H:i:s\" %}
\\{% now \"H\" %}
\\{% now \"G\" %}
\\{% now \"i\" %}
\\{% now \"s\" %}
\\{% now \"a\" %}
\\{% now \"A\" %}
\\{% now \"P\" %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
// try testing.expect(std.mem.indexOf(u8, buf.items, "Maior") != null);
}
test "renderer - csrf_token in context" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("15 - csrf_token in context\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const token: []const u8 = "zh5fyUSICjXNsDTtJCjl9A3O2dDSHhYFlIngAEO6PXK9NX56Z1XLEy7doYuPcE0u";
try ctx.set("csrf_token", token);
const template =
\\{% csrf_token %}
;
const expected =
\\<input type="hidden" name="csrfmiddlewaretoken" value="zh5fyUSICjXNsDTtJCjl9A3O2dDSHhYFlIngAEO6PXK9NX56Z1XLEy7doYuPcE0u">
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - csrf_token not in context" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("16 - csrf_token not in context\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% csrf_token %}
;
const expected =
\\
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
// TODO: add parse filters to variables
test "renderer - firstof withtout fallback" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("17 - firstof without fallback\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("var1", "");
try ctx.set("var2", "baz");
const template =
\\{% firstof var1 var2 %}
;
const expected =
\\baz
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof with fallback" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("18 - firstof with fallback\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("var1", "");
try ctx.set("var2", 0);
const template =
\\{% firstof var1 var2 "Oops no value!" %}
;
const expected =
\\Oops no value!
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof without value in context" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("19 - firstof without value in context\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% firstof 0 true "Oops no value!" %}
;
const expected =
\\true
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof missing vars" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("20 - firstof missing vars\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% firstof %}
;
const expected =
\\
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof missing vars with fallback" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("21 - firstof missing vars with fallback\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% firstof "nothing here" %}
;
const expected =
\\nothing here
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - lorem" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("22 - lorem\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, buf.items, "Lorem ipsum") != null);
}
test "renderer - lorem with count and method words" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("23 - lorem with count and method words\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 3 w %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("lorem ipsum dolor", buf.items);
}
test "renderer - lorem with count and method paragraphs" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("24 - lorem with count and method paragraphs\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 5 p %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
const qty = std.mem.count(u8, buf.items, "<p>");
try testing.expect(std.mem.indexOf(u8, buf.items, "Lorem ipsum dolor") != null);
try testing.expect(qty == 5);
}
test "renderer - lorem only random" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("25 - lorem only random\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem true %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(buf.items.len > 0);
}
test "renderer - lorem words random" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("26 - lorem words random\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 6 w true %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
const spaces = std.mem.count(u8, buf.items, " ");
try testing.expect(spaces == 5);
}
test "renderer - lorem paragraphs random" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("26 - lorem paragraphs random\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 3 p true %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
const spaces = std.mem.count(u8, buf.items, "<p>");
try testing.expect(spaces == 3);
}
test "renderer - svg" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("27 - svg\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
try cache.initIcons();
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% svg material kangaroo %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, buf.items, "<div class=\"svg-container\">") != null);
// const spaces = std.mem.count(u8, buf.items, "<p>");
// try testing.expect(spaces == 3);
}

View file

@ -1,23 +1,11 @@
//! 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);
}
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");

117
src/svg/icons.zig Normal file
View file

@ -0,0 +1,117 @@
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>
;

878
src/time.zig Normal file
View file

@ -0,0 +1,878 @@
// 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);
// }

149
src/util.zig Normal file
View file

@ -0,0 +1,149 @@
// 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));
}

26
templates/base.html Normal file
View file

@ -0,0 +1,26 @@
<!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>

28
templates/home.html Normal file
View file

@ -0,0 +1,28 @@
{% 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

@ -0,0 +1,12 @@
<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>