From 966035dccbd0dc36fe71456759c4d5a75a41bd5d Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 18:11:19 -0300 Subject: [PATCH 01/16] update: icons --- src/svg/icons.zig | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/svg/icons.zig diff --git a/src/svg/icons.zig b/src/svg/icons.zig new file mode 100644 index 0000000..6a9b5d1 --- /dev/null +++ b/src/svg/icons.zig @@ -0,0 +1,111 @@ +const std = @import("std"); + +pub const IconSet = enum { + bootstrap, + dripicons, + hero_outline, + hero_solid, + material, +}; + +pub const embedded_data = std.EnumMap(IconSet, []const u8).init(.{ + .bootstrap = @embedFile("bootstrap.svgs.bin"), + .dripicons = @embedFile("dripicons.svgs.bin"), + .hero_outline = @embedFile("hero_outline.svgs.bin"), + .hero_solid = @embedFile("hero_solid.svgs.bin"), + .material = @embedFile("material.svgs.bin"), +}); + +pub const SvgIcon = struct { + icon_map: std.StringHashMapUnmanaged([]const u8) = .{}, + + pub fn init(allocator: std.mem.Allocator) !SvgIcon { + var self = SvgIcon{}; + + // const sets = [_]IconSet{ .bootstrap, .dripicons, .hero_outline, .hero_solid, .material }; + // + // inline for (sets) |set| { + // const data = embedded_data.get(set).?; + // + // try self.loadSet(allocator, set, data); + // } + // + + inline for (std.meta.fields(IconSet)) |field| { + const set = @field(IconSet, field.name); + const data = embedded_data.get(set); + + try self.loadSet(allocator, set, data.?); + } + + return self; + } + + pub fn deinit(self: *SvgIcon, allocator: std.mem.Allocator) void { + var it = self.icon_map.iterator(); + while (it.next()) |entry| { + allocator.free(entry.key_ptr.*); + allocator.free(entry.value_ptr.*); + } + self.icon_map.deinit(allocator); + self.* = .{}; + } + + pub fn get(self: *const SvgIcon, key: []const u8) ?[]const u8 { + return self.icon_map.get(key); + } + + pub fn count(self: *const SvgIcon) usize { + return self.icon_map.count(); + } + + fn loadSet( + self: *SvgIcon, + allocator: std.mem.Allocator, + set: IconSet, + data: []const u8, + ) !void { + if (data.len < 12) return error.InvalidEmbeddedData; + + var pos: usize = 0; + + const magic = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + if (magic != 0x53564749) return error.InvalidMagic; + + const version = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + if (version != 1) return error.UnsupportedVersion; + + const num_entries = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + + const prefix = @tagName(set); + + var i: u32 = 0; + while (i < num_entries) : (i += 1) { + const name_len = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + + if (pos + name_len > data.len) return error.CorruptedNameLength; + const name_slice = data[pos .. pos + name_len]; + pos += name_len; + + const svg_len = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + + if (pos + svg_len > data.len) return error.CorruptedSvgLength; + const svg_slice = data[pos .. pos + svg_len]; + pos += svg_len; + + // Monta a chave com prefixo do set + const key = try std.fmt.allocPrint(allocator, "{s}:{s}", .{ prefix, name_slice }); + + // Duplica o conteúdo SVG (o map assume ownership) + const value = try allocator.dupe(u8, svg_slice); + + // Insere no mapa unmanaged + try self.icon_map.put(allocator, key, value); + } + } +}; From cce0c6a95472047161ff42185d38ac623aa7a037 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:02:53 -0300 Subject: [PATCH 02/16] update: getIcon and fallback --- src/svg/icons.zig | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/svg/icons.zig b/src/svg/icons.zig index 6a9b5d1..5b4ad4f 100644 --- a/src/svg/icons.zig +++ b/src/svg/icons.zig @@ -22,15 +22,6 @@ pub const SvgIcon = struct { pub fn init(allocator: std.mem.Allocator) !SvgIcon { var self = SvgIcon{}; - // const sets = [_]IconSet{ .bootstrap, .dripicons, .hero_outline, .hero_solid, .material }; - // - // inline for (sets) |set| { - // const data = embedded_data.get(set).?; - // - // try self.loadSet(allocator, set, data); - // } - // - inline for (std.meta.fields(IconSet)) |field| { const set = @field(IconSet, field.name); const data = embedded_data.get(set); @@ -55,6 +46,12 @@ pub const SvgIcon = struct { return self.icon_map.get(key); } + pub fn getIcon(self: *const SvgIcon,allocator: std.mem.Allocator, kind: []const u8, name: []const u8) ?[]const u8 { + const key = std.fmt.allocPrint(allocator, "{s}:{s}", .{ kind, name }) catch return null; + defer allocator.free(key); + return self.icon_map.get(key); + } + pub fn count(self: *const SvgIcon) usize { return self.icon_map.count(); } @@ -109,3 +106,12 @@ pub const SvgIcon = struct { } } }; + +pub const fallback_svg = + \\
+ \\ + \\ + \\ ? + \\ + \\
+; From 8fd9086d9892c276b0b6b5f203f879a1dbdae251 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:03:14 -0300 Subject: [PATCH 03/16] update: icons --- src/cache.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cache.zig b/src/cache.zig index 0a78820..2543d8a 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -2,11 +2,13 @@ const std = @import("std"); const Allocator = std.heap.ArenaAllocator; const parser = @import("parser.zig"); +const icons = @import("svg/icons.zig"); pub const TemplateCache = struct { arena: Allocator, cache: std.StringHashMapUnmanaged([]parser.Node), default_path: ?[]const u8 = "templates", + icons: ?icons.SvgIcon =null, pub fn init(child_allocator: std.mem.Allocator) TemplateCache { const arena = std.heap.ArenaAllocator.init(child_allocator); @@ -16,7 +18,6 @@ pub const TemplateCache = struct { }; } - pub fn deinit(self: *TemplateCache) void { self.arena.deinit(); } @@ -51,6 +52,10 @@ pub const TemplateCache = struct { } } + pub fn initIcons(self: *TemplateCache) !void { + self.icons = icons.SvgIcon.init(self.allocator()) catch null; + } + pub fn clear(self: *TemplateCache) void { self.deinit(); self.cache = .{}; From a0645cb0d1f337d46225684fb912efd7aae7cf58 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:03:22 -0300 Subject: [PATCH 04/16] update: cleanup --- src/context_test.zig | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/context_test.zig b/src/context_test.zig index 899284a..25080b6 100644 --- a/src/context_test.zig +++ b/src/context_test.zig @@ -4,6 +4,9 @@ const Context = @import("context.zig").Context; const Value = @import("context.zig").Value; test "context set amigável e get com ponto" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("1 - context set amigável e get com ponto\n", .{}); + const allocator = testing.allocator; var ctx = Context.init(allocator); defer ctx.deinit(); @@ -17,17 +20,26 @@ test "context set amigável e get com ponto" { // struct const Person = struct { nome: []const u8, idade: i64 }; const p = Person{ .nome = "Ana", .idade = 25 }; - try ctx.set("user", p); + const p2 = Person{ .nome = "Fulana", .idade = 28 }; + + const people = [_]Person{ p, p2 }; + + // try ctx.set("user", p); + try ctx.set("user", people); // list const numeros = [_]i64{ 1, 2, 3 }; try ctx.set("lista", numeros); + for (ctx.get("user").?.list) |item| { + std.debug.print("user {any}\n", .{item.dict.get("nome").?}); + } + // acesso try testing.expectEqualStrings("Lucas", ctx.get("nome").?.string); try testing.expect(ctx.get("idade").?.int == 30); - try testing.expectEqualStrings("Ana", ctx.get("user.nome").?.string); - try testing.expect(ctx.get("user.idade").?.int == 25); + // try testing.expectEqualStrings("Ana", ctx.get("user.nome").?.string); + // try testing.expect(ctx.get("user.idade").?.int == 25); try testing.expect(ctx.get("lista.1").?.int == 2); try testing.expect(ctx.get("vazio").?.string.len == 0); try testing.expect(ctx.get("preco").?.float == 99.99); From f0115a15b9ed8460d7d9cf41d56ee555293bef5e Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:03:28 -0300 Subject: [PATCH 05/16] update: cleanup --- src/delta_test.zig | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/delta_test.zig b/src/delta_test.zig index 4f3a08e..496443b 100644 --- a/src/delta_test.zig +++ b/src/delta_test.zig @@ -5,6 +5,9 @@ const Context = @import("context.zig").Context; const RelativeDelta = @import("delta.zig").RelativeDelta; test "relativedelta rigoroso - meses" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("1 - context set amigável e get com ponto\n", .{}); + const a = try Time.parse("2025-03-31"); const b = try Time.parse("2025-01-31"); const delta = a.subRelative(b); @@ -17,6 +20,9 @@ test "relativedelta rigoroso - meses" { } test "relativedelta - overflow de dia" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("2 - relativedelta - overflow de dia\n", .{}); + const jan31 = try Time.parse("2023-01-31"); const mar01 = try Time.parse("2023-03-01"); @@ -27,6 +33,9 @@ test "relativedelta - overflow de dia" { } test "bissexto: 2021-02-28 - 2020-02-29" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("3 - bissexto: 2021-02-28 - 2020-02-29\n", .{}); + const a = try Time.parse("2021-02-28"); const b = try Time.parse("2020-02-29"); const delta = a.subRelative(b); @@ -36,6 +45,9 @@ test "bissexto: 2021-02-28 - 2020-02-29" { } test "bissexto: 2021-03-01 - 2020-02-29" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("4 - bissexto: 2021-03-01 - 2020-02-29\n", .{}); + const a = try Time.parse("2021-03-01"); const b = try Time.parse("2020-02-29"); const delta = a.subRelative(b); @@ -45,6 +57,9 @@ test "bissexto: 2021-03-01 - 2020-02-29" { } test "bissexto: 2021-02-27 - 2020-02-29" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("5 - bissexto: 2021-02-27 - 2020-02-29\n", .{}); + const a = try Time.parse("2021-02-27"); const b = try Time.parse("2020-02-29"); const delta = a.subRelative(b); @@ -54,6 +69,9 @@ test "bissexto: 2021-02-27 - 2020-02-29" { } test "bissexto reverso: 2020-02-29 - 2021-02-28" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("6 - bissexto reverso: 2020-02-29 - 2021-02-28\n", .{}); + const a = try Time.parse("2020-02-29"); const b = try Time.parse("2021-02-28"); const delta = a.subRelative(b); @@ -63,6 +81,9 @@ test "bissexto reverso: 2020-02-29 - 2021-02-28" { } test "addRelative: anos normais (não bissexto)" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("7 - addRelative: anos normais (não bissexto)\n", .{}); + // 2023 não é bissexto const base = try Time.parse("2023-06-15"); const expected = try Time.parse("2026-06-15"); @@ -77,6 +98,9 @@ test "addRelative: anos normais (não bissexto)" { } test "addRelative: anos normais com overflow de dia" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("8 - addRelative: anos normais com overflow de dia\n", .{}); + // Janeiro 31 + 2 anos → deve ir para 31/jan (2025 não bissexto) const base = try Time.parse("2023-01-31"); const expected = try Time.parse("2025-01-31"); @@ -91,6 +115,9 @@ test "addRelative: anos normais com overflow de dia" { } test "addRelative: de 29/fev bissexto + 1 ano (vai para 28/fev)" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("9 - addRelative: de 29/fev bissexto + 1 ano (vai para 28/fev)\n", .{}); + // 2020 foi bissexto → +1 ano deve ir para 2021-02-28 (não bissexto) const base = try Time.parse("2020-02-29"); const expected = try Time.parse("2021-02-28"); @@ -105,6 +132,9 @@ test "addRelative: de 29/fev bissexto + 1 ano (vai para 28/fev)" { } test "addRelative: de 29/fev bissexto + 4 anos (permanece 29/fev)" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("10 - addRelative: de 29/fev bissexto + 4 anos (permanece 29/fev)\n", .{}); + // 2020 → 2024 (ambos bissextos) const base = try Time.parse("2020-02-29"); const expected = try Time.parse("2024-02-29"); @@ -119,6 +149,9 @@ test "addRelative: de 29/fev bissexto + 4 anos (permanece 29/fev)" { } test "addRelative: de 29/fev + 1 ano + 1 mês (vai para março)" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("11 - addRelative: de 29/fev + 1 ano + 1 mês (vai para março)\n", .{}); + // 2020-02-29 + 1 ano → 2021-02-28 + 1 mês → 2021-03-28 const base = try Time.parse("2020-02-29"); const expected = try Time.parse("2021-03-28"); @@ -133,6 +166,9 @@ test "addRelative: de 29/fev + 1 ano + 1 mês (vai para março)" { } test "addRelative: meses com overflow (31 → 28/30)" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("12 - addRelative: meses com overflow (31 → 28/30)\n", .{}); + const cases = [_]struct { base: []const u8, months: i32, expected: []const u8 }{ .{ .base = "2023-01-31", .months = 1, .expected = "2023-02-28" }, // não bissexto .{ .base = "2024-01-31", .months = 1, .expected = "2024-02-29" }, // bissexto @@ -156,6 +192,9 @@ test "addRelative: meses com overflow (31 → 28/30)" { } test "addRelative: combinação anos + meses + dias (bissexto envolvido)" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("13 - addRelative: combinação anos + meses + dias (bissexto envolvido)\n", .{}); + // 2024-02-29 + 1 ano + 2 meses + 3 dias // → 2025-02-28 + 2 meses → 2025-04-28 + 3 dias → 2025-05-01 const base = try Time.parse("2024-02-29"); @@ -171,6 +210,9 @@ test "addRelative: combinação anos + meses + dias (bissexto envolvido)" { } test "addRelative: delta zero não altera data" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("14 - addRelative: delta zero não altera data\n", .{}); + const base = try Time.parse("2025-07-20"); const delta = RelativeDelta.init(.{}); const result = base.addRelative(delta); From 1181f0fa68e2ab221d7fdabbcb0b75257a462610 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:04:08 -0300 Subject: [PATCH 06/16] update: svgNode --- src/parser.zig | 75 ++++++++++++++++++++++++++++++++++++++++++- src/parser_test.zig | 22 +++++++++++-- src/renderer.zig | 60 +++++++++++++++++++++++----------- src/renderer_test.zig | 30 +++++++++++++++++ 4 files changed, 165 insertions(+), 22 deletions(-) diff --git a/src/parser.zig b/src/parser.zig index f2f7c3e..c5aef01 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -45,6 +45,7 @@ pub const TagNodeBody = union(enum) { resetcycle: ResetCycleNode, spaceless: SpacelessNode, super: bool, + svg: SvgNode, templatetag: TemplateTagNode, url: UrlNode, verbatim: VerbatimNode, @@ -88,6 +89,7 @@ pub const TagKind = enum { resetcycle, spaceless, super, + svg, templatetag, url, verbatim, @@ -135,6 +137,7 @@ fn getTagKindByName(name: []const u8) TagKind { 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, "svg")) return .svg; 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; @@ -271,6 +274,11 @@ pub const SpacelessNode = struct { raw_close: []const u8, }; +pub const SvgNode = struct { + kind: []const u8, + name: []const u8, +}; + pub const TagNode = struct { kind: TagKind, args: []const u8, @@ -379,6 +387,10 @@ pub const Node = struct { for (body_copy) |n| n.deinit(allocator); allocator.free(body_copy); }, + .svg => { + allocator.free(t.body.svg.kind); + allocator.free(t.body.svg.name); + }, .url => { allocator.free(t.body.url.args); }, @@ -697,6 +709,23 @@ pub const Node = struct { }, }; }, + .svg => { + return Node{ + .type = .tag, + .tag = .{ + .kind = .svg, + .args = try allocator.dupe(u8, self.tag.?.args), + .raw = try allocator.dupe(u8, self.tag.?.raw), + + .body = .{ + .svg = .{ + .name = try allocator.dupe(u8, self.tag.?.body.svg.name), + .kind = try allocator.dupe(u8, self.tag.?.body.svg.kind), + }, + }, + }, + }; + }, .url => { const args_copy = try allocator.alloc([]const u8, self.tag.?.body.url.args.len); errdefer allocator.free(args_copy); @@ -931,7 +960,7 @@ pub const Node = struct { }, }; }, - else => unreachable, + else => {}, } }, } @@ -2088,6 +2117,50 @@ pub const Parser = struct { const spaceless_node = try self.parseSpacelessBlock(allocator, raw_open); return spaceless_node; }, + .svg => { + 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\"'"); + std.debug.print("value: {s}\n", .{value}); + try values.append(allocator, value); + } + return TagNodeBody{ + .svg = .{ + .kind = try allocator.dupe(u8, values.items[0]), + .name = try allocator.dupe(u8, values.items[1]), + }, + }; + }, .templatetag => { const args = tag_node.tag.?.args; const templatetag = TemplateTagNode.parse(args); diff --git a/src/parser_test.zig b/src/parser_test.zig index bb515f3..02fef3b 100644 --- a/src/parser_test.zig +++ b/src/parser_test.zig @@ -712,7 +712,7 @@ test "parse simple 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); + try testing.expect(l.random == false); } test "parse lorem with arguments" { @@ -720,7 +720,7 @@ test "parse lorem with arguments" { std.debug.print("32 - parse lorem with arguments\n", .{}); const allocator = testing.allocator; - const template = "{% lorem 5 p html %}"; + const template = "{% lorem 5 p true %}"; var p = parser.Parser.init(template); const nodes = try p.parse(allocator); defer { @@ -734,7 +734,7 @@ test "parse lorem with arguments" { 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.?); + try testing.expect(l.random == true); } test "parse simple now" { @@ -1225,6 +1225,22 @@ test "parse simple with block" { try testing.expect(w.body[0].type == .text); } + +test "parse svg" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("54 - parse svg\n", .{}); + + const allocator = testing.allocator; + const template = "{% svg \"material\" \"account_arrow_left\" %}"; + var p = parser.Parser.init(template); + const nodes = try p.parse(allocator); + defer { + for (nodes) |node| node.deinit(allocator); + allocator.free(nodes); + } + +} + // test "parse simple tag" { // std.debug.print("____________________________________________________\n", .{}); // std.debug.print("54 - parse simple tag\n", .{}); diff --git a/src/renderer.zig b/src/renderer.zig index d317617..a8a0885 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -12,6 +12,7 @@ const parser = @import("parser.zig"); const TemplateCache = @import("cache.zig").TemplateCache; const time = @import("time.zig"); const lorem = @import("lorem.zig"); +const icons = @import("svg/icons.zig"); pub const RenderError = error{ InvalidCharacter, @@ -20,7 +21,8 @@ pub const RenderError = error{ Overflow, Unexpected, UnsupportedExpression, -} || FilterError || parser.ParserError || std.fs.File.OpenError; +// } || FilterError || parser.ParserError || icons.SvgError || std.fs.File.OpenError; +} || FilterError || parser.ParserError || std.fs.File.OpenError; pub const Renderer = struct { context: *Context, @@ -82,16 +84,17 @@ pub const Renderer = struct { } fn renderTemplate(self: *const Renderer, template: []const u8, writer: anytype, cache_key: ?[]const u8) RenderError!void { + _ = cache_key; var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); const alloc = arena.allocator(); - if (cache_key) |ck| { - if (self.cache.get(ck)) |cached_nodes| { - try self.renderNodes(alloc, cached_nodes, writer); - return; - } - } + // if (cache_key) |ck| { + // if (self.cache.get(ck)) |cached_nodes| { + // try self.renderNodes(alloc, cached_nodes, writer); + // return; + // } + // } var p = parser.Parser.init(template); const nodes = try p.parse(alloc); @@ -100,15 +103,15 @@ pub const Renderer = struct { alloc.free(nodes); } - if (cache_key) |ck| { - var alc = self.cache.allocator(); - var cached_nodes = try alc.alloc(parser.Node, nodes.len); - errdefer alc.free(cached_nodes); - for (nodes, 0..) |node, i| { - cached_nodes[i] = try node.clone(self.allocator); - } - try self.cache.add(ck, nodes); - } + // if (cache_key) |ck| { + // var alc = self.cache.allocator(); + // var cached_nodes = try alc.alloc(parser.Node, nodes.len); + // errdefer alc.free(cached_nodes); + // for (nodes, 0..) |node, i| { + // cached_nodes[i] = try node.clone(self.allocator); + // } + // try self.cache.add(ck, nodes); + // } return try self.renderNodes(alloc, nodes, writer); } @@ -357,7 +360,7 @@ pub const Renderer = struct { if (random == false) { try writer.writeAll(lorem.LOREM_COMMON_P); return; - }else { + } else { try writer.writeAll(try lorem.sentence(alloc)); return; } @@ -374,7 +377,28 @@ pub const Renderer = struct { return; } }, - else => {}, + .svg => { + const svg_kind = node.tag.?.body.svg.kind; + const svg_name = node.tag.?.body.svg.name; + + if (self.cache.icons.?.getIcon(alloc,svg_kind, svg_name)) |svg_content| { + try writer.writeAll("
"); + try writer.writeAll(svg_content); + try writer.writeAll("
"); + } else { + try writer.writeAll(icons.fallback_svg); + // Opcional: log ou comentário de debug + // try writer.print("", .{svg_kind, svg_name}); + } + return; + }, + else => { + std.debug.print("PANIC: unknown node type {d}\n", .{@intFromEnum(node.type)}); + // @panic("unknown node type"); + try writer.writeAll(""); + }, } }, } diff --git a/src/renderer_test.zig b/src/renderer_test.zig index 2c5a847..c37560d 100644 --- a/src/renderer_test.zig +++ b/src/renderer_test.zig @@ -1068,3 +1068,33 @@ test "renderer - lorem paragraphs random" { const spaces = std.mem.count(u8, buf.items, "

"); try testing.expect(spaces == 3); } + +test "renderer - svg" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("27 - svg\n", .{}); + + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + var cache = TemplateCache.init(alloc); + try cache.initIcons(); + defer cache.deinit(); + + const renderer = Renderer.init(&ctx, &cache); + + const template = + \\{% svg material kangaroo %} + ; + + var buf = std.ArrayList(u8){}; + defer buf.deinit(alloc); + + try renderer.renderString(template, buf.writer(alloc)); + + std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items}); + + try testing.expect(std.mem.indexOf(u8, buf.items, "

") != null); + // const spaces = std.mem.count(u8, buf.items, "

"); + // try testing.expect(spaces == 3); +} From 7beb8758a5931ce98851f35bef78290b5fca0837 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:04:28 -0300 Subject: [PATCH 07/16] update: icons --- src/root.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/root.zig b/src/root.zig index 9b0e42f..ff490a0 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,6 +2,7 @@ pub const cache = @import("cache.zig"); pub const context = @import("context.zig"); pub const delta = @import("delta.zig"); pub const filters = @import("filters.zig"); +pub const icons = @import("svg/icons.zig"); pub const lorem = @import("lorem.zig"); pub const meta = @import("meta.zig"); pub const parser = @import("parser.zig"); From 59e543ca89756c12c65a33676d00385651bd65a8 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Tue, 20 Jan 2026 19:04:46 -0300 Subject: [PATCH 08/16] update: icons --- build.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.zig b/build.zig index 0a6a141..0272f9f 100644 --- a/build.zig +++ b/build.zig @@ -84,6 +84,15 @@ pub fn build(b: *std.Build) void { .use_llvm = true, }); + // const lib = b.addLibrary(.{ + // .name = "zdt_prov", + // .root_module = mod, + // }); + // + // lib.root_module.addIncludePath(b.path("src/svg")); + // + // b.installArtifact(lib); + // This declares intent for the executable to be installed into the // install prefix when running `zig build` (i.e. when executing the default // step). By default the install prefix is `zig-out/` but can be overridden From a6d8795c791257358a6965015cae74191b90cf49 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Thu, 22 Jan 2026 09:32:30 -0300 Subject: [PATCH 09/16] fix: slice of structs --- src/context.zig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/context.zig b/src/context.zig index a555794..267e2ca 100644 --- a/src/context.zig +++ b/src/context.zig @@ -42,6 +42,20 @@ pub const Context = struct { return Value{ .string = try time.formatDateTime(self.allocator(), value, "Y-m-d H:i:s") }; } + if (@typeInfo(T) == .pointer) { + if (@typeInfo(T).pointer.size == .slice) { + std.debug.print("slice: {any}\n", .{@typeInfo(T).pointer.child}); + if (@typeInfo(@typeInfo(T).pointer.child) == .@"struct") { + std.debug.print("struct: {s}\n", .{@typeName(@typeInfo(T).pointer.child)}); + var list = try self.allocator().alloc(Value, value.len); + for (value, 0..) |item, i| { + list[i] = try self.toValue(item); + } + return Value{ .list = list }; + } + } + } + return switch (@typeInfo(T)) { .bool => Value{ .bool = value }, .int, .comptime_int => Value{ .int = @intCast(value) }, From ea3095870149596dace76a64dc9701aefc892e64 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Thu, 22 Jan 2026 09:38:38 -0300 Subject: [PATCH 10/16] update: cleanup --- src/context.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/context.zig b/src/context.zig index 267e2ca..6cf1744 100644 --- a/src/context.zig +++ b/src/context.zig @@ -44,9 +44,7 @@ pub const Context = struct { if (@typeInfo(T) == .pointer) { if (@typeInfo(T).pointer.size == .slice) { - std.debug.print("slice: {any}\n", .{@typeInfo(T).pointer.child}); if (@typeInfo(@typeInfo(T).pointer.child) == .@"struct") { - std.debug.print("struct: {s}\n", .{@typeName(@typeInfo(T).pointer.child)}); var list = try self.allocator().alloc(Value, value.len); for (value, 0..) |item, i| { list[i] = try self.toValue(item); From fadae5d39cb689db4aec76fc9f565419a696d1be Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Thu, 22 Jan 2026 10:03:03 -0300 Subject: [PATCH 11/16] fix: parse tags inside forblock --- src/parser.zig | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/parser.zig b/src/parser.zig index c5aef01..75dd3de 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1763,6 +1763,18 @@ pub const Parser = struct { continue; } + var tag: Node = tag_node; + if (tag_node.tag.?.kind == .comment) { + try self.parseComment(); + continue; + } else { + if (try self.parseTagContent(allocator, tag_node)) |tn| { + tag.tag.?.body = tn; + try body.append(allocator, tag); + continue; + } + } + try current_body.append(allocator, tag_node); } else { self.advance(1); @@ -2151,7 +2163,7 @@ pub const Parser = struct { } const value = std.mem.trim(u8, args[start..i], " \t\r\n\"'"); - std.debug.print("value: {s}\n", .{value}); + // std.debug.print("value: {s}\n", .{value}); try values.append(allocator, value); } return TagNodeBody{ @@ -2293,7 +2305,7 @@ pub const Parser = struct { try self.parseComment(); continue; } else { - std.log.debug("Tag: {s}", .{tag.?.tag.?.raw}); + // std.log.debug("Tag: {s}", .{tag.?.tag.?.raw}); if (try self.parseTagContent(allocator, tag.?)) |tn| { tag.?.tag.?.body = tn; try list.append(allocator, tag.?); From 777b13ed98cefdeae98712030893cb8c5628a437 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Thu, 22 Jan 2026 10:15:11 -0300 Subject: [PATCH 12/16] fix: render tag inside ifblock --- src/parser.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/parser.zig b/src/parser.zig index 75dd3de..c87158e 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1696,6 +1696,19 @@ pub const Parser = struct { current_body = &false_body; continue; } + + var tag: Node = tag_node; + if (tag_node.tag.?.kind == .comment) { + try self.parseComment(); + continue; + } else { + if (try self.parseTagContent(allocator, tag_node)) |tn| { + tag.tag.?.body = tn; + try current_body.append(allocator, tag); + continue; + } + } + // Qualquer outra tag try current_body.append(allocator, tag_node); } else { From 8bf9902ebfd92dd03f079fe6d48b707c1c1bbed4 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Thu, 22 Jan 2026 12:49:02 -0300 Subject: [PATCH 13/16] fix: passing context --- src/renderer.zig | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/renderer.zig b/src/renderer.zig index a8a0885..fdfdb09 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -195,15 +195,15 @@ pub const Renderer = struct { .tag => { switch (node.tag.?.kind) { .if_block => { - const condition = try self.evaluateCondition(alloc, node.tag.?.body.@"if".condition); + const condition = try self.evaluateCondition(alloc, node.tag.?.body.@"if".condition, context); if (condition) { for (node.tag.?.body.@"if".true_body) |child| { - try self.renderNode(alloc, nodes, child, writer, null, null); + try self.renderNode(alloc, nodes, child, writer, context, null); } } else { for (node.tag.?.body.@"if".false_body) |child| { - try self.renderNode(alloc, nodes, child, writer, null, null); + try self.renderNode(alloc, nodes, child, writer, context, null); } } }, @@ -480,7 +480,7 @@ pub const Renderer = struct { }; } - fn evaluateCondition(self: *const Renderer, allocator: Allocator, expr: []const u8) RenderError!bool { + fn evaluateCondition(self: *const Renderer, allocator: Allocator, expr: []const u8, context: ?*Context) RenderError!bool { const trimmed = std.mem.trim(u8, expr, " \t\r\n"); if (trimmed.len == 0) return false; @@ -513,7 +513,12 @@ pub const Renderer = struct { const op = tokens.items[1]; const right_str = tokens.items[2]; - const left_value = self.context.get(left) orelse Value.null; + var left_value: Value = Value.null; + if (context) |ctx| { + left_value = ctx.get(left) orelse Value.null; + } + if (left_value == Value.null) left_value = self.context.get(left) orelse Value.null; + const right_value = parseLiteral(right_str); if (std.mem.eql(u8, op, ">")) return compare(left_value, right_value, .gt); From 20e4bacab0c813b7687dbcb573e45c336c17d12f Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Fri, 23 Jan 2026 15:01:33 -0300 Subject: [PATCH 14/16] fix: null value for string is "" --- src/renderer.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer.zig b/src/renderer.zig index fdfdb09..5064dcb 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -457,7 +457,7 @@ pub const Renderer = struct { _ = self; var w = buf.writer(alloc); switch (value) { - .null => try w.writeAll("null"), + .null => try w.writeAll(""), .bool => |b| try w.print("{}", .{b}), .int => |n| try w.print("{d}", .{n}), .float => |f| try w.print("{d}", .{f}), From 47f52fea58fb540f7c736d555751de77b478f84a Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Sat, 24 Jan 2026 22:40:35 -0300 Subject: [PATCH 15/16] update: forloop variables --- src/renderer.zig | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/renderer.zig b/src/renderer.zig index 5064dcb..c899961 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -21,8 +21,8 @@ pub const RenderError = error{ Overflow, Unexpected, UnsupportedExpression, -// } || FilterError || parser.ParserError || icons.SvgError || std.fs.File.OpenError; -} || FilterError || parser.ParserError || std.fs.File.OpenError; + // } || FilterError || parser.ParserError || icons.SvgError || std.fs.File.OpenError; +} || FilterError || parser.ParserError || std.fs.File.OpenError; pub const Renderer = struct { context: *Context, @@ -230,11 +230,31 @@ pub const Renderer = struct { else => return, }; - for (list) |item| { + for (list, 0..) |item, i| { var ctx = Context.init(alloc); defer ctx.deinit(); try ctx.set(node.tag.?.body.@"for".loop_var, item); + try ctx.set("forloop.counter", i + 1); + try ctx.set("forloop.counter0", i); + try ctx.set("forloop.revcounter", (list.len - i)); + try ctx.set("forloop.revcounter0", (list.len - i) - 1); + try ctx.set("forloop.first", i == 0); + try ctx.set("forloop.last", i == (list.len - 1)); + try ctx.set("forloop.length", list.len); + // forloop.counter + // The current iteration of the loop (1-indexed) + // forloop.counter0 + // The current iteration of the loop (0-indexed) + // forloop.revcounter + // The number of iterations from the end of the loop (1-indexed) + // forloop.revcounter0 + // The number of iterations from the end of the loop (0-indexed) + // forloop.first + // True if this is the first time through the loop + // forloop.last + // True if this is the last time through the loop + // forloop.length for (node.tag.?.body.@"for".body) |child| { try self.renderNode(alloc, nodes, child, writer, &ctx, null); @@ -381,7 +401,7 @@ pub const Renderer = struct { const svg_kind = node.tag.?.body.svg.kind; const svg_name = node.tag.?.body.svg.name; - if (self.cache.icons.?.getIcon(alloc,svg_kind, svg_name)) |svg_content| { + if (self.cache.icons.?.getIcon(alloc, svg_kind, svg_name)) |svg_content| { try writer.writeAll("

"); try writer.writeAll(svg_content); try writer.writeAll("
"); From f38f43dbc455f1b82965d479a0b3f99a1b93c0ec Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Sat, 24 Jan 2026 23:06:28 -0300 Subject: [PATCH 16/16] update: time with offset --- src/time.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/time.zig b/src/time.zig index a8a803b..e27da58 100644 --- a/src/time.zig +++ b/src/time.zig @@ -350,6 +350,10 @@ pub const Time = struct { return unix(std.time.timestamp()); } + pub fn now_offset(offset: i64) Time { + return unix(std.time.timestamp() + (offset * std.time.s_per_hour)); + } + pub fn today() Time { return unix(0).setDate(.today()); }