Compare commits

...

16 commits
0.1.0 ... main

Author SHA1 Message Date
Lucas F.
f38f43dbc4 update: time with offset 2026-01-24 23:06:28 -03:00
Lucas F.
47f52fea58 update: forloop variables 2026-01-24 22:40:35 -03:00
Lucas F.
20e4bacab0 fix: null value for string is "" 2026-01-23 15:01:33 -03:00
Lucas F.
8bf9902ebf fix: passing context 2026-01-22 12:49:02 -03:00
Lucas F.
777b13ed98 fix: render tag inside ifblock 2026-01-22 10:15:11 -03:00
Lucas F.
fadae5d39c fix: parse tags inside forblock 2026-01-22 10:03:03 -03:00
Lucas F.
ea30958701 update: cleanup 2026-01-22 09:38:38 -03:00
Lucas F.
a6d8795c79 fix: slice of structs 2026-01-22 09:32:30 -03:00
Lucas F.
59e543ca89 update: icons 2026-01-20 19:04:46 -03:00
Lucas F.
7beb8758a5 update: icons 2026-01-20 19:04:28 -03:00
Lucas F.
1181f0fa68 update: svgNode 2026-01-20 19:04:08 -03:00
Lucas F.
f0115a15b9 update: cleanup 2026-01-20 19:03:28 -03:00
Lucas F.
a0645cb0d1 update: cleanup 2026-01-20 19:03:22 -03:00
Lucas F.
8fd9086d98 update: icons 2026-01-20 19:03:14 -03:00
Lucas F.
cce0c6a954 update: getIcon and fallback 2026-01-20 19:02:53 -03:00
Lucas F.
966035dccb update: icons 2026-01-20 18:11:19 -03:00
12 changed files with 428 additions and 33 deletions

View file

@ -84,6 +84,15 @@ pub fn build(b: *std.Build) void {
.use_llvm = true,
});
// const lib = b.addLibrary(.{
// .name = "zdt_prov",
// .root_module = mod,
// });
//
// lib.root_module.addIncludePath(b.path("src/svg"));
//
// b.installArtifact(lib);
// This declares intent for the executable to be installed into the
// install prefix when running `zig build` (i.e. when executing the default
// step). By default the install prefix is `zig-out/` but can be overridden

View file

@ -2,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 = .{};

View file

@ -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) },

View file

@ -4,6 +4,9 @@ const Context = @import("context.zig").Context;
const Value = @import("context.zig").Value;
test "context set amigável e get com ponto" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("1 - context set amigável e get com ponto\n", .{});
const allocator = testing.allocator;
var ctx = Context.init(allocator);
defer ctx.deinit();
@ -17,17 +20,26 @@ test "context set amigável e get com ponto" {
// struct
const Person = struct { nome: []const u8, idade: i64 };
const p = Person{ .nome = "Ana", .idade = 25 };
try ctx.set("user", p);
const p2 = Person{ .nome = "Fulana", .idade = 28 };
const people = [_]Person{ p, p2 };
// try ctx.set("user", p);
try ctx.set("user", people);
// list
const numeros = [_]i64{ 1, 2, 3 };
try ctx.set("lista", numeros);
for (ctx.get("user").?.list) |item| {
std.debug.print("user {any}\n", .{item.dict.get("nome").?});
}
// acesso
try testing.expectEqualStrings("Lucas", ctx.get("nome").?.string);
try testing.expect(ctx.get("idade").?.int == 30);
try testing.expectEqualStrings("Ana", ctx.get("user.nome").?.string);
try testing.expect(ctx.get("user.idade").?.int == 25);
// try testing.expectEqualStrings("Ana", ctx.get("user.nome").?.string);
// try testing.expect(ctx.get("user.idade").?.int == 25);
try testing.expect(ctx.get("lista.1").?.int == 2);
try testing.expect(ctx.get("vazio").?.string.len == 0);
try testing.expect(ctx.get("preco").?.float == 99.99);

View file

@ -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);

View file

@ -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.?);

View file

@ -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", .{});

View file

@ -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("<div class=\"svg-container\">");
try writer.writeAll(svg_content);
try writer.writeAll("</div>");
} else {
try writer.writeAll(icons.fallback_svg);
// Opcional: log ou comentário de debug
// try writer.print("<!-- SVG não encontrado: {s}/{s} -->", .{svg_kind, svg_name});
}
return;
},
else => {
std.debug.print("PANIC: unknown node type {d}\n", .{@intFromEnum(node.type)});
// @panic("unknown node type");
try writer.writeAll("<!-- tag não suportada: ");
// try writer.writeAll(try std.fmt.allocPrint(alloc, "{any}", .{node.tag.?.kind}));
try writer.writeAll(" -->");
},
}
},
}
@ -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);

View file

@ -1068,3 +1068,33 @@ test "renderer - lorem paragraphs random" {
const spaces = std.mem.count(u8, buf.items, "<p>");
try testing.expect(spaces == 3);
}
test "renderer - svg" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("27 - svg\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
try cache.initIcons();
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% svg material kangaroo %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, buf.items, "<div class=\"svg-container\">") != null);
// const spaces = std.mem.count(u8, buf.items, "<p>");
// try testing.expect(spaces == 3);
}

View file

@ -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");

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

@ -0,0 +1,117 @@
const std = @import("std");
pub const IconSet = enum {
bootstrap,
dripicons,
hero_outline,
hero_solid,
material,
};
pub const embedded_data = std.EnumMap(IconSet, []const u8).init(.{
.bootstrap = @embedFile("bootstrap.svgs.bin"),
.dripicons = @embedFile("dripicons.svgs.bin"),
.hero_outline = @embedFile("hero_outline.svgs.bin"),
.hero_solid = @embedFile("hero_solid.svgs.bin"),
.material = @embedFile("material.svgs.bin"),
});
pub const SvgIcon = struct {
icon_map: std.StringHashMapUnmanaged([]const u8) = .{},
pub fn init(allocator: std.mem.Allocator) !SvgIcon {
var self = SvgIcon{};
inline for (std.meta.fields(IconSet)) |field| {
const set = @field(IconSet, field.name);
const data = embedded_data.get(set);
try self.loadSet(allocator, set, data.?);
}
return self;
}
pub fn deinit(self: *SvgIcon, allocator: std.mem.Allocator) void {
var it = self.icon_map.iterator();
while (it.next()) |entry| {
allocator.free(entry.key_ptr.*);
allocator.free(entry.value_ptr.*);
}
self.icon_map.deinit(allocator);
self.* = .{};
}
pub fn get(self: *const SvgIcon, key: []const u8) ?[]const u8 {
return self.icon_map.get(key);
}
pub fn getIcon(self: *const SvgIcon,allocator: std.mem.Allocator, kind: []const u8, name: []const u8) ?[]const u8 {
const key = std.fmt.allocPrint(allocator, "{s}:{s}", .{ kind, name }) catch return null;
defer allocator.free(key);
return self.icon_map.get(key);
}
pub fn count(self: *const SvgIcon) usize {
return self.icon_map.count();
}
fn loadSet(
self: *SvgIcon,
allocator: std.mem.Allocator,
set: IconSet,
data: []const u8,
) !void {
if (data.len < 12) return error.InvalidEmbeddedData;
var pos: usize = 0;
const magic = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (magic != 0x53564749) return error.InvalidMagic;
const version = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (version != 1) return error.UnsupportedVersion;
const num_entries = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
const prefix = @tagName(set);
var i: u32 = 0;
while (i < num_entries) : (i += 1) {
const name_len = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (pos + name_len > data.len) return error.CorruptedNameLength;
const name_slice = data[pos .. pos + name_len];
pos += name_len;
const svg_len = std.mem.readInt(u32, data[pos..][0..4], .little);
pos += 4;
if (pos + svg_len > data.len) return error.CorruptedSvgLength;
const svg_slice = data[pos .. pos + svg_len];
pos += svg_len;
// Monta a chave com prefixo do set
const key = try std.fmt.allocPrint(allocator, "{s}:{s}", .{ prefix, name_slice });
// Duplica o conteúdo SVG (o map assume ownership)
const value = try allocator.dupe(u8, svg_slice);
// Insere no mapa unmanaged
try self.icon_map.put(allocator, key, value);
}
}
};
pub const fallback_svg =
\\<div class="svg-container">
\\<svg width="24" height="24" fill="none">
\\ <rect width="24" height="24" rx="4" fill="#f0f0f0"/>
\\ <text x="12" y="16" font-size="10" text-anchor="middle" fill="#999">?</text>
\\</svg>
\\</div>
;

View file

@ -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());
}