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 = "
    {% 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, 3), nodes.len); - try testing.expect(nodes[0].type == .text); - try testing.expectEqualStrings("Antes ", nodes[0].text.?.content); + try testing.expectEqualStrings("
    ", 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[1].tag.?.kind == .csrf_token); try testing.expect(nodes[2].type == .text); - try testing.expectEqualStrings(" Depois", nodes[2].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 = "
    {% csrf_token %}
    "; - 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[0].text.?.content); - - try testing.expect(nodes[1].type == .csrf_token); - - try testing.expect(nodes[2].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 = "
    Bem-vindo
    "; @@ -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, "
    Bem-vindo
    ") != 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)); +}