diff --git a/src/context.zig b/src/context.zig
index 63da514..a555794 100644
--- a/src/context.zig
+++ b/src/context.zig
@@ -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,12 @@ 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") };
+ }
+
return switch (@typeInfo(T)) {
.bool => Value{ .bool = value },
.int, .comptime_int => Value{ .int = @intCast(value) },
@@ -109,6 +115,11 @@ pub const Context = struct {
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;
// const opt_value = self.getOptional(T, key);
diff --git a/src/delta.zig b/src/delta.zig
new file mode 100644
index 0000000..e82bad1
--- /dev/null
+++ b/src/delta.zig
@@ -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;
+ }
+ }
+};
diff --git a/src/delta_test.zig b/src/delta_test.zig
new file mode 100644
index 0000000..4f3a08e
--- /dev/null
+++ b/src/delta_test.zig
@@ -0,0 +1,182 @@
+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" {
+ 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" {
+ 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" {
+ 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" {
+ 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" {
+ 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" {
+ 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)" {
+ // 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" {
+ // 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)" {
+ // 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)" {
+ // 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)" {
+ // 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)" {
+ 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)" {
+ // 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" {
+ 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),
+ );
+}
diff --git a/src/filters.zig b/src/filters.zig
index 48cb09d..5f4fb3b 100644
--- a/src/filters.zig
+++ b/src/filters.zig
@@ -2,11 +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,
@@ -122,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;
@@ -656,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) {
@@ -959,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 {
@@ -1191,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;
@@ -1319,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 "";
@@ -1463,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 },
@@ -1488,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 },
@@ -1499,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 },
diff --git a/src/filters_test.zig b/src/filters_test.zig
index c5fe77d..d31cb1b 100644
--- a/src/filters_test.zig
+++ b/src/filters_test.zig
@@ -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" {
\\
item2
\\
;
- 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;
+}
diff --git a/src/lorem.zig b/src/lorem.zig
new file mode 100644
index 0000000..b3ad1e0
--- /dev/null
+++ b/src/lorem.zig
@@ -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, "{s}
", .{pg}));
+ }
+ }
+
+ return try std.mem.join(allocator, "\n", pa.items);
+ }
+
+ const first = try std.fmt.allocPrint(allocator, "{s}
", .{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, "{s}
", .{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);
+}
diff --git a/src/meta.zig b/src/meta.zig
new file mode 100644
index 0000000..723a54d
--- /dev/null
+++ b/src/meta.zig
@@ -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 ©
+ },
+ };
+}
+
+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;
+}
diff --git a/src/parser.zig b/src/parser.zig
index d77a292..f2f7c3e 100644
--- a/src/parser.zig
+++ b/src/parser.zig
@@ -23,18 +23,61 @@ pub const ParserError = error{
UnexpectedToken,
};
-pub const NodeType = enum {
+pub const TagNodeBody = union(enum) {
+ autoescape: AutoescapeNode,
+ block: BlockNode,
+ csrf_token: bool,
+ cycle: CycleNode,
+ debug: bool,
+ extends: ExtendsNode,
+ filter_block: FilterBlockNode,
+ firstof: FirstOfNode,
+ @"for": ForNode,
+ @"if": IfNode,
+ include: IncludeNode,
+ load: LoadNode,
+ lorem: LoremNode,
+ now: NowNode,
+ partial: PartialNode,
+ partialdef: PartialDefNode,
+ querystring: QueryStringNode,
+ regroup: RegroupNode,
+ resetcycle: ResetCycleNode,
+ spaceless: SpacelessNode,
+ super: bool,
+ templatetag: TemplateTagNode,
+ url: UrlNode,
+ verbatim: VerbatimNode,
+ widthratio: WidthRatioNode,
+ with: WithNode,
+ initial: bool,
+};
+
+pub const TagKind = enum {
autoescape,
block,
csrf_token,
+ comment,
cycle,
debug,
+ else_block,
+ empty,
+ endautoescape,
+ endblock,
+ endfilter,
+ endfor_block,
+ endif_block,
+ endpartialdef,
+ endspaceless,
+ endverbatim,
+ endwith_block,
extends,
filter_block,
firstof,
for_block,
if_block,
include,
+ initial,
load,
lorem,
now,
@@ -45,15 +88,61 @@ pub const NodeType = enum {
resetcycle,
spaceless,
super,
- tag,
templatetag,
- text,
url,
- variable,
+ verbatim,
widthratio,
with_block,
};
+pub const NodeType = enum {
+ tag,
+ text,
+ variable,
+};
+
+fn getTagKindByName(name: []const u8) TagKind {
+ if (std.mem.eql(u8, name, "autoescape")) return .autoescape;
+ if (std.mem.eql(u8, name, "block")) return .block;
+ if (std.mem.eql(u8, name, "csrf_token")) return .csrf_token;
+ if (std.mem.eql(u8, name, "comment")) return .comment;
+ if (std.mem.eql(u8, name, "cycle")) return .cycle;
+ if (std.mem.eql(u8, name, "debug")) return .debug;
+ if (std.mem.eql(u8, name, "else")) return .else_block;
+ if (std.mem.eql(u8, name, "empty")) return .empty;
+ if (std.mem.eql(u8, name, "endautoescape")) return .endautoescape;
+ if (std.mem.eql(u8, name, "endblock")) return .endblock;
+ if (std.mem.eql(u8, name, "endfilter")) return .endfilter;
+ if (std.mem.eql(u8, name, "endfor")) return .endfor_block;
+ if (std.mem.eql(u8, name, "endif")) return .endif_block;
+ if (std.mem.eql(u8, name, "endpartialdef")) return .endpartialdef;
+ if (std.mem.eql(u8, name, "endspaceless")) return .endspaceless;
+ if (std.mem.eql(u8, name, "endverbatim")) return .endverbatim;
+ if (std.mem.eql(u8, name, "endwith")) return .endwith_block;
+ if (std.mem.eql(u8, name, "extends")) return .extends;
+ if (std.mem.eql(u8, name, "filter")) return .filter_block;
+ if (std.mem.eql(u8, name, "firstof")) return .firstof;
+ if (std.mem.eql(u8, name, "for")) return .for_block;
+ if (std.mem.eql(u8, name, "if")) return .if_block;
+ if (std.mem.eql(u8, name, "include")) return .include;
+ if (std.mem.eql(u8, name, "load")) return .load;
+ if (std.mem.eql(u8, name, "lorem")) return .lorem;
+ if (std.mem.eql(u8, name, "now")) return .now;
+ if (std.mem.eql(u8, name, "partial")) return .partial;
+ if (std.mem.eql(u8, name, "partialdef")) return .partialdef;
+ if (std.mem.eql(u8, name, "querystring")) return .querystring;
+ if (std.mem.eql(u8, name, "regroup")) return .regroup;
+ if (std.mem.eql(u8, name, "resetcycle")) return .resetcycle;
+ if (std.mem.eql(u8, name, "spaceless")) return .spaceless;
+ if (std.mem.eql(u8, name, "super")) return .super;
+ if (std.mem.eql(u8, name, "templatetag")) return .templatetag;
+ if (std.mem.eql(u8, name, "url")) return .url;
+ if (std.mem.eql(u8, name, "verbatim")) return .verbatim;
+ if (std.mem.eql(u8, name, "widthratio")) return .widthratio;
+ if (std.mem.eql(u8, name, "with")) return .with_block;
+ return .initial;
+}
+
pub const TemplateTagNode = struct {
kind: enum { openblock, closeblock, openvariable, closevariable, openbrace, closebrace, opencomment, closecomment },
@@ -71,24 +160,84 @@ pub const TemplateTagNode = struct {
}
};
-pub const WidthRatioNode = struct {
- value: []const u8,
- max_value: []const u8,
- divisor: ?[]const u8 = null, // opcional
+pub const Assignment = struct {
+ key: []const u8,
+ value_expr: []const u8,
+ is_literal: bool,
};
-pub const ResetCycleNode = struct {
- cycle_name: ?[]const u8 = null, // null = reseta todos, ou nome específico
+pub const AutoescapeNode = struct {
+ body: []Node,
+ raw_open: []const u8,
+ raw_close: []const u8,
+ enabled: bool,
};
-pub const RegroupNode = struct {
- source: []const u8,
- by: []const u8,
- as_var: []const u8,
+pub const BlockNode = struct {
+ name: []const u8,
+ body: []Node,
+ raw_open: []const u8,
+ raw_close: []const u8,
};
-pub const QueryStringNode = struct {
- modifications: []const []const u8, // array de "key=value" ou "key=None"
+pub const CycleNode = struct {
+ values: []const []const u8,
+};
+
+pub const ExtendsNode = struct {
+ parent_name: []const u8,
+};
+
+pub const Filter = struct {
+ name: []const u8,
+ arg: ?[]const u8 = null,
+};
+
+pub const FilterBlockNode = struct {
+ filters: []const u8, // "upper|escape|truncatewords:30"
+ body: []Node,
+ raw_open: []const u8,
+ raw_close: []const u8,
+};
+
+pub const FirstOfNode = struct {
+ values: []const []const u8,
+ fallback: []const u8,
+};
+
+pub const ForNode = struct {
+ loop_var: []const u8,
+ iterable: []const u8,
+ body: []Node,
+ empty_body: []Node,
+ raw_open: []const u8,
+ raw_close: []const u8,
+};
+
+pub const IfNode = struct {
+ condition: []const u8,
+ true_body: []Node,
+ false_body: []Node,
+ raw_open: []const u8,
+ raw_close: []const u8,
+};
+
+pub const IncludeNode = struct {
+ template_path: []const u8,
+};
+
+pub const LoadNode = struct {
+ libraries: []const []const u8,
+};
+
+pub const LoremNode = struct {
+ count: ?[]const u8 = null, // "3" ou null
+ method: ?[]const u8 = null, // "p" ou "w" ou null
+ random: bool = false,
+};
+
+pub const NowNode = struct {
+ format: []const u8,
};
pub const PartialDefNode = struct {
@@ -102,34 +251,18 @@ pub const PartialNode = struct {
name: []const u8,
};
-pub const LoremNode = struct {
- count: ?[]const u8 = null, // "3" ou null
- method: ?[]const u8 = null, // "p" ou "w" ou null
- format: ?[]const u8 = null, // "html" ou null
+pub const QueryStringNode = struct {
+ modifications: []const []const u8, // array de "key=value" ou "key=None"
};
-pub const LoadNode = struct {
- libraries: []const []const u8,
+pub const RegroupNode = struct {
+ source: []const u8,
+ by: []const u8,
+ as_var: []const u8,
};
-pub const AutoescapeNode = struct {
- body: []Node,
- raw_open: []const u8,
- raw_close: []const u8,
- enabled: bool,
-};
-
-pub const FirstOfNode = struct {
- values: []const []const u8,
-};
-
-pub const CycleNode = struct {
- values: []const []const u8,
-};
-
-pub const UrlNode = struct {
- name: []const u8,
- args: []const []const u8,
+pub const ResetCycleNode = struct {
+ cycle_name: ?[]const u8 = null, // null = reseta todos, ou nome específico
};
pub const SpacelessNode = struct {
@@ -138,16 +271,27 @@ pub const SpacelessNode = struct {
raw_close: []const u8,
};
-pub const FilterBlockNode = struct {
- filters: []const u8, // "upper|escape|truncatewords:30"
- body: []Node,
- raw_open: []const u8,
- raw_close: []const u8,
+pub const TagNode = struct {
+ kind: TagKind,
+ args: []const u8,
+ raw: []const u8,
+ body: TagNodeBody,
};
-pub const Filter = struct {
+pub const TextNode = struct {
+ content: []const u8,
+};
+
+pub const UrlNode = struct {
name: []const u8,
- arg: ?[]const u8 = null, // null ou string com argumento
+ args: []const []const u8,
+};
+
+pub const VerbatimNode = struct {
+ name: ?[]const u8,
+ content: []const u8,
+ raw_open: []const u8,
+ raw_close: []const u8,
};
pub const VariableNode = struct {
@@ -155,25 +299,10 @@ pub const VariableNode = struct {
filters: []const Filter,
};
-pub const NowNode = struct {
- format: []const u8,
-};
-
-pub const ExtendsNode = struct {
- parent_name: []const u8,
-};
-
-pub const BlockNode = struct {
- name: []const u8,
- body: []Node,
- raw_open: []const u8,
- raw_close: []const u8,
-};
-
-pub const Assignment = struct {
- key: []const u8,
- value_expr: []const u8,
- is_literal: bool,
+pub const WidthRatioNode = struct {
+ value: []const u8,
+ max_value: []const u8,
+ divisor: ?[]const u8 = null, // opcional
};
pub const WithNode = struct {
@@ -183,203 +312,98 @@ pub const WithNode = struct {
raw_close: []const u8,
};
-pub const IncludeNode = struct {
- template_name: []const u8,
-};
-
-pub const TextNode = struct {
- content: []const u8,
-};
-
-pub const TagNode = struct {
- name: []const u8,
- args: []const u8,
- raw: []const u8,
-};
-
-pub const IfNode = struct {
- condition: []const u8, // dupe esse sim
- true_body: []Node,
- false_body: []Node,
- raw_open: []const u8, // slice original, NÃO free
- raw_close: []const u8, // slice original, NÃO free
-};
-
-pub const ForNode = struct {
- loop_var: []const u8,
- iterable: []const u8,
- body: []Node,
- empty_body: []Node,
- raw_open: []const u8,
- raw_close: []const u8,
-};
-
pub const Node = struct {
type: NodeType,
text: ?TextNode = null,
variable: ?VariableNode = null,
tag: ?TagNode = null,
- @"if": ?IfNode = null,
- @"for": ?ForNode = null,
- include: ?IncludeNode = null,
- with: ?WithNode = null,
- now: ?NowNode = null,
- extends: ?ExtendsNode = null,
- block: ?BlockNode = null,
- filter_block: ?FilterBlockNode = null,
- autoescape: ?AutoescapeNode = null,
- spaceless: ?SpacelessNode = null,
- url: ?UrlNode = null,
- cycle: ?CycleNode = null,
- firstof: ?FirstOfNode = null,
- load: ?LoadNode = null,
- lorem: ?LoremNode = null,
- partialdef: ?PartialDefNode = null,
- partial: ?PartialNode = null,
- querystring: ?QueryStringNode = null,
- regroup: ?RegroupNode = null,
- resetcycle: ?ResetCycleNode = null,
- widthratio: ?WidthRatioNode = null,
- templatetag: ?TemplateTagNode = null,
- super: bool = false,
- csrf_token: bool = false,
- debug: bool = false,
pub fn deinit(self: Node, allocator: std.mem.Allocator) void {
switch (self.type) {
- .text => if (self.text) |t| allocator.free(t.content),
+ // .text => if (self.text) |t| allocator.free(t.content),
+ .text => {},
.variable => if (self.variable) |v| {
- allocator.free(v.expr);
- for (v.filters) |f| {
- allocator.free(f.name);
- if (f.arg) |a| allocator.free(a);
- }
allocator.free(v.filters);
},
- .tag => if (self.tag) |t| {
- allocator.free(t.name);
- allocator.free(t.args);
- allocator.free(t.raw);
- },
- .if_block => if (self.@"if") |ib| {
- allocator.free(ib.condition);
- const true_copy = ib.true_body;
- for (true_copy) |n| n.deinit(allocator);
- allocator.free(ib.true_body);
- const false_copy = ib.false_body;
- for (false_copy) |n| n.deinit(allocator);
- allocator.free(ib.false_body);
- allocator.free(ib.raw_open);
- allocator.free(ib.raw_close);
- },
- .for_block => if (self.@"for") |fb| {
- allocator.free(fb.loop_var);
- allocator.free(fb.iterable);
- const body_copy = fb.body;
- for (body_copy) |n| n.deinit(allocator);
- allocator.free(fb.body);
- const empty_copy = fb.empty_body;
- for (empty_copy) |n| n.deinit(allocator);
- allocator.free(fb.empty_body);
- allocator.free(fb.raw_open);
- allocator.free(fb.raw_close);
- },
- .include => if (self.include) |inc| allocator.free(inc.template_name),
- .with_block => if (self.with) |w| {
- for (w.assignments) |a| {
- allocator.free(a.key);
- allocator.free(a.value_expr);
+ .tag => {
+ if (self.tag) |t| {
+ switch (t.body) {
+ .autoescape => {
+ allocator.free(t.body.autoescape.body);
+ },
+ .block => {
+ const body_copy = t.body.block.body;
+ for (body_copy) |n| n.deinit(allocator);
+ allocator.free(body_copy);
+ },
+ .cycle => {
+ allocator.free(t.body.cycle.values);
+ },
+ .filter_block => {
+ const body_copy = t.body.filter_block.body;
+ for (body_copy) |n| n.deinit(allocator);
+ allocator.free(body_copy);
+ },
+ .firstof => {
+ allocator.free(t.body.firstof.values);
+ },
+ .@"for" => {
+ const body_copy = t.body.@"for".body;
+ for (body_copy) |n| n.deinit(allocator);
+ const empty_body_copy = t.body.@"for".empty_body;
+ for (empty_body_copy) |n| n.deinit(allocator);
+ allocator.free(body_copy);
+ allocator.free(empty_body_copy);
+ },
+ .@"if" => {
+ const true_copy = t.body.@"if".true_body;
+ for (true_copy) |n| n.deinit(allocator);
+ const false_copy = t.body.@"if".false_body;
+ for (false_copy) |n| n.deinit(allocator);
+ allocator.free(true_copy);
+ allocator.free(false_copy);
+ },
+ .load => {
+ allocator.free(t.body.load.libraries);
+ },
+ .partialdef => {
+ const body_copy = t.body.partialdef.body;
+ for (body_copy) |n| n.deinit(allocator);
+ allocator.free(body_copy);
+ },
+ .querystring => {
+ allocator.free(t.body.querystring.modifications);
+ },
+ .spaceless => {
+ const body_copy = t.body.spaceless.body;
+ for (body_copy) |n| n.deinit(allocator);
+ allocator.free(body_copy);
+ },
+ .url => {
+ allocator.free(t.body.url.args);
+ },
+ .with => {
+ const body_copy = t.body.with.body;
+ for (body_copy) |n| n.deinit(allocator);
+ allocator.free(body_copy);
+ allocator.free(t.body.with.assignments);
+ },
+ else => {},
+ }
}
- allocator.free(w.assignments);
- const body_copy = w.body;
- for (body_copy) |n| n.deinit(allocator);
- allocator.free(w.body);
- allocator.free(w.raw_open);
- allocator.free(w.raw_close);
},
- .now => if (self.now) |n| allocator.free(n.format),
- .extends => if (self.extends) |e| allocator.free(e.parent_name),
- .block => if (self.block) |b| {
- allocator.free(b.name);
- const body_copy = b.body;
- for (body_copy) |n| n.deinit(allocator);
- allocator.free(b.body);
- allocator.free(b.raw_open);
- allocator.free(b.raw_close);
- },
- .super => {},
- .filter_block => if (self.filter_block) |fb| {
- allocator.free(fb.filters);
- const body_copy = fb.body;
- for (body_copy) |n| n.deinit(allocator);
- allocator.free(fb.body);
- },
- .autoescape => if (self.autoescape) |ae| {
- const body_copy = ae.body;
- for (body_copy) |n| n.deinit(allocator);
- allocator.free(ae.body);
- allocator.free(ae.raw_open);
- allocator.free(ae.raw_close);
- },
- .spaceless => if (self.spaceless) |sl| {
- const body_copy = sl.body;
- for (body_copy) |n| n.deinit(allocator);
- allocator.free(sl.body);
- allocator.free(sl.raw_open);
- allocator.free(sl.raw_close);
- },
- .url => if (self.url) |u| {
- allocator.free(u.name);
- for (u.args) |a| allocator.free(a);
- allocator.free(u.args);
- },
- .cycle => if (self.cycle) |c| {
- for (c.values) |v| allocator.free(v);
- allocator.free(c.values);
- },
- .firstof => if (self.firstof) |fo| {
- for (fo.values) |v| allocator.free(v);
- allocator.free(fo.values);
- },
- .load => if (self.load) |l| {
- for (l.libraries) |lib| allocator.free(lib);
- allocator.free(l.libraries);
- },
- .csrf_token => {},
- .lorem => if (self.lorem) |l| {
- if (l.count) |c| allocator.free(c);
- if (l.method) |m| allocator.free(m);
- if (l.format) |f| allocator.free(f);
- },
- .debug => {},
- .partialdef => if (self.partialdef) |pd| {
- allocator.free(pd.name);
- allocator.free(pd.raw_open);
- allocator.free(pd.raw_close);
- for (pd.body) |n| n.deinit(allocator);
- allocator.free(pd.body);
- },
- .partial => if (self.partial) |p| {
- allocator.free(p.name);
- },
- .querystring => if (self.querystring) |qs| {
- for (qs.modifications) |m| allocator.free(m);
- allocator.free(qs.modifications);
- },
- .regroup => if (self.regroup) |r| {
- allocator.free(r.source);
- allocator.free(r.by);
- allocator.free(r.as_var);
- },
- .resetcycle => if (self.resetcycle) |rc| {
- if (rc.cycle_name) |name| allocator.free(name);
- },
- .widthratio => if (self.widthratio) |wr| {
- allocator.free(wr.value);
- allocator.free(wr.max_value);
- if (wr.divisor) |d| allocator.free(d);
- },
- .templatetag => {},
+ // .with_block => if (self.with) |w| {
+ // for (w.assignments) |a| {
+ // allocator.free(a.key);
+ // allocator.free(a.value_expr);
+ // }
+ // allocator.free(w.assignments);
+ // const body_copy = w.body;
+ // for (body_copy) |n| n.deinit(allocator);
+ // allocator.free(w.body);
+ // allocator.free(w.raw_open);
+ // allocator.free(w.raw_close);
+ // },
}
}
@@ -411,312 +435,504 @@ pub const Node = struct {
},
};
},
-
- .if_block => {
- const true_body_copy = try allocator.alloc(Node, self.@"if".?.true_body.len);
- errdefer allocator.free(true_body_copy);
-
- for (self.@"if".?.true_body, 0..) |child, j| {
- true_body_copy[j] = try child.clone(allocator);
- }
-
- const false_body_copy = try allocator.alloc(Node, self.@"if".?.false_body.len);
- errdefer allocator.free(false_body_copy);
-
- for (self.@"if".?.false_body, 0..) |child, j| {
- false_body_copy[j] = try child.clone(allocator);
- }
-
- return Node{
- .type = .if_block,
- .@"if" = .{
- .condition = try allocator.dupe(u8, self.@"if".?.condition),
- .true_body = true_body_copy,
- .false_body = false_body_copy,
- .raw_open = try allocator.dupe(u8, self.@"if".?.raw_open),
- .raw_close = try allocator.dupe(u8, self.@"if".?.raw_close),
- },
- };
- },
-
- .for_block => {
- const body_copy = try allocator.alloc(Node, self.@"for".?.body.len);
- errdefer allocator.free(body_copy);
-
- for (self.@"for".?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
- }
-
- const empty_body_copy = try allocator.alloc(Node, self.@"for".?.empty_body.len);
- errdefer allocator.free(empty_body_copy);
-
- for (self.@"for".?.empty_body, 0..) |child, j| {
- empty_body_copy[j] = try child.clone(allocator);
- }
-
- return Node{
- .type = .for_block,
- .@"for" = .{
- .loop_var = try allocator.dupe(u8, self.@"for".?.loop_var),
- .iterable = try allocator.dupe(u8, self.@"for".?.iterable),
- .body = body_copy,
- .empty_body = empty_body_copy,
- .raw_open = try allocator.dupe(u8, self.@"for".?.raw_open),
- .raw_close = try allocator.dupe(u8, self.@"for".?.raw_close),
- },
- };
- },
-
- .block => {
- const body_copy = try allocator.alloc(Node, self.block.?.body.len);
- errdefer allocator.free(body_copy);
-
- for (self.block.?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
- }
-
- return Node{
- .type = .block,
- .block = .{
- .name = try allocator.dupe(u8, self.block.?.name),
- .body = body_copy,
- .raw_open = try allocator.dupe(u8, self.block.?.raw_open),
- .raw_close = try allocator.dupe(u8, self.block.?.raw_close),
- },
- };
- },
-
- .extends => {
- return Node{
- .type = .extends,
- .extends = .{
- .parent_name = try allocator.dupe(u8, self.extends.?.parent_name),
- },
- };
- },
-
- .super => {
- return Node{
- .type = .super,
- };
- },
.tag => {
- return Node{
- .type = .tag,
- .tag = .{
- .name = try allocator.dupe(u8, self.tag.?.name),
- .args = try allocator.dupe(u8, self.tag.?.args),
- .raw = try allocator.dupe(u8, self.tag.?.raw),
+ switch (self.tag.?.kind) {
+ .debug => return Node{ .type = .tag, .tag = .{
+ .kind = .debug,
+ .args = "",
+ .body = .{ .debug = true },
+ .raw = self.tag.?.raw,
+ } },
+ .firstof => {
+ const values_copy = try allocator.alloc([]const u8, self.tag.?.body.firstof.values.len);
+ errdefer allocator.free(values_copy);
+
+ for (self.tag.?.body.firstof.values, 0..) |f, i| {
+ values_copy[i] = try allocator.dupe(u8, f);
+ }
+
+ const fallback_copy = try allocator.dupe(u8, self.tag.?.body.firstof.fallback);
+
+ return Node{ .type = .tag, .tag = .{
+ .kind = .firstof,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .body = .{ .firstof = .{ .values = values_copy, .fallback = fallback_copy } },
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ } };
},
- };
- },
- .include => {
- return Node{
- .type = .include,
- .include = .{
- .template_name = try allocator.dupe(u8, self.include.?.template_name),
+ .if_block => {
+ const true_body_copy = try allocator.alloc(Node, self.tag.?.body.@"if".true_body.len);
+ errdefer allocator.free(true_body_copy);
+
+ for (self.tag.?.body.@"if".true_body, 0..) |child, j| {
+ true_body_copy[j] = try child.clone(allocator);
+ }
+
+ const false_body_copy = try allocator.alloc(Node, self.tag.?.body.@"if".false_body.len);
+ errdefer allocator.free(false_body_copy);
+
+ for (self.tag.?.body.@"if".false_body, 0..) |child, j| {
+ false_body_copy[j] = try child.clone(allocator);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .if_block,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .body = .{
+ .@"if" = .{
+ .condition = try allocator.dupe(u8, self.tag.?.body.@"if".condition),
+ .true_body = true_body_copy,
+ .false_body = false_body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.@"if".raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.@"if".raw_close),
+ },
+ },
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ },
+ };
},
- };
- },
- .with_block => {
- const body_copy = try allocator.alloc(Node, self.with.?.body.len);
- errdefer allocator.free(body_copy);
+ .for_block => {
+ const body_copy = try allocator.alloc(Node, self.tag.?.body.@"for".body.len);
+ errdefer allocator.free(body_copy);
- for (self.with.?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
- }
+ for (self.tag.?.body.@"for".body, 0..) |child, j| {
+ body_copy[j] = try child.clone(allocator);
+ }
- const assignments_copy = try allocator.alloc(Assignment, self.with.?.assignments.len);
- errdefer allocator.free(assignments_copy);
+ const empty_body_copy = try allocator.alloc(Node, self.tag.?.body.@"for".empty_body.len);
+ errdefer allocator.free(empty_body_copy);
- for (self.with.?.assignments, 0..) |f, i| {
- assignments_copy[i] = .{
- .key = try allocator.dupe(u8, f.key),
- .value_expr = try allocator.dupe(u8, f.value_expr),
- .is_literal = f.is_literal,
- };
- }
+ for (self.tag.?.body.@"for".empty_body, 0..) |child, j| {
+ empty_body_copy[j] = try child.clone(allocator);
+ }
- return Node{
- .type = .with_block,
- .with = .{
- .assignments = assignments_copy,
- .body = body_copy,
- .raw_open = try allocator.dupe(u8, self.with.?.raw_open),
- .raw_close = try allocator.dupe(u8, self.with.?.raw_close),
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .for_block,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .body = .{
+ .@"for" = .{
+ .loop_var = try allocator.dupe(u8, self.tag.?.body.@"for".loop_var),
+ .iterable = try allocator.dupe(u8, self.tag.?.body.@"for".iterable),
+ .body = body_copy,
+ .empty_body = empty_body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.@"for".raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.@"for".raw_close),
+ },
+ },
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ },
+ };
},
- };
- },
- .now => {
- return Node{ .type = .now, .now = .{ .format = try allocator.dupe(u8, self.now.?.format) } };
- },
- .filter_block => {
- const body_copy = try allocator.alloc(Node, self.filter_block.?.body.len);
- errdefer allocator.free(body_copy);
+ .block => {
+ const body_copy = try allocator.alloc(Node, self.tag.?.body.block.body.len);
+ errdefer allocator.free(body_copy);
- for (self.filter_block.?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
- }
+ for (self.tag.?.body.block.body, 0..) |child, j| {
+ body_copy[j] = try child.clone(allocator);
+ }
- return Node{
- .type = .filter_block,
- .filter_block = .{
- .filters = try allocator.dupe(u8, self.filter_block.?.filters),
- .body = body_copy,
- .raw_open = try allocator.dupe(u8, self.filter_block.?.raw_open),
- .raw_close = try allocator.dupe(u8, self.filter_block.?.raw_close),
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .block,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .body = .{
+ .block = .{
+ .name = try allocator.dupe(u8, self.tag.?.body.block.name),
+ .body = body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.block.raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.block.raw_close),
+ },
+ },
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ },
+ };
},
- };
- },
- .autoescape => {
- const body_copy = try allocator.alloc(Node, self.autoescape.?.body.len);
- errdefer allocator.free(body_copy);
-
- for (self.autoescape.?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
- }
-
- return Node{
- .type = .autoescape,
- .autoescape = .{
- .body = body_copy,
- .raw_open = try allocator.dupe(u8, self.autoescape.?.raw_open),
- .raw_close = try allocator.dupe(u8, self.autoescape.?.raw_close),
- .enabled = self.autoescape.?.enabled,
+ .extends => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .extends,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .body = .{
+ .extends = .{
+ .parent_name = try allocator.dupe(u8, self.tag.?.body.extends.parent_name),
+ },
+ },
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ },
+ };
},
- };
- },
- .spaceless => {
- const body_copy = try allocator.alloc(Node, self.spaceless.?.body.len);
- errdefer allocator.free(body_copy);
-
- for (self.spaceless.?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
- }
-
- return Node{
- .type = .spaceless,
- .spaceless = .{
- .body = body_copy,
- .raw_open = try allocator.dupe(u8, self.spaceless.?.raw_open),
- .raw_close = try allocator.dupe(u8, self.spaceless.?.raw_close),
+ .super => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .super,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .body = .{ .super = true },
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ },
+ };
},
- };
- },
- .url => {
- const args_copy = try allocator.alloc([]const u8, self.url.?.args.len);
- errdefer allocator.free(args_copy);
-
- for (self.url.?.args, 0..) |f, i| {
- args_copy[i] = try allocator.dupe(u8, f);
- }
-
- return Node{ .type = .url, .url = .{
- .name = try allocator.dupe(u8, self.url.?.name),
- .args = args_copy,
- } };
- },
- .cycle => {
- const values_copy = try allocator.alloc([]const u8, self.cycle.?.values.len);
- errdefer allocator.free(values_copy);
-
- for (self.cycle.?.values, 0..) |f, i| {
- values_copy[i] = try allocator.dupe(u8, f);
- }
-
- return Node{ .type = .cycle, .cycle = .{ .values = values_copy } };
- },
- .firstof => {
- const values_copy = try allocator.alloc([]const u8, self.firstof.?.values.len);
- errdefer allocator.free(values_copy);
-
- for (self.firstof.?.values, 0..) |f, i| {
- values_copy[i] = try allocator.dupe(u8, f);
- }
-
- return Node{ .type = .firstof, .firstof = .{ .values = values_copy } };
- },
- .load => {
- const libraries_copy = try allocator.alloc([]const u8, self.load.?.libraries.len);
- errdefer allocator.free(libraries_copy);
-
- for (self.load.?.libraries, 0..) |f, i| {
- libraries_copy[i] = try allocator.dupe(u8, f);
- }
-
- return Node{ .type = .load, .load = .{ .libraries = libraries_copy } };
- },
- .csrf_token => {
- return Node{
- .type = .csrf_token,
- };
- },
- .lorem => {
- return Node{
- .type = .lorem,
- .lorem = .{
- .count = if (self.lorem.?.count) |c| try allocator.dupe(u8, c) else null,
- .method = if (self.lorem.?.method) |m| try allocator.dupe(u8, m) else null,
- .format = if (self.lorem.?.format) |f| try allocator.dupe(u8, f) else null,
+ .include => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .include,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ .body = .{
+ .include = .{
+ .template_path = try allocator.dupe(u8, self.tag.?.body.include.template_path),
+ },
+ },
+ },
+ };
},
- };
- },
- .debug => {
- return Node{ .type = .debug };
- },
- .partialdef => {
- const body_copy = try allocator.alloc(Node, self.partialdef.?.body.len);
- errdefer allocator.free(body_copy);
+ .with_block => {
+ const body_copy = try allocator.alloc(Node, self.tag.?.body.with.body.len);
+ errdefer allocator.free(body_copy);
- for (self.partialdef.?.body, 0..) |child, j| {
- body_copy[j] = try child.clone(allocator);
+ for (self.tag.?.body.with.body, 0..) |child, j| {
+ body_copy[j] = try child.clone(allocator);
+ }
+
+ const assignments_copy = try allocator.alloc(Assignment, self.tag.?.body.with.assignments.len);
+ errdefer allocator.free(assignments_copy);
+
+ for (self.tag.?.body.with.assignments, 0..) |f, i| {
+ assignments_copy[i] = .{
+ .key = try allocator.dupe(u8, f.key),
+ .value_expr = try allocator.dupe(u8, f.value_expr),
+ .is_literal = f.is_literal,
+ };
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .with_block,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .with = .{
+ .assignments = assignments_copy,
+ .body = body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.with.raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.with.raw_close),
+ },
+ },
+ },
+ };
+ },
+ .now => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .now,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .now = .{
+ .format = try allocator.dupe(u8, self.tag.?.body.now.format),
+ },
+ },
+ },
+ };
+ },
+ .autoescape => {
+ const body_copy = try allocator.alloc(Node, self.tag.?.body.autoescape.body.len);
+ errdefer allocator.free(body_copy);
+
+ for (self.tag.?.body.autoescape.body, 0..) |child, j| {
+ body_copy[j] = try child.clone(allocator);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .autoescape,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .autoescape = .{
+ .body = body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.autoescape.raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.autoescape.raw_close),
+ .enabled = self.tag.?.body.autoescape.enabled,
+ },
+ },
+ },
+ };
+ },
+ .spaceless => {
+ const body_copy = try allocator.alloc(Node, self.tag.?.body.spaceless.body.len);
+ errdefer allocator.free(body_copy);
+
+ for (self.tag.?.body.spaceless.body, 0..) |child, j| {
+ body_copy[j] = try child.clone(allocator);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .spaceless,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .spaceless = .{
+ .body = body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.spaceless.raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.spaceless.raw_close),
+ },
+ },
+ },
+ };
+ },
+ .url => {
+ const args_copy = try allocator.alloc([]const u8, self.tag.?.body.url.args.len);
+ errdefer allocator.free(args_copy);
+
+ for (self.tag.?.body.url.args, 0..) |f, i| {
+ args_copy[i] = try allocator.dupe(u8, f);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .url,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .url = .{
+ .name = try allocator.dupe(u8, self.tag.?.body.url.name),
+ .args = args_copy,
+ },
+ },
+ },
+ };
+ },
+ .cycle => {
+ const values_copy = try allocator.alloc([]const u8, self.tag.?.body.cycle.values.len);
+ errdefer allocator.free(values_copy);
+
+ for (self.tag.?.body.cycle.values, 0..) |f, i| {
+ values_copy[i] = try allocator.dupe(u8, f);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .cycle,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .cycle = .{
+ .values = values_copy,
+ },
+ },
+ },
+ };
+ },
+ .load => {
+ const libraries_copy = try allocator.alloc([]const u8, self.tag.?.body.load.libraries.len);
+ errdefer allocator.free(libraries_copy);
+
+ for (self.tag.?.body.load.libraries, 0..) |f, i| {
+ libraries_copy[i] = try allocator.dupe(u8, f);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .load,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .load = .{
+ .libraries = libraries_copy,
+ },
+ },
+ },
+ };
+ },
+ .lorem => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .lorem,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .lorem = .{
+ .count = if (self.tag.?.body.lorem.count) |c| try allocator.dupe(u8, c) else null,
+ .method = if (self.tag.?.body.lorem.method) |m| try allocator.dupe(u8, m) else null,
+ .random = self.tag.?.body.lorem.random,
+ },
+ },
+ },
+ };
+ },
+ .partialdef => {
+ const body_copy = try allocator.alloc(Node, self.tag.?.body.partialdef.body.len);
+ errdefer allocator.free(body_copy);
+
+ for (self.tag.?.body.partialdef.body, 0..) |child, j| {
+ body_copy[j] = try child.clone(allocator);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .partialdef,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .partialdef = .{
+ .name = try allocator.dupe(u8, self.tag.?.body.partialdef.name),
+ .body = body_copy,
+ .raw_open = try allocator.dupe(u8, self.tag.?.body.partialdef.raw_open),
+ .raw_close = try allocator.dupe(u8, self.tag.?.body.partialdef.raw_close),
+ },
+ },
+ },
+ };
+ },
+ .partial => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .partial,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .partial = .{
+ .name = try allocator.dupe(u8, self.tag.?.body.partial.name),
+ },
+ },
+ },
+ };
+ },
+ .querystring => {
+ const modifications_copy = try allocator.alloc([]const u8, self.tag.?.body.querystring.modifications.len);
+ errdefer allocator.free(modifications_copy);
+
+ for (self.tag.?.body.querystring.modifications, 0..) |f, i| {
+ modifications_copy[i] = try allocator.dupe(u8, f);
+ }
+
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .querystring,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .querystring = .{
+ .modifications = modifications_copy,
+ },
+ },
+ },
+ };
+ },
+ .regroup => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .regroup,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .regroup = .{
+ .source = try allocator.dupe(u8, self.tag.?.body.regroup.source),
+ .by = try allocator.dupe(u8, self.tag.?.body.regroup.by),
+ .as_var = try allocator.dupe(u8, self.tag.?.body.regroup.as_var),
+ },
+ },
+ },
+ };
+ },
+ .resetcycle => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .resetcycle,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .resetcycle = .{
+ .cycle_name = if (self.tag.?.body.resetcycle.cycle_name) |n| try allocator.dupe(u8, n) else null,
+ },
+ },
+ },
+ };
+ },
+ .widthratio => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .widthratio,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .widthratio = .{
+ .value = try allocator.dupe(u8, self.tag.?.body.widthratio.value),
+ .max_value = try allocator.dupe(u8, self.tag.?.body.widthratio.max_value),
+ .divisor = if (self.tag.?.body.widthratio.divisor) |d| try allocator.dupe(u8, d) else null,
+ },
+ },
+ },
+ };
+ },
+ .templatetag => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .templatetag,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+
+ .body = .{
+ .templatetag = .{
+ .kind = self.tag.?.body.templatetag.kind,
+ },
+ },
+ },
+ };
+ },
+ .csrf_token => {
+ return Node{
+ .type = .tag,
+ .tag = .{
+ .kind = .csrf_token,
+ .args = try allocator.dupe(u8, self.tag.?.args),
+ .raw = try allocator.dupe(u8, self.tag.?.raw),
+ .body = .{
+ .csrf_token = true,
+ },
+ },
+ };
+ },
+ else => unreachable,
}
-
- return Node{ .type = .partialdef, .partialdef = .{
- .name = try allocator.dupe(u8, self.partialdef.?.name),
- .body = body_copy,
- .raw_open = try allocator.dupe(u8, self.partialdef.?.raw_open),
- .raw_close = try allocator.dupe(u8, self.partialdef.?.raw_close),
- } };
- },
- .partial => {
- return Node{ .type = .partial, .partial = .{
- .name = try allocator.dupe(u8, self.partial.?.name),
- } };
- },
- .querystring => {
- const modifications = try allocator.alloc([]const u8, self.querystring.?.modifications.len);
- errdefer allocator.free(modifications);
-
- for (self.querystring.?.modifications, 0..) |f, i| {
- modifications[i] = try allocator.dupe(u8, f);
- }
-
- return Node{ .type = .querystring, .querystring = .{
- .modifications = modifications,
- } };
- },
- .regroup => {
- return Node{ .type = .regroup, .regroup = .{
- .source = try allocator.dupe(u8, self.regroup.?.source),
- .by = try allocator.dupe(u8, self.regroup.?.by),
- .as_var = try allocator.dupe(u8, self.regroup.?.as_var),
- } };
- },
- .resetcycle => {
- return Node{ .type = .resetcycle, .resetcycle = .{
- .cycle_name = if (self.resetcycle.?.cycle_name) |n| try allocator.dupe(u8, n) else null,
- } };
- },
- .widthratio => {
- return Node{ .type = .widthratio, .widthratio = .{
- .value = try allocator.dupe(u8, self.widthratio.?.value),
- .max_value = try allocator.dupe(u8, self.widthratio.?.max_value),
- .divisor = if (self.widthratio.?.divisor) |d| try allocator.dupe(u8, d) else null,
- } };
- },
- .templatetag => {
- return Node{ .type = .templatetag, .templatetag = .{ .kind = self.templatetag.?.kind } };
},
}
}
@@ -730,6 +946,35 @@ pub const Parser = struct {
return .{ .template = template };
}
+ pub fn debugNodes(self: Parser, data: struct {
+ nodes: []Node,
+ name: ?[]const u8 = null,
+ }) void {
+ _ = self;
+ var prefix: []const u8 = "";
+
+ if (data.name) |name| {
+ prefix = " - ";
+ std.debug.print("{s}:\n", .{name});
+ }
+
+ for (data.nodes) |node| {
+ if (node.type == .tag) {
+ if (node.type == .tag and node.tag.?.kind == .block) {
+ std.debug.print("{s}tag: {any}, name: {s}\n", .{ prefix, node.tag.?.kind, node.tag.?.body.block.name });
+ } else {
+ std.debug.print("{s}tag: {any}\n", .{ prefix, node.tag.?.kind });
+ }
+ }
+ if (node.type == .text) {
+ std.debug.print("{s}text: {s}\n", .{ prefix, node.text.?.content });
+ }
+ if (node.type == .variable) {
+ std.debug.print("{s}variable: {s}\n", .{ prefix, node.variable.?.expr });
+ }
+ }
+ }
+
fn advance(self: *Parser, n: usize) void {
self.pos += n;
if (self.pos > self.template.len) self.pos = self.template.len;
@@ -798,8 +1043,10 @@ pub const Parser = struct {
}
try list.append(allocator, .{
- .key = try allocator.dupe(u8, key),
- .value_expr = try allocator.dupe(u8, value),
+ // .key = try allocator.dupe(u8, key),
+ // .value_expr = try allocator.dupe(u8, value),
+ .key = key,
+ .value_expr = value,
.is_literal = in_quote,
});
}
@@ -807,7 +1054,7 @@ pub const Parser = struct {
return try list.toOwnedSlice(allocator);
}
- fn parsePartialDefBlock(self: *Parser, allocator: std.mem.Allocator, name: []const u8, raw_open: []const u8) ParserError!Node {
+ fn parsePartialDefBlock(self: *Parser, allocator: std.mem.Allocator, name: []const u8, raw_open: []const u8) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
@@ -825,31 +1072,23 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "partialdef")) {
+ if (tag_node.tag.?.kind == .partialdef) {
depth += 1;
try body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endpartialdef")) {
+ if (tag_node.tag.?.kind == .endpartialdef) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- return Node{
- .type = .partialdef,
- .partialdef = .{
- .name = name,
- .body = try body.toOwnedSlice(allocator),
- .raw_open = raw_open,
- .raw_close = raw_close,
- },
- };
+ return TagNodeBody{ .partialdef = .{
+ .name = name,
+ .body = try body.toOwnedSlice(allocator),
+ .raw_open = raw_open,
+ .raw_close = raw_close,
+ } };
}
try body.append(allocator, tag_node);
@@ -865,7 +1104,12 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseAutoescapeBlock(self: *Parser, allocator: std.mem.Allocator, enabled: bool, raw_open: []const u8) ParserError!Node {
+ fn parseAutoescapeBlock(
+ self: *Parser,
+ allocator: std.mem.Allocator,
+ enabled: bool,
+ raw_open: []const u8,
+ ) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
@@ -883,24 +1127,18 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "autoescape")) {
+ if (tag_node.tag.?.kind == .autoescape) {
depth += 1;
try body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endautoescape")) {
+ if (tag_node.tag.?.kind == .endautoescape) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- return Node{
- .type = .autoescape,
+ return TagNodeBody{
.autoescape = .{
.enabled = enabled,
.body = try body.toOwnedSlice(allocator),
@@ -913,8 +1151,6 @@ pub const Parser = struct {
try body.append(allocator, tag_node);
continue;
}
-
- try body.append(allocator, tag_node);
} else {
self.advance(1);
}
@@ -923,11 +1159,11 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseVerbatim(self: *Parser, allocator: std.mem.Allocator) ParserError!Node {
+ fn parseVerbatimBlock(self: *Parser, allocator: std.mem.Allocator, name: []const u8, raw_open: []const u8) ParserError!TagNodeBody {
+ _ = allocator;
const start = self.pos;
var depth: usize = 1;
-
while (self.pos < self.template.len and depth > 0) {
if (self.peek(2)) |p| {
if (std.mem.eql(u8, p, "{%")) {
@@ -943,19 +1179,22 @@ pub const Parser = struct {
const inner = std.mem.trim(u8, self.template[content_start..self.pos], " \t\r\n");
+ const endverbatim_idx = std.mem.indexOf(u8, inner, "endverbatim");
if (std.mem.eql(u8, inner, "verbatim")) {
depth += 1;
- } else if (std.mem.eql(u8, inner, "endverbatim")) {
+ } else if (endverbatim_idx != null) {
depth -= 1;
if (depth == 0) {
// Copia até o início da tag endverbatim
const content_end = content_start - 3; // retrocede "{% "
- const content = try allocator.dupe(u8, self.template[start..content_end]);
+ const content = self.template[start..content_end];
self.advance(2); // consome %}
- return Node{
- .type = .text,
- .text = .{ .content = content },
- };
+ return TagNodeBody{ .verbatim = .{
+ .name = name,
+ .content = content,
+ .raw_open = raw_open,
+ .raw_close = self.template[start..self.pos],
+ } };
}
}
@@ -963,13 +1202,14 @@ pub const Parser = struct {
continue;
}
}
+
self.advance(1);
}
return error.UnclosedVerbatim;
}
- fn parseSpacelessBlock(self: *Parser, allocator: std.mem.Allocator, raw_open: []const u8) ParserError!Node {
+ fn parseSpacelessBlock(self: *Parser, allocator: std.mem.Allocator, raw_open: []const u8) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
@@ -988,26 +1228,17 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "spaceless")) {
+ if (tag_node.tag.?.kind == .spaceless) {
depth += 1;
- // Ignora a tag open aninhada
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ try body.append(allocator, tag_node);
continue;
}
-
- if (std.mem.eql(u8, tag_name, "endspaceless")) {
+ if (tag_node.tag.?.kind == .endspaceless) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- return Node{
- .type = .spaceless,
+ return TagNodeBody{
.spaceless = .{
.body = try body.toOwnedSlice(allocator),
.raw_open = raw_open,
@@ -1015,9 +1246,6 @@ pub const Parser = struct {
},
};
}
-
- // depth > 0: endspaceless aninhado — IGNORA a tag
- // NÃO adicione ao body
continue;
}
@@ -1030,7 +1258,7 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseFilterBlock(self: *Parser, allocator: std.mem.Allocator, filters_raw: []const u8, raw_open: []const u8) ParserError!Node {
+ fn parseFilterBlock(self: *Parser, allocator: std.mem.Allocator, filters_raw: []const u8, raw_open: []const u8) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
@@ -1048,35 +1276,25 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "filter")) {
+ if (tag_node.tag.?.kind == .filter_block) {
depth += 1;
try body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endfilter")) {
+ if (tag_node.tag.?.kind == .endfilter) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- // const filters = try allocator.dupe(u8, filters_raw);
-
- return Node{
- .type = .filter_block,
+ return TagNodeBody{
.filter_block = .{
- .filters = filters_raw, // já duped
+ .filters = filters_raw,
.body = try body.toOwnedSlice(allocator),
.raw_open = raw_open,
.raw_close = raw_close,
},
};
}
-
try body.append(allocator, tag_node);
continue;
}
@@ -1090,7 +1308,12 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseBlockBlock(self: *Parser, allocator: std.mem.Allocator, name: []const u8, raw_open: []const u8) ParserError!Node {
+ fn parseBlockBlock(
+ self: *Parser,
+ allocator: std.mem.Allocator,
+ name: []const u8,
+ raw_open: []const u8,
+ ) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
@@ -1105,8 +1328,7 @@ pub const Parser = struct {
if (try self.parseVariable(allocator)) |node| {
if (node.variable) |v| {
if (std.mem.eql(u8, v.expr, "block.super")) {
- try body.append(allocator, Node{ .type = .super, .super = true });
- allocator.free(v.expr);
+ try body.append(allocator, Node{ .type = .tag, .tag = .{ .kind = .super, .args = "", .raw = raw_open, .body = .{ .super = true } } });
continue;
}
}
@@ -1115,46 +1337,51 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "block")) {
+ if (tag_node.tag.?.kind == .block) {
depth += 1;
try body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endblock")) {
+ if (tag_node.tag.?.kind == .endblock) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- return Node{
- .type = .block,
- .block = .{
- .name = name,
- .body = try body.toOwnedSlice(allocator),
- .raw_open = raw_open,
- .raw_close = raw_close,
- },
- };
+ return TagNodeBody{ .block = .{
+ .name = name,
+ .body = try body.toOwnedSlice(allocator),
+ .raw_open = raw_open,
+ .raw_close = raw_close,
+ } };
}
try body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "comment")) {
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ // var tag = try self.parseTag(allocator);
+ // if (tag != null) {
+ // if (tag.?.tag.?.kind == .comment) {
+ // std.debug.print("vou parsear comentário\n", .{});
+ // try self.parseComment();
+ // continue;
+ // } else {
+ // if (try self.parseTagContent(allocator, tag.?)) |tn| {
+ // tag.?.tag.?.body = tn;
+ // try body.append(allocator, tag.?);
+ // }
+ // continue;
+ // }
+ // }
+ var tag: Node = tag_node;
+ if (tag_node.tag.?.kind == .comment) {
try self.parseComment();
continue;
} else {
- const node_parsed: ?Node = self.parseTagContent(allocator, tag_node) catch null;
- if (node_parsed) |n| {
- try body.append(allocator, n);
+ if (try self.parseTagContent(allocator, tag_node)) |tn| {
+ tag.tag.?.body = tn;
+ try body.append(allocator, tag);
continue;
}
}
@@ -1168,7 +1395,7 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseWithBlock(self: *Parser, allocator: std.mem.Allocator, assignments: []const Assignment, raw_open: []const u8) ParserError!Node {
+ fn parseWithBlock(self: *Parser, allocator: std.mem.Allocator, assignments: []const Assignment, raw_open: []const u8) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
@@ -1186,24 +1413,18 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "with")) {
+ if (tag_node.tag.?.kind == .with_block) {
depth += 1;
try body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endwith")) {
+ if (tag_node.tag.?.kind == .endwith_block) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- return Node{
- .type = .with_block,
+ return TagNodeBody{
.with = .{
.assignments = assignments,
.body = try body.toOwnedSlice(allocator),
@@ -1271,6 +1492,7 @@ pub const Parser = struct {
}
fn parseText(self: *Parser, allocator: std.mem.Allocator) ParserError!?Node {
+ _ = allocator;
const start = self.pos;
while (self.pos < self.template.len) {
@@ -1284,10 +1506,9 @@ pub const Parser = struct {
if (self.pos == start) return null;
- const content = try allocator.dupe(u8, self.template[start..self.pos]);
return Node{
.type = .text,
- .text = .{ .content = content },
+ .text = .{ .content = self.template[start..self.pos] },
};
}
@@ -1329,15 +1550,17 @@ pub const Parser = struct {
const filter_arg = if (colon_pos) |cp| std.mem.trim(u8, filter_part[cp + 1 ..], " \"") else null;
try filters.append(allocator, .{
- .name = try allocator.dupe(u8, filter_name),
- .arg = if (filter_arg) |a| try allocator.dupe(u8, a) else null,
+ // .name = try allocator.dupe(u8, filter_name),
+ .name = filter_name,
+ // .arg = if (filter_arg) |a| try allocator.dupe(u8, a) else null,
+ .arg = if (filter_arg) |a| a else null,
});
pipe_pos = std.mem.lastIndexOfScalar(u8, full_expr[0..expr_end], '|');
}
const base_expr = std.mem.trim(u8, full_expr[0..expr_end], " \t\r\n");
- const duped_expr = try allocator.dupe(u8, base_expr);
+ // const duped_expr = try allocator.dupe(u8, base_expr);
// Inverte os filters (porque usamos lastIndexOf)
std.mem.reverse(Filter, filters.items);
@@ -1345,13 +1568,15 @@ pub const Parser = struct {
return Node{
.type = .variable,
.variable = .{
- .expr = duped_expr,
+ // .expr = duped_expr,
+ .expr = base_expr,
.filters = try filters.toOwnedSlice(allocator),
},
};
}
fn parseTag(self: *Parser, allocator: std.mem.Allocator) ParserError!?Node {
+ _ = allocator;
if (self.peek(2)) |p| {
if (!std.mem.eql(u8, p, "{%")) return null;
} else return null;
@@ -1378,22 +1603,23 @@ pub const Parser = struct {
const name_raw = inner[0..space_idx];
const args_raw = if (space_idx < inner.len) std.mem.trim(u8, inner[space_idx + 1 ..], " \t\r\n") else "";
- const name = try allocator.dupe(u8, name_raw);
- const args = try allocator.dupe(u8, args_raw);
-
self.advance(2);
- return Node{
- .type = .tag,
- .tag = .{
- .name = name,
- .args = args,
- .raw = raw_slice, // slice original, sem dupe
- },
- };
+ const kind: TagKind = getTagKindByName(name_raw);
+ return Node{ .type = .tag, .tag = .{
+ .kind = kind,
+ .raw = raw_slice,
+ .args = args_raw,
+ .body = .{ .initial = true },
+ } };
}
- fn parseIfBlock(self: *Parser, allocator: std.mem.Allocator, condition: []const u8, raw_open: []const u8) ParserError!Node {
+ fn parseIfBlock(
+ self: *Parser,
+ allocator: std.mem.Allocator,
+ condition: []const u8,
+ raw_open: []const u8,
+ ) ParserError!TagNodeBody {
var true_body = std.ArrayList(Node){};
defer true_body.deinit(allocator);
var false_body = std.ArrayList(Node){};
@@ -1414,47 +1640,33 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "if")) {
+ if (tag_node.tag.?.kind == .if_block) {
depth += 1;
try current_body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endif")) {
+ if (tag_node.tag.?.kind == .endif_block) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
- // Libera name e args da tag endif
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
if (depth == 0) {
- return Node{
- .type = .if_block,
- .@"if" = .{
- .condition = condition,
- .true_body = try true_body.toOwnedSlice(allocator),
- .false_body = try false_body.toOwnedSlice(allocator),
- .raw_open = raw_open,
- .raw_close = raw_close,
- },
- };
+ return TagNodeBody{ .@"if" = .{
+ .condition = condition,
+ .true_body = try true_body.toOwnedSlice(allocator),
+ .false_body = try false_body.toOwnedSlice(allocator),
+ .raw_open = raw_open,
+ .raw_close = raw_close,
+ } };
}
-
- // Se depth > 0, é endif aninhado — adiciona como tag normal
try current_body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "else") and depth == 1) {
+ if (tag_node.tag.?.kind == .else_block and depth == 1) {
current_body = &false_body;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
continue;
}
-
// Qualquer outra tag
try current_body.append(allocator, tag_node);
} else {
@@ -1465,7 +1677,13 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseForBlock(self: *Parser, allocator: std.mem.Allocator, loop_var: []const u8, iterable: []const u8, raw_open: []const u8) ParserError!Node {
+ fn parseForBlock(
+ self: *Parser,
+ allocator: std.mem.Allocator,
+ loop_var: []const u8,
+ iterable: []const u8,
+ raw_open: []const u8,
+ ) ParserError!TagNodeBody {
var body = std.ArrayList(Node){};
defer body.deinit(allocator);
var empty_body = std.ArrayList(Node){};
@@ -1486,45 +1704,33 @@ pub const Parser = struct {
}
if (try self.parseTag(allocator)) |tag_node| {
- const tag_name = tag_node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "for")) {
+ if (tag_node.tag.?.kind == .for_block) {
depth += 1;
try current_body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "endfor")) {
+ if (tag_node.tag.?.kind == .endfor_block) {
depth -= 1;
const raw_close = tag_node.tag.?.raw;
if (depth == 0) {
- // Libera name e args — essa é a tag de fechamento final
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .for_block,
- .@"for" = .{
- .loop_var = loop_var,
- .iterable = iterable,
- .body = try body.toOwnedSlice(allocator),
- .empty_body = try empty_body.toOwnedSlice(allocator),
- .raw_open = raw_open,
- .raw_close = raw_close,
- },
- };
+ return TagNodeBody{ .@"for" = .{
+ .loop_var = loop_var,
+ .iterable = iterable,
+ .body = try body.toOwnedSlice(allocator),
+ .empty_body = try empty_body.toOwnedSlice(allocator),
+ .raw_open = raw_open,
+ .raw_close = raw_close,
+ } };
}
- // depth > 0: endfor aninhado — adiciona como tag normal
try current_body.append(allocator, tag_node);
continue;
}
- if (std.mem.eql(u8, tag_name, "empty") and depth == 1) {
+ if (tag_node.tag.?.kind == .empty and depth == 1) {
current_body = &empty_body;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
continue;
}
@@ -1537,594 +1743,470 @@ pub const Parser = struct {
return error.UnclosedBlock;
}
- fn parseTagContent(self: *Parser, allocator: std.mem.Allocator, tag_node: Node) ParserError!?Node {
- const tag_name = tag_node.tag.?.name;
- if (std.mem.eql(u8, tag_name, "autoescape")) {
- const args = std.mem.trim(u8, tag_node.tag.?.args, " \t\r\n");
- const raw_open = tag_node.tag.?.raw;
+ fn parseTagContent(self: *Parser, allocator: std.mem.Allocator, tag_node: Node) ParserError!?TagNodeBody {
+ switch (tag_node.tag.?.kind) {
+ .autoescape => {
+ const args = std.mem.trim(u8, tag_node.tag.?.args, " \t\r\n");
+ const raw_open = tag_node.tag.?.raw;
- const enabled = if (std.mem.eql(u8, args, "on"))
- true
- else if (std.mem.eql(u8, args, "off"))
- false
- else
- return error.InvalidAutoescapeArgument;
+ const enabled = if (std.mem.eql(u8, args, "on"))
+ true
+ else if (std.mem.eql(u8, args, "off"))
+ false
+ else
+ return error.InvalidAutoescapeArgument;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ const ae_node = try self.parseAutoescapeBlock(allocator, enabled, raw_open);
+ return ae_node;
+ },
+ .block => {
+ const block_name_raw = tag_node.tag.?.args;
+ const raw_open = tag_node.tag.?.raw;
+ const block_name = std.mem.trim(u8, block_name_raw, " \t\r\n");
- const ae_node = try self.parseAutoescapeBlock(allocator, enabled, raw_open);
- return ae_node;
- }
+ const block_node = try self.parseBlockBlock(allocator, block_name, raw_open);
+ return block_node;
+ },
+ .csrf_token => {
+ if (tag_node.tag.?.args.len > 0 and !std.mem.allEqual(u8, tag_node.tag.?.args, ' ')) {
+ return error.InvalidCsrfTokenArgs;
+ }
+ return TagNodeBody{
+ .csrf_token = true,
+ };
+ },
+ .cycle => {
+ const args = tag_node.tag.?.args;
- if (std.mem.eql(u8, tag_name, "spaceless")) {
- const raw_open = tag_node.tag.?.raw;
+ var values = std.ArrayList([]const u8){};
+ defer values.deinit(allocator);
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ var i: usize = 0;
+ while (i < args.len) {
+ while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- const spaceless_node = try self.parseSpacelessBlock(allocator, raw_open);
- return spaceless_node;
- }
+ if (i >= args.len) break;
- if (std.mem.eql(u8, tag_name, "partialdef")) {
- const partial_name = std.mem.trim(u8, tag_node.tag.?.args, " \t\r\n");
- const raw_open = tag_node.tag.?.raw;
+ const start = i;
+ var in_quote = false;
+ var quote_char: u8 = 0;
+ if (args[i] == '"' or args[i] == '\'') {
+ in_quote = true;
+ quote_char = args[i];
+ i += 1;
+ }
- const duped_name = try allocator.dupe(u8, partial_name);
- errdefer allocator.free(duped_name);
+ while (i < args.len) {
+ if (in_quote) {
+ if (args[i] == quote_char) {
+ i += 1;
+ break;
+ }
+ } else {
+ if (std.ascii.isWhitespace(args[i])) break;
+ }
+ i += 1;
+ }
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- const partialdef_node = try self.parsePartialDefBlock(allocator, duped_name, raw_open);
- return partialdef_node;
- }
-
- if (std.mem.eql(u8, tag_name, "partial")) {
- const partial_name = std.mem.trim(u8, tag_node.tag.?.args, " \t\r\n\"'");
-
- const duped_name = try allocator.dupe(u8, partial_name);
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .partial,
- .partial = .{ .name = duped_name },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "lorem")) {
- const args = tag_node.tag.?.args;
-
- var count: ?[]const u8 = null;
- var method: ?[]const u8 = null;
- var format: ?[]const u8 = null;
-
- var parts = std.mem.splitScalar(u8, args, ' ');
- while (parts.next()) |part| {
- const trimmed = std.mem.trim(u8, part, " \t\r\n");
- if (trimmed.len == 0) continue;
-
- const num: usize = std.fmt.parseInt(usize, trimmed, 10) catch 0;
- if (num > 0) {
- count = try allocator.dupe(u8, trimmed);
+ const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
+ try values.append(allocator, value);
}
- if (std.mem.eql(u8, trimmed, "p") or std.mem.eql(u8, trimmed, "w")) {
- method = try allocator.dupe(u8, trimmed);
- } else if (std.mem.eql(u8, trimmed, "html")) {
- format = try allocator.dupe(u8, trimmed);
+ return TagNodeBody{
+ .cycle = .{
+ .values = try values.toOwnedSlice(allocator),
+ },
+ };
+ },
+ .debug => {
+ if (tag_node.tag.?.args.len > 0 and !std.mem.allEqual(u8, tag_node.tag.?.args, ' ')) {
+ return error.InvalidDebugArgs;
}
- }
+ return TagNodeBody{
+ .debug = true,
+ };
+ },
+ .extends => {
+ const parent = std.mem.trim(u8, tag_node.tag.?.args, " \t\"");
+ return TagNodeBody{
+ .extends = .{
+ .parent_name = parent,
+ },
+ };
+ },
+ .filter_block => {
+ const filters_raw = tag_node.tag.?.args;
+ const raw_open = tag_node.tag.?.raw;
+ const filter_node = try self.parseFilterBlock(allocator, filters_raw, raw_open);
+ return filter_node;
+ },
+ .firstof => {
+ const args = tag_node.tag.?.args;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ var values = std.ArrayList([]const u8){};
+ defer values.deinit(allocator);
+ var i: usize = 0;
+ var fallback: []const u8 = "";
+ while (i < args.len) {
+ while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- return Node{
- .type = .lorem,
- .lorem = .{
- .count = count,
- .method = method,
- .format = format,
- },
- };
- }
+ if (i >= args.len) break;
- if (std.mem.eql(u8, tag_name, "filter")) {
- const filters_raw = tag_node.tag.?.args;
- const raw_open = tag_node.tag.?.raw;
+ const start = i;
+ var in_quote = false;
+ var quote_char: u8 = 0;
+ if (args[i] == '"' or args[i] == '\'') {
+ in_quote = true;
+ quote_char = args[i];
+ i += 1;
+ }
- // DUPE O FILTERS IMEDIATAMENTE
- const duped_filters = try allocator.dupe(u8, filters_raw);
+ while (i < args.len) {
+ if (in_quote) {
+ if (args[i] == quote_char) {
+ i += 1;
+ break;
+ }
+ } else {
+ if (std.ascii.isWhitespace(args[i])) break;
+ }
+ i += 1;
+ }
- // Agora libera a tag open
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
+ if (in_quote) {
+ fallback = value;
+ } else {
+ try values.append(allocator, value);
+ }
+ }
- const filter_node = try self.parseFilterBlock(allocator, duped_filters, raw_open);
- return filter_node;
- }
+ return TagNodeBody{
+ .firstof = .{
+ .values = try values.toOwnedSlice(allocator),
+ .fallback = fallback,
+ },
+ };
+ },
+ .for_block => {
+ const args = tag_node.tag.?.args;
+ const raw_open = tag_node.tag.?.raw;
- if (std.mem.eql(u8, tag_name, "extends")) {
- const parent = std.mem.trim(u8, tag_node.tag.?.args, " \t\"");
+ const in_pos = std.mem.indexOf(u8, args, " in ") orelse return error.InvalidForSyntax;
+ const loop_var_raw = std.mem.trim(u8, args[0..in_pos], " \t");
+ const iterable_raw = std.mem.trim(u8, args[in_pos + 4 ..], " \t");
- const duped = try allocator.dupe(u8, parent);
+ const for_node = try self.parseForBlock(allocator, loop_var_raw, iterable_raw, raw_open);
+ return for_node;
+ },
+ .if_block => {
+ const args = tag_node.tag.?.args;
+ const raw_open = tag_node.tag.?.raw;
+ const if_node = try self.parseIfBlock(allocator, args, raw_open);
+ return if_node;
+ },
+ .include => {
+ const args = tag_node.tag.?.args;
+ var template_path = args;
+ if (template_path.len >= 2 and template_path[0] == '"' and template_path[template_path.len - 1] == '"') {
+ template_path = template_path[1 .. template_path.len - 1];
+ }
+ return TagNodeBody{
+ .include = .{
+ .template_path = template_path,
+ },
+ };
+ },
+ .load => {
+ const args = tag_node.tag.?.args;
+ var libraries = std.ArrayList([]const u8){};
+ defer libraries.deinit(allocator);
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ var i: usize = 0;
+ while (i < args.len) {
+ while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- return Node{
- .type = .extends,
- .extends = .{ .parent_name = duped },
- };
- }
+ if (i >= args.len) break;
- if (std.mem.eql(u8, tag_name, "block")) {
- const block_name_raw = tag_node.tag.?.args;
- const raw_open = tag_node.tag.?.raw;
-
- const block_name = std.mem.trim(u8, block_name_raw, " \t\r\n");
-
- // DUPE O NOME ANTES DE LIBERAR A TAG
- const duped_name = try allocator.dupe(u8, block_name);
-
- // Agora libera a tag open
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- const block_node = try self.parseBlockBlock(allocator, duped_name, raw_open);
- return block_node;
- }
-
- if (std.mem.eql(u8, tag_name, "now")) {
- const args = tag_node.tag.?.args;
-
- // Remove aspas se existirem
- var format_str = args;
- if (format_str.len >= 2 and format_str[0] == '"' and format_str[format_str.len - 1] == '"') {
- format_str = format_str[1 .. format_str.len - 1];
- }
-
- const duped_format = try allocator.dupe(u8, format_str);
-
- // Libera a tag now
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .now,
- .now = .{ .format = duped_format },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "with")) {
- const args = tag_node.tag.?.args;
- const raw_open = tag_node.tag.?.raw;
-
- const assignments = try self.parseAssignments(allocator, args);
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- const with_node = try self.parseWithBlock(allocator, assignments, raw_open);
- return with_node;
- }
-
- if (std.mem.eql(u8, tag_name, "include")) {
- const args = tag_node.tag.?.args;
-
- // Remove aspas se existirem
- var template_name = args;
- if (template_name.len >= 2 and template_name[0] == '"' and template_name[template_name.len - 1] == '"') {
- template_name = template_name[1 .. template_name.len - 1];
- }
-
- const duped_name = try allocator.dupe(u8, template_name);
-
- // Libera a tag include
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .include,
- .include = .{ .template_name = duped_name },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "if")) {
- const condition_raw = tag_node.tag.?.args;
- const raw_open = tag_node.tag.?.raw;
-
- const condition = try allocator.dupe(u8, condition_raw);
-
- // Libera apenas name e args da tag open
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- // NÃO chame node.deinit aqui — raw_open ainda é usado
-
- const if_node = try self.parseIfBlock(allocator, condition, raw_open);
- return if_node;
- }
-
- if (std.mem.eql(u8, tag_name, "for")) {
- const args = tag_node.tag.?.args;
- const raw_open = tag_node.tag.?.raw;
-
- const in_pos = std.mem.indexOf(u8, args, " in ") orelse return error.InvalidForSyntax;
- const loop_var_raw = std.mem.trim(u8, args[0..in_pos], " \t");
- const iterable_raw = std.mem.trim(u8, args[in_pos + 4 ..], " \t");
-
- // DUPE ANTES DE LIBERAR!
- const loop_var = try allocator.dupe(u8, loop_var_raw);
- const iterable = try allocator.dupe(u8, iterable_raw);
-
- // Agora sim, libera a tag open
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- const for_node = try self.parseForBlock(allocator, loop_var, iterable, raw_open);
- return for_node;
- }
-
- if (std.mem.eql(u8, tag_name, "verbatim")) {
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- const verbatim_node = try self.parseVerbatim(allocator);
- return verbatim_node;
- }
-
- if (std.mem.eql(u8, tag_name, "url")) {
- const args = tag_node.tag.?.args;
-
- var arg_list = std.ArrayList([]const u8){};
- defer arg_list.deinit(allocator);
-
- var i: usize = 0;
- // Pula o nome da view (entre aspas)
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- if (i >= args.len or args[i] != '\'') return error.InvalidUrlSyntax;
- i += 1;
- const view_start = i;
- while (i < args.len and args[i] != '\'') : (i += 1) {}
- if (i >= args.len or args[i] != '\'') return error.InvalidUrlSyntax;
- const view_name = args[view_start..i];
- i += 1;
-
- const duped_view = try allocator.dupe(u8, view_name);
-
- // Agora os argumentos
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- if (i >= args.len) break;
-
- const arg_start = i;
- if (args[i] == '"' or args[i] == '\'') {
- const quote = args[i];
- i += 1;
- while (i < args.len and args[i] != quote) : (i += 1) {}
- if (i >= args.len) return error.UnclosedQuoteInUrl;
- i += 1;
- } else {
+ const start = i;
while (i < args.len and !std.ascii.isWhitespace(args[i])) : (i += 1) {}
+
+ const lib_name = args[start..i];
+ try libraries.append(allocator, lib_name);
}
- const arg = args[arg_start..i];
- try arg_list.append(allocator, try allocator.dupe(u8, arg));
- }
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ return TagNodeBody{
+ .load = .{
+ .libraries = try libraries.toOwnedSlice(allocator),
+ },
+ };
+ },
+ .lorem => {
+ const args = tag_node.tag.?.args;
+ var count: ?[]const u8 = null;
+ var method: ?[]const u8 = null;
+ var random: bool = false;
- return Node{
- .type = .url,
- .url = .{
- .name = duped_view,
- .args = try arg_list.toOwnedSlice(allocator),
- },
- };
- }
+ var parts = std.mem.splitScalar(u8, args, ' ');
+ while (parts.next()) |part| {
+ const trimmed = std.mem.trim(u8, part, " \t\r\n");
+ if (trimmed.len == 0) continue;
- if (std.mem.eql(u8, tag_name, "cycle")) {
- const args = tag_node.tag.?.args;
+ const num: usize = std.fmt.parseInt(usize, trimmed, 10) catch 0;
+ if (num > 0) {
+ count = trimmed;
+ }
- var values = std.ArrayList([]const u8){};
- defer values.deinit(allocator);
+ if (std.mem.eql(u8, trimmed, "p") or std.mem.eql(u8, trimmed, "w")) {
+ method = trimmed;
+ } else if (std.mem.eql(u8, trimmed, "true")) {
+ random = true;
+ }
+ }
- var i: usize = 0;
- while (i < args.len) {
+ return TagNodeBody{
+ .lorem = .{
+ .count = count,
+ .method = method,
+ .random = random,
+ },
+ };
+ },
+ .now => {
+ const args = tag_node.tag.?.args;
+ var format_str = args;
+ if (format_str.len >= 2 and format_str[0] == '"' and format_str[format_str.len - 1] == '"') {
+ format_str = format_str[1 .. format_str.len - 1];
+ }
+ return TagNodeBody{
+ .now = .{
+ .format = format_str,
+ },
+ };
+ },
+ .partial => {
+ const args = tag_node.tag.?.args;
+ var partial_name = args;
+ if (partial_name.len >= 2 and partial_name[0] == '"' and partial_name[partial_name.len - 1] == '"') {
+ partial_name = partial_name[1 .. partial_name.len - 1];
+ }
+ return TagNodeBody{
+ .partial = .{
+ .name = partial_name,
+ },
+ };
+ },
+ .partialdef => {
+ const args = tag_node.tag.?.args;
+ const raw_open = tag_node.tag.?.raw;
+ const partialdef_node = try self.parsePartialDefBlock(allocator, args, raw_open);
+ return partialdef_node;
+ },
+ .querystring => {
+ const args = tag_node.tag.?.args;
+ var modifications = std.ArrayList([]const u8){};
+ defer modifications.deinit(allocator);
+
+ var i: usize = 0;
+ while (i < args.len) {
+ while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
+
+ if (i >= args.len) break;
+
+ const start = i;
+ var in_quote = false;
+ var quote_char: u8 = 0;
+ if (args[i] == '"' or args[i] == '\'') {
+ in_quote = true;
+ quote_char = args[i];
+ i += 1;
+ }
+
+ while (i < args.len) {
+ if (in_quote) {
+ if (args[i] == quote_char) {
+ i += 1;
+ break;
+ }
+ } else {
+ if (std.ascii.isWhitespace(args[i])) {
+ break;
+ }
+ }
+
+ i += 1;
+ }
+
+ // const mod = args[start..i];
+ const mod = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
+ try modifications.append(allocator, mod);
+ }
+
+ return TagNodeBody{
+ .querystring = .{
+ .modifications = try modifications.toOwnedSlice(allocator),
+ },
+ };
+ },
+ .regroup => {
+ const args = tag_node.tag.?.args;
+
+ const by_pos = std.mem.indexOf(u8, args, " by ") orelse return error.InvalidRegroupSyntax;
+ const as_pos = std.mem.indexOf(u8, args[by_pos + 4 ..], " as ") orelse return error.InvalidRegroupSyntax;
+ const as_pos_abs = by_pos + 4 + as_pos;
+
+ const source_raw = std.mem.trim(u8, args[0..by_pos], " \t\r\n");
+ const by_raw = std.mem.trim(u8, args[by_pos + 4 .. as_pos_abs], " \t\r\n");
+ const as_raw = std.mem.trim(u8, args[as_pos_abs + 4 ..], " \t\r\n");
+
+ const source = source_raw;
+ const by = by_raw;
+ const as = as_raw;
+
+ return TagNodeBody{
+ .regroup = .{
+ .source = source,
+ .by = by,
+ .as_var = as,
+ },
+ };
+ },
+ .resetcycle => {
+ const args = tag_node.tag.?.args;
+ var cycle_name = args;
+ if (cycle_name.len >= 2 and cycle_name[0] == '"' and cycle_name[cycle_name.len - 1] == '"') {
+ cycle_name = cycle_name[1 .. cycle_name.len - 1];
+ }
+ return TagNodeBody{
+ .resetcycle = .{
+ .cycle_name = if (args.len > 0) cycle_name else null,
+ },
+ };
+ },
+ .spaceless => {
+ const raw_open = tag_node.tag.?.raw;
+ const spaceless_node = try self.parseSpacelessBlock(allocator, raw_open);
+ return spaceless_node;
+ },
+ .templatetag => {
+ const args = tag_node.tag.?.args;
+ const templatetag = TemplateTagNode.parse(args);
+ if (templatetag == null) return error.InvalidTemplateTag;
+
+ return TagNodeBody{ .templatetag = templatetag.? };
+ },
+ .url => {
+ const args = tag_node.tag.?.args;
+
+ var arg_list = std.ArrayList([]const u8){};
+ defer arg_list.deinit(allocator);
+
+ var i: usize = 0;
+ // Pula o nome da url (entre aspas)
while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
+ if (i >= args.len or args[i] != '\'') return error.InvalidUrlSyntax;
+ i += 1;
+ const name_start = i;
+ while (i < args.len and args[i] != '\'') : (i += 1) {}
+ if (i >= args.len or args[i] != '\'') return error.InvalidUrlSyntax;
+ const url_name = args[name_start..i];
+ i += 1;
- if (i >= args.len) break;
-
- const start = i;
- var in_quote = false;
- var quote_char: u8 = 0;
- if (args[i] == '"' or args[i] == '\'') {
- in_quote = true;
- quote_char = args[i];
- i += 1;
- }
+ // const duped_view = try allocator.dupe(u8, url_name);
+ // try arg_list.append(duped_view);
while (i < args.len) {
- if (in_quote) {
- if (args[i] == quote_char) {
- i += 1;
- break;
- }
+ while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
+ if (i >= args.len) break;
+
+ const arg_start = i;
+ if (args[i] == '"' or args[i] == '\'') {
+ const quote = args[i];
+ i += 1;
+ while (i < args.len and args[i] != quote) : (i += 1) {}
+ if (i >= args.len) return error.UnclosedQuoteInUrl;
+ i += 1;
} else {
- if (std.ascii.isWhitespace(args[i])) break;
+ while (i < args.len and !std.ascii.isWhitespace(args[i])) : (i += 1) {}
}
- i += 1;
+
+ const arg = args[arg_start..i];
+ // const duped_arg = try allocator.dupe(u8, arg);
+ try arg_list.append(allocator, arg);
}
- const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
- try values.append(allocator, try allocator.dupe(u8, value));
- }
+ return TagNodeBody{
+ .url = .{
+ .name = url_name,
+ .args = try arg_list.toOwnedSlice(allocator),
+ },
+ };
+ },
+ .verbatim => {
+ const raw_open = tag_node.tag.?.raw;
+ const block_name_raw = tag_node.tag.?.args;
+ const block_name = std.mem.trim(u8, block_name_raw, " \t\r\n");
+ const verbatim_node = try self.parseVerbatimBlock(allocator, block_name, raw_open);
+ return verbatim_node;
+ },
+ .widthratio => {
+ const args = tag_node.tag.?.args;
+ var parts = std.mem.splitScalar(u8, args, ' ');
+ var value_part: ?[]const u8 = null;
+ var max_part: ?[]const u8 = null;
+ var divisor_part: ?[]const u8 = null;
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
+ if (parts.next()) |p| value_part = std.mem.trim(u8, p, " \t\r\n");
+ if (parts.next()) |p| max_part = std.mem.trim(u8, p, " \t\r\n");
+ if (parts.next()) |p| divisor_part = std.mem.trim(u8, p, " \t\r\n");
- return Node{
- .type = .cycle,
- .cycle = .{
- .values = try values.toOwnedSlice(allocator),
- },
- };
+ if (value_part == null or max_part == null) return error.InvalidWidthRatioSyntax;
+
+ const value = value_part.?;
+ const max_value = max_part.?;
+ const divisor = if (divisor_part) |d| d else null;
+
+ return TagNodeBody{
+ .widthratio = .{
+ .value = value,
+ .max_value = max_value,
+ .divisor = divisor,
+ },
+ };
+ },
+ .with_block => {
+ const args = tag_node.tag.?.args;
+ const raw_open = tag_node.tag.?.raw;
+
+ const assignments = try self.parseAssignments(allocator, args);
+ const with_node = try self.parseWithBlock(allocator, assignments, raw_open);
+ return with_node;
+ },
+ else => return null,
}
+ // if (std.mem.eql(u8, tag_name, "with")) {
+ // const args = tag_node.tag.?.args;
+ // const raw_open = tag_node.tag.?.raw;
+ //
+ // const assignments = try self.parseAssignments(allocator, args);
+ //
+ // allocator.free(tag_node.tag.?.name);
+ // allocator.free(tag_node.tag.?.args);
+ //
+ // const with_node = try self.parseWithBlock(allocator, assignments, raw_open);
+ // return with_node;
+ // }
+ //
+ //
+ // if (std.mem.eql(u8, tag_name, "verbatim")) {
+ // allocator.free(tag_node.tag.?.name);
+ // allocator.free(tag_node.tag.?.args);
+ //
+ // const verbatim_node = try self.parseVerbatim(allocator);
+ // return verbatim_node;
+ // }
- if (std.mem.eql(u8, tag_name, "firstof")) {
- const args = tag_node.tag.?.args;
-
- var values = std.ArrayList([]const u8){};
- defer values.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- var in_quote = false;
- var quote_char: u8 = 0;
- if (args[i] == '"' or args[i] == '\'') {
- in_quote = true;
- quote_char = args[i];
- i += 1;
- }
-
- while (i < args.len) {
- if (in_quote) {
- if (args[i] == quote_char) {
- i += 1;
- break;
- }
- } else {
- if (std.ascii.isWhitespace(args[i])) break;
- }
- i += 1;
- }
-
- const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
- try values.append(allocator, try allocator.dupe(u8, value));
- }
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .firstof,
- .firstof = .{
- .values = try values.toOwnedSlice(allocator),
- },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "load")) {
- const args = tag_node.tag.?.args;
-
- var libraries = std.ArrayList([]const u8){};
- defer libraries.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- while (i < args.len and !std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- const lib_name = args[start..i];
- try libraries.append(allocator, try allocator.dupe(u8, lib_name));
- }
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .load,
- .load = .{
- .libraries = try libraries.toOwnedSlice(allocator),
- },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "debug")) {
- // Verifica se tem argumentos (não deve ter)
- if (tag_node.tag.?.args.len > 0 and !std.mem.allEqual(u8, tag_node.tag.?.args, ' ')) {
- return error.InvalidDebugArgs;
- }
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .debug,
- .debug = true,
- };
- }
-
- if (std.mem.eql(u8, tag_name, "querystring")) {
- const args = tag_node.tag.?.args;
-
- var modifications = std.ArrayList([]const u8){};
- defer modifications.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- var in_quote = false;
- var quote_char: u8 = 0;
- if (args[i] == '"' or args[i] == '\'') {
- in_quote = true;
- quote_char = args[i];
- i += 1;
- }
-
- while (i < args.len) {
- if (in_quote) {
- if (args[i] == quote_char) {
- i += 1;
- break;
- }
- } else {
- if (std.ascii.isWhitespace(args[i])) break;
- }
- i += 1;
- }
-
- const mod_str = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
- try modifications.append(allocator, try allocator.dupe(u8, mod_str));
- }
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .querystring,
- .querystring = .{
- .modifications = try modifications.toOwnedSlice(allocator),
- },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "regroup")) {
- const args = tag_node.tag.?.args;
-
- // Esperado: lista by atributo as nome_grupo
- const by_pos = std.mem.indexOf(u8, args, " by ") orelse return error.InvalidRegroupSyntax;
- const as_pos = std.mem.indexOf(u8, args[by_pos + 4 ..], " as ") orelse return error.InvalidRegroupSyntax;
- const as_pos_abs = by_pos + 4 + as_pos;
-
- const source_raw = std.mem.trim(u8, args[0..by_pos], " \t\r\n");
- const by_raw = std.mem.trim(u8, args[by_pos + 4 .. as_pos_abs], " \t\r\n");
- const as_raw = std.mem.trim(u8, args[as_pos_abs + 4 ..], " \t\r\n");
-
- const source = try allocator.dupe(u8, source_raw);
- const by = try allocator.dupe(u8, by_raw);
- const as_var = try allocator.dupe(u8, as_raw);
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .regroup,
- .regroup = .{
- .source = source,
- .by = by,
- .as_var = as_var,
- },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "resetcycle")) {
- const args = std.mem.trim(u8, tag_node.tag.?.args, " \t\r\n");
-
- var cycle_name: ?[]const u8 = null;
- if (args.len > 0) {
- cycle_name = try allocator.dupe(u8, args);
- }
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .resetcycle,
- .resetcycle = .{
- .cycle_name = cycle_name,
- },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "widthratio")) {
- const args = tag_node.tag.?.args;
-
- var parts = std.mem.splitScalar(u8, args, ' ');
- var value_part: ?[]const u8 = null;
- var max_part: ?[]const u8 = null;
- var divisor_part: ?[]const u8 = null;
-
- if (parts.next()) |p| value_part = std.mem.trim(u8, p, " \t\r\n");
- if (parts.next()) |p| max_part = std.mem.trim(u8, p, " \t\r\n");
- if (parts.next()) |p| divisor_part = std.mem.trim(u8, p, " \t\r\n");
-
- if (value_part == null or max_part == null) return error.InvalidWidthRatioSyntax;
-
- const value = try allocator.dupe(u8, value_part.?);
- const max_value = try allocator.dupe(u8, max_part.?);
- const divisor = if (divisor_part) |d| try allocator.dupe(u8, d) else null;
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .widthratio,
- .widthratio = .{
- .value = value,
- .max_value = max_value,
- .divisor = divisor,
- },
- };
- }
-
- if (std.mem.eql(u8, tag_name, "templatetag")) {
- const arg = std.mem.trim(u8, tag_node.tag.?.args, " \t\r\n");
-
- const templatetag = TemplateTagNode.parse(arg);
- if (templatetag == null) return error.InvalidTemplateTag;
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .templatetag,
- .templatetag = templatetag,
- };
- }
-
- if (std.mem.eql(u8, tag_name, "csrf_token")) {
- // Verifica se tem argumentos (não deve ter)
- if (tag_node.tag.?.args.len > 0 and !std.mem.allEqual(u8, tag_node.tag.?.args, ' ')) {
- return error.InvalidCsrfTokenArgs;
- }
-
- allocator.free(tag_node.tag.?.name);
- allocator.free(tag_node.tag.?.args);
-
- return Node{
- .type = .csrf_token,
- .csrf_token = true,
- };
- }
- return null;
+ // return null;
}
pub fn parse(self: *Parser, allocator: std.mem.Allocator) ParserError![]Node {
@@ -2132,20 +2214,18 @@ pub const Parser = struct {
defer list.deinit(allocator);
while (self.pos < self.template.len) {
- if (try self.parseTag(allocator)) |node| {
- const tag_name = node.tag.?.name;
-
- if (std.mem.eql(u8, tag_name, "comment")) {
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
+ var tag = try self.parseTag(allocator);
+ if (tag != null) {
+ if (tag.?.tag.?.kind == .comment) {
try self.parseComment();
continue;
} else {
- const node_parsed: ?Node = self.parseTagContent(allocator, node) catch null;
- if (node_parsed) |n| {
- try list.append(allocator, n);
- continue;
+ std.log.debug("Tag: {s}", .{tag.?.tag.?.raw});
+ if (try self.parseTagContent(allocator, tag.?)) |tn| {
+ tag.?.tag.?.body = tn;
+ try list.append(allocator, tag.?);
}
+ continue;
}
}
@@ -2164,661 +2244,6 @@ pub const Parser = struct {
return try list.toOwnedSlice(allocator);
}
-
- pub fn parse_(self: *Parser, allocator: std.mem.Allocator) ParserError![]Node {
- var list = std.ArrayList(Node){};
- defer list.deinit(allocator);
-
- while (self.pos < self.template.len) {
- if (try self.parseTag(allocator)) |node| {
- const tag_name = node.tag.?.name;
- try self.parseTagContent(allocator, node);
-
- if (std.mem.eql(u8, tag_name, "autoescape")) {
- const args = std.mem.trim(u8, node.tag.?.args, " \t\r\n");
- const raw_open = node.tag.?.raw;
-
- const enabled = if (std.mem.eql(u8, args, "on"))
- true
- else if (std.mem.eql(u8, args, "off"))
- false
- else
- return error.InvalidAutoescapeArgument;
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const ae_node = try self.parseAutoescapeBlock(allocator, enabled, raw_open);
- try list.append(allocator, ae_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "spaceless")) {
- const raw_open = node.tag.?.raw;
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const spaceless_node = try self.parseSpacelessBlock(allocator, raw_open);
- try list.append(allocator, spaceless_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "comment")) {
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
- try self.parseComment();
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "partialdef")) {
- const partial_name = std.mem.trim(u8, node.tag.?.args, " \t\r\n");
- const raw_open = node.tag.?.raw;
-
- const duped_name = try allocator.dupe(u8, partial_name);
- errdefer allocator.free(duped_name);
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const partialdef_node = try self.parsePartialDefBlock(allocator, duped_name, raw_open);
- try list.append(allocator, partialdef_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "partial")) {
- const partial_name = std.mem.trim(u8, node.tag.?.args, " \t\r\n\"'");
-
- const duped_name = try allocator.dupe(u8, partial_name);
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .partial,
- .partial = .{ .name = duped_name },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "lorem")) {
- const args = node.tag.?.args;
-
- var count: ?[]const u8 = null;
- var method: ?[]const u8 = null;
- var format: ?[]const u8 = null;
-
- var parts = std.mem.splitScalar(u8, args, ' ');
- while (parts.next()) |part| {
- const trimmed = std.mem.trim(u8, part, " \t\r\n");
- if (trimmed.len == 0) continue;
-
- const num: usize = std.fmt.parseInt(usize, trimmed, 10) catch 0;
- if (num > 0) {
- count = try allocator.dupe(u8, trimmed);
- }
-
- if (std.mem.eql(u8, trimmed, "p") or std.mem.eql(u8, trimmed, "w")) {
- method = try allocator.dupe(u8, trimmed);
- } else if (std.mem.eql(u8, trimmed, "html")) {
- format = try allocator.dupe(u8, trimmed);
- }
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .lorem,
- .lorem = .{
- .count = count,
- .method = method,
- .format = format,
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "filter")) {
- const filters_raw = node.tag.?.args;
- const raw_open = node.tag.?.raw;
-
- // DUPE O FILTERS IMEDIATAMENTE
- const duped_filters = try allocator.dupe(u8, filters_raw);
-
- // Agora libera a tag open
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const filter_node = try self.parseFilterBlock(allocator, duped_filters, raw_open);
- try list.append(allocator, filter_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "extends")) {
- const parent = std.mem.trim(u8, node.tag.?.args, " \t\"");
-
- const duped = try allocator.dupe(u8, parent);
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .extends,
- .extends = .{ .parent_name = duped },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "block")) {
- const block_name_raw = node.tag.?.args;
- const raw_open = node.tag.?.raw;
-
- const block_name = std.mem.trim(u8, block_name_raw, " \t\r\n");
-
- // DUPE O NOME ANTES DE LIBERAR A TAG
- const duped_name = try allocator.dupe(u8, block_name);
-
- // Agora libera a tag open
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const block_node = try self.parseBlockBlock(allocator, duped_name, raw_open);
- try list.append(allocator, block_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "now")) {
- const args = node.tag.?.args;
-
- // Remove aspas se existirem
- var format_str = args;
- if (format_str.len >= 2 and format_str[0] == '"' and format_str[format_str.len - 1] == '"') {
- format_str = format_str[1 .. format_str.len - 1];
- }
-
- const duped_format = try allocator.dupe(u8, format_str);
-
- // Libera a tag now
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .now,
- .now = .{ .format = duped_format },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "with")) {
- const args = node.tag.?.args;
- const raw_open = node.tag.?.raw;
-
- const assignments = try self.parseAssignments(allocator, args);
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const with_node = try self.parseWithBlock(allocator, assignments, raw_open);
- try list.append(allocator, with_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "include")) {
- const args = node.tag.?.args;
-
- // Remove aspas se existirem
- var template_name = args;
- if (template_name.len >= 2 and template_name[0] == '"' and template_name[template_name.len - 1] == '"') {
- template_name = template_name[1 .. template_name.len - 1];
- }
-
- const duped_name = try allocator.dupe(u8, template_name);
-
- // Libera a tag include
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .include,
- .include = .{ .template_name = duped_name },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "if")) {
- const condition_raw = node.tag.?.args;
- const raw_open = node.tag.?.raw;
-
- const condition = try allocator.dupe(u8, condition_raw);
-
- // Libera apenas name e args da tag open
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- // NÃO chame node.deinit aqui — raw_open ainda é usado
-
- const if_node = try self.parseIfBlock(allocator, condition, raw_open);
- try list.append(allocator, if_node);
-
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "for")) {
- const args = node.tag.?.args;
- const raw_open = node.tag.?.raw;
-
- const in_pos = std.mem.indexOf(u8, args, " in ") orelse return error.InvalidForSyntax;
- const loop_var_raw = std.mem.trim(u8, args[0..in_pos], " \t");
- const iterable_raw = std.mem.trim(u8, args[in_pos + 4 ..], " \t");
-
- // DUPE ANTES DE LIBERAR!
- const loop_var = try allocator.dupe(u8, loop_var_raw);
- const iterable = try allocator.dupe(u8, iterable_raw);
-
- // Agora sim, libera a tag open
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const for_node = try self.parseForBlock(allocator, loop_var, iterable, raw_open);
-
- try list.append(allocator, for_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "verbatim")) {
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- const verbatim_node = try self.parseVerbatim(allocator);
- try list.append(allocator, verbatim_node);
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "url")) {
- const args = node.tag.?.args;
-
- var arg_list = std.ArrayList([]const u8){};
- defer arg_list.deinit(allocator);
-
- var i: usize = 0;
- // Pula o nome da view (entre aspas)
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- if (i >= args.len or args[i] != '\'') return error.InvalidUrlSyntax;
- i += 1;
- const view_start = i;
- while (i < args.len and args[i] != '\'') : (i += 1) {}
- if (i >= args.len or args[i] != '\'') return error.InvalidUrlSyntax;
- const view_name = args[view_start..i];
- i += 1;
-
- const duped_view = try allocator.dupe(u8, view_name);
-
- // Agora os argumentos
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
- if (i >= args.len) break;
-
- const arg_start = i;
- if (args[i] == '"' or args[i] == '\'') {
- const quote = args[i];
- i += 1;
- while (i < args.len and args[i] != quote) : (i += 1) {}
- if (i >= args.len) return error.UnclosedQuoteInUrl;
- i += 1;
- } else {
- while (i < args.len and !std.ascii.isWhitespace(args[i])) : (i += 1) {}
- }
- const arg = args[arg_start..i];
- try arg_list.append(allocator, try allocator.dupe(u8, arg));
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .url,
- .url = .{
- .name = duped_view,
- .args = try arg_list.toOwnedSlice(allocator),
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "cycle")) {
- const args = node.tag.?.args;
-
- var values = std.ArrayList([]const u8){};
- defer values.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- var in_quote = false;
- var quote_char: u8 = 0;
- if (args[i] == '"' or args[i] == '\'') {
- in_quote = true;
- quote_char = args[i];
- i += 1;
- }
-
- while (i < args.len) {
- if (in_quote) {
- if (args[i] == quote_char) {
- i += 1;
- break;
- }
- } else {
- if (std.ascii.isWhitespace(args[i])) break;
- }
- i += 1;
- }
-
- const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
- try values.append(allocator, try allocator.dupe(u8, value));
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .cycle,
- .cycle = .{
- .values = try values.toOwnedSlice(allocator),
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "firstof")) {
- const args = node.tag.?.args;
-
- var values = std.ArrayList([]const u8){};
- defer values.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- var in_quote = false;
- var quote_char: u8 = 0;
- if (args[i] == '"' or args[i] == '\'') {
- in_quote = true;
- quote_char = args[i];
- i += 1;
- }
-
- while (i < args.len) {
- if (in_quote) {
- if (args[i] == quote_char) {
- i += 1;
- break;
- }
- } else {
- if (std.ascii.isWhitespace(args[i])) break;
- }
- i += 1;
- }
-
- const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
- try values.append(allocator, try allocator.dupe(u8, value));
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .firstof,
- .firstof = .{
- .values = try values.toOwnedSlice(allocator),
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "load")) {
- const args = node.tag.?.args;
-
- var libraries = std.ArrayList([]const u8){};
- defer libraries.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- while (i < args.len and !std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- const lib_name = args[start..i];
- try libraries.append(allocator, try allocator.dupe(u8, lib_name));
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .load,
- .load = .{
- .libraries = try libraries.toOwnedSlice(allocator),
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "debug")) {
- // Verifica se tem argumentos (não deve ter)
- if (node.tag.?.args.len > 0 and !std.mem.allEqual(u8, node.tag.?.args, ' ')) {
- return error.InvalidDebugArgs;
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .debug,
- .debug = true,
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "querystring")) {
- const args = node.tag.?.args;
-
- var modifications = std.ArrayList([]const u8){};
- defer modifications.deinit(allocator);
-
- var i: usize = 0;
- while (i < args.len) {
- while (i < args.len and std.ascii.isWhitespace(args[i])) : (i += 1) {}
-
- if (i >= args.len) break;
-
- const start = i;
- var in_quote = false;
- var quote_char: u8 = 0;
- if (args[i] == '"' or args[i] == '\'') {
- in_quote = true;
- quote_char = args[i];
- i += 1;
- }
-
- while (i < args.len) {
- if (in_quote) {
- if (args[i] == quote_char) {
- i += 1;
- break;
- }
- } else {
- if (std.ascii.isWhitespace(args[i])) break;
- }
- i += 1;
- }
-
- const mod_str = std.mem.trim(u8, args[start..i], " \t\r\n\"'");
- try modifications.append(allocator, try allocator.dupe(u8, mod_str));
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .querystring,
- .querystring = .{
- .modifications = try modifications.toOwnedSlice(allocator),
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "regroup")) {
- const args = node.tag.?.args;
-
- // Esperado: lista by atributo as nome_grupo
- const by_pos = std.mem.indexOf(u8, args, " by ") orelse return error.InvalidRegroupSyntax;
- const as_pos = std.mem.indexOf(u8, args[by_pos + 4 ..], " as ") orelse return error.InvalidRegroupSyntax;
- const as_pos_abs = by_pos + 4 + as_pos;
-
- const source_raw = std.mem.trim(u8, args[0..by_pos], " \t\r\n");
- const by_raw = std.mem.trim(u8, args[by_pos + 4 .. as_pos_abs], " \t\r\n");
- const as_raw = std.mem.trim(u8, args[as_pos_abs + 4 ..], " \t\r\n");
-
- const source = try allocator.dupe(u8, source_raw);
- const by = try allocator.dupe(u8, by_raw);
- const as_var = try allocator.dupe(u8, as_raw);
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .regroup,
- .regroup = .{
- .source = source,
- .by = by,
- .as_var = as_var,
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "resetcycle")) {
- const args = std.mem.trim(u8, node.tag.?.args, " \t\r\n");
-
- var cycle_name: ?[]const u8 = null;
- if (args.len > 0) {
- cycle_name = try allocator.dupe(u8, args);
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .resetcycle,
- .resetcycle = .{
- .cycle_name = cycle_name,
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "widthratio")) {
- const args = node.tag.?.args;
-
- var parts = std.mem.splitScalar(u8, args, ' ');
- var value_part: ?[]const u8 = null;
- var max_part: ?[]const u8 = null;
- var divisor_part: ?[]const u8 = null;
-
- if (parts.next()) |p| value_part = std.mem.trim(u8, p, " \t\r\n");
- if (parts.next()) |p| max_part = std.mem.trim(u8, p, " \t\r\n");
- if (parts.next()) |p| divisor_part = std.mem.trim(u8, p, " \t\r\n");
-
- if (value_part == null or max_part == null) return error.InvalidWidthRatioSyntax;
-
- const value = try allocator.dupe(u8, value_part.?);
- const max_value = try allocator.dupe(u8, max_part.?);
- const divisor = if (divisor_part) |d| try allocator.dupe(u8, d) else null;
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .widthratio,
- .widthratio = .{
- .value = value,
- .max_value = max_value,
- .divisor = divisor,
- },
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "templatetag")) {
- const arg = std.mem.trim(u8, node.tag.?.args, " \t\r\n");
-
- const templatetag = TemplateTagNode.parse(arg);
- if (templatetag == null) return error.InvalidTemplateTag;
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .templatetag,
- .templatetag = templatetag,
- });
- continue;
- }
-
- if (std.mem.eql(u8, tag_name, "csrf_token")) {
- // Verifica se tem argumentos (não deve ter)
- if (node.tag.?.args.len > 0 and !std.mem.allEqual(u8, node.tag.?.args, ' ')) {
- return error.InvalidCsrfTokenArgs;
- }
-
- allocator.free(node.tag.?.name);
- allocator.free(node.tag.?.args);
-
- try list.append(allocator, Node{
- .type = .csrf_token,
- .csrf_token = true,
- });
- continue;
- }
-
- // Para tags normais
- try list.append(allocator, node);
- continue;
- }
-
- if (try self.parseVariable(allocator)) |node| {
- try list.append(allocator, node);
- continue;
- }
-
- if (try self.parseText(allocator)) |node| {
- try list.append(allocator, node);
- continue;
- }
-
- self.advance(1);
- }
-
- // std.debug.print("\nO resultado disso foi esse:\n", .{});
- // for (list.items) |item| {
- // std.debug.print(" -> type: {s}\n", .{@tagName(item.type)});
- // }
- // std.debug.print("\n", .{});
- return try list.toOwnedSlice(allocator);
- }
};
pub fn parse_t(allocator: std.mem.Allocator, template: []const u8) ParserError![]Node {
diff --git a/src/parser_test.zig b/src/parser_test.zig
index 9a57ddb..bb515f3 100644
--- a/src/parser_test.zig
+++ b/src/parser_test.zig
@@ -2,10 +2,13 @@ const std = @import("std");
const testing = std.testing;
const parser = @import("parser.zig");
-test "parse texto simples" {
+test "parse simple text" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("1 - parse simple text\n", .{});
const allocator = testing.allocator;
const template = "Olá mundo!";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| {
node.deinit(allocator);
@@ -18,10 +21,13 @@ test "parse texto simples" {
try testing.expectEqualStrings("Olá mundo!", nodes[0].text.?.content);
}
-test "parse variável simples" {
+test "parse simple var" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("2 - parse simple var\n", .{});
const allocator = testing.allocator;
const template = "Olá {{ nome }}!";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| {
node.deinit(allocator);
@@ -41,10 +47,13 @@ test "parse variável simples" {
try testing.expectEqualStrings("!", nodes[2].text.?.content);
}
-test "parse variável com espaços" {
+test "parse var with spaces" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("3 - parse var with spaces\n", .{});
const allocator = testing.allocator;
const template = "{{ espacos }}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| {
node.deinit(allocator);
@@ -57,331 +66,353 @@ test "parse variável com espaços" {
try testing.expectEqualStrings("espacos", nodes[0].variable.?.expr);
}
-test "parse tag simples" {
+test "parse autoescape on" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("4 - parse autoescape on\n", .{});
const allocator = testing.allocator;
- const template = "Antes {% minha_tag %} Depois";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% autoescape on %}Texto negrito{% endautoescape %}";
+ const expected = "Texto negrito";
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
- for (nodes) |node| {
- node.deinit(allocator);
- }
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ if (nodes[0].tag) |tag| {
+ try testing.expect(tag.body.autoescape.enabled == true);
+ try testing.expectEqual(@as(usize, 1), tag.body.autoescape.body.len);
+ try testing.expectEqualStrings(expected, tag.body.autoescape.body[0].text.?.content);
+ }
+}
+
+test "parse autoescape off" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("5 - parse autoescape off\n", .{});
+ const allocator = testing.allocator;
+ const template = "{% autoescape off %}Texto negrito{% endautoescape %}";
+ const expected = "Texto negrito";
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ if (nodes[0].tag) |tag| {
+ try testing.expect(tag.body.autoescape.enabled == false);
+ try testing.expectEqual(@as(usize, 1), tag.body.autoescape.body.len);
+ try testing.expectEqualStrings(expected, tag.body.autoescape.body[0].text.?.content);
+ }
+}
+test "parse simple block" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("6 - parse simple block\n", .{});
+ const allocator = testing.allocator;
+ const template = "{% block titulo %}Meu Título{% endblock %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expectEqual(nodes[0].tag.?.kind, .block);
+ try testing.expectEqualStrings("titulo", nodes[0].tag.?.body.block.name);
+ const b = nodes[0].tag.?.body.block.body;
+ try testing.expectEqual(@as(usize, 1), b.len);
+ try testing.expectEqual(b[0].type, .text);
+ try testing.expectEqualStrings("Meu Título", b[0].text.?.content);
+}
+
+test "parse block with super" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("7 - parse block with super\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% block conteudo %}{{ block.super }} Conteúdo filho{% endblock %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expectEqualStrings("conteudo", nodes[0].tag.?.body.block.name);
+ const b = nodes[0].tag.?.body.block.body;
+ try testing.expectEqual(@as(usize, 2), b.len);
+ try testing.expect(b[0].type == .tag);
+ try testing.expect(b[0].tag.?.kind == .super);
+ try testing.expect(b[1].type == .text);
+ try testing.expectEqualStrings(" Conteúdo filho", b[1].text.?.content);
+}
+
+// TODO: check it
+test "parse block with block inside" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("8 - parse block with block inside\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% block conteudo %}{% block inner %}Conteúdo filho{% endblock %}{% endblock %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expectEqualStrings("conteudo", nodes[0].tag.?.body.block.name);
+ const b = nodes[0].tag.?.body.block.body;
+ try testing.expectEqual(@as(usize, 3), b.len);
+ try testing.expect(b[0].type == .tag);
+ try testing.expect(b[0].tag.?.kind == .block);
+ try testing.expect(b[1].type == .text);
+ try testing.expectEqualStrings("Conteúdo filho", b[1].text.?.content);
+ try testing.expect(b[2].type == .tag);
+ // std.debug.print("block:\n {any}",.{nodes[0].tag.?.body.block});
+ // for(b) |node| std.debug.print("block:\n {any}\n",.{node});
+ // try testing.expectEqualStrings("inner",b[0].tag.?.body.block.name);
+ // try testing.expect(b[1].type == .text);
+ // try testing.expectEqualStrings(" Conteúdo filho",b[1].text.?.content);
+}
+
+test "parse simple csrf_token" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("9 - parse simple csrf_token\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 3), nodes.len);
-
try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("Antes ", nodes[0].text.?.content);
+ try testing.expectEqualStrings("", nodes[2].text.?.content);
}
-test "parse if block básico" {
+test "parse just csrf_token" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("10 - parse just csrf_token\n", .{});
+
const allocator = testing.allocator;
- const template = "{% if usuario.logado %}Bem-vindo!{% endif %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| {
- node.deinit(allocator);
- }
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .if_block);
-
- const ib = nodes[0].@"if".?;
- try testing.expectEqualStrings("usuario.logado", ib.condition);
- try testing.expectEqual(@as(usize, 1), ib.true_body.len);
- try testing.expect(nodes[0].@"if".?.true_body[0].type == .text);
- try testing.expectEqualStrings("Bem-vindo!", ib.true_body[0].text.?.content);
- try testing.expectEqual(@as(usize, 0), ib.false_body.len);
-}
-
-test "parse if block sem else" {
- const allocator = testing.allocator;
- const template = "{% if cond %}Verdadeiro{% endif %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| {
- node.deinit(allocator);
- }
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .if_block);
- const ib = nodes[0].@"if".?;
- try testing.expectEqualStrings("cond", ib.condition);
- try testing.expectEqual(@as(usize, 1), ib.true_body.len);
- try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content);
- try testing.expectEqual(@as(usize, 0), ib.false_body.len);
-}
-
-test "parse if block com else" {
- const allocator = testing.allocator;
- const template = "{% if cond %}Verdadeiro{% else %}Falso{% endif %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| {
- node.deinit(allocator);
- }
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .if_block);
- const ib = nodes[0].@"if".?;
- try testing.expectEqualStrings("cond", ib.condition);
- try testing.expectEqual(@as(usize, 1), ib.true_body.len);
- try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content);
- try testing.expectEqual(@as(usize, 1), ib.false_body.len);
- try testing.expectEqualStrings("Falso", ib.false_body[0].text.?.content);
-}
-
-test "parse for block sem empty" {
- const allocator = testing.allocator;
- const template = "{% for item in lista %}Item: {{ item }}{% endfor %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% csrf_token %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .for_block);
- const fb = nodes[0].@"for".?;
- try testing.expectEqualStrings("item", fb.loop_var);
- try testing.expectEqualStrings("lista", fb.iterable);
- try testing.expectEqual(@as(usize, 2), fb.body.len); // <--- corrigido: 2 nós
- try testing.expectEqual(@as(usize, 0), fb.empty_body.len);
-
- try testing.expect(fb.body[0].type == .text);
- try testing.expectEqualStrings("Item: ", fb.body[0].text.?.content);
- try testing.expect(fb.body[1].type == .variable);
- try testing.expectEqualStrings("item", fb.body[1].variable.?.expr);
-}
-
-test "parse for block com empty" {
- const allocator = testing.allocator;
- const template = "{% for item in lista %}Tem{% empty %}Vazio{% endfor %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .for_block);
- const fb = nodes[0].@"for".?;
- try testing.expectEqual(@as(usize, 1), fb.body.len);
- try testing.expectEqualStrings("Tem", fb.body[0].text.?.content);
- try testing.expectEqual(@as(usize, 1), fb.empty_body.len);
- try testing.expectEqualStrings("Vazio", fb.empty_body[0].text.?.content);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .csrf_token);
}
test "parse comment" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("11 - parse comment\n", .{});
+
const allocator = testing.allocator;
const template = "Bazinga! {% comment %}{% for item in lista %}Tem{% empty %}Vazio{% endfor %}{% endcomment %}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
-
try testing.expectEqual(@as(usize, 1), nodes.len);
try testing.expect(nodes[0].type == .text);
try testing.expectEqualStrings("Bazinga! ", nodes[0].text.?.content);
}
-test "parse include simples" {
+test "parse comment inside block" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("12 - parse comment\n", .{});
+
const allocator = testing.allocator;
- const template = "Cabeçalho {% include \"header.zdt\" %} Conteúdo {% include \"footer.zdt\" %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% block conteudo %}Bazinga!{% comment %}{% for item in lista %}Tem{% empty %}Vazio{% endfor %}{% endcomment %} haha{% endblock %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
- try testing.expectEqual(@as(usize, 4), nodes.len);
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ const child_nodes = nodes[0].tag.?.body.block.body;
+
+ try testing.expectEqual(@as(usize, 2), child_nodes.len);
+
+ try testing.expect(child_nodes[0].type == .text);
+ try testing.expect(child_nodes[1].type == .text);
+ try testing.expectEqualStrings("Bazinga!", child_nodes[0].text.?.content);
+ try testing.expectEqualStrings(" haha", child_nodes[1].text.?.content);
+}
+
+test "parse simple cycle" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("13 - parse simple cycle\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% cycle 'row1' 'row2' %}";
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+
+ const args = nodes[0].tag.?.body.cycle.values;
+ try testing.expectEqual(@as(usize, 2), args.len);
+ try testing.expectEqualStrings("row1", args[0]);
+ try testing.expectEqualStrings("row2", args[1]);
+}
+
+test "parse cycle values without quotes" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("14 - parse cycle values without quotes\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% cycle row1 row2 row3 %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ const args = nodes[0].tag.?.body.cycle.values;
+ try testing.expectEqual(@as(usize, 3), args.len);
+ try testing.expectEqualStrings("row1", args[0]);
+ try testing.expectEqualStrings("row2", args[1]);
+ try testing.expectEqualStrings("row3", args[2]);
+}
+
+test "parse simple debug" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("15 - parse simple debug\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Antes {% debug %} Depois";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 3), nodes.len);
try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("Cabeçalho ", nodes[0].text.?.content);
+ try testing.expectEqualStrings("Antes ", nodes[0].text.?.content);
- try testing.expect(nodes[1].type == .include);
- try testing.expectEqualStrings("header.zdt", nodes[1].include.?.template_name);
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .debug);
try testing.expect(nodes[2].type == .text);
- try testing.expectEqualStrings(" Conteúdo ", nodes[2].text.?.content);
-
- try testing.expect(nodes[3].type == .include);
- try testing.expectEqualStrings("footer.zdt", nodes[3].include.?.template_name);
-
-}
-
-test "parse include sem aspas (erro esperado no futuro, mas por enquanto aceita)" {
- const allocator = testing.allocator;
- const template = "{% include header.zdt %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .include);
- try testing.expectEqualStrings("header.zdt", nodes[0].include.?.template_name);
-}
-
-test "parse with simples" {
- const allocator = testing.allocator;
- const template = "{% with nome=\"Lucas\" idade=30 %}Olá {{ nome }}, você tem {{ idade }} anos.{% endwith %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .with_block);
- const w = nodes[0].with.?;
- try testing.expectEqual(@as(usize, 2), w.assignments.len);
- try testing.expectEqual(@as(usize, 5), w.body.len);
- try testing.expect(w.body[0].type == .text);
-}
-
-test "parse now simples" {
- const allocator = testing.allocator;
- const template = "Data atual: {% now \"Y-m-d\" %} às {% now \"H:i\" %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 4), nodes.len);
-
- try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("Data atual: ", nodes[0].text.?.content);
-
- try testing.expect(nodes[1].type == .now);
- try testing.expectEqualStrings("Y-m-d", nodes[1].now.?.format);
-
- try testing.expect(nodes[2].type == .text);
- try testing.expectEqualStrings(" às ", nodes[2].text.?.content);
-
- try testing.expect(nodes[3].type == .now);
- try testing.expectEqualStrings("H:i", nodes[3].now.?.format);
-
-}
-
-test "parse now sem aspas" {
- const allocator = testing.allocator;
- const template = "{% now Y-m-d %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .now);
- try testing.expectEqualStrings("Y-m-d", nodes[0].now.?.format);
+ try testing.expectEqualStrings(" Depois", nodes[2].text.?.content);
}
test "parse extends" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("16 - parse extends\n", .{});
+
const allocator = testing.allocator;
- const template = "{% extends \"base.zdt\" %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% extends \"base.html\" %}";
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .extends);
- try testing.expectEqualStrings("base.zdt", nodes[0].extends.?.parent_name);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .extends);
+ try testing.expectEqualStrings("base.html", nodes[0].tag.?.body.extends.parent_name);
}
-test "parse block simples" {
- const allocator = testing.allocator;
- const template = "{% block titulo %}Meu Título{% endblock %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
+test "parse simple filter block" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("17 - parse simple filter block\n", .{});
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .block);
- const b = nodes[0].block.?;
- try testing.expectEqualStrings("titulo", b.name);
- try testing.expectEqual(@as(usize, 1), b.body.len);
- try testing.expectEqualStrings("Meu Título", b.body[0].text.?.content);
-}
-
-test "parse block com super" {
- const allocator = testing.allocator;
- const template = "{% block conteudo %}{{ block.super }} Conteúdo filho{% endblock %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .block);
- const b = nodes[0].block.?;
- try testing.expectEqual(@as(usize, 2), b.body.len);
- try testing.expect(b.body[0].type == .super);
- // try testing.expectEqualStrings("conteudo", b.name);
- try testing.expectEqualStrings(" Conteúdo filho", b.body[1].text.?.content);
-}
-
-
-test "parse filter block simples" {
const allocator = testing.allocator;
const template = "{% filter upper %}olá mundo{% endfilter %}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .filter_block);
- const fb = nodes[0].filter_block.?;
- try testing.expectEqualStrings("upper", fb.filters); // correto
+ try testing.expect(nodes[0].type == .tag);
+ const fb = nodes[0].tag.?.body.filter_block;
+ try testing.expectEqualStrings("upper", fb.filters);
try testing.expectEqual(@as(usize, 1), fb.body.len);
try testing.expectEqualStrings("olá mundo", fb.body[0].text.?.content);
}
-test "parse filter block com múltiplos filtros" {
+test "parse filter block multiple filters" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("18 - parse filter block multiple filters\n", .{});
+
const allocator = testing.allocator;
const template = "{% filter upper|escape %}Conteúdo negrito{% endfilter %}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .filter_block);
- const fb = nodes[0].filter_block.?;
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .filter_block);
+ const fb = nodes[0].tag.?.body.filter_block;
try testing.expectEqualStrings("upper|escape", fb.filters);
try testing.expectEqual(@as(usize, 1), fb.body.len);
try testing.expectEqualStrings("Conteúdo negrito", fb.body[0].text.?.content);
}
-test "parse variable com filtros" {
+test "parse variable with filters" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("19 - parse variable with filters\n", .{});
+
const allocator = testing.allocator;
const template = "{{ nome|upper|default:\"Visitante\" }}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
@@ -398,521 +429,826 @@ test "parse variable com filtros" {
try testing.expectEqualStrings("Visitante", v.filters[1].arg.?);
}
-test "parse autoescape on" {
- const allocator = testing.allocator;
- const template = "{% autoescape on %}Texto negrito{% endautoescape %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
+test "parse simple firstof" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("20 - parse simple firstof\n", .{});
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .autoescape);
- const ae = nodes[0].autoescape.?;
- try testing.expect(ae.enabled == true);
- try testing.expectEqual(@as(usize, 1), ae.body.len);
- try testing.expectEqualStrings("Texto negrito", ae.body[0].text.?.content);
-}
-
-test "parse autoescape off" {
- const allocator = testing.allocator;
- const template = "{% autoescape off %}Texto negrito{% endautoescape %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .autoescape);
- const ae = nodes[0].autoescape.?;
- try testing.expect(ae.enabled == false);
- try testing.expectEqual(@as(usize, 1), ae.body.len);
-}
-
-test "parse spaceless simples" {
- const allocator = testing.allocator;
- const template = "{% spaceless %} Texto com espaços
{% endspaceless %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .spaceless);
- const sl = nodes[0].spaceless.?;
- try testing.expectEqual(@as(usize, 1), sl.body.len);
- try testing.expectEqualStrings(" Texto com espaços
", sl.body[0].text.?.content);
-}
-
-test "parse spaceless aninhado" {
- const allocator = testing.allocator;
- const template = "{% spaceless %}Outer {% spaceless %}Inner{% endspaceless %} Outer{% endspaceless %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .spaceless);
- const sl = nodes[0].spaceless.?;
- for (sl.body) |b| {
- std.debug.print("type: {s}\n", .{@tagName(b.type)});
- if (b.type == .spaceless) {
- std.debug.print(" - tag -> spaceless\n", .{});
- }
- if(b.type == .text) {
- std.debug.print(" - text -> {s}\n", .{b.text.?.content});
- }
- std.debug.print("----------\n", .{});
- }
- try testing.expectEqual(@as(usize, 3), sl.body.len);
-}
-
-test "parse verbatim simples" {
- const allocator = testing.allocator;
- const template = "Texto {% verbatim %}{{ variável }}{% endblock %}{% endverbatim %} Texto";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 3), nodes.len);
- try testing.expectEqualStrings("Texto ", nodes[0].text.?.content);
- try testing.expectEqualStrings("{{ variável }}{% endblock %}", nodes[1].text.?.content);
- try testing.expectEqualStrings(" Texto", nodes[2].text.?.content);
-}
-
-test "parse verbatim aninhado" {
- const allocator = testing.allocator;
- const template = "{% verbatim %}Outer {% verbatim %}Inner{% endverbatim %} Outer{% endverbatim %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("Outer {% verbatim %}Inner{% endverbatim %} Outer", nodes[0].text.?.content);
-}
-
-test "parse url simples" {
- const allocator = testing.allocator;
- const template = "Link: {% url 'home' %} Fim";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 3), nodes.len);
- try testing.expect(nodes[1].type == .url);
- const u = nodes[1].url.?;
- try testing.expectEqualStrings("home", u.name);
- try testing.expectEqual(@as(usize, 0), u.args.len);
-}
-
-test "parse url com argumentos" {
- const allocator = testing.allocator;
- const template = "{% url 'post_detail' post.id \"comentario\" %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .url);
- const u = nodes[0].url.?;
- try testing.expectEqualStrings("post_detail", u.name);
- try testing.expectEqual(@as(usize, 2), u.args.len);
- try testing.expectEqualStrings("post.id", u.args[0]);
- try testing.expectEqualStrings("\"comentario\"", u.args[1]);
-}
-
-test "parse cycle simples" {
- const allocator = testing.allocator;
- const template = "{% cycle 'row1' 'row2' %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .cycle);
- const c = nodes[0].cycle.?;
- try testing.expectEqual(@as(usize, 2), c.values.len);
- try testing.expectEqualStrings("row1", c.values[0]);
- try testing.expectEqualStrings("row2", c.values[1]);
-}
-
-test "parse cycle com valores sem aspas" {
- const allocator = testing.allocator;
- const template = "{% cycle row1 row2 row3 %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .cycle);
- const c = nodes[0].cycle.?;
- try testing.expectEqual(@as(usize, 3), c.values.len);
- try testing.expectEqualStrings("row1", c.values[0]);
- try testing.expectEqualStrings("row2", c.values[1]);
- try testing.expectEqualStrings("row3", c.values[2]);
-}
-
-test "parse firstof simples" {
const allocator = testing.allocator;
const template = "Valor: {% firstof var1 var2 var3 %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 2), nodes.len); // corrigido
-
- try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("Valor: ", nodes[0].text.?.content);
-
- try testing.expect(nodes[1].type == .firstof);
- const fo = nodes[1].firstof.?;
- try testing.expectEqual(@as(usize, 3), fo.values.len);
- try testing.expectEqualStrings("var1", fo.values[0]);
- try testing.expectEqualStrings("var2", fo.values[1]);
- try testing.expectEqualStrings("var3", fo.values[2]);
-}
-
-test "parse firstof com fallback" {
- const allocator = testing.allocator;
- const template = "{% firstof var1 var2 \"Nenhum valor\" %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .firstof);
- const fo = nodes[0].firstof.?;
- try testing.expectEqual(@as(usize, 3), fo.values.len);
- try testing.expectEqualStrings("var1", fo.values[0]);
- try testing.expectEqualStrings("var2", fo.values[1]);
- try testing.expectEqualStrings("Nenhum valor", fo.values[2]);
-}
-
-test "parse load simples" {
- const allocator = testing.allocator;
- const template = "{% load i18n humanize %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .load);
- const l = nodes[0].load.?;
- try testing.expectEqual(@as(usize, 2), l.libraries.len);
- try testing.expectEqualStrings("i18n", l.libraries[0]);
- try testing.expectEqualStrings("humanize", l.libraries[1]);
-}
-
-test "parse load com múltiplas" {
- const allocator = testing.allocator;
- const template = "{% load admin_urls static %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .load);
- const l = nodes[0].load.?;
- try testing.expectEqual(@as(usize, 2), l.libraries.len);
- try testing.expectEqualStrings("admin_urls", l.libraries[0]);
- try testing.expectEqualStrings("static", l.libraries[1]);
-}
-
-test "parse csrf_token simples" {
- const allocator = testing.allocator;
- const template = "";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 3), nodes.len);
- try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("", nodes[2].text.?.content);
-}
-
-test "parse csrf_token sozinho" {
- const allocator = testing.allocator;
- const template = "{% csrf_token %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .csrf_token);
-}
-
-test "parse lorem padrão" {
- const allocator = testing.allocator;
- const template = "{% lorem %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .lorem);
- const l = nodes[0].lorem.?;
- try testing.expect(l.count == null);
- try testing.expect(l.method == null);
- try testing.expect(l.format == null);
-}
-
-test "parse lorem com argumentos" {
- const allocator = testing.allocator;
- const template = "{% lorem 5 p html %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .lorem);
- const l = nodes[0].lorem.?;
- try testing.expectEqualStrings("5", l.count.?);
- try testing.expectEqualStrings("p", l.method.?);
- try testing.expectEqualStrings("html", l.format.?);
-}
-
-test "parse debug simples" {
- const allocator = testing.allocator;
- const template = "Antes {% debug %} Depois";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 3), nodes.len);
- try testing.expect(nodes[0].type == .text);
- try testing.expectEqualStrings("Antes ", nodes[0].text.?.content);
-
- try testing.expect(nodes[1].type == .debug);
-
- try testing.expect(nodes[2].type == .text);
- try testing.expectEqualStrings(" Depois", nodes[2].text.?.content);
-}
-
-test "parse debug sozinho" {
- const allocator = testing.allocator;
- const template = "{% debug %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .debug);
-}
-
-test "parse partialdef simples" {
- const allocator = testing.allocator;
- const template = "{% partialdef cabecalho %}Cabeçalho{% endpartialdef %}";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .partialdef);
- const pd = nodes[0].partialdef.?;
- try testing.expectEqualStrings("cabecalho", pd.name);
- try testing.expectEqual(@as(usize, 1), pd.body.len);
- try testing.expectEqualStrings("Cabeçalho", pd.body[0].text.?.content);
-}
-
-test "parse partial uso" {
- const allocator = testing.allocator;
- const template = "Início {% partial \"cabecalho\" %} Fim";
- const nodes = try parser.parse(allocator, template);
- defer {
- for (nodes) |node| node.deinit(allocator);
- allocator.free(nodes);
- }
-
- try testing.expectEqual(@as(usize, 3), nodes.len);
- try testing.expect(nodes[1].type == .partial);
- try testing.expectEqualStrings("cabecalho", nodes[1].partial.?.name);
-}
-
-test "parse querystring simples" {
- const allocator = testing.allocator;
- const template = "Link: {% querystring \"page=2\" %}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 2), nodes.len);
- try testing.expect(nodes[1].type == .querystring);
- const qs = nodes[1].querystring.?;
- try testing.expectEqual(@as(usize, 1), qs.modifications.len);
- try testing.expectEqualStrings("page=2", qs.modifications[0]);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Valor: ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ const fo = nodes[1].tag.?.body.firstof.values;
+ try testing.expectEqual(@as(usize, 3), fo.len);
+ try testing.expectEqualStrings("var1", fo[0]);
+ try testing.expectEqualStrings("var2", fo[1]);
+ try testing.expectEqualStrings("var3", fo[2]);
+ try testing.expectEqualStrings("", nodes[1].tag.?.body.firstof.fallback);
}
-test "parse querystring múltiplos" {
+test "parse firstof with fallback" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("21 - parse firstof with fallback\n", .{});
+
const allocator = testing.allocator;
- const template = "{% querystring \"ordenar=-nome\" \"pagina\" None %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% firstof var1 var2 \"Nenhum valor\" %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .querystring);
- const qs = nodes[0].querystring.?;
- try testing.expectEqual(@as(usize, 3), qs.modifications.len);
- try testing.expectEqualStrings("ordenar=-nome", qs.modifications[0]);
- try testing.expectEqualStrings("pagina", qs.modifications[1]);
- try testing.expectEqualStrings("None", qs.modifications[2]);
+ try testing.expect(nodes[0].type == .tag);
+ const fo = nodes[0].tag.?.body.firstof.values;
+ try testing.expectEqual(@as(usize, 2), fo.len);
+ try testing.expectEqualStrings("var1", fo[0]);
+ try testing.expectEqualStrings("var2", fo[1]);
+ try testing.expectEqualStrings("Nenhum valor", nodes[0].tag.?.body.firstof.fallback);
}
-test "parse regroup simples" {
+test "parse for block whithout empty" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("22 - parse for block whithout empty\n", .{});
+
const allocator = testing.allocator;
- const template = "{% regroup pessoas by cidade as grupos_cidade %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% for item in lista %}Item: {{ item }}{% endfor %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .regroup);
- const r = nodes[0].regroup.?;
- try testing.expectEqualStrings("pessoas", r.source);
- try testing.expectEqualStrings("cidade", r.by);
- try testing.expectEqualStrings("grupos_cidade", r.as_var);
+ try testing.expect(nodes[0].type == .tag);
+ const fb = nodes[0].tag.?.body.@"for";
+ try testing.expectEqualStrings("item", fb.loop_var);
+ try testing.expectEqualStrings("lista", fb.iterable);
+ try testing.expectEqual(@as(usize, 2), fb.body.len);
+ try testing.expectEqual(@as(usize, 0), fb.empty_body.len);
+
+ try testing.expect(fb.body[0].type == .text);
+ try testing.expectEqualStrings("Item: ", fb.body[0].text.?.content);
+ try testing.expect(fb.body[1].type == .variable);
+ try testing.expectEqualStrings("item", fb.body[1].variable.?.expr);
}
-test "parse resetcycle simples" {
+test "parse for block with empty" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("23 - parse for block with empty\n", .{});
+
const allocator = testing.allocator;
- const template = "{% resetcycle %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% for item in lista %}Tem{% empty %}Vazio{% endfor %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .resetcycle);
- const rc = nodes[0].resetcycle.?;
- try testing.expect(rc.cycle_name == null);
+ try testing.expect(nodes[0].type == .tag);
+ const fb = nodes[0].tag.?.body.@"for";
+ try testing.expectEqual(@as(usize, 1), fb.body.len);
+ try testing.expectEqualStrings("Tem", fb.body[0].text.?.content);
+ try testing.expectEqual(@as(usize, 1), fb.empty_body.len);
+ try testing.expectEqualStrings("Vazio", fb.empty_body[0].text.?.content);
}
-test "parse resetcycle com nome" {
+test "parse basic if block" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("24 - parse basic if block\n", .{});
+
const allocator = testing.allocator;
- const template = "{% resetcycle rowclass %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% if user.logged %}Welcome!{% endif %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| {
+ node.deinit(allocator);
+ }
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+
+ const ib = nodes[0].tag.?.body.@"if";
+ try testing.expectEqualStrings("user.logged", ib.condition);
+ try testing.expectEqual(@as(usize, 1), ib.true_body.len);
+ try testing.expect(ib.true_body[0].type == .text);
+ try testing.expectEqualStrings("Welcome!", ib.true_body[0].text.?.content);
+ try testing.expectEqual(@as(usize, 0), ib.false_body.len);
+}
+
+test "parse if block whithout else" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("25 - parse if block whithout else\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% if cond %}Verdadeiro{% endif %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| {
+ node.deinit(allocator);
+ }
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ const ib = nodes[0].tag.?.body.@"if";
+ try testing.expectEqualStrings("cond", ib.condition);
+ try testing.expectEqual(@as(usize, 1), ib.true_body.len);
+ try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content);
+ try testing.expectEqual(@as(usize, 0), ib.false_body.len);
+}
+
+test "parse if block with else" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("26 - parse if block with else\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% if cond %}Verdadeiro{% else %}Falso{% endif %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| {
+ node.deinit(allocator);
+ }
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .if_block);
+ const ib = nodes[0].tag.?.body.@"if";
+ try testing.expectEqualStrings("cond", ib.condition);
+ try testing.expectEqual(@as(usize, 1), ib.true_body.len);
+ try testing.expectEqualStrings("Verdadeiro", ib.true_body[0].text.?.content);
+ try testing.expectEqual(@as(usize, 1), ib.false_body.len);
+ try testing.expectEqualStrings("Falso", ib.false_body[0].text.?.content);
+}
+
+test "parse simple include" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("27 - parse simple include\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Cabeçalho {% include \"header.html\" %} Conteúdo {% include \"footer.html\" %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 4), nodes.len);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Cabeçalho ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .include);
+ try testing.expectEqualStrings("header.html", nodes[1].tag.?.body.include.template_path);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings(" Conteúdo ", nodes[2].text.?.content);
+
+ try testing.expect(nodes[3].type == .tag);
+ try testing.expectEqualStrings("footer.html", nodes[3].tag.?.body.include.template_path);
+}
+
+test "parse include without quotes (it is ok for now)" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("28 - parse include without quotes (it is ok for now)\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% include header.html %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .resetcycle);
- const rc = nodes[0].resetcycle.?;
- try testing.expect(rc.cycle_name != null);
- try testing.expectEqualStrings("rowclass", rc.cycle_name.?);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .include);
+ try testing.expectEqualStrings("header.html", nodes[0].tag.?.body.include.template_path);
}
-test "parse widthratio simples" {
+test "parse simple load" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("29 - parse simple load\n", .{});
+
const allocator = testing.allocator;
- const template = "Progresso: {% widthratio progresso 0 100 %}%";
- const nodes = try parser.parse(allocator, template);
+ const template = "{% load i18n %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .load);
+ const l = nodes[0].tag.?.body.load;
+ try testing.expectEqual(@as(usize, 1), l.libraries.len);
+ try testing.expectEqualStrings("i18n", l.libraries[0]);
+}
+
+test "parse load with multiple libraries" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("30 - parse load with multiple libraries\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% load admin_urls static %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .load);
+ const l = nodes[0].tag.?.body.load;
+ try testing.expectEqual(@as(usize, 2), l.libraries.len);
+ try testing.expectEqualStrings("admin_urls", l.libraries[0]);
+ try testing.expectEqualStrings("static", l.libraries[1]);
+}
+test "parse simple lorem" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("31 - parse simple lorem\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% lorem %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .lorem);
+ const l = nodes[0].tag.?.body.lorem;
+ try testing.expect(l.count == null);
+ try testing.expect(l.method == null);
+ try testing.expect(l.format == null);
+}
+
+test "parse lorem with arguments" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("32 - parse lorem with arguments\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% lorem 5 p html %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .lorem);
+ const l = nodes[0].tag.?.body.lorem;
+ try testing.expectEqualStrings("5", l.count.?);
+ try testing.expectEqualStrings("p", l.method.?);
+ try testing.expectEqualStrings("html", l.format.?);
+}
+
+test "parse simple now" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("33 - parse simple now\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Data atual: {% now \"Y-m-d\" %} às {% now \"H:i\" %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 4), nodes.len);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Data atual: ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .now);
+ try testing.expectEqualStrings("Y-m-d", nodes[1].tag.?.body.now.format);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings(" às ", nodes[2].text.?.content);
+
+ try testing.expect(nodes[3].type == .tag);
+ try testing.expect(nodes[3].tag.?.kind == .now);
+ try testing.expectEqualStrings("H:i", nodes[3].tag.?.body.now.format);
+}
+
+test "parse now whithout quotes" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("34 - parse now whithout quotes\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% now Y-m-d %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .now);
+ try testing.expectEqualStrings("Y-m-d", nodes[0].tag.?.body.now.format);
+}
+
+test "parse simple partial" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("35 - parse simple partial\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Início {% partial \"cabecalho\" %} Fim";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 3), nodes.len);
- try testing.expect(nodes[1].type == .widthratio);
- const wr = nodes[1].widthratio.?;
- try testing.expectEqualStrings("progresso", wr.value);
- try testing.expectEqualStrings("0", wr.max_value);
- try testing.expectEqualStrings("100", wr.divisor.?);
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Início ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .partial);
+ try testing.expectEqualStrings("cabecalho", nodes[1].tag.?.body.partial.name);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings(" Fim", nodes[2].text.?.content);
}
-test "parse widthratio sem divisor" {
+test "parse partial whithout quotes" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("36 - parse partial whithout quotes\n", .{});
+
const allocator = testing.allocator;
- const template = "{% widthratio valor 100 %}";
- const nodes = try parser.parse(allocator, template);
+ const template = "Início {% partial cabecalho %} Fim";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 3), nodes.len);
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Início ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .partial);
+ try testing.expectEqualStrings("cabecalho", nodes[1].tag.?.body.partial.name);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings(" Fim", nodes[2].text.?.content);
+}
+
+test "parse simple partialdef" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("37 - parse simple partialdef\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% partialdef cabecalho %}Cabeçalho{% endpartialdef %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .widthratio);
- const wr = nodes[0].widthratio.?;
- try testing.expectEqualStrings("valor", wr.value);
- try testing.expectEqualStrings("100", wr.max_value);
- try testing.expect(wr.divisor == null);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .partialdef);
+ const pd = nodes[0].tag.?.body.partialdef;
+ try testing.expectEqualStrings("cabecalho", pd.name);
+ try testing.expectEqual(@as(usize, 1), pd.body.len);
+ try testing.expectEqualStrings("Cabeçalho", pd.body[0].text.?.content);
}
+test "parse simple querystring" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("38 - parse simple querystring\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Link: {% querystring \"page=2\" %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 2), nodes.len);
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .querystring);
+ const qs = nodes[1].tag.?.body.querystring;
+ try testing.expectEqual(@as(usize, 1), qs.modifications.len);
+ try testing.expectEqualStrings("page=2", qs.modifications[0]);
+}
+
+test "parse querystring with multiple modifications" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("39 - parse querystring with multiple modifications\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% querystring \"ordenar=-nome\" \"pagina\" None %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .querystring);
+ const qs = nodes[0].tag.?.body.querystring;
+ try testing.expectEqual(@as(usize, 3), qs.modifications.len);
+ try testing.expectEqualStrings("ordenar=-nome", qs.modifications[0]);
+ try testing.expectEqualStrings("pagina", qs.modifications[1]);
+ try testing.expectEqualStrings("None", qs.modifications[2]);
+}
+
+test "parse simple regroup" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("40 - parse simple regroup\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% regroup pessoas by cidade as grupos_cidade %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .regroup);
+ const r = nodes[0].tag.?.body.regroup;
+ try testing.expectEqualStrings("pessoas", r.source);
+ try testing.expectEqualStrings("cidade", r.by);
+ try testing.expectEqualStrings("grupos_cidade", r.as_var);
+}
+
+test "parse simple resetcycle" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("41 - parse simple resetcycle\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% resetcycle %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .resetcycle);
+ const rc = nodes[0].tag.?.body.resetcycle;
+ try testing.expect(rc.cycle_name == null);
+}
+
+test "parse resetcycle with name" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("42 - parse resetcycle with name\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% resetcycle rowclass %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .resetcycle);
+ const rc = nodes[0].tag.?.body.resetcycle;
+ try testing.expect(rc.cycle_name != null);
+ try testing.expectEqualStrings("rowclass", rc.cycle_name.?);
+}
+test "parse simple spaceless" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("43 - parse simple spaceless\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% spaceless %} Foo
{% endspaceless %}";
+
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .spaceless);
+ const sl = nodes[0].tag.?.body.spaceless;
+ try testing.expectEqual(@as(usize, 1), sl.body.len);
+ try testing.expectEqualStrings(" Foo
", sl.body[0].text.?.content);
+}
+
+// TODO: check nested spaceless
+test "parse spaceless nested" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("44 - parse spaceless nested\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% spaceless %}Outer {% spaceless %}Inner{% endspaceless %} Outer{% endspaceless %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .spaceless);
+ const sl = nodes[0].tag.?.body.spaceless;
+
+ try testing.expectEqual(@as(usize, 4), sl.body.len);
+}
test "parse templatetag openblock" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("45 - parse templatetag openblock\n", .{});
+
const allocator = testing.allocator;
const template = "{% templatetag openblock %}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .templatetag);
- try testing.expect(nodes[0].templatetag.?.kind == .openblock);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .templatetag);
+ try testing.expect(nodes[0].tag.?.body.templatetag.kind == .openblock);
}
test "parse templatetag closevariable" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("46 - parse templatetag closevariable\n", .{});
+
const allocator = testing.allocator;
const template = "{% templatetag closevariable %}";
- const nodes = try parser.parse(allocator, template);
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
defer {
for (nodes) |node| node.deinit(allocator);
allocator.free(nodes);
}
try testing.expectEqual(@as(usize, 1), nodes.len);
- try testing.expect(nodes[0].type == .templatetag);
- try testing.expect(nodes[0].templatetag.?.kind == .closevariable);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .templatetag);
+ try testing.expect(nodes[0].tag.?.body.templatetag.kind == .closevariable);
}
+
+test "parse simple url" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("47 - parse simple url\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Link: {% url 'home' %} Fim";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 3), nodes.len);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Link: ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .url);
+ const u = nodes[1].tag.?.body.url;
+ try testing.expectEqualStrings("home", u.name);
+ try testing.expectEqual(@as(usize, 0), u.args.len);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings(" Fim", nodes[2].text.?.content);
+}
+
+test "parse url with arguments" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("48 - parse url with arguments\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% url 'post_detail' post.id \"comentario\" %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .url);
+ const u = nodes[0].tag.?.body.url;
+ try testing.expectEqualStrings("post_detail", u.name);
+ try testing.expectEqual(@as(usize, 2), u.args.len);
+ try testing.expectEqualStrings("post.id", u.args[0]);
+ try testing.expectEqualStrings("\"comentario\"", u.args[1]);
+}
+
+test "parse simple widthratio" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("49 - parse simple widthratio\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Progresso: {% widthratio progresso 0 100 %}%";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 3), nodes.len);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Progresso: ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .widthratio);
+ const wr = nodes[1].tag.?.body.widthratio;
+ try testing.expectEqualStrings("progresso", wr.value);
+ try testing.expectEqualStrings("0", wr.max_value);
+ try testing.expectEqualStrings("100", wr.divisor.?);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings("%", nodes[2].text.?.content);
+}
+
+test "parse simple widthratio without divisor" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("50 - parse simple widthratio without divisor\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Progresso: {% widthratio progresso 0 %}%";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 3), nodes.len);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Progresso: ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .widthratio);
+ const wr = nodes[1].tag.?.body.widthratio;
+ try testing.expectEqualStrings("progresso", wr.value);
+ try testing.expectEqualStrings("0", wr.max_value);
+ try testing.expect(wr.divisor == null);
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings("%", nodes[2].text.?.content);
+}
+
+test "parse simple verbatim" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("51 - parse simple verbatim\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "Texto {% verbatim %}{{ variável }}{% endblock %}{% endverbatim %} Texto";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 3), nodes.len);
+
+ try testing.expect(nodes[0].type == .text);
+ try testing.expectEqualStrings("Texto ", nodes[0].text.?.content);
+
+ try testing.expect(nodes[1].type == .tag);
+ try testing.expect(nodes[1].tag.?.kind == .verbatim);
+ const vb = nodes[1].tag.?.body.verbatim;
+ try testing.expectEqualStrings("{{ variável }}{% endblock %}", vb.content);
+ try testing.expect(std.mem.eql(u8, vb.name.?, ""));
+
+ try testing.expect(nodes[2].type == .text);
+ try testing.expectEqualStrings(" Texto", nodes[2].text.?.content);
+}
+
+test "parse verbatim nested" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("52 - parse verbatim nested\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% verbatim baz %}Outer {% verbatim %}Inner{% endverbatim %} Outer{% endverbatim baz %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .verbatim);
+ const vb = nodes[0].tag.?.body.verbatim;
+ try testing.expectEqualStrings("Outer {% verbatim %}Inner{% endverbatim %} Outer", vb.content);
+ try testing.expect(std.mem.eql(u8, vb.name.?, "baz"));
+}
+
+test "parse simple with block" {
+ std.debug.print("____________________________________________________\n", .{});
+ std.debug.print("53 - parse with block\n", .{});
+
+ const allocator = testing.allocator;
+ const template = "{% with nome=\"Lucas\" idade=30 %}Olá {{ nome }}, você tem {{ idade }} anos.{% endwith %}";
+ var p = parser.Parser.init(template);
+ const nodes = try p.parse(allocator);
+ defer {
+ for (nodes) |node| node.deinit(allocator);
+ allocator.free(nodes);
+ }
+
+ try testing.expectEqual(@as(usize, 1), nodes.len);
+ try testing.expect(nodes[0].type == .tag);
+ try testing.expect(nodes[0].tag.?.kind == .with_block);
+ const w = nodes[0].tag.?.body.with;
+ try testing.expectEqual(@as(usize, 2), w.assignments.len);
+ try testing.expectEqual(@as(usize, 5), w.body.len);
+ try testing.expect(w.body[0].type == .text);
+}
+
+// test "parse simple tag" {
+// std.debug.print("____________________________________________________\n", .{});
+// std.debug.print("54 - parse simple tag\n", .{});
+//
+// const allocator = testing.allocator;
+// const template = "Antes {% minha_tag %} Depois";
+// var p = parser.Parser.init(template);
+// const nodes = try p.parse(allocator);
+// defer {
+// for (nodes) |node| {
+// node.deinit(allocator);
+// }
+// allocator.free(nodes);
+// }
+//
+// try testing.expectEqual(@as(usize, 3), nodes.len);
+//
+// try testing.expect(nodes[0].type == .text);
+// try testing.expectEqualStrings("Antes ", nodes[0].text.?.content);
+//
+// try testing.expect(nodes[1].type == .tag);
+// try testing.expectEqualStrings("minha_tag", nodes[1].tag.?.name);
+// try testing.expectEqualStrings("", nodes[1].tag.?.args);
+//
+// try testing.expect(nodes[2].type == .text);
+// try testing.expectEqualStrings(" Depois", nodes[2].text.?.content);
+// }
diff --git a/src/renderer.zig b/src/renderer.zig
index 97d7d83..d317617 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -10,6 +10,8 @@ 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");
pub const RenderError = error{
InvalidCharacter,
@@ -36,7 +38,8 @@ 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;
}
@@ -60,7 +63,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);
@@ -122,19 +126,18 @@ 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);
}
}
@@ -186,167 +189,244 @@ pub const Renderer = struct {
try self.valueToString(alloc, &buf, value);
try writer.writeAll(buf.items);
},
- .if_block => {
- const condition = try self.evaluateCondition(alloc, node.@"if".?.condition);
+ .tag => {
+ switch (node.tag.?.kind) {
+ .if_block => {
+ const condition = try self.evaluateCondition(alloc, node.tag.?.body.@"if".condition);
- 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);
- }
- }
- },
- .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);
- }
-
- if (node.@"for".?.body.len == 0) {
- for (node.@"for".?.empty_body) |child| {
- try self.renderNode(alloc, nodes, child, writer, &ctx, null);
+ if (condition) {
+ for (node.tag.?.body.@"if".true_body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, null, null);
+ }
+ } else {
+ for (node.tag.?.body.@"if".false_body) |child| {
+ try self.renderNode(alloc, nodes, child, writer, null, 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) |item| {
+ var ctx = Context.init(alloc);
+ defer ctx.deinit();
+
+ try ctx.set(node.tag.?.body.@"for".loop_var, item);
+
+ 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, "", .{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;
+ }
+ },
+ else => {},
}
},
- .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);
- }
- },
- .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.widthratio.?.value, "")) {
- value = Value{ .string = node.widthratio.?.value };
- if (self.context.get(node.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.widthratio.?.max_value, "")) {
- max_value = Value{ .string = node.widthratio.?.max_value };
- if (self.context.get(node.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.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");
- },
- 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 toValue(self: *Context, value: anytype) RenderError!Value {
- const T = @TypeOf(value);
- return switch (@typeInfo(T)) {
- .bool => Value{ .bool = value },
- .int, .comptime_int => Value{ .int = @intCast(value) },
- .float, .comptime_float => Value{ .float = @floatCast(value) },
- .pointer => Value{ .string = try std.fmt.allocPrint(self.allocator(), "{s}", .{value}) },
- .@"struct" => blk: {
- var dict = std.StringHashMapUnmanaged(Value){};
- inline for (std.meta.fields(T)) |field| {
- const field_val = @field(value, field.name);
- const converted = try self.toValue(field_val);
- try dict.put(self.allocator(), field.name, converted);
- }
- break :blk Value{ .dict = dict };
- },
- .array => blk: {
- var list = try self.allocator().alloc(Value, value.len);
- for (value, 0..) |item, i| {
- list[i] = try self.toValue(item);
- }
- break :blk Value{ .list = list };
- },
- .optional => if (value) |v| try self.toValue(v) else .null,
- .null => .null,
- // CASO ESPECIAL: o valor já é um Value (ex: lista de Value)
- .@"union" => if (T == Value) value else @compileError("Unsupported union type: " ++ @typeName(T)),
- else => @compileError("Unsupported type: " ++ @typeName(T)),
+ 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, "&"),
+ '<' => try result.appendSlice(self.allocator, "<"),
+ '>' => try result.appendSlice(self.allocator, ">"),
+ '"' => try result.appendSlice(self.allocator, """),
+ '\'' => try result.appendSlice(self.allocator, "'"),
+ 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 {
diff --git a/src/renderer_test.zig b/src/renderer_test.zig
index 4b36c79..2c5a847 100644
--- a/src/renderer_test.zig
+++ b/src/renderer_test.zig
@@ -10,7 +10,7 @@ const TemplateCache = @import("cache.zig").TemplateCache;
test "renderer: literal + variável simples" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n1 - renderer: literal + variável simples\n\n", .{});
+ std.debug.print("1 - renderer: literal + variável simples\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -20,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);
@@ -33,12 +33,12 @@ test "renderer: literal + variável simples" {
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
- try testing.expectEqualStrings("Olá, Mariana! Bem-vinda.", buf.items);
+ try testing.expectEqualStrings("Olá, Fulana! Bem-vinda.", buf.items);
}
test "renderer: filtros + autoescape" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n2 - renderer: filtros + autoescape\n\n", .{});
+ std.debug.print("2 - renderer: filtros + autoescape\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -68,7 +68,6 @@ test "renderer: filtros + autoescape" {
\\Filtrado: maiusculo-e-slug
;
-
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
@@ -76,7 +75,7 @@ test "renderer: filtros + autoescape" {
test "literal simples" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n3 - literal simples\n\n", .{});
+ std.debug.print("3 - literal simples\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -100,7 +99,7 @@ test "literal simples" {
test "variável com filtro encadeado e autoescape" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n4 - variável com filtro encadeado e autoescape\n\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();
@@ -126,7 +125,7 @@ test "variável com filtro encadeado e autoescape" {
test "autoescape com safe" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n5 - autoescape com safe\n\n", .{});
+ std.debug.print("5 - autoescape com safe\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -150,10 +149,10 @@ test "autoescape com safe" {
try testing.expectEqualStrings("Escape: <div>conteúdo</div> | Safe: conteúdo
", buf.items);
}
-// TODO: evaluationConditions more complex
+// // TODO: evaluationConditions more complex
test "renderer - if and for" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n6 - renderer - if and for\n\n", .{});
+ std.debug.print("6 - renderer - if and for\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -195,7 +194,7 @@ test "renderer - if and for" {
test "renderer - block and extends" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n7 - renderer - block and extends\n\n", .{});
+ std.debug.print("7 - renderer - block and extends\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -260,7 +259,7 @@ test "renderer - block and extends" {
test "renderer - block and extends with super" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n8 - renderer - block and extends with super\n\n", .{});
+ std.debug.print("8 - renderer - block and extends with super\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
@@ -327,7 +326,7 @@ test "renderer - block and extends with super" {
test "renderer - include" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n9 - renderer - include\n\n", .{});
+ std.debug.print("9 - renderer - include\n", .{});
const alloc = testing.allocator;
const header =
@@ -386,7 +385,7 @@ test "renderer - include" {
test "renderer - comment" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n10 - renderer - comment\n\n", .{});
+ std.debug.print("10 - renderer - comment\n", .{});
const alloc = testing.allocator;
const template =
@@ -434,7 +433,7 @@ test "renderer - comment" {
test "renderer - full template with extends, super, include, comment" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n11 - renderer - full template with extends, super, include, comment\n\n", .{});
+ std.debug.print("11 - renderer - full template with extends, super, include, comment\n", .{});
const alloc = testing.allocator;
const header = "";
@@ -482,14 +481,16 @@ test "renderer - full template with extends, super, include, comment" {
// std.debug.print("OUTPUT:\n\n{s}\n", .{output});
try testing.expect(std.mem.indexOf(u8, output, "") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "") != 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("\n12 - render - if inside block\n\n", .{});
+ std.debug.print("12 - render - if inside block\n", .{});
const alloc = testing.allocator;
const base =
@@ -541,11 +542,12 @@ test "renderer - if inside block" {
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("\n13 - render - if inside block\n\n", .{});
+ std.debug.print("13 - render - if inside block\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
@@ -585,7 +587,7 @@ test "renderer - if with operators" {
test "renderer - widthratio inside block" {
std.debug.print("____________________________________________________\n", .{});
- std.debug.print("\n14 - render - widthratio inside block\n\n", .{});
+ std.debug.print("14 - render - widthratio inside block\n", .{});
const alloc = testing.allocator;
const base =
@@ -633,3 +635,436 @@ test "renderer - widthratio inside block" {
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 =
+ \\
+ ;
+ 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, "");
+ 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, "
");
+ try testing.expect(spaces == 3);
+}
diff --git a/src/time.zig b/src/time.zig
new file mode 100644
index 0000000..a8a803b
--- /dev/null
+++ b/src/time.zig
@@ -0,0 +1,874 @@
+// 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 já 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 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 (já lida com excessos, mas aqui é só 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);
+// }
diff --git a/src/util.zig b/src/util.zig
new file mode 100644
index 0000000..a36f8ff
--- /dev/null
+++ b/src/util.zig
@@ -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 só 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));
+}