aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2026-04-26 23:10:17 +0200
committerAnhgelus Morhtuuzh <william@herges.fr>2026-04-26 23:17:26 +0200
commitde948492e8b38a79d5db9c506c1b7b82e86c6b12 (patch)
tree122e4c004d37193c64d6b8b89d9a252f7e237cfa
parentae6ee68d6f4ef79fef609b4d09b543fc06326e95 (diff)
feat(): support code block
-rw-r--r--src/code.zig61
-rw-r--r--src/eval/blocks.zig40
-rw-r--r--src/lexer/Lexer.zig9
-rw-r--r--src/lexer/Token.zig11
-rw-r--r--src/link.zig26
-rw-r--r--src/list.zig30
-rw-r--r--src/paragraph.zig50
-rw-r--r--src/parser.zig14
-rw-r--r--src/root.zig2
9 files changed, 171 insertions, 72 deletions
diff --git a/src/code.zig b/src/code.zig
new file mode 100644
index 0000000..98bba42
--- /dev/null
+++ b/src/code.zig
@@ -0,0 +1,61 @@
+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 doTest = testing.do;
+const doTestError = testing.doError;
+
+pub const Error = error{InvalidCodeBlock} || Allocator.Error;
+
+pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
+ _ = l.next();
+ var beg = l.next() orelse return Error.InvalidCodeBlock;
+ var data: ?[]const u8 = null;
+ switch (beg.kind) {
+ .literal => {
+ data = beg.content;
+ beg = l.next() orelse return Error.InvalidCodeBlock;
+ if (!beg.kind.isDelimiter()) return Error.InvalidCodeBlock;
+ },
+ else => if (!beg.kind.isDelimiter()) return Error.InvalidCodeBlock,
+ }
+ const code = try Element.Code.init(alloc);
+ code.attribute = data;
+ const el = try Element.Figure.init(alloc, code.element());
+ errdefer el.deinit(alloc);
+ while (l.next()) |it| {
+ if (it.kind == .code_block) return Error.InvalidCodeBlock;
+ if (it.kind.isDelimiter()) {
+ const next = l.peek() orelse return Error.InvalidCodeBlock;
+ if (next.kind == .code_block) break;
+ }
+ try code.content.append(alloc, (try Element.Literal.init(alloc, it.content)).element());
+ // restore modifications done by the lexer
+ if (it.kind.requiresSpace())
+ try code.content.append(alloc, (try Element.Literal.init(alloc, " ")).element());
+ }
+ var end = l.next() orelse return Error.InvalidCodeBlock;
+ if (end.kind != .code_block) return Error.InvalidCodeBlock;
+ end = l.next() orelse return el.element();
+ if (!end.kind.isDelimiter()) return Error.InvalidCodeBlock;
+ return el.element();
+}
+
+test "code" {
+ const alloc = std.testing.allocator;
+
+ try doTest(parse, alloc,
+ \\```
+ \\hey
+ \\```
+ , "<figure><pre><code>hey</code></pre></figure>");
+ try doTest(parse, alloc,
+ \\```td another
+ \\hey
+ \\```
+ , "<figure><pre data-code=\"td another\"><code>hey</code></pre></figure>");
+ // cannot test content with \n
+}
diff --git a/src/eval/blocks.zig b/src/eval/blocks.zig
index 9ad10ba..8ec42da 100644
--- a/src/eval/blocks.zig
+++ b/src/eval/blocks.zig
@@ -3,7 +3,45 @@ const Allocator = std.mem.Allocator;
const HTML = @import("html/Element.zig");
const Element = @import("Element.zig");
-pub const Code = Element.Simple("pre");
+pub const Code = struct {
+ content: std.ArrayList(Element),
+ attribute: ?[]const u8 = null,
+
+ 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, "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;
+ }
+};
pub const Figure = struct {
content: Element,
diff --git a/src/lexer/Lexer.zig b/src/lexer/Lexer.zig
index 983aa23..4137b40 100644
--- a/src/lexer/Lexer.zig
+++ b/src/lexer/Lexer.zig
@@ -54,7 +54,7 @@ pub fn next(self: *Self) ?Token {
// conds here to avoid creating complex condition in while
const next_rune = self.iter.peek(1);
const next_kind = self.getCurrentKind(current_kind, next_rune, self.content[beg..end]).kind;
- if (requiresSpace(current_kind.?) and next_kind != current_kind.?) {
+ if (current_kind.?.requiresSpace() and next_kind != current_kind.?) {
if (eql(u8, next_rune, " ")) {
// consume next space
_ = self.iter.nextCodepoint();
@@ -166,13 +166,6 @@ fn isOneOrThree(op: []const u8, rune: []const u8, p: []const u8, one: Token.Kind
};
}
-fn requiresSpace(k: Token.Kind) bool {
- return switch (k) {
- .title, .list_ordored, .list_unordored => true,
- else => false,
- };
-}
-
fn doTest(l: *Self, k: Token.Kind, v: []const u8) !void {
var first = l.next().?;
std.testing.expect(first.equals(k, v)) catch |err| {
diff --git a/src/lexer/Token.zig b/src/lexer/Token.zig
index bd2a07b..bd0bdc2 100644
--- a/src/lexer/Token.zig
+++ b/src/lexer/Token.zig
@@ -21,19 +21,26 @@ pub const Kind = enum {
list_unordored,
tag,
- pub fn isDelimiter(self: @This()) bool {
+ pub inline fn isDelimiter(self: @This()) bool {
return switch (self) {
.weak_delimiter, .strong_delimiter => true,
else => false,
};
}
- pub fn isPar(self: @This()) bool {
+ pub inline fn isPar(self: @This()) bool {
return switch (self) {
.literal, .link, .code, .math, .bold, .italic, .ref => true,
else => false,
};
}
+
+ pub inline fn requiresSpace(self: @This()) bool {
+ return switch (self) {
+ .title, .list_ordored, .list_unordored => true,
+ else => false,
+ };
+ }
};
kind: Kind,
diff --git a/src/link.zig b/src/link.zig
index 9bfbde5..ef92b1c 100644
--- a/src/link.zig
+++ b/src/link.zig
@@ -19,20 +19,18 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element {
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| {
- switch (next.kind) {
- .weak_delimiter, .strong_delimiter => return Error.InvalidLink,
- .link => {
- l.consume();
- if (!eql(u8, next.content, "](")) return Error.InvalidLink;
- break;
- },
- else => {
- const in = try content.parse(alloc, l);
- try el.content.append(alloc, in);
- },
- }
- }
+ while (l.peek()) |next| switch (next.kind) {
+ .weak_delimiter, .strong_delimiter => return Error.InvalidLink,
+ .link => {
+ l.consume();
+ if (!eql(u8, next.content, "](")) return Error.InvalidLink;
+ break;
+ },
+ else => {
+ const in = try content.parse(alloc, l);
+ try el.content.append(alloc, in);
+ },
+ };
const href = l.next() orelse return Error.InvalidLink;
if (href.kind != .literal) return Error.InvalidLink;
const finisher = l.next() orelse return Error.InvalidLink;
diff --git a/src/list.zig b/src/list.zig
index b8c7458..0facef9 100644
--- a/src/list.zig
+++ b/src/list.zig
@@ -25,23 +25,19 @@ pub fn parseUnordored(alloc: Allocator, l: *Lexer) Error!Element {
}
fn parse(alloc: Allocator, content: *std.ArrayList(Element), l: *Lexer, comptime kind: Token.Kind) !void {
- while (l.peek()) |next| {
- switch (next.kind) {
- kind => {
- l.consume();
- continue;
- },
- .weak_delimiter => {
- l.consume();
- if (l.peek()) |it| if (it.kind != kind) return;
- continue;
- },
- .strong_delimiter => return,
- else => {
- try content.append(alloc, try paragraph.parseLine(alloc, l));
- },
- }
- }
+ while (l.peek()) |next| switch (next.kind) {
+ kind => {
+ l.consume();
+ continue;
+ },
+ .weak_delimiter => {
+ l.consume();
+ if (l.peek()) |it| if (it.kind != kind) return;
+ continue;
+ },
+ .strong_delimiter => return,
+ else => try content.append(alloc, try paragraph.parseLine(alloc, l)),
+ };
}
test "parse ordored list" {
diff --git a/src/paragraph.zig b/src/paragraph.zig
index c2e0175..b50b2ec 100644
--- a/src/paragraph.zig
+++ b/src/paragraph.zig
@@ -16,39 +16,35 @@ 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);
errdefer el.deinit(alloc);
- while (l.peek()) |next| {
- switch (next.kind) {
- .strong_delimiter => return el.element(),
- .weak_delimiter => {
- l.consume();
- const future = l.peek() orelse 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)),
- }
- }
+ while (l.peek()) |next| switch (next.kind) {
+ .strong_delimiter => return el.element(),
+ .weak_delimiter => {
+ l.consume();
+ const future = l.peek() orelse 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)),
+ };
return el.element();
}
pub fn parseLine(alloc: Allocator, l: *Lexer) Error!Element {
var line = try Element.Empty.init(alloc);
errdefer line.deinit(alloc);
- while (l.peek()) |next| {
- switch (next.kind) {
- .weak_delimiter, .strong_delimiter => return line.element(),
- .link => {
- var el = try link.parse(alloc, l);
- errdefer el.deinit(alloc);
- try line.content.append(alloc, el);
- },
- else => {
- var el = try content.parse(alloc, l);
- errdefer el.deinit(alloc);
- try line.content.append(alloc, el);
- },
- }
- }
+ while (l.peek()) |next| switch (next.kind) {
+ .weak_delimiter, .strong_delimiter => return line.element(),
+ .link => {
+ var el = try link.parse(alloc, l);
+ errdefer el.deinit(alloc);
+ try line.content.append(alloc, el);
+ },
+ else => {
+ var el = try content.parse(alloc, l);
+ errdefer el.deinit(alloc);
+ try line.content.append(alloc, el);
+ },
+ };
return line.element();
}
diff --git a/src/parser.zig b/src/parser.zig
index a5a49cd..c35d47f 100644
--- a/src/parser.zig
+++ b/src/parser.zig
@@ -7,10 +7,17 @@ const paragraph = @import("paragraph.zig");
const title = @import("title.zig");
const link = @import("link.zig");
const list = @import("list.zig");
+const code = @import("code.zig");
-pub const Error = error{
- FeatureNotSupported,
-} || Lexer.Error || paragraph.Error || title.Error || link.Error || list.Error || link.ImageError || Allocator.Error;
+pub const Error = error{FeatureNotSupported} ||
+ Lexer.Error ||
+ paragraph.Error ||
+ title.Error ||
+ link.Error ||
+ list.Error ||
+ link.ImageError ||
+ code.Error ||
+ Allocator.Error;
pub const Document = struct {
arena: std.heap.ArenaAllocator,
@@ -56,6 +63,7 @@ fn gen(parent: Allocator, l: *Lexer) Error!Document {
.list_ordored => try list.parseOrdored(alloc, l),
.list_unordored => try list.parseUnordored(alloc, l),
.image => try link.parseImage(alloc, l),
+ .code_block => try code.parse(alloc, l),
.weak_delimiter, .strong_delimiter => {
l.consume();
continue :base;
diff --git a/src/root.zig b/src/root.zig
index 5062832..9da89af 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -15,6 +15,7 @@ inline fn getErrorCode(err: Error) u8 {
Error.IllegalPlacement => 6,
Error.InvalidLink => 7,
Error.InvalidImage => 8,
+ Error.InvalidCodeBlock => 9,
};
}
@@ -29,6 +30,7 @@ export fn typdown_getErrorString(code: u8) [*:0]const u8 {
6 => "illegal placement",
7 => "invalid link",
8 => "invalid image",
+ 9 => "invalid code block",
else => unreachable,
};
}