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, }; 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) !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) !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) !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) ![]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, "'); } return acc.toOwnedSlice(alloc); } fn renderAttribute(self: *Self, alloc: Allocator) !?[]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) !?[]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) !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) !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) !void { const alloc = self.arena.allocator(); return self.content.append(alloc, content); } pub fn initImg(alloc: Allocator, src: []const u8, alt: []const u8) !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) !Self { var el = try init(alloc, .content, tag); for (content) |it| try el.appendContent(it); return el; } /// Init a paragraph tag with an automatically escaped content. pub fn initParagraph(alloc: Allocator, content: []const u8) !Self { var el = try init(alloc, .content, "p"); try el.appendContent(try initLitEscaped(alloc, content)); 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" { var arena = std.heap.DebugAllocator(.{}).init; defer if (arena.deinit() == .leak) std.debug.print("leaking!\n", .{}); const alloc = arena.allocator(); var br = try init(alloc, .void, "br"); defer br.deinit(); try doTest(alloc, &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, "\"bar\""); var img2 = try initImg(alloc, "foo", "bar"); defer img2.deinit(); try doTest(alloc, &img2, "\"bar\""); } test "content element" { var arena = std.heap.DebugAllocator(.{}).init; defer if (arena.deinit() == .leak) std.debug.print("leaking!\n", .{}); const alloc = arena.allocator(); var p = try init(alloc, .content, "p"); defer p.deinit(); var content = try initLit(alloc, "hello world"); try p.appendContent(content); try doTest(alloc, &content, "hello world"); try doTest(alloc, &p, "

hello world

"); var p_managed = try initParagraph(alloc, "hello world"); defer p_managed.deinit(); try doTest(alloc, &p_managed, "

hello world

"); var div = try init(alloc, .content, "div"); defer div.deinit(); try div.appendClass("foo-bar"); try div.appendContent(try initParagraph(alloc, "hello world")); try div.appendContent(try initImg(alloc, "example.org", "example")); try doTest(alloc, &div, "

hello world

\"example\"
"); }