aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2026-04-26 21:38:06 +0200
committerAnhgelus Morhtuuzh <william@herges.fr>2026-04-26 21:38:06 +0200
commitdca42e27fe9c7d28c72bb6cb8e5cc4ec481572e8 (patch)
tree5492f9c4b46b48e58d8002fd36deebd13c059291 /src
parentb0902c05ffc84d282e10a0179e041948d49fabf8 (diff)
feat(): support image
Diffstat (limited to 'src')
-rw-r--r--src/content.zig2
-rw-r--r--src/eval/Element.zig1
-rw-r--r--src/eval/Image.zig68
-rw-r--r--src/eval/html/Element.zig21
-rw-r--r--src/lexer/Lexer.zig23
-rw-r--r--src/lexer/Token.zig7
-rw-r--r--src/link.zig85
-rw-r--r--src/list.zig10
-rw-r--r--src/paragraph.zig10
-rw-r--r--src/parser.zig12
-rw-r--r--src/root.zig2
-rw-r--r--src/title.zig2
12 files changed, 184 insertions, 59 deletions
diff --git a/src/content.zig b/src/content.zig
index 53f9418..b1ac0c7 100644
--- a/src/content.zig
+++ b/src/content.zig
@@ -9,7 +9,7 @@ const testing = @import("testing.zig");
const doTest = testing.do;
const doTestError = testing.doError;
-pub const Error = error{ ModifierNotClosed, IllegalPlacement } || Lexer.Error || Allocator.Error;
+pub const Error = error{ ModifierNotClosed, IllegalPlacement } || Allocator.Error;
pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
var content = try Element.Empty.init(alloc);
diff --git a/src/eval/Element.zig b/src/eval/Element.zig
index 5d6fce5..a8b424d 100644
--- a/src/eval/Element.zig
+++ b/src/eval/Element.zig
@@ -4,6 +4,7 @@ pub const HTML = @import("html/Element.zig");
pub const paragraph = @import("paragraph.zig");
pub const Title = @import("Title.zig");
pub const list = @import("list.zig");
+pub const Image = @import("Image.zig");
const Element = @This();
diff --git a/src/eval/Image.zig b/src/eval/Image.zig
new file mode 100644
index 0000000..771c8ee
--- /dev/null
+++ b/src/eval/Image.zig
@@ -0,0 +1,68 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const HTML = @import("html/Element.zig");
+const Element = @import("Element.zig");
+
+const Self = @This();
+
+src: []const u8,
+alt: ?[]const u8 = null,
+source: ?Element = null,
+
+pub fn init(alloc: Allocator, src: []const u8) !*Self {
+ const v = try alloc.create(Self);
+ v.* = .{
+ .src = src,
+ };
+ 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 {
+ const self: *Self = @ptrCast(@alignCast(context));
+ if (self.source) |it| it.deinit(alloc);
+ alloc.destroy(self);
+}
+
+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();
+ try img.setAttribute("src", self.src);
+ if (self.alt) |it| try img.setAttribute("alt", it);
+ var el = try HTML.init(alloc, .content, "figure");
+ errdefer el.deinit();
+ try el.appendContent(img);
+
+ const source = self.source orelse return el;
+ var caption = try HTML.init(alloc, .content, "figcaption");
+ errdefer caption.deinit();
+ try caption.content.append(alloc, try source.html(alloc));
+ try el.appendContent(caption);
+ return el;
+}
+
+test "html" {
+ const alloc = std.testing.allocator;
+ const expect = std.testing.expect;
+ const eql = std.mem.eql;
+
+ var img = try init(alloc, "foo");
+ defer img.deinit(alloc);
+ const h = try img.element().renderHTML(alloc);
+ defer alloc.free(h);
+ try expect(eql(u8, h, "<figure><img src=\"foo\"></figure>"));
+
+ img.alt = "bar";
+ const h2 = try img.element().renderHTML(alloc);
+ defer alloc.free(h2);
+ try expect(eql(u8, h2, "<figure><img src=\"foo\" alt=\"bar\"></figure>"));
+}
diff --git a/src/eval/html/Element.zig b/src/eval/html/Element.zig
index 87fd876..666e6f0 100644
--- a/src/eval/html/Element.zig
+++ b/src/eval/html/Element.zig
@@ -178,19 +178,6 @@ pub fn appendContent(self: *Self, content: Self) Error!void {
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);
@@ -214,10 +201,6 @@ test "void element" {
try img.setAttribute("alt", "bar");
try doTest(alloc, &img, "<img src=\"foo\" alt=\"bar\">");
-
- var img2 = try initImg(alloc, "foo", "bar");
- defer img2.deinit();
- try doTest(alloc, &img2, "<img src=\"foo\" alt=\"bar\">");
}
test "content element" {
@@ -235,7 +218,7 @@ test "content element" {
defer div.deinit();
try div.appendClass("foo-bar");
try div.appendContent(p);
- try div.appendContent(try initImg(alloc, "example.org", "example"));
+ try div.appendContent(try init(alloc, .void, "br"));
- try doTest(alloc, &div, "<div class=\"foo-bar\"><p>hello world</p><img src=\"example.org\" alt=\"example\"></div>");
+ try doTest(alloc, &div, "<div class=\"foo-bar\"><p>hello world</p><br></div>");
}
diff --git a/src/lexer/Lexer.zig b/src/lexer/Lexer.zig
index cdf1bd8..983aa23 100644
--- a/src/lexer/Lexer.zig
+++ b/src/lexer/Lexer.zig
@@ -223,6 +223,29 @@ test "lexer common" {
try std.testing.expect(l.next() == null);
}
+test "lexer image" {
+ var l = try init("![alt](src)");
+
+ try doTest(&l, .image, "!");
+ try doTest(&l, .link, "[");
+ try doTest(&l, .literal, "alt");
+ try doTest(&l, .link, "](");
+ try doTest(&l, .literal, "src");
+ try doTest(&l, .link, ")");
+
+ try std.testing.expect(l.next() == null);
+
+ l = try init("![](src)");
+
+ try doTest(&l, .image, "!");
+ try doTest(&l, .link, "[");
+ try doTest(&l, .link, "](");
+ try doTest(&l, .literal, "src");
+ try doTest(&l, .link, ")");
+
+ try std.testing.expect(l.next() == null);
+}
+
test "lexer multiline" {
var l = try init(
\\# Title
diff --git a/src/lexer/Token.zig b/src/lexer/Token.zig
index 18b2d10..bd2a07b 100644
--- a/src/lexer/Token.zig
+++ b/src/lexer/Token.zig
@@ -27,6 +27,13 @@ pub const Kind = enum {
else => false,
};
}
+
+ pub fn isPar(self: @This()) bool {
+ return switch (self) {
+ .literal, .link, .code, .math, .bold, .italic, .ref => true,
+ else => false,
+ };
+ }
};
kind: Kind,
diff --git a/src/link.zig b/src/link.zig
index 8eb5778..50e89f5 100644
--- a/src/link.zig
+++ b/src/link.zig
@@ -6,32 +6,17 @@ const Lexer = @import("lexer/Lexer.zig");
const Element = @import("eval/Element.zig");
const Link = Element.paragraph.Link;
const content = @import("content.zig");
+const paragraph = @import("paragraph.zig");
const testing = @import("testing.zig");
const doTest = testing.do;
const doTestError = testing.doError;
-pub const Error = error{InvalidLink} || Lexer.Error || content.Error || Allocator.Error;
+pub const Error = error{InvalidLink} || content.Error || Allocator.Error;
pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
- const data = try parseData(alloc, l);
- 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 Link.init(alloc, in, data.second.?)).element();
-}
-
-pub const Data = struct {
- first: ?Element,
- second: ?[]const u8,
-};
-
-pub fn parseData(alloc: Allocator, l: *Lexer) Error!Data {
const v = l.next().?;
if (v.kind != .link) return Error.InvalidLink;
- if (!eql(u8, v.content, "[")) {
- const el = try Element.Literal.init(alloc, v.content);
- return .{ .first = el.element(), .second = null };
- }
+ if (!eql(u8, v.content, "[")) return (try Element.Literal.init(alloc, v.content)).element();
var el = try Element.Empty.init(alloc);
errdefer el.deinit(alloc);
while (l.peek()) |next| {
@@ -52,15 +37,58 @@ pub fn parseData(alloc: Allocator, l: *Lexer) Error!Data {
if (href.kind != .literal) return Error.InvalidLink;
const finisher = l.next() orelse return Error.InvalidLink;
if (!finisher.equals(.link, ")")) return Error.InvalidLink;
- var res = Data{
- .first = el.element(),
- .second = href.content,
- };
- if (el.content.items.len == 0) {
- res.first = null;
+ var in: Element = undefined;
+ if (el.content.items.len > 0) {
+ in = el.element();
+ } else {
el.deinit(alloc);
+ in = (try Element.Literal.init(alloc, href.content)).element();
}
- return res;
+ errdefer in.deinit(alloc);
+ return (try Link.init(alloc, in, href.content)).element();
+}
+
+pub const ImageError = error{InvalidImage} || paragraph.Error || Allocator.Error;
+
+pub fn parseImage(alloc: Allocator, l: *Lexer) ImageError!Element {
+ _ = l.next().?;
+ const beg = l.next() orelse return ImageError.InvalidImage;
+ if (!eql(u8, beg.content, "[")) return ImageError.InvalidImage;
+ var it = l.next() orelse return ImageError.InvalidImage;
+ var alt: ?[]const u8 = null;
+ switch (it.kind) {
+ .link => if (!eql(u8, it.content, "](")) return ImageError.InvalidImage,
+ .literal => {
+ alt = it.content;
+ const next = l.next() orelse return ImageError.InvalidImage;
+ if (!next.equals(.link, "](")) return ImageError.InvalidImage;
+ },
+ else => return ImageError.InvalidImage,
+ }
+ it = l.next() orelse return ImageError.InvalidImage;
+ if (it.kind != .literal) return ImageError.InvalidImage;
+ const src = it.content;
+ it = l.next() orelse return ImageError.InvalidImage;
+ if (!it.equals(.link, ")")) return ImageError.InvalidImage;
+ const el = try Element.Image.init(alloc, src);
+ errdefer el.deinit(alloc);
+ el.alt = alt;
+ it = l.peek() orelse return el.element();
+ switch (it.kind) {
+ .strong_delimiter => return el.element(),
+ .weak_delimiter => l.consume(),
+ else => return ImageError.InvalidImage,
+ }
+ const p = try paragraph.parse(alloc, l);
+ errdefer p.deinit(alloc);
+ el.source = p;
+ const p_el: *Element.paragraph.Block = @ptrCast(@alignCast(p.ptr));
+ defer p_el.deinit(alloc);
+ const in = try Element.Empty.init(alloc);
+ errdefer in.deinit(alloc);
+ in.content = try p_el.content.clone(alloc);
+ el.source = in.element();
+ return el.element();
}
test "parse links" {
@@ -76,3 +104,10 @@ test "parse links" {
try doTestError(parse, alloc, "[foo](", Error.InvalidLink);
try doTestError(parse, alloc, "[foo]()", Error.InvalidLink);
}
+
+test "parse image" {
+ const alloc = std.testing.allocator;
+
+ try doTest(parseImage, alloc, "![](src)", "<figure><img src=\"src\"></figure>");
+ try doTest(parseImage, alloc, "![alt](src)", "<figure><img src=\"src\" alt=\"alt\"></figure>");
+}
diff --git a/src/list.zig b/src/list.zig
index 1375d86..b8c7458 100644
--- a/src/list.zig
+++ b/src/list.zig
@@ -8,14 +8,16 @@ const testing = @import("testing.zig");
const doTest = testing.do;
const doTestError = testing.doError;
-pub fn parseOrdored(alloc: Allocator, l: *Lexer) !Element {
+pub const Error = paragraph.Error || Allocator.Error;
+
+pub fn parseOrdored(alloc: Allocator, l: *Lexer) Error!Element {
const el = try Element.list.Ordored.init(alloc);
errdefer el.deinit(alloc);
try parse(alloc, &el.content, l, .list_ordored);
return el.element();
}
-pub fn parseUnordored(alloc: Allocator, l: *Lexer) !Element {
+pub fn parseUnordored(alloc: Allocator, l: *Lexer) Error!Element {
const el = try Element.list.Unordored.init(alloc);
errdefer el.deinit(alloc);
try parse(alloc, &el.content, l, .list_unordored);
@@ -54,6 +56,8 @@ test "parse ordored list" {
\\. two
\\no more
, "<ol><li>one</li><li>two</li></ol>");
+
+ try doTestError(parseOrdored, alloc, ".one :::", Error.IllegalPlacement);
}
test "parse unordored list" {
@@ -68,4 +72,6 @@ test "parse unordored list" {
\\- two
\\no more
, "<ul><li>one</li><li>two</li></ul>");
+
+ try doTestError(parseOrdored, alloc, "- one :::", Error.IllegalPlacement);
}
diff --git a/src/paragraph.zig b/src/paragraph.zig
index f03bc8f..c2e0175 100644
--- a/src/paragraph.zig
+++ b/src/paragraph.zig
@@ -11,7 +11,7 @@ const testing = @import("testing.zig");
const doTest = testing.do;
const doTestError = testing.doError;
-pub const Error = content.Error || link.Error || Lexer.Error || Allocator.Error;
+pub const Error = content.Error || link.Error || Allocator.Error;
pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
var el = try Paragraph.Block.init(alloc);
@@ -22,12 +22,8 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
.weak_delimiter => {
l.consume();
const future = l.peek() orelse return el.element();
- switch (future.kind) {
- .literal, .italic, .code, .bold, .link => {
- try el.content.append(alloc, (try Element.Literal.init(alloc, " ")).element());
- },
- else => return el.element(),
- }
+ if (!future.kind.isPar()) return el.element();
+ try el.content.append(alloc, (try Element.Literal.init(alloc, " ")).element());
},
else => try el.content.append(alloc, try parseLine(alloc, l)),
}
diff --git a/src/parser.zig b/src/parser.zig
index 8c170e7..a5a49cd 100644
--- a/src/parser.zig
+++ b/src/parser.zig
@@ -10,7 +10,7 @@ const list = @import("list.zig");
pub const Error = error{
FeatureNotSupported,
-} || Lexer.Error || paragraph.Error || title.Error || link.Error || Allocator.Error;
+} || Lexer.Error || paragraph.Error || title.Error || link.Error || list.Error || link.ImageError || Allocator.Error;
pub const Document = struct {
arena: std.heap.ArenaAllocator,
@@ -51,17 +51,21 @@ fn gen(parent: Allocator, l: *Lexer) Error!Document {
var elements = try std.ArrayList(Element).initCapacity(alloc, 2);
base: while (l.peek()) |it| {
try elements.append(alloc, switch (it.kind) {
- // block paragraph
- .literal, .bold, .italic, .code, .link => try paragraph.parse(alloc, l),
// other blocks
.title => try title.parse(alloc, l),
.list_ordored => try list.parseOrdored(alloc, l),
.list_unordored => try list.parseUnordored(alloc, l),
+ .image => try link.parseImage(alloc, l),
.weak_delimiter, .strong_delimiter => {
l.consume();
continue :base;
},
- else => return Error.FeatureNotSupported,
+ else =>
+ // block paragraph
+ if (it.kind.isPar())
+ try paragraph.parse(alloc, l)
+ else
+ return Error.FeatureNotSupported,
});
}
return .{ .root = try elements.toOwnedSlice(alloc), .arena = arena };
diff --git a/src/root.zig b/src/root.zig
index 185e6b6..5062832 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -14,6 +14,7 @@ inline fn getErrorCode(err: Error) u8 {
Error.InvalidTitleContent => 5,
Error.IllegalPlacement => 6,
Error.InvalidLink => 7,
+ Error.InvalidImage => 8,
};
}
@@ -27,6 +28,7 @@ export fn typdown_getErrorString(code: u8) [*:0]const u8 {
5 => "invalid title content",
6 => "illegal placement",
7 => "invalid link",
+ 8 => "invalid image",
else => unreachable,
};
}
diff --git a/src/title.zig b/src/title.zig
index 9fdc116..87d7f5f 100644
--- a/src/title.zig
+++ b/src/title.zig
@@ -8,7 +8,7 @@ const testing = @import("testing.zig");
const doTest = testing.do;
const doTestError = testing.doError;
-pub const Error = error{InvalidTitleContent} || paragraph.Error || Lexer.Error;
+pub const Error = error{InvalidTitleContent} || paragraph.Error;
pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
const v = l.next().?;