aboutsummaryrefslogtreecommitdiff
path: root/src/eval/html/Element.zig
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2026-04-25 17:18:00 +0200
committerAnhgelus Morhtuuzh <william@herges.fr>2026-04-25 17:18:00 +0200
commitc008d747534f4c8aa59b045efbca754618e14b41 (patch)
tree68c094fed420230985724c300195c0389d2339e8 /src/eval/html/Element.zig
parenta3e7c462dadadc6986d93f6f0203ca7a02863ef8 (diff)
style(eval): move in its own package
Diffstat (limited to 'src/eval/html/Element.zig')
-rw-r--r--src/eval/html/Element.zig241
1 files changed, 241 insertions, 0 deletions
diff --git a/src/eval/html/Element.zig b/src/eval/html/Element.zig
new file mode 100644
index 0000000..87fd876
--- /dev/null
+++ b/src/eval/html/Element.zig
@@ -0,0 +1,241 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const eql = std.mem.eql;
+const html = @import("html.zig");
+
+pub const Kind = enum {
+ void,
+ content,
+ literal,
+};
+
+pub const Error = html.Error || Allocator.Error;
+
+const Self = @This();
+
+kind: Kind,
+arena: std.heap.ArenaAllocator,
+tag: ?[]const u8 = null,
+attributes: std.StringArrayHashMap([]const u8),
+class_list: std.BufSet,
+content: std.ArrayList(Self) = .empty,
+literal: ?[]const u8 = null,
+
+/// Init a new Element with the given kind.
+/// The tag will never be escaped.
+/// It always duplicates strings.
+pub fn init(alloc: Allocator, knd: Kind, tag: []const u8) Error!Self {
+ var v = Self{
+ .kind = knd,
+ .arena = .init(alloc),
+ .attributes = .init(alloc),
+ .class_list = .init(alloc),
+ };
+ var a = v.arena.allocator();
+ v.tag = try a.dupe(u8, tag);
+ return v;
+}
+
+pub fn initEmpty(alloc: Allocator) Self {
+ return .{
+ .kind = .content,
+ .arena = .init(alloc),
+ .attributes = .init(alloc),
+ .class_list = .init(alloc),
+ };
+}
+
+/// Init a new literal element.
+/// The literal content will never be escaped, see initLitEscaped if you want to escape it.
+/// It always duplicates strings.
+pub fn initLit(alloc: Allocator, literal: []const u8) Error!Self {
+ var v = Self{
+ .kind = .literal,
+ .arena = .init(alloc),
+ .attributes = .init(alloc),
+ .class_list = .init(alloc),
+ };
+ var a = v.arena.allocator();
+ v.literal = try a.dupe(u8, literal);
+ return v;
+}
+
+/// Init a new literal element that is escaped.
+/// The literal content will be escaped, see initLit if you don't want this behavior.
+/// It always duplicates strings.
+pub fn initLitEscaped(alloc: Allocator, literal: []const u8) Error!Self {
+ const escaped = try html.escape(alloc, literal);
+ defer alloc.free(escaped);
+ return .initLit(alloc, escaped);
+}
+
+pub fn deinit(self: *Self) void {
+ self.attributes.deinit();
+ self.class_list.deinit();
+ for (self.content.items) |it| {
+ var v = it;
+ v.deinit();
+ }
+ self.content.deinit(self.arena.allocator());
+ self.arena.deinit();
+}
+
+pub fn render(self: *Self, alloc: Allocator) Error![]const u8 {
+ const attr = try self.renderAttribute(alloc);
+ defer if (attr) |it| alloc.free(it);
+ var acc = try std.ArrayList(u8).initCapacity(alloc, self.content.items.len + if (self.literal) |it| it.len else 0);
+ errdefer acc.deinit(alloc);
+ if (self.tag) |tag| {
+ try acc.append(alloc, '<');
+ try acc.appendSlice(alloc, tag);
+ if (attr) |it| try acc.appendSlice(alloc, it);
+ try acc.append(alloc, '>');
+ }
+ switch (self.kind) {
+ .void => return acc.toOwnedSlice(alloc),
+ .content => {
+ for (self.content.items) |it| {
+ var v = it;
+ const sub = try v.render(alloc);
+ defer alloc.free(sub);
+ try acc.appendSlice(alloc, sub);
+ }
+ },
+ .literal => try acc.appendSlice(alloc, self.literal.?),
+ }
+ if (self.tag) |tag| {
+ try acc.appendSlice(alloc, "</");
+ try acc.appendSlice(alloc, tag);
+ try acc.append(alloc, '>');
+ }
+ return acc.toOwnedSlice(alloc);
+}
+
+fn renderAttribute(self: *Self, alloc: Allocator) Error!?[]const u8 {
+ const class = try self.renderClass(alloc);
+ defer if (class) |it| alloc.free(it);
+ if (class) |it| try self.setAttribute("class", it);
+ var iter = self.attributes.iterator();
+ if (iter.len == 0) return null;
+ var acc = try std.ArrayList(u8).initCapacity(alloc, iter.len);
+ errdefer acc.deinit(alloc);
+ try acc.append(alloc, ' ');
+ var i: usize = 0;
+ while (iter.next()) |it| : (i += 1) {
+ try acc.appendSlice(alloc, it.key_ptr.*);
+ try acc.appendSlice(alloc, "=\"");
+ const escape = try html.escape(alloc, it.value_ptr.*);
+ defer alloc.free(escape);
+ try acc.appendSlice(alloc, escape);
+ try acc.append(alloc, '"');
+ if (i < iter.len - 1) try acc.append(alloc, ' ');
+ }
+ return try acc.toOwnedSlice(alloc);
+}
+
+fn renderClass(self: *const Self, alloc: Allocator) Error!?[]const u8 {
+ var iter = self.class_list.iterator();
+ if (iter.len == 0) return null;
+ const n = self.class_list.count();
+ var acc = try std.ArrayList(u8).initCapacity(alloc, n);
+ errdefer acc.deinit(alloc);
+ var i: usize = 0;
+ while (iter.next()) |it| : (i += 1) {
+ try acc.appendSlice(alloc, it.*);
+ if (i < n - 1) try acc.append(alloc, ' ');
+ }
+ return try acc.toOwnedSlice(alloc);
+}
+
+pub fn setAttribute(self: *Self, k: []const u8, v: []const u8) Error!void {
+ var alloc = self.arena.allocator();
+ try self.attributes.put(try alloc.dupe(u8, k), try alloc.dupe(u8, v));
+}
+
+pub fn removeAttribute(self: *Self, k: []const u8) void {
+ _ = self.attributes.orderedRemove(k);
+}
+
+pub fn hasAttribute(self: *Self, k: []const u8) bool {
+ return self.attributes.contains(k);
+}
+
+pub fn appendClass(self: *Self, v: []const u8) Error!void {
+ var alloc = self.arena.allocator();
+ try self.class_list.insert(try alloc.dupe(u8, v));
+}
+
+pub fn hasClass(self: *Self, v: []const u8) bool {
+ return self.class_list.contains(v);
+}
+
+pub fn removeClass(self: *Self, v: []const u8) void {
+ self.class_list.remove(v);
+}
+
+pub fn appendContent(self: *Self, content: Self) Error!void {
+ const alloc = self.arena.allocator();
+ return self.content.append(alloc, content);
+}
+
+pub fn initImg(alloc: Allocator, src: []const u8, alt: []const u8) Error!Self {
+ var el = try init(alloc, .void, "img");
+ try el.setAttribute("src", src);
+ try el.setAttribute("alt", alt);
+ return el;
+}
+
+pub fn initContent(alloc: Allocator, tag: []const u8, content: []Self) Error!Self {
+ var el = try init(alloc, .content, tag);
+ for (content) |it| try el.appendContent(it);
+ return el;
+}
+
+fn doTest(alloc: Allocator, el: *Self, exp: []const u8) !void {
+ const got = try el.render(alloc);
+ defer alloc.free(got);
+ std.testing.expect(eql(u8, got, exp)) catch |err| {
+ std.debug.print("{s}\n", .{got});
+ return err;
+ };
+}
+
+test "void element" {
+ const alloc = std.testing.allocator;
+
+ var br = try init(alloc, .void, "br");
+ defer br.deinit();
+
+ try doTest(alloc, &br, "<br>");
+
+ var img = try init(alloc, .void, "img");
+ defer img.deinit();
+ try img.setAttribute("src", "foo");
+ try img.setAttribute("alt", "bar");
+
+ try doTest(alloc, &img, "<img src=\"foo\" alt=\"bar\">");
+
+ var img2 = try initImg(alloc, "foo", "bar");
+ defer img2.deinit();
+ try doTest(alloc, &img2, "<img src=\"foo\" alt=\"bar\">");
+}
+
+test "content element" {
+ const alloc = std.testing.allocator;
+
+ var p = try init(alloc, .content, "p");
+
+ var content = try initLit(alloc, "hello world");
+ try p.appendContent(content);
+
+ try doTest(alloc, &content, "hello world");
+ try doTest(alloc, &p, "<p>hello world</p>");
+
+ var div = try init(alloc, .content, "div");
+ defer div.deinit();
+ try div.appendClass("foo-bar");
+ try div.appendContent(p);
+ try div.appendContent(try initImg(alloc, "example.org", "example"));
+
+ try doTest(alloc, &div, "<div class=\"foo-bar\"><p>hello world</p><img src=\"example.org\" alt=\"example\"></div>");
+}