diff --git a/crates/zx_document_core/src/markdown.rs b/crates/zx_document_core/src/markdown.rs index 4af81a3..8c676af 100644 --- a/crates/zx_document_core/src/markdown.rs +++ b/crates/zx_document_core/src/markdown.rs @@ -146,6 +146,18 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) { id: block_id(), }), // Recurse into containers: Document, Item, TableCell, TableRow, etc. + NodeValue::HtmlBlock(block) => { + let html = block.literal.clone(); + if html.trim().is_empty() { + None + } else { + Some(DocumentBlock::CodeBlock { + id: block_id(), + language: Some("html".into()), + code: html, + }) + } + } NodeValue::Document | NodeValue::Item(_) | NodeValue::TableCell diff --git a/crates/zx_document_ffi/src/lib.rs b/crates/zx_document_ffi/src/lib.rs index 1666404..7a94d4e 100644 --- a/crates/zx_document_ffi/src/lib.rs +++ b/crates/zx_document_ffi/src/lib.rs @@ -4,8 +4,6 @@ uniffi::setup_scaffolding!(); -use std::sync::Arc; - pub use zx_document_core::material_type::{MaterialType, PreviewMode}; pub use zx_document_core::image_meta::ImageMeta; pub use zx_document_core::text::TextStats; @@ -16,8 +14,8 @@ pub use zx_document_core::events::ReadingEvent; use zx_document_core::blocks as core_blocks; -// FFI-compatible DocumentBlock (tuple variants for UDL) -#[derive(Debug)] +// FFI-compatible DocumentBlock (tuple variants, UniFFI proc-macro) +#[derive(Debug, uniffi::Enum)] pub enum DocumentBlock { Heading(String, u8, String), Paragraph(String, String), @@ -60,7 +58,7 @@ impl From for DocumentBlock { } } -#[derive(Debug)] +#[derive(Debug, uniffi::Error)] pub enum DocumentError { FileNotFound, UnsupportedFormat, @@ -95,58 +93,151 @@ impl From for DocumentError { } } +#[uniffi::export] fn detect_material_type(file_path: String) -> Result { zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into) } -fn read_image_meta(file_path: String) -> Result, DocumentError> { - let meta = zx_document_core::image_meta::read_image_meta(&file_path)?; - Ok(Arc::new(meta)) +#[uniffi::export] +fn read_image_meta(file_path: String) -> Result { + zx_document_core::image_meta::read_image_meta(&file_path).map_err(Into::into) } -fn read_text_stats(file_path: String) -> Result, DocumentError> { +#[uniffi::export] +fn read_text_stats(file_path: String) -> Result { let content = std::fs::read_to_string(&file_path).map_err(|_| DocumentError::FileNotFound)?; - Ok(Arc::new(zx_document_core::text::text_stats(&content))) + Ok(zx_document_core::text::text_stats(&content)) } +#[uniffi::export] fn parse_text(content: String) -> Result, DocumentError> { let blocks = zx_document_core::text::parse_text_content(&content); - Ok(blocks.into_iter().map(Into::into).collect()) + let result: Vec = blocks.into_iter().map(Into::into).collect(); + Ok(result) } +#[uniffi::export] fn parse_markdown(content: String) -> Result, 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()) + let result: Vec = blocks.into_iter().map(Into::into).collect(); + Ok(result) } +/// Out-pointer free: avoids struct-passing ABI issues +#[no_mangle] +pub extern "C" fn ffi_zx_document_ffi_rustbuffer_free_separate( + capacity: u64, + len: u64, + data: *mut u8, +) { + if data.is_null() { return; } + unsafe { + let _v = Vec::from_raw_parts(data, len as usize, capacity as usize); + // _v drops here, freeing the memory + } +} + +/// Workaround: receive raw bytes via separate len/data args, return via out-pointers +/// Avoids all struct-passing ABI issues on ARM64 iOS. +#[no_mangle] +pub extern "C" fn ffi_zx_document_ffi_rustbuffer_from_bytes_separate( + len: i32, + data: *const u8, + out_capacity: *mut u64, + out_len: *mut u64, + out_data: *mut *mut u8, +) { + let mut call_status = uniffi::RustCallStatus::default(); + let buf = unsafe { + uniffi::ffi::uniffi_rustbuffer_from_bytes( + uniffi::ForeignBytes::from_raw_parts(data, len), + &mut call_status, + ) + }; + unsafe { + *out_capacity = buf.capacity() as u64; + *out_len = buf.len() as u64; + *out_data = buf.data_pointer() as *mut u8; + } + // Transfer ownership to caller — don't drop the buffer + std::mem::forget(buf); +} + +/// Full parse_markdown via raw bytes, result via out-pointers — avoids all struct-passing ABI issues +#[no_mangle] +pub extern "C" fn ffi_zx_document_ffi_parse_markdown_separate( + len: i32, + data: *const u8, + out_capacity: *mut u64, + out_len: *mut u64, + out_data: *mut *mut u8, + out_error_code: *mut i8, +) { + let slice = unsafe { std::slice::from_raw_parts(data, len as usize) }; + let content = match std::str::from_utf8(slice) { + Ok(s) => s.to_string(), + Err(_) => { + unsafe { *out_error_code = -1; } + return; + } + }; + + let result = crate::parse_markdown(content); + + // Serialize result using UniFFI + use uniffi::LowerReturn; + let lowered = , DocumentError> as LowerReturn>::lower_return(result); + match lowered { + Ok(buf) => { + unsafe { + *out_capacity = buf.capacity() as u64; + *out_len = buf.len() as u64; + *out_data = buf.data_pointer() as *mut u8; + *out_error_code = 0; + } + std::mem::forget(buf); + } + Err(e) => { + unsafe { *out_error_code = -1; } + } + } +} + +#[uniffi::export] fn search_markdown_blocks(blocks: Vec, query: String) -> Vec { let core_blocks: Vec = blocks.into_iter().map(core_block_from_ffi).collect(); zx_document_core::search::search_blocks(&core_blocks, &query) } +#[uniffi::export] fn search_text_content(content: String, query: String) -> Vec { zx_document_core::search::search_text(&content, &query) } +#[uniffi::export] fn create_note_anchor(material_id: String, position: Option) -> NoteAnchor { zx_document_core::anchors::NoteAnchor::from_position(&material_id, position.as_ref()) } +#[uniffi::export] fn push_reading_event(event: ReadingEvent) { zx_document_core::events::push_reading_event(event) } +#[uniffi::export] fn update_reading_position(material_id: String, position: ReadingPosition) { zx_document_core::events::update_reading_position(&material_id, position) } +#[uniffi::export] fn export_pending_events() -> Vec { zx_document_core::events::export_pending_events() } +#[uniffi::export] fn clear_exported_events(count: u32) { zx_document_core::events::clear_exported_events(count as usize) }