From e154408e8ddeaee83242002f4c7af68b29d3a677 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 27 Apr 2026 20:49:13 +0200 Subject: feat(): support callout --- src/callout.zig | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/code.zig | 2 +- src/eval/Element.zig | 1 + src/eval/blocks.zig | 27 ++++++++++++++++ src/eval/html/Void.zig | 4 +-- src/parser.zig | 2 ++ src/root.zig | 2 ++ src/testing.zig | 9 ++++-- 8 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 src/callout.zig (limited to 'src') 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 + \\::: + , "

hey

"); + try doTest(parse, alloc, + \\:::info + \\hey + \\::: + , "

hey

"); + try doTest(parse, alloc, + \\:::info Title + \\hey + \\::: + , "

hey

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