aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2026-04-27 16:16:49 +0200
committerAnhgelus Morhtuuzh <william@herges.fr>2026-04-27 16:16:49 +0200
commit9f1a0bf3b0437770a7b62fd28a8748908c38dac4 (patch)
tree0967564e0903b5b27a5f39381011cc8a684a7539 /src
parentde948492e8b38a79d5db9c506c1b7b82e86c6b12 (diff)
perf(html): reduce memory syscall
Diffstat (limited to 'src')
-rw-r--r--src/code.zig15
-rw-r--r--src/eval/Element.zig20
-rw-r--r--src/eval/Image.zig6
-rw-r--r--src/eval/Title.zig7
-rw-r--r--src/eval/blocks.zig30
-rw-r--r--src/eval/html/Element.zig390
-rw-r--r--src/eval/list.zig11
-rw-r--r--src/eval/paragraph.zig11
8 files changed, 286 insertions, 204 deletions
diff --git a/src/code.zig b/src/code.zig
index 98bba42..7b7d23a 100644
--- a/src/code.zig
+++ b/src/code.zig
@@ -58,4 +58,19 @@ test "code" {
\\```
, "<figure><pre data-code=\"td another\"><code>hey</code></pre></figure>");
// cannot test content with \n
+
+ try doTestError(parse, alloc, "```", Error.InvalidCodeBlock);
+ try doTestError(parse, alloc,
+ \\```
+ \\hey
+ , Error.InvalidCodeBlock);
+ try doTestError(parse, alloc,
+ \\```
+ \\hey```
+ , Error.InvalidCodeBlock);
+ try doTestError(parse, alloc,
+ \\```
+ \\hey
+ \\``` nope
+ , Error.InvalidCodeBlock);
}
diff --git a/src/eval/Element.zig b/src/eval/Element.zig
index 0dbba59..265ccf1 100644
--- a/src/eval/Element.zig
+++ b/src/eval/Element.zig
@@ -18,8 +18,9 @@ vtable: struct {
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();
+ const root = try HTML.Root.init(alloc);
+ defer root.deinit();
+ var el = try self.vtable.html(self.ptr, root.allocator());
return el.render(alloc);
}
@@ -59,10 +60,10 @@ pub const Empty = struct {
fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
const self: *Self = @ptrCast(@alignCast(context));
- var el = HTML.initEmpty(alloc);
+ var el = try HTML.Root.init(alloc);
errdefer el.deinit();
- for (self.content.items) |it| try el.appendContent(try it.html(alloc));
- return el;
+ for (self.content.items) |it| try el.append(try it.html(el.allocator()));
+ return el.element();
}
};
@@ -92,7 +93,7 @@ pub const Literal = struct {
fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
const self: *Self = @ptrCast(@alignCast(context));
- return HTML.initLitEscaped(alloc, self.content);
+ return (try HTML.Literal.init(alloc, self.content)).element();
}
};
@@ -143,10 +144,9 @@ pub fn Simple(comptime tag: []const u8) type {
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;
+ var el = try HTML.Content.init(alloc, tag);
+ for (self.content.items) |it| try el.append(try it.html(alloc));
+ return el.element();
}
};
}
diff --git a/src/eval/Image.zig b/src/eval/Image.zig
index 740d876..aa30585 100644
--- a/src/eval/Image.zig
+++ b/src/eval/Image.zig
@@ -31,12 +31,10 @@ fn destroy(context: *anyopaque, alloc: Allocator) void {
fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
const self: *Self = @ptrCast(@alignCast(context));
-
- var img = try HTML.init(alloc, .void, "img");
- errdefer img.deinit();
+ var img = try HTML.Void.init(alloc, "img");
try img.setAttribute("src", self.src);
if (self.alt) |it| try img.setAttribute("alt", it);
- return img;
+ return img.element();
}
test "html" {
diff --git a/src/eval/Title.zig b/src/eval/Title.zig
index 730d512..2e89953 100644
--- a/src/eval/Title.zig
+++ b/src/eval/Title.zig
@@ -30,7 +30,7 @@ fn destroy(context: *anyopaque, alloc: Allocator) void {
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) {
+ var el = try HTML.Content.init(alloc, switch (self.level) {
1 => "h1",
2 => "h2",
3 => "h3",
@@ -39,7 +39,6 @@ fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
6 => "h6",
else => unreachable,
});
- errdefer el.deinit();
- try el.appendContent(try self.content.html(alloc));
- return el;
+ try el.append(try self.content.html(alloc));
+ return el.element();
}
diff --git a/src/eval/blocks.zig b/src/eval/blocks.zig
index 8ec42da..63a8f12 100644
--- a/src/eval/blocks.zig
+++ b/src/eval/blocks.zig
@@ -32,14 +32,12 @@ pub const Code = struct {
fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
const self: *Self = @ptrCast(@alignCast(context));
- var el = try HTML.init(alloc, .content, "pre");
- errdefer el.deinit();
- if (self.attribute) |attr| try el.setAttribute("data-code", attr);
- var code = try HTML.init(alloc, .content, "code");
- errdefer code.deinit();
- for (self.content.items) |it| try code.appendContent(try it.html(alloc));
- try el.appendContent(code);
- return el;
+ var el = try HTML.Content.init(alloc, "pre");
+ if (self.attribute) |attr| try el.base.setAttribute("data-code", attr);
+ var code = try HTML.Content.init(alloc, "code");
+ for (self.content.items) |it| try code.append(try it.html(alloc));
+ try el.append(code.element());
+ return el.element();
}
};
@@ -72,14 +70,12 @@ pub const Figure = struct {
fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
const self: *Self = @ptrCast(@alignCast(context));
- var el = try HTML.init(alloc, .content, "figure");
- errdefer el.deinit();
- try el.appendContent(try self.content.html(alloc));
- const caption = self.caption orelse return el;
- var figcap = try HTML.init(alloc, .content, "figcaption");
- errdefer figcap.deinit();
- try figcap.appendContent(try caption.html(alloc));
- try el.appendContent(figcap);
- return el;
+ var el = try HTML.Content.init(alloc, "figure");
+ try el.append(try self.content.html(alloc));
+ const caption = self.caption orelse return el.element();
+ var figcap = try HTML.Content.init(alloc, "figcaption");
+ try figcap.append(try caption.html(alloc));
+ try el.append(figcap.element());
+ return el.element();
}
};
diff --git a/src/eval/html/Element.zig b/src/eval/html/Element.zig
index 666e6f0..a0e2a02 100644
--- a/src/eval/html/Element.zig
+++ b/src/eval/html/Element.zig
@@ -1,123 +1,33 @@
const std = @import("std");
+const Arena = std.heap.ArenaAllocator;
const Allocator = std.mem.Allocator;
const eql = std.mem.eql;
+const List = std.ArrayList;
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;
-}
+const Element = @This();
-/// 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);
-}
+vtable: struct {
+ render: *const fn (self: *anyopaque, alloc: Allocator) Error![]const u8,
+},
+ptr: *anyopaque,
-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: Element, alloc: Allocator) Error![]const u8 {
+ return self.vtable.render(self.ptr, alloc);
}
-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();
+fn renderAttribute(alloc: Allocator, attributes: *std.StringArrayHashMap([]const u8), class_list: *std.BufSet) Error!?[]const u8 {
+ const class = try renderClass(alloc, class_list);
+ defer if (class) |it| {
+ _ = attributes.orderedRemove("class");
+ alloc.free(it);
+ };
+ if (class) |it| try attributes.put("class", it);
+ var iter = attributes.iterator();
if (iter.len == 0) return null;
- var acc = try std.ArrayList(u8).initCapacity(alloc, iter.len);
+ var acc = try List(u8).initCapacity(alloc, iter.len);
errdefer acc.deinit(alloc);
try acc.append(alloc, ' ');
var i: usize = 0;
@@ -133,12 +43,12 @@ fn renderAttribute(self: *Self, alloc: Allocator) Error!?[]const u8 {
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);
+fn renderClass(alloc: Allocator, class_list: *std.BufSet) Error!?[]const u8 {
+ const n = class_list.count();
+ if (n == 0) return null;
+ var acc = try List(u8).initCapacity(alloc, n);
errdefer acc.deinit(alloc);
+ var iter = class_list.iterator();
var i: usize = 0;
while (iter.next()) |it| : (i += 1) {
try acc.appendSlice(alloc, it.*);
@@ -147,38 +57,187 @@ fn renderClass(self: *const Self, alloc: Allocator) Error!?[]const u8 {
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 const Void = struct {
+ alloc: Allocator,
+ tag: []const u8,
+ attributes: std.StringArrayHashMap([]const u8),
+ class_list: std.BufSet,
-pub fn removeAttribute(self: *Self, k: []const u8) void {
- _ = self.attributes.orderedRemove(k);
-}
+ pub const Self = @This();
-pub fn hasAttribute(self: *Self, k: []const u8) bool {
- return self.attributes.contains(k);
-}
+ pub fn init(alloc: Allocator, tag: []const u8) Error!*Self {
+ const v = try alloc.create(Self);
+ v.* = .{
+ .alloc = alloc,
+ .tag = tag,
+ .attributes = .init(alloc),
+ .class_list = .init(alloc),
+ };
+ return v;
+ }
-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 element(self: *Self) Element {
+ return .{ .vtable = .{ .render = Self.render }, .ptr = self };
+ }
-pub fn hasClass(self: *Self, v: []const u8) bool {
- return self.class_list.contains(v);
-}
+ pub fn setAttribute(self: *Self, k: []const u8, v: []const u8) Error!void {
+ try self.attributes.put(try self.alloc.dupe(u8, k), try self.alloc.dupe(u8, v));
+ }
-pub fn removeClass(self: *Self, v: []const u8) void {
- self.class_list.remove(v);
-}
+ pub fn removeAttribute(self: *Self, k: []const u8) void {
+ _ = self.attributes.orderedRemove(k);
+ }
-pub fn appendContent(self: *Self, content: Self) Error!void {
- const alloc = self.arena.allocator();
- return self.content.append(alloc, content);
-}
+ pub fn hasAttribute(self: *Self, k: []const u8) bool {
+ return self.attributes.contains(k);
+ }
+
+ pub fn appendClass(self: *Self, v: []const u8) Error!void {
+ try self.class_list.insert(v);
+ }
-fn doTest(alloc: Allocator, el: *Self, exp: []const u8) !void {
+ 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);
+ }
+
+ fn render(context: *anyopaque, alloc: Allocator) Error![]const u8 {
+ const self: *Self = @ptrCast(@alignCast(context));
+ const attr = try renderAttribute(alloc, &self.attributes, &self.class_list);
+ defer if (attr) |it| alloc.free(it);
+ var acc = try List(u8).initCapacity(alloc, self.tag.len + 2);
+ errdefer acc.deinit(alloc);
+ try acc.append(alloc, '<');
+ try acc.appendSlice(alloc, self.tag);
+ if (attr) |it| try acc.appendSlice(alloc, it);
+ try acc.append(alloc, '>');
+ return acc.toOwnedSlice(alloc);
+ }
+};
+
+pub const Content = struct {
+ base: Void,
+ content: List(Element),
+
+ pub const Self = @This();
+
+ pub fn init(alloc: Allocator, tag: []const u8) Error!*Self {
+ const v = try alloc.create(Self);
+ v.* = .{
+ .base = .{
+ .alloc = alloc,
+ .tag = tag,
+ .attributes = .init(alloc),
+ .class_list = .init(alloc),
+ },
+ .content = try .initCapacity(alloc, 2),
+ };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .vtable = .{ .render = Self.render }, .ptr = self };
+ }
+
+ pub fn append(self: *Self, content: Element) Error!void {
+ return self.content.append(self.base.alloc, content);
+ }
+
+ fn render(context: *anyopaque, alloc: Allocator) Error![]const u8 {
+ const self: *Self = @ptrCast(@alignCast(context));
+ var base = self.base;
+ const b = try base.element().render(alloc);
+ defer alloc.free(b);
+ var acc = try List(u8).initCapacity(alloc, b.len + self.content.items.len);
+ try acc.appendSlice(alloc, b);
+ for (self.content.items) |it| {
+ var v = it;
+ const sub = try v.render(alloc);
+ defer alloc.free(sub);
+ try acc.appendSlice(alloc, sub);
+ }
+ try acc.appendSlice(alloc, "</");
+ try acc.appendSlice(alloc, base.tag);
+ try acc.append(alloc, '>');
+ return acc.toOwnedSlice(alloc);
+ }
+};
+
+pub const Literal = struct {
+ literal: []const u8,
+
+ const Self = @This();
+
+ pub fn init(alloc: Allocator, literal: []const u8) Error!*Literal {
+ const v = try alloc.create(Self);
+ v.* = .{ .literal = try html.escape(alloc, literal) };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .vtable = .{ .render = Self.render }, .ptr = self };
+ }
+
+ fn render(context: *anyopaque, alloc: Allocator) Error![]const u8 {
+ const self: *Self = @ptrCast(@alignCast(context));
+ return try alloc.dupe(u8, self.literal);
+ }
+};
+
+pub const Root = struct {
+ content: List(Element),
+ arena: Arena,
+
+ const Self = @This();
+
+ pub fn init(parent: Allocator) Error!*Self {
+ var s = Self{
+ .content = undefined,
+ .arena = .init(parent),
+ };
+ var alloc = s.arena.allocator();
+ s.content = try .initCapacity(alloc, 2);
+ const v = try alloc.create(Self);
+ v.* = s;
+ return v;
+ }
+
+ pub fn deinit(self: *Self) void {
+ self.arena.deinit();
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .vtable = .{ .render = Self.render, }, .ptr = self };
+ }
+
+ pub fn allocator(self: *Self) Allocator {
+ return self.arena.allocator();
+ }
+
+ pub fn append(self: *Self, el: Element) Error!void {
+ try self.content.append(self.allocator(), el);
+ }
+
+ fn render(context: *anyopaque, alloc: Allocator) Error![]const u8 {
+ const self: *Self = @ptrCast(@alignCast(context));
+ if (self.content.items.len == 0) return "";
+ var acc = try List(u8).initCapacity(alloc, self.content.items.len);
+ errdefer acc.deinit(alloc);
+
+ var arena = Arena.init(alloc);
+ defer arena.deinit();
+ for (self.content.items) |it| {
+ const res = try it.render(arena.allocator());
+ try acc.appendSlice(alloc, res);
+ }
+ return acc.toOwnedSlice(alloc);
+ }
+};
+
+fn doTest(alloc: Allocator, el: Element, exp: []const u8) !void {
const got = try el.render(alloc);
defer alloc.free(got);
std.testing.expect(eql(u8, got, exp)) catch |err| {
@@ -188,37 +247,54 @@ fn doTest(alloc: Allocator, el: *Self, exp: []const u8) !void {
}
test "void element" {
- const alloc = std.testing.allocator;
+ var arena = Arena.init(std.testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
- var br = try init(alloc, .void, "br");
- defer br.deinit();
+ var br = try Void.init(alloc, "br");
- try doTest(alloc, &br, "<br>");
+ try doTest(alloc, br.element(), "<br>");
- var img = try init(alloc, .void, "img");
- defer img.deinit();
+ var img = try Void.init(alloc, "img");
try img.setAttribute("src", "foo");
try img.setAttribute("alt", "bar");
- try doTest(alloc, &img, "<img src=\"foo\" alt=\"bar\">");
+ try doTest(alloc, img.element(), "<img src=\"foo\" alt=\"bar\">");
}
test "content element" {
- const alloc = std.testing.allocator;
+ var arena = Arena.init(std.testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
- var p = try init(alloc, .content, "p");
+ var p = try Content.init(alloc, "p");
+
+ var content = try Literal.init(alloc, "hello world");
+ try p.append(content.element());
+
+ try doTest(alloc, content.element(), "hello world");
+ try doTest(alloc, p.element(), "<p>hello world</p>");
+
+ var div = try Content.init(alloc, "div");
+ try div.base.appendClass("foo-bar");
+ try div.append(p.element());
+ try div.append((try Void.init(alloc, "br")).element());
+
+ try doTest(alloc, div.element(), "<div class=\"foo-bar\"><p>hello world</p><br></div>");
+}
- var content = try initLit(alloc, "hello world");
- try p.appendContent(content);
+test "root element" {
+ const root = try Root.init(std.testing.allocator);
+ defer root.deinit();
+ const alloc = root.allocator();
- try doTest(alloc, &content, "hello world");
- try doTest(alloc, &p, "<p>hello world</p>");
+ var p = try Content.init(alloc, "p");
+ var content = try Literal.init(alloc, "hello world");
+ try p.append(content.element());
+ try root.append(p.element());
- var div = try init(alloc, .content, "div");
- defer div.deinit();
- try div.appendClass("foo-bar");
- try div.appendContent(p);
- try div.appendContent(try init(alloc, .void, "br"));
+ var br = try Void.init(alloc, "br");
+ try root.append(br.element());
- try doTest(alloc, &div, "<div class=\"foo-bar\"><p>hello world</p><br></div>");
+ try doTest(alloc, root.element(), "<p>hello world</p><br>");
}
diff --git a/src/eval/list.zig b/src/eval/list.zig
index 6954fd1..08f180a 100644
--- a/src/eval/list.zig
+++ b/src/eval/list.zig
@@ -34,14 +34,13 @@ fn List(comptime tag: []const u8) type {
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();
+ var el = try HTML.Content.init(alloc, tag);
for (self.content.items) |it| {
- var li = try HTML.init(alloc, .content, "li");
- try li.appendContent(try it.html(alloc));
- try el.appendContent(li);
+ var li = try HTML.Content.init(alloc, "li");
+ try li.append(try it.html(alloc));
+ try el.append(li.element());
}
- return el;
+ return el.element();
}
};
}
diff --git a/src/eval/paragraph.zig b/src/eval/paragraph.zig
index 468b92e..b076081 100644
--- a/src/eval/paragraph.zig
+++ b/src/eval/paragraph.zig
@@ -41,12 +41,11 @@ pub const Link = struct {
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;
+ var el = try HTML.Content.init(alloc, "a");
+ try el.append(try self.content.html(alloc));
+ try el.base.setAttribute("href", self.link);
+ if (self.target) |target| try el.base.setAttribute("target", target);
+ return el.element();
}
};