aboutsummaryrefslogtreecommitdiff
path: root/src/eval
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
parenta3e7c462dadadc6986d93f6f0203ca7a02863ef8 (diff)
style(eval): move in its own package
Diffstat (limited to 'src/eval')
-rw-r--r--src/eval/Element.zig136
-rw-r--r--src/eval/Title.zig45
-rw-r--r--src/eval/html/Element.zig241
-rw-r--r--src/eval/html/html.zig47
-rw-r--r--src/eval/paragraph.zig79
5 files changed, 548 insertions, 0 deletions
diff --git a/src/eval/Element.zig b/src/eval/Element.zig
new file mode 100644
index 0000000..e017bc5
--- /dev/null
+++ b/src/eval/Element.zig
@@ -0,0 +1,136 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+pub const HTML = @import("html/Element.zig");
+pub const Paragraph = struct {
+ const paragraph = @import("paragraph.zig");
+ pub const Block = paragraph.Block;
+ pub const Bold = paragraph.Bold;
+ pub const Code = paragraph.Code;
+ pub const Italic = paragraph.Italic;
+ pub const Link = paragraph.Link;
+};
+pub const Title = @import("Title.zig");
+
+const Element = @This();
+
+vtable: struct {
+ deinit: *const fn (*anyopaque, Allocator) void,
+ html: *const fn (*anyopaque, Allocator) HTML.Error!HTML,
+},
+ptr: *anyopaque,
+
+pub fn renderHTML(self: Element, alloc: Allocator) HTML.Error![]const u8 {
+ var el = try self.vtable.html(self.ptr, alloc);
+ defer el.deinit();
+ return el.render(alloc);
+}
+
+pub fn deinit(self: Element, alloc: Allocator) void {
+ self.vtable.deinit(self.ptr, alloc);
+}
+
+pub fn html(self: Element, alloc: Allocator) HTML.Error!HTML {
+ return self.vtable.html(self.ptr, alloc);
+}
+
+pub const Empty = struct {
+ content: std.ArrayList(Element),
+
+ const Self = @This();
+
+ pub fn init(alloc: Allocator) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{ .content = try .initCapacity(alloc, 2) };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .ptr = self, .vtable = .{ .deinit = destroy, .html = Self.html } };
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ destroy(self, alloc);
+ }
+
+ fn destroy(context: *anyopaque, alloc: Allocator) void {
+ const self: *Self = @ptrCast(@alignCast(context));
+ for (self.content.items) |it| it.deinit(alloc);
+ self.content.deinit(alloc);
+ alloc.destroy(self);
+ }
+
+ fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
+ const self: *Self = @ptrCast(@alignCast(context));
+ var el = HTML.initEmpty(alloc);
+ errdefer el.deinit();
+ for (self.content.items) |it| try el.appendContent(try it.html(alloc));
+ return el;
+ }
+};
+
+pub const Literal = struct {
+ content: []const u8,
+
+ const Self = @This();
+
+ pub fn init(alloc: Allocator, content: []const u8) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{ .content = content };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .ptr = self, .vtable = .{ .deinit = destroy, .html = Self.html } };
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ destroy(self, alloc);
+ }
+
+ fn destroy(context: *anyopaque, alloc: Allocator) void {
+ const self: *Self = @ptrCast(@alignCast(context));
+ alloc.destroy(self);
+ }
+
+ fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
+ const self: *Self = @ptrCast(@alignCast(context));
+ return HTML.initLitEscaped(alloc, self.content);
+ }
+};
+
+pub fn Simple(comptime tag: []const u8) type {
+ return struct {
+ content: std.ArrayList(Element),
+
+ const Self = @This();
+
+ pub fn init(alloc: Allocator) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{ .content = try .initCapacity(alloc, 2) };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .ptr = self, .vtable = .{ .deinit = destroy, .html = Self.html } };
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ destroy(self, alloc);
+ }
+
+ fn destroy(context: *anyopaque, alloc: Allocator) void {
+ var self: *Self = @ptrCast(@alignCast(context));
+ for (self.content.items) |it| it.deinit(alloc);
+ self.content.deinit(alloc);
+ alloc.destroy(self);
+ }
+
+ fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
+ const self: *Self = @ptrCast(@alignCast(context));
+ var el = try HTML.init(alloc, .content, tag);
+ errdefer el.deinit();
+ for (self.content.items) |it| try el.appendContent(try it.html(alloc));
+ return el;
+ }
+ };
+}
diff --git a/src/eval/Title.zig b/src/eval/Title.zig
new file mode 100644
index 0000000..56524ad
--- /dev/null
+++ b/src/eval/Title.zig
@@ -0,0 +1,45 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const HTML = Parent.HTML;
+const Parent = @import("Element.zig");
+
+level: u3,
+content: Parent,
+
+const Self = @This();
+
+pub fn init(alloc: Allocator, level: u3, content: Parent) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{ .level = level, .content = content };
+ return v;
+}
+
+pub fn element(self: *Self) Parent {
+ return .{ .ptr = self, .vtable = .{ .deinit = destroy, .html = html } };
+}
+
+pub fn deinit(self: *Self, alloc: Allocator) void {
+ self.element().deinit(alloc);
+}
+
+fn destroy(context: *anyopaque, alloc: Allocator) void {
+ var self: *Self = @ptrCast(@alignCast(context));
+ self.content.deinit(alloc);
+ alloc.destroy(self);
+}
+
+fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
+ const self: *Self = @ptrCast(@alignCast(context));
+ var el = try HTML.init(alloc, .content, switch (self.level) {
+ 1 => "h1",
+ 2 => "h2",
+ 3 => "h3",
+ 4 => "h4",
+ 5 => "h5",
+ 6 => "h6",
+ else => unreachable,
+ });
+ errdefer el.deinit();
+ try el.appendContent(try self.content.html(alloc));
+ return el;
+}
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, "&amp;");
+ } else if (eql(u8, rune, "'")) {
+ try acc.appendSlice(alloc, "&#39;");
+ } else if (eql(u8, rune, "<")) {
+ try acc.appendSlice(alloc, "&lt;");
+ } else if (eql(u8, rune, ">")) {
+ try acc.appendSlice(alloc, "&gt;");
+ } else if (eql(u8, rune, "\"")) {
+ try acc.appendSlice(alloc, "&#34;");
+ } 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&amp;world");
+ try doTest(alloc, "hello'world", "hello&#39;world");
+ try doTest(alloc, "hello<world", "hello&lt;world");
+ try doTest(alloc, "hello>world", "hello&gt;world");
+ try doTest(alloc, "hello\"world", "hello&#34;world");
+}
diff --git a/src/eval/paragraph.zig b/src/eval/paragraph.zig
new file mode 100644
index 0000000..e56b7ff
--- /dev/null
+++ b/src/eval/paragraph.zig
@@ -0,0 +1,79 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const HTML = Element.HTML;
+const Element = @import("Element.zig");
+
+pub const Block = Element.Simple("p");
+
+pub const Bold = Element.Simple("b");
+pub const Italic = Element.Simple("em");
+pub const Code = Element.Simple("code");
+
+
+pub const Link = struct {
+ link: []const u8,
+ content: Element,
+ target: ?[]const u8 = null,
+
+ const Self = @This();
+
+ pub fn init(alloc: Allocator, content: Element, link: []const u8) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{
+ .content = content,
+ .link = link,
+ };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .ptr = self, .vtable = .{ .deinit = destroy, .html = html } };
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ destroy(self, alloc);
+ }
+
+ fn destroy(context: *anyopaque, alloc: Allocator) void {
+ var self: *Self = @ptrCast(@alignCast(context));
+ self.content.deinit(alloc);
+ alloc.destroy(self);
+ }
+
+ fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
+ const self: *Self = @ptrCast(@alignCast(context));
+ var el = try HTML.init(alloc, .content, "a");
+ errdefer el.deinit();
+ try el.appendContent(try self.content.html(alloc));
+ try el.setAttribute("href", self.link);
+ if (self.target) |target| try el.setAttribute("target", target);
+ return el;
+ }
+};
+
+fn doTest(alloc: Allocator, el: Element, exp: []const u8) !void {
+ const got = try el.renderHTML(alloc);
+ defer alloc.free(got);
+ std.testing.expect(std.mem.eql(u8, got, exp)) catch |err| {
+ std.debug.print("{s}\n", .{got});
+ return err;
+ };
+}
+
+test "paragraph" {
+ const alloc = std.testing.allocator;
+
+ const lit = (try Element.Literal.init(alloc, "hello world")).element();
+ try doTest(alloc, lit, "hello world");
+
+ var p = try Block.init(alloc);
+ try p.content.append(alloc, lit);
+ defer p.deinit(alloc);
+ try doTest(alloc, p.element(), "<p>hello world</p>");
+
+ const link = (try Link.init(alloc, (try Element.Literal.init(alloc, "foo")).element(), "example.org")).element();
+ try doTest(alloc, link, "<a href=\"example.org\">foo</a>");
+
+ try p.content.append(alloc, link);
+ try doTest(alloc, p.element(), "<p>hello world<a href=\"example.org\">foo</a></p>");
+}