diff options
Diffstat (limited to 'src/eval/html')
| -rw-r--r-- | src/eval/html/Element.zig | 241 | ||||
| -rw-r--r-- | src/eval/html/html.zig | 47 |
2 files changed, 288 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>"); +} diff --git a/src/eval/html/html.zig b/src/eval/html/html.zig new file mode 100644 index 0000000..064ebb1 --- /dev/null +++ b/src/eval/html/html.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const eql = std.mem.eql; + +pub const Error = error{InvalidUtf8} || std.mem.Allocator.Error; + +pub fn escape(alloc: std.mem.Allocator, v: []const u8) Error![]const u8 { + var acc = try std.ArrayList(u8).initCapacity(alloc, v.len); + errdefer acc.deinit(alloc); + const view = try std.unicode.Utf8View.init(v); + var iter = view.iterator(); + while (iter.nextCodepointSlice()) |rune| { + if (eql(u8, rune, "&")) { + try acc.appendSlice(alloc, "&"); + } else if (eql(u8, rune, "'")) { + try acc.appendSlice(alloc, "'"); + } else if (eql(u8, rune, "<")) { + try acc.appendSlice(alloc, "<"); + } else if (eql(u8, rune, ">")) { + try acc.appendSlice(alloc, ">"); + } else if (eql(u8, rune, "\"")) { + try acc.appendSlice(alloc, """); + } else { + try acc.appendSlice(alloc, rune); + } + } + return acc.toOwnedSlice(alloc); +} + +fn doTest(alloc: std.mem.Allocator, el: []const u8, exp: []const u8) !void { + const got = try escape(alloc, el); + defer alloc.free(got); + std.testing.expect(eql(u8, got, exp)) catch |err| { + std.debug.print("{s}\n", .{got}); + return err; + }; +} + +test "escaping html" { + const alloc = std.testing.allocator; + + try doTest(alloc, "hello world", "hello world"); + try doTest(alloc, "hello&world", "hello&world"); + try doTest(alloc, "hello'world", "hello'world"); + try doTest(alloc, "hello<world", "hello<world"); + try doTest(alloc, "hello>world", "hello>world"); + try doTest(alloc, "hello\"world", "hello"world"); +} |
