From de948492e8b38a79d5db9c506c1b7b82e86c6b12 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Sun, 26 Apr 2026 23:10:17 +0200 Subject: feat(): support code block --- src/code.zig | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/eval/blocks.zig | 40 ++++++++++++++++++++++++++++++++++- src/lexer/Lexer.zig | 9 +------- src/lexer/Token.zig | 11 ++++++++-- src/link.zig | 26 +++++++++++------------ src/list.zig | 30 ++++++++++++-------------- src/paragraph.zig | 50 ++++++++++++++++++++----------------------- src/parser.zig | 14 +++++++++--- src/root.zig | 2 ++ 9 files changed, 171 insertions(+), 72 deletions(-) create mode 100644 src/code.zig (limited to 'src') 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 + \\``` + , "
hey
"); + try doTest(parse, alloc, + \\```td another + \\hey + \\``` + , "
hey
"); + // 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, }; } -- cgit v1.2.3