feat: expose parse_markdown via FFI - returns [DocumentBlock] with all 8 block types

This commit is contained in:
wangdl 2026-06-02 19:53:42 +08:00
parent cfbee9ea53
commit f8c0864b61
7 changed files with 339 additions and 3 deletions

View File

@ -416,6 +416,22 @@ fileprivate final class UniffiHandleMap<T>: @unchecked Sendable {
// Public interface members begin here.
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterUInt8: FfiConverterPrimitive {
typealias FfiType = UInt8
typealias SwiftType = UInt8
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt8 {
return try lift(readInt(&buf))
}
public static func write(_ value: UInt8, into buf: inout [UInt8]) {
writeInt(&buf, lower(value))
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@ -480,6 +496,30 @@ fileprivate struct FfiConverterFloat: FfiConverterPrimitive {
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterBool : FfiConverter {
typealias FfiType = Int8
typealias SwiftType = Bool
public static func lift(_ value: Int8) throws -> Bool {
return value != 0
}
public static func lower(_ value: Bool) -> Int8 {
return value ? 1 : 0
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool {
return try lift(readInt(&buf))
}
public static func write(_ value: Bool, into buf: inout [UInt8]) {
writeInt(&buf, lower(value))
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@ -781,6 +821,151 @@ public func FfiConverterTypeTextStats_lower(_ value: TextStats) -> RustBuffer {
return FfiConverterTypeTextStats.lower(value)
}
// Note that we don't yet support `indirect` for enums.
// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
public enum DocumentBlock: Equatable, Hashable {
case heading(id: String, level: UInt8, text: String
)
case paragraph(id: String, text: String
)
case list(id: String, ordered: Bool, items: [String]
)
case codeBlock(id: String, language: String?, code: String
)
case quote(id: String, text: String
)
case table(id: String, headers: [String], rows: [[String]]
)
case imageBlock(id: String, src: String, alt: String?
)
case horizontalRule(id: String
)
}
#if compiler(>=6)
extension DocumentBlock: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeDocumentBlock: FfiConverterRustBuffer {
typealias SwiftType = DocumentBlock
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocumentBlock {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .heading(id: try FfiConverterString.read(from: &buf), level: try FfiConverterUInt8.read(from: &buf), text: try FfiConverterString.read(from: &buf)
)
case 2: return .paragraph(id: try FfiConverterString.read(from: &buf), text: try FfiConverterString.read(from: &buf)
)
case 3: return .list(id: try FfiConverterString.read(from: &buf), ordered: try FfiConverterBool.read(from: &buf), items: try FfiConverterSequenceString.read(from: &buf)
)
case 4: return .codeBlock(id: try FfiConverterString.read(from: &buf), language: try FfiConverterOptionString.read(from: &buf), code: try FfiConverterString.read(from: &buf)
)
case 5: return .quote(id: try FfiConverterString.read(from: &buf), text: try FfiConverterString.read(from: &buf)
)
case 6: return .table(id: try FfiConverterString.read(from: &buf), headers: try FfiConverterSequenceString.read(from: &buf), rows: try FfiConverterSequenceSequenceString.read(from: &buf)
)
case 7: return .imageBlock(id: try FfiConverterString.read(from: &buf), src: try FfiConverterString.read(from: &buf), alt: try FfiConverterOptionString.read(from: &buf)
)
case 8: return .horizontalRule(id: try FfiConverterString.read(from: &buf)
)
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: DocumentBlock, into buf: inout [UInt8]) {
switch value {
case let .heading(id,level,text):
writeInt(&buf, Int32(1))
FfiConverterString.write(id, into: &buf)
FfiConverterUInt8.write(level, into: &buf)
FfiConverterString.write(text, into: &buf)
case let .paragraph(id,text):
writeInt(&buf, Int32(2))
FfiConverterString.write(id, into: &buf)
FfiConverterString.write(text, into: &buf)
case let .list(id,ordered,items):
writeInt(&buf, Int32(3))
FfiConverterString.write(id, into: &buf)
FfiConverterBool.write(ordered, into: &buf)
FfiConverterSequenceString.write(items, into: &buf)
case let .codeBlock(id,language,code):
writeInt(&buf, Int32(4))
FfiConverterString.write(id, into: &buf)
FfiConverterOptionString.write(language, into: &buf)
FfiConverterString.write(code, into: &buf)
case let .quote(id,text):
writeInt(&buf, Int32(5))
FfiConverterString.write(id, into: &buf)
FfiConverterString.write(text, into: &buf)
case let .table(id,headers,rows):
writeInt(&buf, Int32(6))
FfiConverterString.write(id, into: &buf)
FfiConverterSequenceString.write(headers, into: &buf)
FfiConverterSequenceSequenceString.write(rows, into: &buf)
case let .imageBlock(id,src,alt):
writeInt(&buf, Int32(7))
FfiConverterString.write(id, into: &buf)
FfiConverterString.write(src, into: &buf)
FfiConverterOptionString.write(alt, into: &buf)
case let .horizontalRule(id):
writeInt(&buf, Int32(8))
FfiConverterString.write(id, into: &buf)
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeDocumentBlock_lift(_ buf: RustBuffer) throws -> DocumentBlock {
return try FfiConverterTypeDocumentBlock.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeDocumentBlock_lower(_ value: DocumentBlock) -> RustBuffer {
return FfiConverterTypeDocumentBlock.lower(value)
}
public enum DocumentError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
@ -1512,6 +1697,81 @@ fileprivate struct FfiConverterOptionTypeReadingPosition: FfiConverterRustBuffer
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer {
typealias SwiftType = [String]
public static func write(_ value: [String], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterString.write(item, into: &buf)
}
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String] {
let len: Int32 = try readInt(&buf)
var seq = [String]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterString.read(from: &buf))
}
return seq
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceTypeDocumentBlock: FfiConverterRustBuffer {
typealias SwiftType = [DocumentBlock]
public static func write(_ value: [DocumentBlock], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterTypeDocumentBlock.write(item, into: &buf)
}
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [DocumentBlock] {
let len: Int32 = try readInt(&buf)
var seq = [DocumentBlock]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterTypeDocumentBlock.read(from: &buf))
}
return seq
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceSequenceString: FfiConverterRustBuffer {
typealias SwiftType = [[String]]
public static func write(_ value: [[String]], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterSequenceString.write(item, into: &buf)
}
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [[String]] {
let len: Int32 = try readInt(&buf)
var seq = [[String]]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterSequenceString.read(from: &buf))
}
return seq
}
}
public func detectMaterialType(filePath: String)throws -> MaterialType {
return try FfiConverterTypeMaterialType_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_detect_material_type(
@ -1519,6 +1779,13 @@ public func detectMaterialType(filePath: String)throws -> MaterialType {
)
})
}
public func parseMarkdown(content: String)throws -> [DocumentBlock] {
return try FfiConverterSequenceTypeDocumentBlock.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_parse_markdown(
FfiConverterString.lower(content),$0
)
})
}
public func readImageMeta(filePath: String)throws -> ImageMeta {
return try FfiConverterTypeImageMeta_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_read_image_meta(
@ -1552,6 +1819,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_zx_document_ffi_checksum_func_detect_material_type() != 55020) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_zx_document_ffi_checksum_func_parse_markdown() != 11780) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_zx_document_ffi_checksum_func_read_image_meta() != 62824) {
return InitializationResult.apiChecksumMismatch
}

View File

@ -2,12 +2,56 @@ uniffi::setup_scaffolding!();
use std::sync::Arc;
// Re-export types so the UDL scaffolding can find them
pub use zx_document_core::material_type::{MaterialType, PreviewMode};
pub use zx_document_core::image_meta::ImageMeta;
pub use zx_document_core::text::TextStats;
// Flat DocumentError for UDL [Error]
use zx_document_core::blocks as core_blocks;
// FFI-compatible DocumentBlock (tuple variants for UDL)
#[derive(Debug)]
pub enum DocumentBlock {
Heading(String, u8, String),
Paragraph(String, String),
List(String, bool, Vec<String>),
CodeBlock(String, Option<String>, String),
Quote(String, String),
Table(String, Vec<String>, Vec<Vec<String>>),
ImageBlock(String, String, Option<String>),
HorizontalRule(String),
}
impl From<core_blocks::DocumentBlock> for DocumentBlock {
fn from(b: core_blocks::DocumentBlock) -> Self {
match b {
core_blocks::DocumentBlock::Heading { id, level, text } => {
DocumentBlock::Heading(id, level, text)
}
core_blocks::DocumentBlock::Paragraph { id, text } => {
DocumentBlock::Paragraph(id, text)
}
core_blocks::DocumentBlock::List { id, ordered, items } => {
DocumentBlock::List(id, ordered, items)
}
core_blocks::DocumentBlock::CodeBlock { id, language, code } => {
DocumentBlock::CodeBlock(id, language, code)
}
core_blocks::DocumentBlock::Quote { id, text } => {
DocumentBlock::Quote(id, text)
}
core_blocks::DocumentBlock::Table { id, headers, rows } => {
DocumentBlock::Table(id, headers, rows)
}
core_blocks::DocumentBlock::Image { id, src, alt } => {
DocumentBlock::ImageBlock(id, src, alt)
}
core_blocks::DocumentBlock::HorizontalRule { id } => {
DocumentBlock::HorizontalRule(id)
}
}
}
}
#[derive(Debug)]
pub enum DocumentError {
FileNotFound,
@ -43,7 +87,6 @@ impl From<zx_document_core::error::DocumentError> for DocumentError {
}
}
// FFI functions matching UDL namespace declarations
fn detect_material_type(file_path: String) -> Result<MaterialType, DocumentError> {
zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into)
}
@ -57,3 +100,11 @@ fn read_text_stats(file_path: String) -> Result<Arc<TextStats>, DocumentError> {
let content = std::fs::read_to_string(&file_path).map_err(|_| DocumentError::FileNotFound)?;
Ok(Arc::new(zx_document_core::text::text_stats(&content)))
}
fn parse_markdown(content: String) -> Result<Vec<DocumentBlock>, DocumentError> {
let blocks = zx_document_core::markdown::parse_markdown(&content).map_err(|e| match e {
zx_document_core::error::DocumentError::ParseError(_) => DocumentError::ParseError,
_ => DocumentError::ParseError,
})?;
Ok(blocks.into_iter().map(Into::into).collect())
}

View File

@ -7,6 +7,9 @@ namespace zx_document {
[Throws=DocumentError]
TextStats read_text_stats([ByRef] string file_path);
[Throws=DocumentError]
sequence<DocumentBlock> parse_markdown([ByRef] string content);
};
[Error]
@ -100,3 +103,15 @@ dictionary TextStats {
u32 word_count;
};
[Enum]
interface DocumentBlock {
Heading(string id, u8 level, string text);
Paragraph(string id, string text);
List(string id, boolean ordered, sequence<string> items);
CodeBlock(string id, string? language, string code);
Quote(string id, string text);
Table(string id, sequence<string> headers, sequence<sequence<string>> rows);
ImageBlock(string id, string src, string? alt);
HorizontalRule(string id);
};