From 9f1a0bf3b0437770a7b62fd28a8748908c38dac4 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 27 Apr 2026 16:16:49 +0200 Subject: perf(html): reduce memory syscall --- example.td | 2 +- grammar.ebnf | 2 +- src/code.zig | 15 ++ src/eval/Element.zig | 20 +-- src/eval/Image.zig | 6 +- src/eval/Title.zig | 7 +- src/eval/blocks.zig | 30 ++-- src/eval/html/Element.zig | 390 +++++++++++++++++++++++++++------------------- src/eval/list.zig | 11 +- src/eval/paragraph.zig | 11 +- 10 files changed, 288 insertions(+), 206 deletions(-) diff --git a/example.td b/example.td index e4fab16..83762f9 100644 --- a/example.td +++ b/example.td @@ -18,7 +18,7 @@ Existing blocks: > yay with an attribution :D -::: info +:::info A beautiful callout ::: diff --git a/grammar.ebnf b/grammar.ebnf index 2acd7b0..b717aec 100644 --- a/grammar.ebnf +++ b/grammar.ebnf @@ -10,7 +10,7 @@ title = ? #{1,6} ?, " ", content; paragraph = content, { weak-delimiter, content }; quote = ">", content, { weak-delimiter, ">", content }, [ paragraph ]; callout = ":::", [ ? [a-z]+ ? ], { delimiter, content }, weak-delimiter, ":::"; -code-block = "```", [ ? [a-z]+ ? ], { delimiter, content }, weak-delimiter, "```"; +code-block = "```", [ ? [^\n]+ ? ], { delimiter, content }, weak-delimiter, "```"; math-block = "$$$", { delimiter, content }, weak-delimiter, "$$$"; image = "![", [ content ], "](", content, ")", { weak-delimiter, content }; list-unordored = "- ", paragraph, { weak-delimiter, "- ", paragraph }; 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" { \\``` , "
hey
"); // 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, "'); - } - 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, "'); + 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, "
"); + try doTest(alloc, br.element(), "
"); - 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, "\"bar\""); + try doTest(alloc, img.element(), "\"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(), "

hello world

"); + + 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(), "

hello world


"); +} - 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, "

hello world

"); + 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, "

hello world


"); + try doTest(alloc, root.element(), "

hello world


"); } 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(); } }; -- cgit v1.2.3