From 308c8893d9706318e8e756069f40c1435a70df91 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 4 May 2026 16:31:51 +0200 Subject: feat(lib): return multiple errors --- examples/main.c | 17 +++++++---- include/typdown.h | 18 ++++++++++-- src/parser.zig | 85 ++++++++++++++++++++++++++++++++++++++++-------------- src/root.zig | 86 ++++++++++++++++++++++++++++++++++++------------------- 4 files changed, 148 insertions(+), 58 deletions(-) diff --git a/examples/main.c b/examples/main.c index 18393c6..d28af79 100644 --- a/examples/main.c +++ b/examples/main.c @@ -4,13 +4,20 @@ #include void foo(char *v) { - uint8_t code; - void *doc = typdown_parse(v, &code); - if (code != 0) { - printf("cannot parse '%s', error: %s (%d)\n", v, typdown_getErrorString(code), code); + struct typdown_Document doc = typdown_parse(v); + if (doc.errors != NULL) { + for (int i = 0; i < doc.errors_len; i++) { + struct typdown_Error error = doc.errors[i]; + printf("cannot parse '%s', error: %s (%d)\n", v, typdown_getErrorString(error.code), error.code); + printf("%d to %d ", error.location.beg, error.location.end); + for (int j = error.location.beg; j < error.location.end; j++) + printf("%c", v[j]); + printf("\n"); + } typdown_free(doc); return; } + uint8_t code; char *res = typdown_renderHTML(doc, &code); if (code != 0) { printf("cannot render '%s', error: %s (%d)\n", v, typdown_getErrorString(code), code); @@ -31,6 +38,6 @@ int main() { // invalid foo("hello *world"); foo("hello world :::"); - foo("# hello :::"); + foo("# hello :::"); return 0; } diff --git a/include/typdown.h b/include/typdown.h index 8de37d8..abf395d 100644 --- a/include/typdown.h +++ b/include/typdown.h @@ -1,8 +1,20 @@ #pragma once #include +#include + +struct typdown_Error { + uint8_t code; + struct {size_t beg; size_t end;} location; +}; + +struct typdown_Document { + void *root; + struct typdown_Error *errors; + size_t errors_len; +}; char * typdown_getErrorString(uint8_t); -void * typdown_parse(char *, uint8_t *); -void typdown_free(void *); -char * typdown_renderHTML(void *, uint8_t *); +struct typdown_Document typdown_parse(char *); +void typdown_free(struct typdown_Document); +char * typdown_renderHTML(struct typdown_Document, uint8_t *); diff --git a/src/parser.zig b/src/parser.zig index 3dba456..e3eb5d5 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -25,33 +25,60 @@ pub const Error = error{FeatureNotSupported} || math.Error || Allocator.Error; -pub const Document = Element.Root; +/// Document represents a parsed typdown document. +pub const Document = struct { + /// Root of the document: used to render the document is other languages. + root: *Element.Root, + /// Errors got while parsing the document. + errors: ?[]DocError = null, -pub fn parseReader(parent: Allocator, r: *std.io.Reader) !*Document { + pub fn deinit(self: @This(), alloc: Allocator) void { + self.root.deinit(); + if (self.errors) |errors| alloc.free(errors); + } +}; + +/// DocError contains information about the error. +pub const DocError = struct { + /// Error returned. + err: Error, + /// Location of the error in the source. + location: struct { beg: usize, end: usize }, + + /// Extract the invalid content from the source. + pub fn extract(self: @This(), source: []const u8) []const u8 { + return source[self.location.beg..self.location.end]; + } +}; + +pub fn parseReader(parent: Allocator, r: *std.io.Reader) !Document { var l = try Lexer.initReader(parent, r); defer parent.free(l.iter.bytes); return gen(parent, &l); } -pub fn parse(parent: Allocator, content: []const u8) Error!*Document { +pub fn parse(parent: Allocator, content: []const u8) !Document { var l = try Lexer.init(content); return gen(parent, &l); } -fn gen(parent: Allocator, l: *Lexer) Error!*Document { - var root = try Document.init(parent); +fn gen(parent: Allocator, l: *Lexer) Allocator.Error!Document { + var root = try Element.Root.init(parent); errdefer root.deinit(); const alloc = root.allocator(); + var doc_errors = std.ArrayList(DocError).empty; + errdefer doc_errors.deinit(parent); base: while (l.peek()) |it| { - root.append(switch (it.kind) { + const beg = l.iter.i; + const v = switch (it.kind) { // 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), - .code_block => try code.parse(alloc, l), - .quote => try quote.parse(alloc, l), - .math_block => try math.parse(alloc, l), + .title => title.parse(alloc, l), + .list_ordored => list.parseOrdored(alloc, l), + .list_unordored => list.parseUnordored(alloc, l), + .image => link.parseImage(alloc, l), + .code_block => code.parse(alloc, l), + .quote => quote.parse(alloc, l), + .math_block => math.parse(alloc, l), .weak_delimiter, .strong_delimiter => { l.consume(); continue :base; @@ -59,18 +86,32 @@ fn gen(parent: Allocator, l: *Lexer) Error!*Document { else => // block paragraph if (it.kind.isInParagraph()) - try paragraph.parse(alloc, l) + paragraph.parse(alloc, l) else - return Error.FeatureNotSupported, - }); + Error.FeatureNotSupported, + } catch |err| { + if (err == Error.OutOfMemory) return Error.OutOfMemory; + var end = l.iter.i; + if (beg == end) end += 1; + try doc_errors.append(parent, .{ .err = err, .location = .{ .beg = beg, .end = end } }); + _ = l.next(); + // consume until next delimiter + while (l.next()) |next| if (next.kind.isDelimiter()) continue :base; + break :base; + }; + root.append(v); } - return root; + return .{ .root = root, .errors = if (doc_errors.items.len == 0) null else try doc_errors.toOwnedSlice(parent) }; } fn doTest(alloc: Allocator, t: []const u8, v: []const u8) !void { const g = try parse(alloc, t); - defer g.deinit(); - const res = try g.renderHTML(alloc); + defer g.deinit(alloc); + if (g.errors) |errors| { + for (errors) |it| std.debug.print("{}: {s}\n", .{ it.err, it.extract(t) }); + try std.testing.expect(false); + } + const res = try g.root.renderHTML(alloc); defer alloc.free(res); std.testing.expect(std.mem.eql(u8, res, v)) catch |err| { std.debug.print("{s}\n", .{res}); @@ -82,6 +123,7 @@ test "parse multilines" { const alloc = std.testing.allocator; try doTest(alloc, "hello world", "

hello world

"); + try doTest(alloc, "# foo", "

foo

"); try doTest(alloc, \\hello \\world @@ -104,7 +146,8 @@ test "multiple render doc" { const alloc = arena.allocator(); const g = try parse(alloc, "hello *world*!"); - const a = try g.renderHTML(alloc); - const b = try g.renderHTML(alloc); + defer g.deinit(alloc); + const a = try g.root.renderHTML(alloc); + const b = try g.root.renderHTML(alloc); try std.testing.expect(std.mem.eql(u8, a, b)); } diff --git a/src/root.zig b/src/root.zig index 3a7c134..30386e9 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,12 +2,14 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const parser = @import("parser.zig"); +const Element = @import("eval/Element.zig"); pub const Document = parser.Document; pub const Error = parser.Error; /// Parse the content. /// /// Use typdown_parse if you are not in Zig. pub const parse = parser.parse; +pub const parseReader = parser.parseReader; inline fn getErrorCode(err: Error) u8 { return switch (err) { @@ -43,6 +45,40 @@ export fn typdown_getErrorString(code: u8) [*:0]const u8 { }; } +/// typdown_Document is a Document for the C ABI. +const typdown_Document = extern struct { + root: ?*anyopaque = null, + errors: ?[*]typdown_Error = null, + errors_len: usize = 0, + + const typdown_Error = extern struct { + code: u8, + location: extern struct { beg: usize, end: usize }, + }; +}; + +fn from(alloc: Allocator, doc: Document) typdown_Document { + var res = typdown_Document{ .root = doc.root }; + if (doc.errors) |errors| { + defer alloc.free(errors); + res.errors = (alloc.alloc(typdown_Document.typdown_Error, errors.len) catch |err| @panic(@errorName(err))).ptr; + for (errors) |err| { + res.errors.?[0] = .{ + .code = getErrorCode(err.err), + .location = .{ .beg = err.location.beg, .end = err.location.end }, + }; + } + res.errors_len = errors.len; + } + return res; +} + +fn fromError(alloc: Allocator, err: Error) typdown_Document { + var v = alloc.alloc(typdown_Document.typdown_Error, 1) catch |e| @panic(@errorName(e)); + v[0] = .{ .code = getErrorCode(err), .location = .{ .beg = 0, .end = 0 } }; + return .{ .errors = v.ptr, .errors_len = 1 }; +} + var default_alloc: std.mem.Allocator = if (builtin.target.isWasiLibC()) std.heap.wasm_allocator @@ -52,31 +88,29 @@ var default_alloc: std.mem.Allocator = std.heap.c_allocator; /// Parse the content. -/// Code is a pointer to an u8 populated with an error code > 0. -/// -/// Returns a non-null void pointer containing the document and set the code to 0 if everything is fine. -/// Else, it returns null and set an error code above 0. -/// Use typdown_getErrorString to retrieve the string linked with the error code. -/// Use parse if you are in Zig. /// /// You must free the document with typdown_free. -export fn typdown_parse(content: [*:0]const u8, code: *u8) ?*anyopaque { - return parse(default_alloc, std.mem.span(content)) catch |err| { - code.* = getErrorCode(err); - return null; - }; +export fn typdown_parse(content: [*:0]const u8) typdown_Document { + const alloc = default_alloc; + return from(alloc, parse(alloc, std.mem.span(content)) catch |err| { + return fromError(alloc, err); + }); } /// Free the document. -export fn typdown_free(document: *anyopaque) void { - const doc: *Document = @ptrCast(@alignCast(document)); - doc.deinit(); +export fn typdown_free(self: typdown_Document) void { + const alloc = default_alloc; + if (self.root) |r| { + const root: *Element.Root = @ptrCast(@alignCast(r)); + root.deinit(); + } + if (self.errors) |errors| alloc.free(errors[0..self.errors_len]); } /// Render an HTML from the document. -export fn typdown_renderHTML(document: *anyopaque, code: *u8) ?[*:0]const u8 { - const doc: *Document = @ptrCast(@alignCast(document)); - const res = doc.renderHTML(default_alloc) catch |err| { +export fn typdown_renderHTML(document: typdown_Document, code: *u8) ?[*:0]const u8 { + const root: *Element.Root = @ptrCast(@alignCast(document.root)); + const res = root.renderHTML(default_alloc) catch |err| { code.* = getErrorCode(err); return null; }; @@ -88,10 +122,6 @@ export fn typdown_renderHTML(document: *anyopaque, code: *u8) ?[*:0]const u8 { }; } -pub fn parseReader(alloc: std.mem.Allocator, r: *std.io.Reader) (Error || std.io.Reader.Error)!*Document { - return parser.parseReader(alloc, r); -} - test { std.testing.refAllDeclsRecursive(@This()); } @@ -99,15 +129,13 @@ test { fn doTest(content: [*:0]const u8, exp: []const u8, comptime exp_code: u8) !void { const expect = std.testing.expect; - var code: u8 = undefined; - const doc = typdown_parse(content, &code) orelse { - expect(code == exp_code) catch |err| { - std.debug.print("{}\n", .{code}); - return err; - }; - return; - }; + const doc = typdown_parse(content); defer typdown_free(doc); + if (doc.errors) |errors| { + for (0..doc.errors_len) |i| if (errors[i].code == exp_code) return; + return try expect(false); + } + var code: u8 = undefined; const raw = typdown_renderHTML(doc, &code) orelse { expect(code == exp_code) catch |err| { std.debug.print("{}\n", .{code}); -- cgit v1.2.3