Skip to content

Commit 3c64d27

Browse files
author
Jonathan D.A. Jewell
committed
feat: absorb docmatrix FFI extras from zig-ffi monorepo
1 parent 497c702 commit 3c64d27

2 files changed

Lines changed: 361 additions & 0 deletions

File tree

examples/example.zig

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//! Example usage of zig-formatrix-ffi
3+
4+
const std = @import("std");
5+
const formatrix = @import("formatrix");
6+
7+
pub fn main() !void {
8+
const allocator = std.heap.page_allocator;
9+
10+
// Print library version
11+
std.debug.print("Formatrix version: {s}\n", .{formatrix.version()});
12+
13+
// Parse some markdown
14+
const markdown_content: [:0]const u8 = "# Hello World\n\nThis is a paragraph.";
15+
16+
var doc = try formatrix.Document.parse(markdown_content, .markdown);
17+
defer doc.deinit();
18+
19+
std.debug.print("Parsed document with {d} blocks\n", .{doc.blockCount()});
20+
std.debug.print("Source format: {s}\n", .{doc.sourceFormat().label()});
21+
22+
// Get title if present
23+
if (try doc.getTitle(allocator)) |title| {
24+
defer allocator.free(title);
25+
std.debug.print("Document title: {s}\n", .{title});
26+
}
27+
28+
// Render to org-mode
29+
const org_output = try doc.render(.org_mode, allocator);
30+
defer allocator.free(org_output);
31+
32+
std.debug.print("\nRendered to Org-mode:\n{s}\n", .{org_output});
33+
34+
// Direct conversion helper
35+
const rst_output = try formatrix.convert(
36+
markdown_content,
37+
.markdown,
38+
.restructured_text,
39+
allocator,
40+
);
41+
defer allocator.free(rst_output);
42+
43+
std.debug.print("\nDirect conversion to RST:\n{s}\n", .{rst_output});
44+
45+
// Format detection
46+
const detected = formatrix.detectFormat("#+TITLE: Test\n* Heading");
47+
std.debug.print("\nDetected format for org content: {s}\n", .{detected.label()});
48+
}

src/formatrix.zig

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//! Zig bindings for formatrix-core
3+
//!
4+
//! Provides a type-safe Zig interface to the Formatrix document library.
5+
//! Links against libformatrix_core.so/dylib/dll
6+
7+
const std = @import("std");
8+
9+
/// Document format types
10+
pub const Format = enum(c_int) {
11+
plain_text = 0,
12+
markdown = 1,
13+
asciidoc = 2,
14+
djot = 3,
15+
org_mode = 4,
16+
restructured_text = 5,
17+
typst = 6,
18+
19+
/// Get file extension for this format
20+
pub fn extension(self: Format) [:0]const u8 {
21+
return switch (self) {
22+
.plain_text => "txt",
23+
.markdown => "md",
24+
.asciidoc => "adoc",
25+
.djot => "dj",
26+
.org_mode => "org",
27+
.restructured_text => "rst",
28+
.typst => "typ",
29+
};
30+
}
31+
32+
/// Get display label for this format
33+
pub fn label(self: Format) [:0]const u8 {
34+
return switch (self) {
35+
.plain_text => "TXT",
36+
.markdown => "MD",
37+
.asciidoc => "ADOC",
38+
.djot => "DJOT",
39+
.org_mode => "ORG",
40+
.restructured_text => "RST",
41+
.typst => "TYP",
42+
};
43+
}
44+
};
45+
46+
/// Result codes from FFI operations
47+
pub const Result = enum(c_int) {
48+
success = 0,
49+
invalid_input = 1,
50+
parse_error = 2,
51+
render_error = 3,
52+
unsupported_format = 4,
53+
null_pointer = 5,
54+
utf8_error = 6,
55+
56+
pub fn isSuccess(self: Result) bool {
57+
return self == .success;
58+
}
59+
60+
pub fn toError(self: Result) ?Error {
61+
return switch (self) {
62+
.success => null,
63+
.invalid_input => Error.InvalidInput,
64+
.parse_error => Error.ParseError,
65+
.render_error => Error.RenderError,
66+
.unsupported_format => Error.UnsupportedFormat,
67+
.null_pointer => Error.NullPointer,
68+
.utf8_error => Error.Utf8Error,
69+
};
70+
}
71+
};
72+
73+
/// Errors that can occur during formatrix operations
74+
pub const Error = error{
75+
InvalidInput,
76+
ParseError,
77+
RenderError,
78+
UnsupportedFormat,
79+
NullPointer,
80+
Utf8Error,
81+
};
82+
83+
/// Opaque document handle
84+
pub const DocumentHandle = opaque {};
85+
86+
// External C functions from libformatrix_core
87+
extern "c" fn formatrix_parse(
88+
content: [*:0]const u8,
89+
format: Format,
90+
out_handle: *?*DocumentHandle,
91+
) Result;
92+
93+
extern "c" fn formatrix_render(
94+
handle: *const DocumentHandle,
95+
format: Format,
96+
out_content: *?[*:0]u8,
97+
out_length: *usize,
98+
) Result;
99+
100+
extern "c" fn formatrix_open_file(
101+
path: [*:0]const u8,
102+
out_handle: *?*DocumentHandle,
103+
out_format: *Format,
104+
) Result;
105+
106+
extern "c" fn formatrix_save_file(
107+
handle: *const DocumentHandle,
108+
path: [*:0]const u8,
109+
) Result;
110+
111+
extern "c" fn formatrix_save_file_as(
112+
handle: *const DocumentHandle,
113+
path: [*:0]const u8,
114+
format: Format,
115+
) Result;
116+
117+
extern "c" fn formatrix_get_title(
118+
handle: *const DocumentHandle,
119+
out_title: *?[*:0]u8,
120+
out_length: *usize,
121+
) Result;
122+
123+
extern "c" fn formatrix_block_count(handle: *const DocumentHandle) usize;
124+
125+
extern "c" fn formatrix_get_format(handle: *const DocumentHandle) Format;
126+
127+
extern "c" fn formatrix_detect_format(content: [*:0]const u8) Format;
128+
129+
extern "c" fn formatrix_detect_file_format(path: [*:0]const u8) Format;
130+
131+
extern "c" fn formatrix_convert(
132+
content: [*:0]const u8,
133+
from_format: Format,
134+
to_format: Format,
135+
out_content: *?[*:0]u8,
136+
out_length: *usize,
137+
) Result;
138+
139+
extern "c" fn formatrix_free_document(handle: ?*DocumentHandle) void;
140+
141+
extern "c" fn formatrix_free_string(s: ?[*:0]u8) void;
142+
143+
extern "c" fn formatrix_version() [*:0]const u8;
144+
145+
/// A parsed document with automatic resource management
146+
pub const Document = struct {
147+
handle: *DocumentHandle,
148+
149+
const Self = @This();
150+
151+
/// Parse content in the specified format
152+
pub fn parse(content: [:0]const u8, format: Format) Error!Self {
153+
var handle: ?*DocumentHandle = null;
154+
const result = formatrix_parse(content.ptr, format, &handle);
155+
156+
if (result.toError()) |err| {
157+
return err;
158+
}
159+
160+
return Self{ .handle = handle.? };
161+
}
162+
163+
/// Open a file and parse it
164+
pub fn openFile(path: [:0]const u8) Error!struct { doc: Self, format: Format } {
165+
var handle: ?*DocumentHandle = null;
166+
var format: Format = .plain_text;
167+
const result = formatrix_open_file(path.ptr, &handle, &format);
168+
169+
if (result.toError()) |err| {
170+
return err;
171+
}
172+
173+
return .{
174+
.doc = Self{ .handle = handle.? },
175+
.format = format,
176+
};
177+
}
178+
179+
/// Free the document resources
180+
pub fn deinit(self: *Self) void {
181+
formatrix_free_document(self.handle);
182+
self.handle = undefined;
183+
}
184+
185+
/// Render the document to the specified format
186+
pub fn render(self: Self, format: Format, allocator: std.mem.Allocator) Error![]u8 {
187+
var content: ?[*:0]u8 = null;
188+
var length: usize = 0;
189+
190+
const result = formatrix_render(self.handle, format, &content, &length);
191+
192+
if (result.toError()) |err| {
193+
return err;
194+
}
195+
196+
defer formatrix_free_string(content);
197+
198+
// Copy to Zig-managed memory
199+
const owned = try allocator.alloc(u8, length);
200+
@memcpy(owned, content.?[0..length]);
201+
return owned;
202+
}
203+
204+
/// Save the document to a file (format detected from extension)
205+
pub fn saveFile(self: Self, path: [:0]const u8) Error!void {
206+
const result = formatrix_save_file(self.handle, path.ptr);
207+
if (result.toError()) |err| {
208+
return err;
209+
}
210+
}
211+
212+
/// Save the document to a file in a specific format
213+
pub fn saveFileAs(self: Self, path: [:0]const u8, format: Format) Error!void {
214+
const result = formatrix_save_file_as(self.handle, path.ptr, format);
215+
if (result.toError()) |err| {
216+
return err;
217+
}
218+
}
219+
220+
/// Get the document title (if any)
221+
pub fn getTitle(self: Self, allocator: std.mem.Allocator) Error!?[]u8 {
222+
var title: ?[*:0]u8 = null;
223+
var length: usize = 0;
224+
225+
const result = formatrix_get_title(self.handle, &title, &length);
226+
227+
if (result.toError()) |err| {
228+
return err;
229+
}
230+
231+
if (length == 0) {
232+
return null;
233+
}
234+
235+
defer formatrix_free_string(title);
236+
237+
const owned = try allocator.alloc(u8, length);
238+
@memcpy(owned, title.?[0..length]);
239+
return owned;
240+
}
241+
242+
/// Get the number of blocks in the document
243+
pub fn blockCount(self: Self) usize {
244+
return formatrix_block_count(self.handle);
245+
}
246+
247+
/// Get the source format of the document
248+
pub fn sourceFormat(self: Self) Format {
249+
return formatrix_get_format(self.handle);
250+
}
251+
};
252+
253+
/// Convert content between formats
254+
pub fn convert(
255+
content: [:0]const u8,
256+
from_format: Format,
257+
to_format: Format,
258+
allocator: std.mem.Allocator,
259+
) Error![]u8 {
260+
var out_content: ?[*:0]u8 = null;
261+
var out_length: usize = 0;
262+
263+
const result = formatrix_convert(
264+
content.ptr,
265+
from_format,
266+
to_format,
267+
&out_content,
268+
&out_length,
269+
);
270+
271+
if (result.toError()) |err| {
272+
return err;
273+
}
274+
275+
defer formatrix_free_string(out_content);
276+
277+
const owned = try allocator.alloc(u8, out_length);
278+
@memcpy(owned, out_content.?[0..out_length]);
279+
return owned;
280+
}
281+
282+
/// Detect format from content using heuristics
283+
pub fn detectFormat(content: [:0]const u8) Format {
284+
return formatrix_detect_format(content.ptr);
285+
}
286+
287+
/// Detect format from file path (by extension)
288+
pub fn detectFileFormat(path: [:0]const u8) Format {
289+
return formatrix_detect_file_format(path.ptr);
290+
}
291+
292+
/// Get the library version
293+
pub fn version() [:0]const u8 {
294+
return std.mem.span(formatrix_version());
295+
}
296+
297+
// Tests
298+
test "format extension" {
299+
try std.testing.expectEqualStrings("md", Format.markdown.extension());
300+
try std.testing.expectEqualStrings("org", Format.org_mode.extension());
301+
}
302+
303+
test "format label" {
304+
try std.testing.expectEqualStrings("MD", Format.markdown.label());
305+
try std.testing.expectEqualStrings("ORG", Format.org_mode.label());
306+
}
307+
308+
test "result conversion" {
309+
try std.testing.expect(Result.success.isSuccess());
310+
try std.testing.expect(!Result.parse_error.isSuccess());
311+
try std.testing.expect(Result.success.toError() == null);
312+
try std.testing.expect(Result.parse_error.toError() == Error.ParseError);
313+
}

0 commit comments

Comments
 (0)