From c008d747534f4c8aa59b045efbca754618e14b41 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Sat, 25 Apr 2026 17:18:00 +0200 Subject: style(eval): move in its own package --- src/Element.zig | 243 ---------------------------------------------- src/content.zig | 4 +- src/dom/Element.zig | 241 --------------------------------------------- src/dom/html.zig | 47 --------- src/eval/Element.zig | 136 ++++++++++++++++++++++++++ src/eval/Title.zig | 45 +++++++++ src/eval/html/Element.zig | 241 +++++++++++++++++++++++++++++++++++++++++++++ src/eval/html/html.zig | 47 +++++++++ src/eval/paragraph.zig | 79 +++++++++++++++ src/link.zig | 5 +- src/paragraph.zig | 5 +- src/parser.zig | 2 +- src/testing.zig | 2 +- src/title.zig | 2 +- 14 files changed, 559 insertions(+), 540 deletions(-) delete mode 100644 src/Element.zig delete mode 100644 src/dom/Element.zig delete mode 100644 src/dom/html.zig create mode 100644 src/eval/Element.zig create mode 100644 src/eval/Title.zig create mode 100644 src/eval/html/Element.zig create mode 100644 src/eval/html/html.zig create mode 100644 src/eval/paragraph.zig diff --git a/src/Element.zig b/src/Element.zig deleted file mode 100644 index 7206125..0000000 --- a/src/Element.zig +++ /dev/null @@ -1,243 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const DOMElement = @import("dom/Element.zig"); - -const Parent = @This(); - -vtable: struct { - deinit: *const fn (*anyopaque, Allocator) void, - dom: *const fn (*anyopaque, Allocator) DOMElement.Error!DOMElement, -}, -ptr: *anyopaque, - -pub fn renderHTML(self: Parent, alloc: Allocator) DOMElement.Error![]const u8 { - var el = try self.vtable.dom(self.ptr, alloc); - defer el.deinit(); - return el.render(alloc); -} - -pub fn deinit(self: Parent, alloc: Allocator) void { - self.vtable.deinit(self.ptr, alloc); -} - -fn dom(self: Parent, alloc: Allocator) DOMElement.Error!DOMElement { - return self.vtable.dom(self.ptr, alloc); -} - -pub const Paragraph = Modifier("p"); -pub const Bold = Modifier("b"); -pub const Italic = Modifier("em"); -pub const Code = Modifier("code"); - -pub const Empty = struct { - content: std.ArrayList(Parent), - - 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) Parent { - return .{ .ptr = self, .vtable = .{ .deinit = destroy, .dom = Self.dom } }; - } - - 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 dom(context: *anyopaque, alloc: Allocator) DOMElement.Error!DOMElement { - const self: *Self = @ptrCast(@alignCast(context)); - var el = DOMElement.initEmpty(alloc); - errdefer el.deinit(); - for (self.content.items) |it| try el.appendContent(try it.dom(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) Parent { - return .{ .ptr = self, .vtable = .{ .deinit = destroy, .dom = Self.dom } }; - } - - 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 dom(context: *anyopaque, alloc: Allocator) DOMElement.Error!DOMElement { - const self: *Self = @ptrCast(@alignCast(context)); - return DOMElement.initLitEscaped(alloc, self.content); - } -}; - -pub fn Modifier(comptime tag: []const u8) type { - return struct { - content: std.ArrayList(Parent), - - 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) Parent { - return .{ .ptr = self, .vtable = .{ .deinit = destroy, .dom = Self.dom } }; - } - - 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 dom(context: *anyopaque, alloc: Allocator) DOMElement.Error!DOMElement { - const self: *Self = @ptrCast(@alignCast(context)); - var el = try DOMElement.init(alloc, .content, tag); - errdefer el.deinit(); - for (self.content.items) |it| try el.appendContent(try it.dom(alloc)); - return el; - } - }; -} - -pub const Link = struct { - link: []const u8, - content: Parent, - target: ?[]const u8 = null, - - const Self = @This(); - - pub fn init(alloc: Allocator, content: Parent, link: []const u8) !*Self { - const v = try alloc.create(Self); - v.* = .{ - .content = content, - .link = link, - }; - return v; - } - - pub fn element(self: *Self) Parent { - return .{ .ptr = self, .vtable = .{ .deinit = destroy, .dom = Self.dom } }; - } - - 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 dom(context: *anyopaque, alloc: Allocator) DOMElement.Error!DOMElement { - const self: *Self = @ptrCast(@alignCast(context)); - var el = try DOMElement.init(alloc, .content, "a"); - errdefer el.deinit(); - try el.appendContent(try self.content.dom(alloc)); - try el.setAttribute("href", self.link); - if (self.target) |target| try el.setAttribute("target", target); - return el; - } -}; - -pub const Title = struct { - 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, .dom = Self.dom } }; - } - - 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 dom(context: *anyopaque, alloc: Allocator) DOMElement.Error!DOMElement { - const self: *Self = @ptrCast(@alignCast(context)); - var el = try DOMElement.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.dom(alloc)); - return el; - } -}; - -fn doTest(alloc: Allocator, el: Parent, 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 Literal.init(alloc, "hello world")).element(); - try doTest(alloc, lit, "hello world"); - - var p = try Paragraph.init(alloc); - try p.content.append(alloc, lit); - defer p.deinit(alloc); - try doTest(alloc, p.element(), "

hello world

"); - - const link = (try Link.init(alloc, (try Literal.init(alloc, "foo")).element(), "example.org")).element(); - try doTest(alloc, link, "foo"); - - try p.content.append(alloc, link); - try doTest(alloc, p.element(), "

hello worldfoo

"); -} diff --git a/src/content.zig b/src/content.zig index 68d88ad..53f9418 100644 --- a/src/content.zig +++ b/src/content.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Token = @import("lexer/Token.zig"); const Lexer = @import("lexer/Lexer.zig"); -const Element = @import("Element.zig"); +const Element = @import("eval/Element.zig"); const parser = @import("parser.zig"); const link = @import("link.zig"); const testing = @import("testing.zig"); @@ -29,7 +29,7 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element { } fn parseModifier(alloc: Allocator, l: *Lexer, knd: Token.Kind, comptime tag: []const u8) Error!Element { - var el = try Element.Modifier(tag).init(alloc); + var el = try Element.Simple(tag).init(alloc); errdefer el.deinit(alloc); while (l.peek()) |next| { if (next.kind == knd) { diff --git a/src/dom/Element.zig b/src/dom/Element.zig deleted file mode 100644 index 87fd876..0000000 --- a/src/dom/Element.zig +++ /dev/null @@ -1,241 +0,0 @@ -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, "'); - } - 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, "
"); - - 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" { - 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, "

hello world

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

hello world

\"example\"
"); -} diff --git a/src/dom/html.zig b/src/dom/html.zig deleted file mode 100644 index 064ebb1..0000000 --- a/src/dom/html.zig +++ /dev/null @@ -1,47 +0,0 @@ -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, "helloworld", "hello>world"); - try doTest(alloc, "hello\"world", "hello"world"); -} 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, "'); + } + 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, "
"); + + 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" { + 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, "

hello world

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

hello world

\"example\"
"); +} 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, "helloworld", "hello>world"); + try doTest(alloc, "hello\"world", "hello"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(), "

hello world

"); + + const link = (try Link.init(alloc, (try Element.Literal.init(alloc, "foo")).element(), "example.org")).element(); + try doTest(alloc, link, "foo"); + + try p.content.append(alloc, link); + try doTest(alloc, p.element(), "

hello worldfoo

"); +} diff --git a/src/link.zig b/src/link.zig index 85156dc..e214b4b 100644 --- a/src/link.zig +++ b/src/link.zig @@ -3,7 +3,8 @@ const Allocator = std.mem.Allocator; const eql = std.mem.eql; const Token = @import("lexer/Token.zig"); const Lexer = @import("lexer/Lexer.zig"); -const Element = @import("Element.zig"); +const Element = @import("eval/Element.zig"); +const Link = Element.Paragraph.Link; const content = @import("content.zig"); const testing = @import("testing.zig"); const doTest = testing.do; @@ -16,7 +17,7 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element { const second = data.second orelse return data.first.?; var in = if (data.first) |first| first else (try Element.Literal.init(alloc, second)).element(); errdefer in.deinit(alloc); - return (try Element.Link.init(alloc, in, data.second.?)).element(); + return (try Link.init(alloc, in, data.second.?)).element(); } pub const Data = struct { diff --git a/src/paragraph.zig b/src/paragraph.zig index 8edfa12..a76b98c 100644 --- a/src/paragraph.zig +++ b/src/paragraph.zig @@ -2,7 +2,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Token = @import("lexer/Token.zig"); const Lexer = @import("lexer/Lexer.zig"); -const Element = @import("Element.zig"); +const Element = @import("eval/Element.zig"); +const Paragraph = Element.Paragraph; const parser = @import("parser.zig"); const link = @import("link.zig"); const content = @import("content.zig"); @@ -13,7 +14,7 @@ const doTestError = testing.doError; pub const Error = content.Error || link.Error || Lexer.Error || Allocator.Error; pub fn parse(alloc: Allocator, l: *Lexer) Error!Element { - var el = try Element.Paragraph.init(alloc); + var el = try Paragraph.Block.init(alloc); errdefer el.deinit(alloc); while (l.peek()) |next| { switch (next.kind) { diff --git a/src/parser.zig b/src/parser.zig index 639160b..163c5a8 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Token = @import("lexer/Token.zig"); const Lexer = @import("lexer/Lexer.zig"); -const Element = @import("Element.zig"); +const Element = @import("eval/Element.zig"); const paragraph = @import("paragraph.zig"); const title = @import("title.zig"); const link = @import("link.zig"); diff --git a/src/testing.zig b/src/testing.zig index 0911438..6d19e0e 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Lexer = @import("lexer/Lexer.zig"); -const Element = @import("Element.zig"); +const Element = @import("eval/Element.zig"); const parser = @import("parser.zig"); pub fn do(comptime parse: fn (Allocator, *Lexer) parser.Error!Element, alloc: Allocator, t: []const u8, v: []const u8) !void { diff --git a/src/title.zig b/src/title.zig index 506af21..9fdc116 100644 --- a/src/title.zig +++ b/src/title.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Token = @import("lexer/Token.zig"); const Lexer = @import("lexer/Lexer.zig"); -const Element = @import("Element.zig"); +const Element = @import("eval/Element.zig"); const paragraph = @import("paragraph.zig"); const testing = @import("testing.zig"); const doTest = testing.do; -- cgit v1.2.3