aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2026-04-27 20:49:13 +0200
committerAnhgelus Morhtuuzh <william@herges.fr>2026-04-27 20:49:13 +0200
commite154408e8ddeaee83242002f4c7af68b29d3a677 (patch)
treed0caadc01e77aa15edb204b1529fac168775329a /src
parent3b0e9424a66058da82d11d432da886ec7b6ce7eb (diff)
feat(): support callout
Diffstat (limited to 'src')
-rw-r--r--src/callout.zig87
-rw-r--r--src/code.zig2
-rw-r--r--src/eval/Element.zig1
-rw-r--r--src/eval/blocks.zig27
-rw-r--r--src/eval/html/Void.zig4
-rw-r--r--src/parser.zig2
-rw-r--r--src/root.zig2
-rw-r--r--src/testing.zig9
8 files changed, 129 insertions, 5 deletions
diff --git a/src/callout.zig b/src/callout.zig
new file mode 100644
index 0000000..2761944
--- /dev/null
+++ b/src/callout.zig
@@ -0,0 +1,87 @@
+const std = @import("std");
+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("eval/Element.zig");
+const testing = @import("testing.zig");
+const paragraph = @import("paragraph.zig");
+const doTest = testing.do;
+const doTestError = testing.doError;
+
+pub const Error = error{InvalidCallout} || paragraph.Error || Allocator.Error;
+
+pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
+ _ = l.next();
+ var beg = l.next() orelse return Error.InvalidCallout;
+ var kind: ?[]const u8 = null;
+ var title: ?[]const u8 = null;
+ switch (beg.kind) {
+ .literal => {
+ var iter = std.mem.splitAny(u8, beg.content, " ");
+ kind = iter.first();
+ if (iter.peek() != null) title = iter.buffer[iter.index.?..];
+ beg = l.next() orelse return Error.InvalidCallout;
+ if (!beg.kind.isDelimiter()) return Error.InvalidCallout;
+ },
+ else => if (!beg.kind.isDelimiter()) return Error.InvalidCallout,
+ }
+ var root = try Element.Root.init(alloc);
+ while (l.peek()) |it| {
+ if (it.kind == .callout) {
+ l.consume();
+ break;
+ }
+ if (it.kind.isDelimiter()) {
+ const next = l.peek() orelse return Error.InvalidCallout;
+ if (next.kind == .callout) {
+ l.consume();
+ break;
+ }
+ }
+ try root.append(try paragraph.parse(root.allocator(), l));
+ _ = l.peek() orelse return Error.InvalidCallout;
+ }
+ var el = try Element.Callout.init(alloc, root.element());
+ el.kind = kind;
+ el.title = title;
+ const end = l.next() orelse return el.element();
+ if (!end.kind.isDelimiter()) return Error.InvalidCallout;
+ return el.element();
+}
+
+test "callout" {
+ const alloc = std.testing.allocator;
+
+ try doTest(parse, alloc,
+ \\:::
+ \\hey
+ \\:::
+ , "<div class=\"callout\"><p>hey</p></div>");
+ try doTest(parse, alloc,
+ \\:::info
+ \\hey
+ \\:::
+ , "<div data-callout=\"info\" class=\"callout\"><p>hey</p></div>");
+ try doTest(parse, alloc,
+ \\:::info Title
+ \\hey
+ \\:::
+ , "<div data-callout=\"info\" class=\"callout\"><p>hey</p></div>");
+ // cannot test content with \n
+
+ try doTestError(parse, alloc, ":::", Error.InvalidCallout);
+ try doTestError(parse, alloc,
+ \\:::
+ \\hey
+ , Error.InvalidCallout);
+ try doTestError(parse, alloc,
+ \\:::
+ \\hey:::
+ , Error.IllegalPlacement);
+ try doTestError(parse, alloc,
+ \\:::
+ \\hey
+ \\::: nope
+ , Error.InvalidCallout);
+}
diff --git a/src/code.zig b/src/code.zig
index 5c5c0f2..c4af74d 100644
--- a/src/code.zig
+++ b/src/code.zig
@@ -24,7 +24,6 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
}
const code = try Element.Code.init(alloc);
code.attribute = data;
- const el = try Element.Figure.init(alloc, code.element());
while (l.next()) |it| {
if (it.kind == .code_block) return Error.InvalidCodeBlock;
if (it.kind.isDelimiter()) {
@@ -38,6 +37,7 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
}
var end = l.next() orelse return Error.InvalidCodeBlock;
if (end.kind != .code_block) return Error.InvalidCodeBlock;
+ const el = try Element.Figure.init(alloc, code.element());
end = l.next() orelse return el.element();
if (!end.kind.isDelimiter()) return Error.InvalidCodeBlock;
return el.element();
diff --git a/src/eval/Element.zig b/src/eval/Element.zig
index 72b6d7d..f93dc1a 100644
--- a/src/eval/Element.zig
+++ b/src/eval/Element.zig
@@ -9,6 +9,7 @@ pub const Root = @import("Root.zig");
const blocks = @import("blocks.zig");
pub const Code = blocks.Code;
pub const Figure = blocks.Figure;
+pub const Callout = blocks.Callout;
const Element = @This();
diff --git a/src/eval/blocks.zig b/src/eval/blocks.zig
index 63c0291..78e28f6 100644
--- a/src/eval/blocks.zig
+++ b/src/eval/blocks.zig
@@ -57,3 +57,30 @@ pub const Figure = struct {
return el.element();
}
};
+
+pub const Callout = struct {
+ content: Element,
+ title: ?[]const u8 = null,
+ kind: ?[]const u8 = null,
+
+ const Self = @This();
+
+ pub fn init(alloc: Allocator, content: Element) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{ .content = content };
+ return v;
+ }
+
+ pub fn element(self: *Self) Element {
+ return .{ .ptr = self, .vtable = .{ .html = Self.html } };
+ }
+
+ fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML {
+ const self: *Self = @ptrCast(@alignCast(context));
+ var el = try HTML.Content.init(alloc, "div");
+ try el.base.appendClass("callout");
+ if (self.kind) |kind| try el.base.setAttribute("data-callout", kind);
+ try el.append(try self.content.html(alloc));
+ return el.element();
+ }
+};
diff --git a/src/eval/html/Void.zig b/src/eval/html/Void.zig
index 58550f4..ec35fc7 100644
--- a/src/eval/html/Void.zig
+++ b/src/eval/html/Void.zig
@@ -28,7 +28,7 @@ pub fn element(self: *Self) Element {
}
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));
+ try self.attributes.put(try self.alloc.dupe(u8, k), try html.escape(self.alloc, v));
}
pub fn removeAttribute(self: *Self, k: []const u8) void {
@@ -40,7 +40,7 @@ pub fn hasAttribute(self: *Self, k: []const u8) bool {
}
pub fn appendClass(self: *Self, v: []const u8) Error!void {
- try self.class_list.insert(v);
+ try self.class_list.insert(try html.escape(self.alloc, v));
}
pub fn hasClass(self: *Self, v: []const u8) bool {
diff --git a/src/parser.zig b/src/parser.zig
index 9d89d7d..db91641 100644
--- a/src/parser.zig
+++ b/src/parser.zig
@@ -8,6 +8,7 @@ const title = @import("title.zig");
const link = @import("link.zig");
const list = @import("list.zig");
const code = @import("code.zig");
+const callout = @import("callout.zig");
pub const Error = error{FeatureNotSupported} ||
Lexer.Error ||
@@ -17,6 +18,7 @@ pub const Error = error{FeatureNotSupported} ||
list.Error ||
link.ImageError ||
code.Error ||
+ callout.Error ||
Allocator.Error;
pub const Document = *Element.Root;
diff --git a/src/root.zig b/src/root.zig
index 9da89af..919c3ee 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -16,6 +16,7 @@ inline fn getErrorCode(err: Error) u8 {
Error.InvalidLink => 7,
Error.InvalidImage => 8,
Error.InvalidCodeBlock => 9,
+ Error.InvalidCallout => 10,
};
}
@@ -31,6 +32,7 @@ export fn typdown_getErrorString(code: u8) [*:0]const u8 {
7 => "invalid link",
8 => "invalid image",
9 => "invalid code block",
+ 10 => "invalid callout",
else => unreachable,
};
}
diff --git a/src/testing.zig b/src/testing.zig
index bf0690e..cfc3505 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -24,6 +24,11 @@ pub fn doError(comptime parse: fn (Allocator, *Lexer) parser.Error!Element, pare
defer arena.deinit();
var l = try Lexer.init(t);
- _ = parse(arena.allocator(), &l) catch |e| return std.testing.expect(err == e);
- return std.testing.expect(false);
+ _ = parse(arena.allocator(), &l) catch |e| {
+ return std.testing.expect(err == e) catch |v| {
+ std.debug.print("{}\n", .{v});
+ return e;
+ };
+ };
+ return error.ExpectingError;
}