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 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 = .{}; diff --git a/src/context.zig b/src/context.zig index a555794..6cf1744 100644 --- a/src/context.zig +++ b/src/context.zig @@ -42,6 +42,18 @@ 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) { + if (@typeInfo(@typeInfo(T).pointer.child) == .@"struct") { + var list = try self.allocator().alloc(Value, value.len); + for (value, 0..) |item, i| { + list[i] = try self.toValue(item); + } + return Value{ .list = list }; + } + } + } + return switch (@typeInfo(T)) { .bool => Value{ .bool = value }, .int, .comptime_int => Value{ .int = @intCast(value) }, 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); 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); diff --git a/src/parser.zig b/src/parser.zig index f2f7c3e..c87158e 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 => {}, } }, } @@ -1667,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 { @@ -1734,6 +1776,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); @@ -2088,6 +2142,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); @@ -2220,7 +2318,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.?); 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..c899961 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,6 +21,7 @@ pub const RenderError = error{ Overflow, Unexpected, UnsupportedExpression, + // } || FilterError || parser.ParserError || icons.SvgError || std.fs.File.OpenError; } || FilterError || parser.ParserError || std.fs.File.OpenError; pub const Renderer = struct { @@ -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); } @@ -192,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); } } }, @@ -227,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); @@ -357,7 +380,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 +397,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(""); + }, } }, } @@ -433,7 +477,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}), @@ -456,7 +500,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; @@ -489,7 +533,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); 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); +} 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"); diff --git a/src/svg/icons.zig b/src/svg/icons.zig new file mode 100644 index 0000000..5b4ad4f --- /dev/null +++ b/src/svg/icons.zig @@ -0,0 +1,117 @@ +const std = @import("std"); + +pub const IconSet = enum { + bootstrap, + dripicons, + hero_outline, + hero_solid, + material, +}; + +pub const embedded_data = std.EnumMap(IconSet, []const u8).init(.{ + .bootstrap = @embedFile("bootstrap.svgs.bin"), + .dripicons = @embedFile("dripicons.svgs.bin"), + .hero_outline = @embedFile("hero_outline.svgs.bin"), + .hero_solid = @embedFile("hero_solid.svgs.bin"), + .material = @embedFile("material.svgs.bin"), +}); + +pub const SvgIcon = struct { + icon_map: std.StringHashMapUnmanaged([]const u8) = .{}, + + pub fn init(allocator: std.mem.Allocator) !SvgIcon { + var self = SvgIcon{}; + + inline for (std.meta.fields(IconSet)) |field| { + const set = @field(IconSet, field.name); + const data = embedded_data.get(set); + + try self.loadSet(allocator, set, data.?); + } + + return self; + } + + pub fn deinit(self: *SvgIcon, allocator: std.mem.Allocator) void { + var it = self.icon_map.iterator(); + while (it.next()) |entry| { + allocator.free(entry.key_ptr.*); + allocator.free(entry.value_ptr.*); + } + self.icon_map.deinit(allocator); + self.* = .{}; + } + + pub fn get(self: *const SvgIcon, key: []const u8) ?[]const u8 { + return self.icon_map.get(key); + } + + pub fn getIcon(self: *const SvgIcon,allocator: std.mem.Allocator, kind: []const u8, name: []const u8) ?[]const u8 { + const key = std.fmt.allocPrint(allocator, "{s}:{s}", .{ kind, name }) catch return null; + defer allocator.free(key); + return self.icon_map.get(key); + } + + pub fn count(self: *const SvgIcon) usize { + return self.icon_map.count(); + } + + fn loadSet( + self: *SvgIcon, + allocator: std.mem.Allocator, + set: IconSet, + data: []const u8, + ) !void { + if (data.len < 12) return error.InvalidEmbeddedData; + + var pos: usize = 0; + + const magic = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + if (magic != 0x53564749) return error.InvalidMagic; + + const version = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + if (version != 1) return error.UnsupportedVersion; + + const num_entries = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + + const prefix = @tagName(set); + + var i: u32 = 0; + while (i < num_entries) : (i += 1) { + const name_len = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + + if (pos + name_len > data.len) return error.CorruptedNameLength; + const name_slice = data[pos .. pos + name_len]; + pos += name_len; + + const svg_len = std.mem.readInt(u32, data[pos..][0..4], .little); + pos += 4; + + if (pos + svg_len > data.len) return error.CorruptedSvgLength; + const svg_slice = data[pos .. pos + svg_len]; + pos += svg_len; + + // Monta a chave com prefixo do set + const key = try std.fmt.allocPrint(allocator, "{s}:{s}", .{ prefix, name_slice }); + + // Duplica o conteúdo SVG (o map assume ownership) + const value = try allocator.dupe(u8, svg_slice); + + // Insere no mapa unmanaged + try self.icon_map.put(allocator, key, value); + } + } +}; + +pub const fallback_svg = + \\

+ \\ + \\ + \\ ? + \\ + \\
+; 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()); }