Compare commits
51 Commits
991afea3d0
...
3c925d7fd7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c925d7fd7 | ||
|
|
fa4e0a31f7 | ||
|
|
f30809c5b7 | ||
|
|
c1bac717fd | ||
|
|
a7b941bd42 | ||
|
|
8968858211 | ||
|
|
ce30798f51 | ||
|
|
d7605774f6 | ||
|
|
7d82a5676c | ||
|
|
7570717ec8 | ||
|
|
1c93308e8b | ||
|
|
c957bf2882 | ||
|
|
c51e116c7e | ||
|
|
8ab767ee4a | ||
|
|
1a0095cd58 | ||
|
|
ce0315d9ae | ||
|
|
4c9ac7cb9f | ||
|
|
254907194c | ||
|
|
b02efe18c7 | ||
|
|
6250dacd71 | ||
|
|
e64f96d43c | ||
|
|
7d678b4bb3 | ||
|
|
d1c6edd57a | ||
|
|
c67dbe3e3d | ||
|
|
f463c63e03 | ||
|
|
6cdf88df35 | ||
|
|
c2c40ef1b3 | ||
|
|
e08056eccd | ||
|
|
2978fd4de9 | ||
|
|
994017e21f | ||
|
|
b9251174ee | ||
|
|
80774ca927 | ||
|
|
dcf148ebc6 | ||
|
|
1d71e2b02a | ||
|
|
ff7f681cd4 | ||
|
|
4c5a725305 | ||
|
|
48101e7bad | ||
|
|
f4336ba05f | ||
|
|
3acf7d464f | ||
|
|
475532cdd9 | ||
|
|
0ad7734d2d | ||
|
|
89ff71aeb8 | ||
|
|
794b0f91b3 | ||
|
|
2aeb72f1e3 | ||
|
|
08f5f0559b | ||
|
|
942ac06230 | ||
|
|
87814802b1 | ||
|
|
7b827ca01a | ||
|
|
4c8a50daec | ||
|
|
e1e1f27b07 | ||
|
|
dba2276b6e |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
188
.github/workflows/ci.yml
vendored
Normal file
188
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,188 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 1. Lint
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-lint-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 2. Test
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Test (all crates)
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Test doc
|
||||
run: cargo test --doc --workspace
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 3. Build
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build release (all crates)
|
||||
run: cargo build --release --workspace
|
||||
|
||||
- name: Verify FFI lib
|
||||
run: |
|
||||
test -f target/release/libzx_document_ffi.a && echo "FFI lib OK"
|
||||
test -f target/release/libzx_document_core.a && echo "Core lib OK"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-libs
|
||||
path: target/release/libzx_document_*.a
|
||||
retention-days: 7
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 4. Release (tagged versions only)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
target: aarch64-apple-ios
|
||||
rust-target: aarch64-apple-ios
|
||||
- os: macos-15
|
||||
target: aarch64-apple-ios-sim
|
||||
rust-target: aarch64-apple-ios-sim
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.rust-target }}
|
||||
|
||||
- name: Build FFI for ${{ matrix.target }}
|
||||
run: cargo build --release -p zx_document_ffi --target ${{ matrix.rust-target }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lib-${{ matrix.target }}
|
||||
path: target/${{ matrix.rust-target }}/release/libzx_document_ffi.a
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 5. Package xcframework (tagged macOS only)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
package-xcframework:
|
||||
name: Package xcframework
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 10
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create xcframework
|
||||
run: |
|
||||
mkdir -p xcframework_staging
|
||||
cp artifacts/lib-aarch64-apple-ios/libzx_document_ffi.a xcframework_staging/libzx_document_ffi_ios.a
|
||||
cp artifacts/lib-aarch64-apple-ios-sim/libzx_document_ffi.a xcframework_staging/libzx_document_ffi_ios_sim.a
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library xcframework_staging/libzx_document_ffi_ios.a \
|
||||
-library xcframework_staging/libzx_document_ffi_ios_sim.a \
|
||||
-output ZxDocumentRuntime.xcframework
|
||||
|
||||
# Package
|
||||
zip -r ZxDocumentRuntime.xcframework.zip ZxDocumentRuntime.xcframework/
|
||||
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ZxDocumentRuntime.xcframework.zip
|
||||
generate_release_notes: true
|
||||
48
README.md
48
README.md
@ -151,16 +151,12 @@ EPUB 不是 Word 那类复杂办公格式,它本质上更接近打包的 Web
|
||||
|
||||
## 7. 推荐 Rust 依赖包
|
||||
|
||||
### 7.1 Core 基础依赖
|
||||
### 7.1 Core 基础依赖(已使用)
|
||||
|
||||
```toml
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
time = { version = "0.3", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
```
|
||||
|
||||
用途:
|
||||
@ -168,14 +164,10 @@ time = { version = "0.3", features = ["serde"] }
|
||||
| 包 | 用途 |
|
||||
| ------------ | --------------------- |
|
||||
| `serde` | 数据结构序列化 / 反序列化 |
|
||||
| `serde_json` | JSON 协议、调试输出、事件序列化 |
|
||||
| `thiserror` | 定义稳定错误类型 |
|
||||
| `anyhow` | CLI / demo / 内部错误快速处理 |
|
||||
| `tracing` | 日志与诊断 |
|
||||
| `uuid` | 事件 ID、session ID |
|
||||
| `time` | 时间戳、阅读事件时间 |
|
||||
| `serde_json` | JSON 协议、事件序列化 |
|
||||
| `uuid` | block ID 生成(v4 随机 UUID) |
|
||||
|
||||
Serde 是 Rust 生态常用的序列化 / 反序列化框架,适合把 Rust 数据结构转换成 JSON 传给 Swift/Kotlin。
|
||||
后续可能需要 `thiserror`(derive Error trait)、`tracing`(日志诊断)、`time`(时间戳序列化),按需引入即可。
|
||||
|
||||
---
|
||||
|
||||
@ -227,8 +219,9 @@ pub enum DocumentBlock {
|
||||
List { id: String, ordered: bool, items: Vec<String> },
|
||||
CodeBlock { id: String, language: Option<String>, code: String },
|
||||
Quote { id: String, text: String },
|
||||
Table { id: String, rows: Vec<Vec<String>> },
|
||||
Table { id: String, headers: Vec<String>, rows: Vec<Vec<String>> },
|
||||
Image { id: String, src: String, alt: Option<String> },
|
||||
HorizontalRule { id: String },
|
||||
}
|
||||
```
|
||||
|
||||
@ -342,10 +335,17 @@ Tantivy 是 Rust 的全文搜索引擎库,类似 Lucene。
|
||||
### 7.8 UniFFI
|
||||
|
||||
```toml
|
||||
uniffi = "latest-compatible"
|
||||
uniffi = "0.28"
|
||||
```
|
||||
|
||||
用于生成 Swift / Kotlin bindings。UniFFI 支持为 Rust crate 生成外部语言绑定,适合本仓库作为跨端内核。
|
||||
UniFFI 0.28 使用 **proc-macro 模式** 生成跨语言绑定:
|
||||
|
||||
- **类型导出**:`#[derive(uniffi::Enum)]`、`#[derive(uniffi::Record)]`、`#[derive(uniffi::Error)]` 标注 Rust 类型
|
||||
- **函数导出**:`#[uniffi::export]` 标注公开函数
|
||||
- **Swift 绑定**:`uniffi-bindgen library --swift-sources libzx_document_ffi.dylib` 从编译产物生成
|
||||
- **Kotlin 绑定**:同理,`uniffi-bindgen library --kotlin-sources` 生成
|
||||
|
||||
> 注意:UniFFI 0.28 的纯 UDL 模式不再生成 extern \"C\" 分发函数。类型定义和函数导出均需 proc macro。
|
||||
|
||||
---
|
||||
|
||||
@ -379,14 +379,15 @@ members = [
|
||||
|
||||
### `crates/zx_document_ffi`
|
||||
|
||||
FFI 绑定层。
|
||||
FFI 绑定层。通过 proc-macro 暴露 API。
|
||||
|
||||
```text
|
||||
负责:
|
||||
- UniFFI 暴露 API
|
||||
- Swift binding
|
||||
- Kotlin binding
|
||||
- 类型转换
|
||||
- #[uniffi::export] 标注导出函数
|
||||
- #[derive(uniffi::*)] 标注导出类型
|
||||
- build.rs 生成 scaffolding
|
||||
- 类型转换(core ↔ FFI)
|
||||
- Swift / Kotlin binding 生成
|
||||
```
|
||||
|
||||
### `crates/xtask`
|
||||
@ -457,10 +458,9 @@ zhixi-document-runtime/
|
||||
│ ├── supported-formats.md
|
||||
│ ├── event-protocol.md
|
||||
│ ├── reading-position-model.md
|
||||
│ ├── note-anchor-model.md
|
||||
│ ├── pdf-strategy.md
|
||||
│ ├── ios-integration.md
|
||||
│ ├── android-integration.md
|
||||
│ └── roadmap.md
|
||||
│ └── app-rust-bridge.md
|
||||
│
|
||||
└── scripts/
|
||||
├── build-ios.sh
|
||||
|
||||
43
bindings/ios/ZxDocumentRuntime.xcframework/Info.plist
Normal file
43
bindings/ios/ZxDocumentRuntime.xcframework/Info.plist
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>libzx_document_ffi.a</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libzx_document_ffi.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>libzx_document_ffi.a</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libzx_document_ffi.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
75
bindings/ios/demo/MaterialReaderDemo/DemoApp.swift
Normal file
75
bindings/ios/demo/MaterialReaderDemo/DemoApp.swift
Normal file
@ -0,0 +1,75 @@
|
||||
import SwiftUI
|
||||
import ZxDocumentRuntime
|
||||
|
||||
/// Minimal demo app to verify zhixi-document-runtime integration.
|
||||
@main
|
||||
struct MaterialReaderDemo: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var filePath = ""
|
||||
@State private var result = ""
|
||||
@State private var events: [ReadingEvent] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("File Type Detection") {
|
||||
TextField("File path", text: $filePath)
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
Button("Detect Type") {
|
||||
detect()
|
||||
}
|
||||
if !result.isEmpty {
|
||||
Text(result)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Reading Events") {
|
||||
if events.isEmpty {
|
||||
Text("No events yet")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
ForEach(0..<events.count, id: \.self) { i in
|
||||
Text("Event \(i): \(String(describing: events[i]))")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
}
|
||||
}
|
||||
Button("Simulate Reading Session") {
|
||||
simulateReading()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("MaterialReader Demo")
|
||||
}
|
||||
}
|
||||
|
||||
func detect() {
|
||||
guard !filePath.isEmpty else {
|
||||
result = "Enter a file path"
|
||||
return
|
||||
}
|
||||
do {
|
||||
let type = try detectMaterialType(filePath: filePath)
|
||||
result = "Detected: \(type)"
|
||||
} catch {
|
||||
result = "Error: \(error)"
|
||||
}
|
||||
}
|
||||
|
||||
func simulateReading() {
|
||||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
events = [
|
||||
ReadingEvent.materialOpened(materialId: "demo-1", timestampMs: now),
|
||||
ReadingEvent.heartbeat(materialId: "demo-1", activeSeconds: 15, position: nil, timestampMs: now + 15000),
|
||||
ReadingEvent.materialClosed(materialId: "demo-1", timestampMs: now + 30000, activeSeconds: 30),
|
||||
]
|
||||
}
|
||||
}
|
||||
910
bindings/ios/device/Headers/zx_documentFFI.h
Normal file
910
bindings/ios/device/Headers/zx_documentFFI.h
Normal file
@ -0,0 +1,910 @@
|
||||
// This file was autogenerated by some hot garbage in the `uniffi` crate.
|
||||
// Trust me, you don't want to mess with it!
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// The following structs are used to implement the lowest level
|
||||
// of the FFI, and thus useful to multiple uniffied crates.
|
||||
// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H.
|
||||
#ifdef UNIFFI_SHARED_H
|
||||
// We also try to prevent mixing versions of shared uniffi header structs.
|
||||
// If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4
|
||||
#ifndef UNIFFI_SHARED_HEADER_V4
|
||||
#error Combining helper code from multiple versions of uniffi is not supported
|
||||
#endif // ndef UNIFFI_SHARED_HEADER_V4
|
||||
#else
|
||||
#define UNIFFI_SHARED_H
|
||||
#define UNIFFI_SHARED_HEADER_V4
|
||||
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
|
||||
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
|
||||
|
||||
typedef struct RustBuffer
|
||||
{
|
||||
uint64_t capacity;
|
||||
uint64_t len;
|
||||
uint8_t *_Nullable data;
|
||||
} RustBuffer;
|
||||
|
||||
typedef struct ForeignBytes
|
||||
{
|
||||
int32_t len;
|
||||
const uint8_t *_Nullable data;
|
||||
} ForeignBytes;
|
||||
|
||||
// Error definitions
|
||||
typedef struct RustCallStatus {
|
||||
int8_t code;
|
||||
RustBuffer errorBuf;
|
||||
} RustCallStatus;
|
||||
|
||||
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
|
||||
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
|
||||
#endif // def UNIFFI_SHARED_H
|
||||
#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
|
||||
#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
|
||||
typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
|
||||
typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
|
||||
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
|
||||
typedef void (*UniffiCallbackInterfaceFree)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
|
||||
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
|
||||
typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
|
||||
typedef struct UniffiForeignFutureDroppedCallbackStruct {
|
||||
uint64_t handle;
|
||||
UniffiForeignFutureDroppedCallback _Nonnull free;
|
||||
} UniffiForeignFutureDroppedCallbackStruct;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
|
||||
typedef struct UniffiForeignFutureResultU8 {
|
||||
uint8_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU8;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
|
||||
typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
|
||||
typedef struct UniffiForeignFutureResultI8 {
|
||||
int8_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI8;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
|
||||
typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
|
||||
typedef struct UniffiForeignFutureResultU16 {
|
||||
uint16_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU16;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
|
||||
typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
|
||||
typedef struct UniffiForeignFutureResultI16 {
|
||||
int16_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI16;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
|
||||
typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
|
||||
typedef struct UniffiForeignFutureResultU32 {
|
||||
uint32_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
|
||||
typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
|
||||
typedef struct UniffiForeignFutureResultI32 {
|
||||
int32_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
|
||||
typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
|
||||
typedef struct UniffiForeignFutureResultU64 {
|
||||
uint64_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
|
||||
typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
|
||||
typedef struct UniffiForeignFutureResultI64 {
|
||||
int64_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
|
||||
typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
|
||||
typedef struct UniffiForeignFutureResultF32 {
|
||||
float returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultF32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
|
||||
typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
|
||||
typedef struct UniffiForeignFutureResultF64 {
|
||||
double returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultF64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
|
||||
typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
|
||||
typedef struct UniffiForeignFutureResultRustBuffer {
|
||||
RustBuffer returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultRustBuffer;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
|
||||
typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
|
||||
typedef struct UniffiForeignFutureResultVoid {
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultVoid;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
|
||||
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor_from_search(RustBuffer material_id, RustBuffer result, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_detect_material_type(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limit, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||
int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_material_closed_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_material_opened_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_position_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT
|
||||
void uniffi_zx_document_ffi_fn_func_push_reading_event(RustBuffer event, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_chapters_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_image_meta(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_pdf_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_text_stats(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_reload_stale_events_v2(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_restore_position_from_anchor(RustBuffer anchor, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_resume_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_epub_chapters_ffi(RustBuffer chapter_ids, RustBuffer chapter_texts, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_markdown_blocks(RustBuffer blocks, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbers, RustBuffer page_texts, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION
|
||||
void uniffi_zx_document_ffi_fn_func_update_reading_position(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE
|
||||
void ffi_zx_document_ffi_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8
|
||||
void ffi_zx_document_ffi_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8
|
||||
void ffi_zx_document_ffi_rust_future_free_u8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8
|
||||
uint8_t ffi_zx_document_ffi_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8
|
||||
void ffi_zx_document_ffi_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8
|
||||
void ffi_zx_document_ffi_rust_future_free_i8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8
|
||||
int8_t ffi_zx_document_ffi_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16
|
||||
void ffi_zx_document_ffi_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16
|
||||
void ffi_zx_document_ffi_rust_future_free_u16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16
|
||||
uint16_t ffi_zx_document_ffi_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16
|
||||
void ffi_zx_document_ffi_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16
|
||||
void ffi_zx_document_ffi_rust_future_free_i16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16
|
||||
int16_t ffi_zx_document_ffi_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32
|
||||
void ffi_zx_document_ffi_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32
|
||||
void ffi_zx_document_ffi_rust_future_free_u32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32
|
||||
uint32_t ffi_zx_document_ffi_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32
|
||||
void ffi_zx_document_ffi_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32
|
||||
void ffi_zx_document_ffi_rust_future_free_i32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32
|
||||
int32_t ffi_zx_document_ffi_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64
|
||||
void ffi_zx_document_ffi_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64
|
||||
void ffi_zx_document_ffi_rust_future_free_u64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64
|
||||
uint64_t ffi_zx_document_ffi_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64
|
||||
void ffi_zx_document_ffi_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64
|
||||
void ffi_zx_document_ffi_rust_future_free_i64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64
|
||||
int64_t ffi_zx_document_ffi_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32
|
||||
void ffi_zx_document_ffi_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_f32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32
|
||||
void ffi_zx_document_ffi_rust_future_free_f32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32
|
||||
float ffi_zx_document_ffi_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64
|
||||
void ffi_zx_document_ffi_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_f64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64
|
||||
void ffi_zx_document_ffi_rust_future_free_f64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64
|
||||
double ffi_zx_document_ffi_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_cancel_rust_buffer(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_free_rust_buffer(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER
|
||||
RustBuffer ffi_zx_document_ffi_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID
|
||||
void ffi_zx_document_ffi_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID
|
||||
void ffi_zx_document_ffi_rust_future_cancel_void(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID
|
||||
void ffi_zx_document_ffi_rust_future_free_void(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID
|
||||
void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_cleanup_stale_sessions_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor_from_search(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_detect_material_type(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_parse_markdown(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_marked_as_read_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_material_closed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_material_opened_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_position_changed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_reading_event(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_chapters_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_metadata_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_image_meta(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_pdf_metadata_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_text_stats(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_reload_stale_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_restore_position_from_anchor(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_resume_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_epub_chapters_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_markdown_blocks(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_start_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_update_reading_position(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION
|
||||
uint32_t ffi_zx_document_ffi_uniffi_contract_version(void
|
||||
|
||||
);
|
||||
#endif
|
||||
|
||||
4
bindings/ios/device/Modules/module.modulemap
Normal file
4
bindings/ios/device/Modules/module.modulemap
Normal file
@ -0,0 +1,4 @@
|
||||
framework module zx_documentFFI {
|
||||
header "zx_documentFFI.h"
|
||||
export *
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
910
bindings/ios/generated/zx_documentFFI.h
Normal file
910
bindings/ios/generated/zx_documentFFI.h
Normal file
@ -0,0 +1,910 @@
|
||||
// This file was autogenerated by some hot garbage in the `uniffi` crate.
|
||||
// Trust me, you don't want to mess with it!
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// The following structs are used to implement the lowest level
|
||||
// of the FFI, and thus useful to multiple uniffied crates.
|
||||
// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H.
|
||||
#ifdef UNIFFI_SHARED_H
|
||||
// We also try to prevent mixing versions of shared uniffi header structs.
|
||||
// If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4
|
||||
#ifndef UNIFFI_SHARED_HEADER_V4
|
||||
#error Combining helper code from multiple versions of uniffi is not supported
|
||||
#endif // ndef UNIFFI_SHARED_HEADER_V4
|
||||
#else
|
||||
#define UNIFFI_SHARED_H
|
||||
#define UNIFFI_SHARED_HEADER_V4
|
||||
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
|
||||
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
|
||||
|
||||
typedef struct RustBuffer
|
||||
{
|
||||
uint64_t capacity;
|
||||
uint64_t len;
|
||||
uint8_t *_Nullable data;
|
||||
} RustBuffer;
|
||||
|
||||
typedef struct ForeignBytes
|
||||
{
|
||||
int32_t len;
|
||||
const uint8_t *_Nullable data;
|
||||
} ForeignBytes;
|
||||
|
||||
// Error definitions
|
||||
typedef struct RustCallStatus {
|
||||
int8_t code;
|
||||
RustBuffer errorBuf;
|
||||
} RustCallStatus;
|
||||
|
||||
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
|
||||
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
|
||||
#endif // def UNIFFI_SHARED_H
|
||||
#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
|
||||
#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
|
||||
typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
|
||||
typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
|
||||
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
|
||||
typedef void (*UniffiCallbackInterfaceFree)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
|
||||
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
|
||||
typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
|
||||
typedef struct UniffiForeignFutureDroppedCallbackStruct {
|
||||
uint64_t handle;
|
||||
UniffiForeignFutureDroppedCallback _Nonnull free;
|
||||
} UniffiForeignFutureDroppedCallbackStruct;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
|
||||
typedef struct UniffiForeignFutureResultU8 {
|
||||
uint8_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU8;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
|
||||
typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
|
||||
typedef struct UniffiForeignFutureResultI8 {
|
||||
int8_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI8;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
|
||||
typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
|
||||
typedef struct UniffiForeignFutureResultU16 {
|
||||
uint16_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU16;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
|
||||
typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
|
||||
typedef struct UniffiForeignFutureResultI16 {
|
||||
int16_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI16;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
|
||||
typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
|
||||
typedef struct UniffiForeignFutureResultU32 {
|
||||
uint32_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
|
||||
typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
|
||||
typedef struct UniffiForeignFutureResultI32 {
|
||||
int32_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
|
||||
typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
|
||||
typedef struct UniffiForeignFutureResultU64 {
|
||||
uint64_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
|
||||
typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
|
||||
typedef struct UniffiForeignFutureResultI64 {
|
||||
int64_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
|
||||
typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
|
||||
typedef struct UniffiForeignFutureResultF32 {
|
||||
float returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultF32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
|
||||
typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
|
||||
typedef struct UniffiForeignFutureResultF64 {
|
||||
double returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultF64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
|
||||
typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
|
||||
typedef struct UniffiForeignFutureResultRustBuffer {
|
||||
RustBuffer returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultRustBuffer;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
|
||||
typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
|
||||
typedef struct UniffiForeignFutureResultVoid {
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultVoid;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
|
||||
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor_from_search(RustBuffer material_id, RustBuffer result, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_detect_material_type(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limit, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||
int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_material_closed_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_material_opened_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_position_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT
|
||||
void uniffi_zx_document_ffi_fn_func_push_reading_event(RustBuffer event, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_chapters_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_image_meta(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_pdf_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_text_stats(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_reload_stale_events_v2(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_restore_position_from_anchor(RustBuffer anchor, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_resume_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_epub_chapters_ffi(RustBuffer chapter_ids, RustBuffer chapter_texts, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_markdown_blocks(RustBuffer blocks, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbers, RustBuffer page_texts, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION
|
||||
void uniffi_zx_document_ffi_fn_func_update_reading_position(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE
|
||||
void ffi_zx_document_ffi_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8
|
||||
void ffi_zx_document_ffi_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8
|
||||
void ffi_zx_document_ffi_rust_future_free_u8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8
|
||||
uint8_t ffi_zx_document_ffi_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8
|
||||
void ffi_zx_document_ffi_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8
|
||||
void ffi_zx_document_ffi_rust_future_free_i8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8
|
||||
int8_t ffi_zx_document_ffi_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16
|
||||
void ffi_zx_document_ffi_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16
|
||||
void ffi_zx_document_ffi_rust_future_free_u16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16
|
||||
uint16_t ffi_zx_document_ffi_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16
|
||||
void ffi_zx_document_ffi_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16
|
||||
void ffi_zx_document_ffi_rust_future_free_i16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16
|
||||
int16_t ffi_zx_document_ffi_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32
|
||||
void ffi_zx_document_ffi_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32
|
||||
void ffi_zx_document_ffi_rust_future_free_u32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32
|
||||
uint32_t ffi_zx_document_ffi_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32
|
||||
void ffi_zx_document_ffi_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32
|
||||
void ffi_zx_document_ffi_rust_future_free_i32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32
|
||||
int32_t ffi_zx_document_ffi_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64
|
||||
void ffi_zx_document_ffi_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64
|
||||
void ffi_zx_document_ffi_rust_future_free_u64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64
|
||||
uint64_t ffi_zx_document_ffi_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64
|
||||
void ffi_zx_document_ffi_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64
|
||||
void ffi_zx_document_ffi_rust_future_free_i64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64
|
||||
int64_t ffi_zx_document_ffi_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32
|
||||
void ffi_zx_document_ffi_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_f32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32
|
||||
void ffi_zx_document_ffi_rust_future_free_f32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32
|
||||
float ffi_zx_document_ffi_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64
|
||||
void ffi_zx_document_ffi_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_f64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64
|
||||
void ffi_zx_document_ffi_rust_future_free_f64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64
|
||||
double ffi_zx_document_ffi_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_cancel_rust_buffer(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_free_rust_buffer(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER
|
||||
RustBuffer ffi_zx_document_ffi_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID
|
||||
void ffi_zx_document_ffi_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID
|
||||
void ffi_zx_document_ffi_rust_future_cancel_void(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID
|
||||
void ffi_zx_document_ffi_rust_future_free_void(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID
|
||||
void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_cleanup_stale_sessions_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor_from_search(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_detect_material_type(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_parse_markdown(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_marked_as_read_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_material_closed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_material_opened_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_position_changed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_reading_event(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_chapters_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_metadata_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_image_meta(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_pdf_metadata_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_text_stats(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_reload_stale_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_restore_position_from_anchor(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_resume_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_epub_chapters_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_markdown_blocks(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_start_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_update_reading_position(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION
|
||||
uint32_t ffi_zx_document_ffi_uniffi_contract_version(void
|
||||
|
||||
);
|
||||
#endif
|
||||
|
||||
7
bindings/ios/generated/zx_documentFFI.modulemap
Normal file
7
bindings/ios/generated/zx_documentFFI.modulemap
Normal file
@ -0,0 +1,7 @@
|
||||
module zx_documentFFI {
|
||||
header "zx_documentFFI.h"
|
||||
export *
|
||||
use "Darwin"
|
||||
use "_Builtin_stdbool"
|
||||
use "_Builtin_stdint"
|
||||
}
|
||||
4
bindings/ios/module.modulemap
Normal file
4
bindings/ios/module.modulemap
Normal file
@ -0,0 +1,4 @@
|
||||
framework module zx_documentFFI {
|
||||
header "zx_documentFFI.h"
|
||||
export *
|
||||
}
|
||||
910
bindings/ios/simulator/Headers/zx_documentFFI.h
Normal file
910
bindings/ios/simulator/Headers/zx_documentFFI.h
Normal file
@ -0,0 +1,910 @@
|
||||
// This file was autogenerated by some hot garbage in the `uniffi` crate.
|
||||
// Trust me, you don't want to mess with it!
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// The following structs are used to implement the lowest level
|
||||
// of the FFI, and thus useful to multiple uniffied crates.
|
||||
// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H.
|
||||
#ifdef UNIFFI_SHARED_H
|
||||
// We also try to prevent mixing versions of shared uniffi header structs.
|
||||
// If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4
|
||||
#ifndef UNIFFI_SHARED_HEADER_V4
|
||||
#error Combining helper code from multiple versions of uniffi is not supported
|
||||
#endif // ndef UNIFFI_SHARED_HEADER_V4
|
||||
#else
|
||||
#define UNIFFI_SHARED_H
|
||||
#define UNIFFI_SHARED_HEADER_V4
|
||||
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
|
||||
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
|
||||
|
||||
typedef struct RustBuffer
|
||||
{
|
||||
uint64_t capacity;
|
||||
uint64_t len;
|
||||
uint8_t *_Nullable data;
|
||||
} RustBuffer;
|
||||
|
||||
typedef struct ForeignBytes
|
||||
{
|
||||
int32_t len;
|
||||
const uint8_t *_Nullable data;
|
||||
} ForeignBytes;
|
||||
|
||||
// Error definitions
|
||||
typedef struct RustCallStatus {
|
||||
int8_t code;
|
||||
RustBuffer errorBuf;
|
||||
} RustCallStatus;
|
||||
|
||||
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
|
||||
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
|
||||
#endif // def UNIFFI_SHARED_H
|
||||
#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
|
||||
#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
|
||||
typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
|
||||
typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
|
||||
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
|
||||
typedef void (*UniffiCallbackInterfaceFree)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
|
||||
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
|
||||
typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
|
||||
typedef struct UniffiForeignFutureDroppedCallbackStruct {
|
||||
uint64_t handle;
|
||||
UniffiForeignFutureDroppedCallback _Nonnull free;
|
||||
} UniffiForeignFutureDroppedCallbackStruct;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
|
||||
typedef struct UniffiForeignFutureResultU8 {
|
||||
uint8_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU8;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
|
||||
typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
|
||||
typedef struct UniffiForeignFutureResultI8 {
|
||||
int8_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI8;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
|
||||
typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
|
||||
typedef struct UniffiForeignFutureResultU16 {
|
||||
uint16_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU16;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
|
||||
typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
|
||||
typedef struct UniffiForeignFutureResultI16 {
|
||||
int16_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI16;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
|
||||
typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
|
||||
typedef struct UniffiForeignFutureResultU32 {
|
||||
uint32_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
|
||||
typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
|
||||
typedef struct UniffiForeignFutureResultI32 {
|
||||
int32_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
|
||||
typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
|
||||
typedef struct UniffiForeignFutureResultU64 {
|
||||
uint64_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultU64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
|
||||
typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
|
||||
typedef struct UniffiForeignFutureResultI64 {
|
||||
int64_t returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultI64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
|
||||
typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
|
||||
typedef struct UniffiForeignFutureResultF32 {
|
||||
float returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultF32;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
|
||||
typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
|
||||
typedef struct UniffiForeignFutureResultF64 {
|
||||
double returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultF64;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
|
||||
typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
|
||||
typedef struct UniffiForeignFutureResultRustBuffer {
|
||||
RustBuffer returnValue;
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultRustBuffer;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
|
||||
typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
|
||||
typedef struct UniffiForeignFutureResultVoid {
|
||||
RustCallStatus callStatus;
|
||||
} UniffiForeignFutureResultVoid;
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
|
||||
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor_from_search(RustBuffer material_id, RustBuffer result, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_detect_material_type(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limit, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||
int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_material_closed_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_material_opened_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_position_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT
|
||||
void uniffi_zx_document_ffi_fn_func_push_reading_event(RustBuffer event, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_chapters_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_image_meta(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_pdf_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_read_text_stats(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
uint32_t uniffi_zx_document_ffi_fn_func_reload_stale_events_v2(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_restore_position_from_anchor(RustBuffer anchor, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2
|
||||
void uniffi_zx_document_ffi_fn_func_resume_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_epub_chapters_ffi(RustBuffer chapter_ids, RustBuffer chapter_texts, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_markdown_blocks(RustBuffer blocks, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbers, RustBuffer page_texts, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||
RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION
|
||||
void uniffi_zx_document_ffi_fn_func_update_reading_position(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE
|
||||
void ffi_zx_document_ffi_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE
|
||||
RustBuffer ffi_zx_document_ffi_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8
|
||||
void ffi_zx_document_ffi_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8
|
||||
void ffi_zx_document_ffi_rust_future_free_u8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8
|
||||
uint8_t ffi_zx_document_ffi_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8
|
||||
void ffi_zx_document_ffi_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8
|
||||
void ffi_zx_document_ffi_rust_future_free_i8(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8
|
||||
int8_t ffi_zx_document_ffi_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16
|
||||
void ffi_zx_document_ffi_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16
|
||||
void ffi_zx_document_ffi_rust_future_free_u16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16
|
||||
uint16_t ffi_zx_document_ffi_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16
|
||||
void ffi_zx_document_ffi_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16
|
||||
void ffi_zx_document_ffi_rust_future_free_i16(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16
|
||||
int16_t ffi_zx_document_ffi_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32
|
||||
void ffi_zx_document_ffi_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32
|
||||
void ffi_zx_document_ffi_rust_future_free_u32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32
|
||||
uint32_t ffi_zx_document_ffi_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32
|
||||
void ffi_zx_document_ffi_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32
|
||||
void ffi_zx_document_ffi_rust_future_free_i32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32
|
||||
int32_t ffi_zx_document_ffi_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64
|
||||
void ffi_zx_document_ffi_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_u64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64
|
||||
void ffi_zx_document_ffi_rust_future_free_u64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64
|
||||
uint64_t ffi_zx_document_ffi_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64
|
||||
void ffi_zx_document_ffi_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_i64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64
|
||||
void ffi_zx_document_ffi_rust_future_free_i64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64
|
||||
int64_t ffi_zx_document_ffi_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32
|
||||
void ffi_zx_document_ffi_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32
|
||||
void ffi_zx_document_ffi_rust_future_cancel_f32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32
|
||||
void ffi_zx_document_ffi_rust_future_free_f32(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32
|
||||
float ffi_zx_document_ffi_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64
|
||||
void ffi_zx_document_ffi_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64
|
||||
void ffi_zx_document_ffi_rust_future_cancel_f64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64
|
||||
void ffi_zx_document_ffi_rust_future_free_f64(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64
|
||||
double ffi_zx_document_ffi_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_cancel_rust_buffer(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER
|
||||
void ffi_zx_document_ffi_rust_future_free_rust_buffer(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER
|
||||
RustBuffer ffi_zx_document_ffi_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID
|
||||
void ffi_zx_document_ffi_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID
|
||||
void ffi_zx_document_ffi_rust_future_cancel_void(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID
|
||||
void ffi_zx_document_ffi_rust_future_free_void(uint64_t handle
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID
|
||||
void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_cleanup_stale_sessions_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor_from_search(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_detect_material_type(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_parse_markdown(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_marked_as_read_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_material_closed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_material_opened_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_position_changed_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_push_reading_event(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_chapters_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_metadata_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_image_meta(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_pdf_metadata_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_read_text_stats(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_reload_stale_events_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_restore_position_from_anchor(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_resume_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_epub_chapters_ffi(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_markdown_blocks(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_start_reading_session_v2(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION
|
||||
uint16_t uniffi_zx_document_ffi_checksum_func_update_reading_position(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION
|
||||
#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION
|
||||
uint32_t ffi_zx_document_ffi_uniffi_contract_version(void
|
||||
|
||||
);
|
||||
#endif
|
||||
|
||||
4
bindings/ios/simulator/Modules/module.modulemap
Normal file
4
bindings/ios/simulator/Modules/module.modulemap
Normal file
@ -0,0 +1,4 @@
|
||||
framework module zx_documentFFI {
|
||||
header "zx_documentFFI.h"
|
||||
export *
|
||||
}
|
||||
@ -1,3 +1,77 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("zhixi-document-runtime xtask");
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
match args.first().map(String::as_str) {
|
||||
Some("test") => run_cargo("test", &[]),
|
||||
Some("build-ios") => {
|
||||
let script = project_root().join("scripts/build-ios.sh");
|
||||
let status = Command::new("bash")
|
||||
.arg(script)
|
||||
.status()
|
||||
.expect("failed to run build-ios.sh");
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
Some("fixtures") => {
|
||||
println!("fixtures/");
|
||||
println!(" markdown/sample.md — all block types");
|
||||
println!(" markdown/large_markdown.md — 200-heading stress test");
|
||||
println!(" text/sample.txt — multi-paragraph plain text");
|
||||
println!(" text/large_text.txt — 500-line stress test");
|
||||
println!(" images/test-red.png — 1×1 red pixel PNG");
|
||||
println!(" pdf/text_pdf.pdf — minimal 2-page PDF");
|
||||
println!(" pdf/scanned_pdf.pdf — minimal 1-page PDF");
|
||||
println!(" epub/simple.epub — 1-chapter EPUB");
|
||||
println!(" epub/epub_with_toc.epub — 3-chapter EPUB with NCX TOC");
|
||||
println!(" invalid_file.bin — 1KB random bytes");
|
||||
}
|
||||
Some("verify-ios") => {
|
||||
let root = project_root();
|
||||
let xcframework = root.join("bindings/ios/ZxDocumentRuntime.xcframework");
|
||||
if !xcframework.exists() {
|
||||
eprintln!("ERROR: XCFramework not found at {}", xcframework.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
let binding = root.join("bindings/ios/generated/zx_document.swift");
|
||||
if !binding.exists() {
|
||||
eprintln!("ERROR: Swift binding not found at {}", binding.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
let header = root.join("bindings/ios/generated/zx_documentFFI.h");
|
||||
if !header.exists() {
|
||||
eprintln!("ERROR: header not found at {}", header.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("✅ XCFramework: {}", xcframework.display());
|
||||
println!("✅ Swift binding: {}", binding.display());
|
||||
println!("✅ Header: {}", header.display());
|
||||
println!("✅ verify-ios passed");
|
||||
}
|
||||
_ => {
|
||||
println!("zhixi-document-runtime xtask");
|
||||
println!();
|
||||
println!("Commands:");
|
||||
println!(" test Run all Rust tests");
|
||||
println!(" build-ios Build iOS XCFramework + Swift bindings");
|
||||
println!(" verify-ios Verify XCFramework/binding/header exist");
|
||||
println!(" fixtures List available fixture files");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn project_root() -> std::path::PathBuf {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
std::path::Path::new(manifest_dir).parent().unwrap().parent().unwrap().to_path_buf()
|
||||
}
|
||||
|
||||
fn run_cargo(cmd: &str, args: &[&str]) {
|
||||
let mut child = Command::new("cargo")
|
||||
.arg(cmd)
|
||||
.args(args)
|
||||
.current_dir(project_root())
|
||||
.spawn()
|
||||
.expect("failed to start cargo");
|
||||
let status = child.wait().expect("failed to wait on cargo");
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
@ -10,4 +10,9 @@ infer = "0.16"
|
||||
mime_guess = "2"
|
||||
comrak = "0.29"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
zip = "2"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||
uniffi = "0.31"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.31", features = ["build"] }
|
||||
|
||||
9
crates/zx_document_core/build.rs
Normal file
9
crates/zx_document_core/build.rs
Normal file
@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
// UniFFI scaffolding for proc-macro derives.
|
||||
// No UDL — types use #[derive(uniffi::*)] directly.
|
||||
uniffi::generate_scaffolding("src/zx_document_core.udl").unwrap_or_else(|_| {
|
||||
// If no UDL exists, create a minimal one for scaffolding generation
|
||||
std::fs::write("src/zx_document_core.udl", "namespace zx_document_core {};").ok();
|
||||
uniffi::generate_scaffolding("src/zx_document_core.udl").unwrap()
|
||||
});
|
||||
}
|
||||
@ -1,71 +1,188 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
use crate::progress::ReadingPosition;
|
||||
use crate::search::SearchResult;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum NoteAnchor {
|
||||
Material {
|
||||
material_id: String,
|
||||
#[serde(rename = "positionSnapshot", skip_serializing_if = "Option::is_none", default)]
|
||||
position_snapshot: Option<ReadingPosition>,
|
||||
},
|
||||
MarkdownBlock {
|
||||
material_id: String,
|
||||
block_id: String,
|
||||
#[serde(rename = "positionSnapshot", skip_serializing_if = "Option::is_none", default)]
|
||||
position_snapshot: Option<ReadingPosition>,
|
||||
},
|
||||
TextLine {
|
||||
material_id: String,
|
||||
line_number: u32,
|
||||
#[serde(rename = "positionSnapshot", skip_serializing_if = "Option::is_none", default)]
|
||||
position_snapshot: Option<ReadingPosition>,
|
||||
},
|
||||
PdfPage {
|
||||
material_id: String,
|
||||
page_number: u32,
|
||||
#[serde(rename = "positionSnapshot", skip_serializing_if = "Option::is_none", default)]
|
||||
position_snapshot: Option<ReadingPosition>,
|
||||
},
|
||||
Image {
|
||||
material_id: String,
|
||||
#[serde(rename = "positionSnapshot", skip_serializing_if = "Option::is_none", default)]
|
||||
position_snapshot: Option<ReadingPosition>,
|
||||
},
|
||||
EpubChapter {
|
||||
material_id: String,
|
||||
chapter_id: String,
|
||||
#[serde(rename = "positionSnapshot", skip_serializing_if = "Option::is_none", default)]
|
||||
position_snapshot: Option<ReadingPosition>,
|
||||
},
|
||||
KnowledgeItem {
|
||||
knowledge_item_id: String,
|
||||
},
|
||||
SearchResultAnchor {
|
||||
material_id: String,
|
||||
block_id: Option<String>,
|
||||
line_number: Option<u32>,
|
||||
page_number: Option<u32>,
|
||||
chapter_id: Option<String>,
|
||||
snippet: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl NoteAnchor {
|
||||
/// Create a NoteAnchor from a material_id and optional ReadingPosition.
|
||||
pub fn from_position(material_id: &str, position: Option<&crate::progress::ReadingPosition>) -> Self {
|
||||
pub fn from_position(material_id: &str, position: Option<&ReadingPosition>) -> Self {
|
||||
match position {
|
||||
Some(crate::progress::ReadingPosition::Markdown { block_id, .. }) => {
|
||||
Some(pos @ ReadingPosition::Markdown { block_id, .. }) => {
|
||||
NoteAnchor::MarkdownBlock {
|
||||
material_id: material_id.to_string(),
|
||||
block_id: block_id.clone(),
|
||||
position_snapshot: Some(pos.clone()),
|
||||
}
|
||||
}
|
||||
Some(crate::progress::ReadingPosition::Text { line_number, .. }) => {
|
||||
Some(pos @ ReadingPosition::Text { line_number, .. }) => {
|
||||
NoteAnchor::TextLine {
|
||||
material_id: material_id.to_string(),
|
||||
line_number: *line_number,
|
||||
position_snapshot: Some(pos.clone()),
|
||||
}
|
||||
}
|
||||
Some(crate::progress::ReadingPosition::Pdf { page_number, .. }) => {
|
||||
Some(pos @ ReadingPosition::Pdf { page_number, .. }) => {
|
||||
NoteAnchor::PdfPage {
|
||||
material_id: material_id.to_string(),
|
||||
page_number: *page_number,
|
||||
position_snapshot: Some(pos.clone()),
|
||||
}
|
||||
}
|
||||
Some(crate::progress::ReadingPosition::Image { .. }) => NoteAnchor::Image {
|
||||
Some(pos @ ReadingPosition::Image { .. }) => NoteAnchor::Image {
|
||||
material_id: material_id.to_string(),
|
||||
position_snapshot: Some(pos.clone()),
|
||||
},
|
||||
Some(crate::progress::ReadingPosition::Epub { chapter_id, .. }) => {
|
||||
Some(pos @ ReadingPosition::Epub { chapter_id, .. }) => {
|
||||
NoteAnchor::EpubChapter {
|
||||
material_id: material_id.to_string(),
|
||||
chapter_id: chapter_id.clone(),
|
||||
position_snapshot: Some(pos.clone()),
|
||||
}
|
||||
}
|
||||
_ => NoteAnchor::Material {
|
||||
material_id: material_id.to_string(),
|
||||
position_snapshot: position.cloned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_search_result(material_id: &str, result: &SearchResult) -> Self {
|
||||
NoteAnchor::SearchResultAnchor {
|
||||
material_id: material_id.to_string(),
|
||||
block_id: Some(result.block_id.clone()),
|
||||
line_number: result.line_number,
|
||||
page_number: result.page_number,
|
||||
chapter_id: result.chapter_id.clone(),
|
||||
snippet: result.snippet.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a ReadingPosition from this anchor.
|
||||
/// Uses position_snapshot if available; otherwise constructs a minimal position
|
||||
/// from the anchor fields. Returns None for anchors without position info.
|
||||
pub fn to_position(&self) -> Option<ReadingPosition> {
|
||||
match self {
|
||||
NoteAnchor::MarkdownBlock { block_id, position_snapshot, .. } => {
|
||||
position_snapshot.clone().or_else(|| {
|
||||
Some(ReadingPosition::Markdown {
|
||||
block_id: block_id.clone(),
|
||||
scroll_progress: 0.0,
|
||||
})
|
||||
})
|
||||
}
|
||||
NoteAnchor::TextLine { line_number, position_snapshot, .. } => {
|
||||
position_snapshot.clone().or_else(|| {
|
||||
Some(ReadingPosition::Text {
|
||||
line_number: *line_number,
|
||||
scroll_progress: 0.0,
|
||||
})
|
||||
})
|
||||
}
|
||||
NoteAnchor::PdfPage { page_number, position_snapshot, .. } => {
|
||||
position_snapshot.clone().or_else(|| {
|
||||
Some(ReadingPosition::Pdf {
|
||||
page_number: *page_number,
|
||||
page_progress: 0.0,
|
||||
overall_progress: 0.0,
|
||||
})
|
||||
})
|
||||
}
|
||||
NoteAnchor::Image { position_snapshot, .. } => position_snapshot.clone(),
|
||||
NoteAnchor::EpubChapter { chapter_id, position_snapshot, .. } => {
|
||||
position_snapshot.clone().or_else(|| {
|
||||
Some(ReadingPosition::Epub {
|
||||
chapter_id: chapter_id.clone(),
|
||||
chapter_progress: 0.0,
|
||||
overall_progress: 0.0,
|
||||
})
|
||||
})
|
||||
}
|
||||
NoteAnchor::Material { position_snapshot, .. } => position_snapshot.clone(),
|
||||
NoteAnchor::SearchResultAnchor {
|
||||
block_id,
|
||||
line_number,
|
||||
page_number,
|
||||
chapter_id,
|
||||
..
|
||||
} => {
|
||||
if let Some(pn) = page_number {
|
||||
Some(ReadingPosition::Pdf {
|
||||
page_number: *pn,
|
||||
page_progress: 0.0,
|
||||
overall_progress: 0.0,
|
||||
})
|
||||
} else if let Some(cid) = chapter_id {
|
||||
Some(ReadingPosition::Epub {
|
||||
chapter_id: cid.clone(),
|
||||
chapter_progress: 0.0,
|
||||
overall_progress: 0.0,
|
||||
})
|
||||
} else if let Some(ln) = line_number {
|
||||
Some(ReadingPosition::Text {
|
||||
line_number: *ln,
|
||||
scroll_progress: 0.0,
|
||||
})
|
||||
} else if let Some(bid) = block_id {
|
||||
Some(ReadingPosition::Markdown {
|
||||
block_id: bid.clone(),
|
||||
scroll_progress: 0.0,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
NoteAnchor::KnowledgeItem { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -76,35 +193,384 @@ mod tests {
|
||||
#[test]
|
||||
fn test_material_anchor() {
|
||||
let a = NoteAnchor::from_position("abc", None);
|
||||
assert_eq!(a, NoteAnchor::Material { material_id: "abc".into() });
|
||||
assert_eq!(
|
||||
a,
|
||||
NoteAnchor::Material {
|
||||
material_id: "abc".into(),
|
||||
position_snapshot: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_markdown_anchor() {
|
||||
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
||||
let pos = ReadingPosition::Markdown {
|
||||
block_id: "h1".into(),
|
||||
scroll_progress: 0.5,
|
||||
};
|
||||
let a = NoteAnchor::from_position("abc", Some(&pos));
|
||||
assert_eq!(a, NoteAnchor::MarkdownBlock { material_id: "abc".into(), block_id: "h1".into() });
|
||||
assert_eq!(
|
||||
a,
|
||||
NoteAnchor::MarkdownBlock {
|
||||
material_id: "abc".into(),
|
||||
block_id: "h1".into(),
|
||||
position_snapshot: Some(pos.clone()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pdf_anchor() {
|
||||
let pos = ReadingPosition::Pdf { page_number: 3, page_progress: 0.5, overall_progress: 0.1 };
|
||||
let pos = ReadingPosition::Pdf {
|
||||
page_number: 3,
|
||||
page_progress: 0.5,
|
||||
overall_progress: 0.1,
|
||||
};
|
||||
let a = NoteAnchor::from_position("abc", Some(&pos));
|
||||
assert_eq!(a, NoteAnchor::PdfPage { material_id: "abc".into(), page_number: 3 });
|
||||
assert_eq!(
|
||||
a,
|
||||
NoteAnchor::PdfPage {
|
||||
material_id: "abc".into(),
|
||||
page_number: 3,
|
||||
position_snapshot: Some(pos.clone()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anchor_serde() {
|
||||
let a = NoteAnchor::MarkdownBlock { material_id: "abc".into(), block_id: "h1".into() };
|
||||
let a = NoteAnchor::MarkdownBlock {
|
||||
material_id: "abc".into(),
|
||||
block_id: "h1".into(),
|
||||
position_snapshot: None,
|
||||
};
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
let back: NoteAnchor = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anchor_serde_with_snapshot() {
|
||||
let pos = ReadingPosition::Markdown {
|
||||
block_id: "h1".into(),
|
||||
scroll_progress: 0.5,
|
||||
};
|
||||
let a = NoteAnchor::MarkdownBlock {
|
||||
material_id: "abc".into(),
|
||||
block_id: "h1".into(),
|
||||
position_snapshot: Some(pos),
|
||||
};
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
assert!(json.contains("\"positionSnapshot\""));
|
||||
let back: NoteAnchor = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_position_falls_back_to_material() {
|
||||
let pos = ReadingPosition::Unknown;
|
||||
let a = NoteAnchor::from_position("abc", Some(&pos));
|
||||
assert_eq!(a, NoteAnchor::Material { material_id: "abc".into() });
|
||||
assert_eq!(
|
||||
a,
|
||||
NoteAnchor::Material {
|
||||
material_id: "abc".into(),
|
||||
position_snapshot: Some(ReadingPosition::Unknown),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_search_result_text_line() {
|
||||
let sr = SearchResult {
|
||||
block_id: "line-42".into(), line_number: Some(42), page_number: None, chapter_id: None,
|
||||
snippet: "found".into(), match_start: 0, match_end: 5,
|
||||
};
|
||||
let a = NoteAnchor::from_search_result("mat-text", &sr);
|
||||
match a {
|
||||
NoteAnchor::SearchResultAnchor { material_id, line_number, .. } => {
|
||||
assert_eq!(material_id, "mat-text");
|
||||
assert_eq!(line_number, Some(42));
|
||||
}
|
||||
_ => panic!("expected SearchResultAnchor"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_search_result_markdown() {
|
||||
let sr = SearchResult {
|
||||
block_id: "h2".into(),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet: "…found text…".into(),
|
||||
match_start: 1,
|
||||
match_end: 10,
|
||||
};
|
||||
let a = NoteAnchor::from_search_result("mat1", &sr);
|
||||
match a {
|
||||
NoteAnchor::SearchResultAnchor {
|
||||
material_id,
|
||||
block_id,
|
||||
line_number,
|
||||
chapter_id,
|
||||
snippet,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(material_id, "mat1");
|
||||
assert_eq!(block_id, Some("h2".to_string()));
|
||||
assert_eq!(line_number, None);
|
||||
assert_eq!(chapter_id, None);
|
||||
assert_eq!(snippet, "…found text…");
|
||||
}
|
||||
_ => panic!("expected SearchResultAnchor"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_search_result_pdf() {
|
||||
let sr = SearchResult {
|
||||
block_id: "page-3".into(),
|
||||
line_number: None,
|
||||
page_number: Some(3),
|
||||
chapter_id: None,
|
||||
snippet: "…pdf hit…".into(),
|
||||
match_start: 0,
|
||||
match_end: 8,
|
||||
};
|
||||
let a = NoteAnchor::from_search_result("mat2", &sr);
|
||||
match a {
|
||||
NoteAnchor::SearchResultAnchor {
|
||||
material_id,
|
||||
page_number,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(material_id, "mat2");
|
||||
assert_eq!(page_number, Some(3));
|
||||
}
|
||||
_ => panic!("expected SearchResultAnchor"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_search_result_epub() {
|
||||
let sr = SearchResult {
|
||||
block_id: "ch2".into(),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: Some("ch2".into()),
|
||||
snippet: "…epub hit…".into(),
|
||||
match_start: 0,
|
||||
match_end: 9,
|
||||
};
|
||||
let a = NoteAnchor::from_search_result("mat3", &sr);
|
||||
match a {
|
||||
NoteAnchor::SearchResultAnchor {
|
||||
material_id,
|
||||
chapter_id,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(material_id, "mat3");
|
||||
assert_eq!(chapter_id, Some("ch2".to_string()));
|
||||
}
|
||||
_ => panic!("expected SearchResultAnchor"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_anchor_serde() {
|
||||
let a = NoteAnchor::SearchResultAnchor {
|
||||
material_id: "mat1".into(),
|
||||
block_id: Some("h1".into()),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet: "…snippet…".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
assert!(json.contains("\"type\":\"SearchResultAnchor\""));
|
||||
let back: NoteAnchor = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backward_compat_no_snapshot() {
|
||||
let old_json = r#"{"type":"MarkdownBlock","material_id":"abc","block_id":"h1"}"#;
|
||||
let a: NoteAnchor = serde_json::from_str(old_json).unwrap();
|
||||
assert_eq!(
|
||||
a,
|
||||
NoteAnchor::MarkdownBlock {
|
||||
material_id: "abc".into(),
|
||||
block_id: "h1".into(),
|
||||
position_snapshot: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_with_snapshot() {
|
||||
let pos = ReadingPosition::Markdown {
|
||||
block_id: "h1".into(),
|
||||
scroll_progress: 0.75,
|
||||
};
|
||||
let anchor = NoteAnchor::MarkdownBlock {
|
||||
material_id: "mat1".into(),
|
||||
block_id: "h1".into(),
|
||||
position_snapshot: Some(pos.clone()),
|
||||
};
|
||||
let restored = anchor.to_position();
|
||||
assert_eq!(restored, Some(pos));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_without_snapshot_uses_minimal() {
|
||||
let anchor = NoteAnchor::PdfPage {
|
||||
material_id: "mat1".into(),
|
||||
page_number: 5,
|
||||
position_snapshot: None,
|
||||
};
|
||||
let restored = anchor.to_position();
|
||||
assert_eq!(
|
||||
restored,
|
||||
Some(ReadingPosition::Pdf {
|
||||
page_number: 5,
|
||||
page_progress: 0.0,
|
||||
overall_progress: 0.0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_from_search_result_pdf() {
|
||||
let anchor = NoteAnchor::SearchResultAnchor {
|
||||
material_id: "mat1".into(),
|
||||
block_id: Some("page-3".into()),
|
||||
line_number: None,
|
||||
page_number: Some(3),
|
||||
chapter_id: None,
|
||||
snippet: "…hit…".into(),
|
||||
};
|
||||
let restored = anchor.to_position();
|
||||
assert_eq!(
|
||||
restored,
|
||||
Some(ReadingPosition::Pdf {
|
||||
page_number: 3,
|
||||
page_progress: 0.0,
|
||||
overall_progress: 0.0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_from_search_result_markdown() {
|
||||
let anchor = NoteAnchor::SearchResultAnchor {
|
||||
material_id: "mat1".into(),
|
||||
block_id: Some("h2".into()),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet: "…hit…".into(),
|
||||
};
|
||||
let restored = anchor.to_position();
|
||||
assert_eq!(
|
||||
restored,
|
||||
Some(ReadingPosition::Markdown {
|
||||
block_id: "h2".into(),
|
||||
scroll_progress: 0.0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_knowledge_item_returns_none() {
|
||||
let anchor = NoteAnchor::KnowledgeItem {
|
||||
knowledge_item_id: "ki1".into(),
|
||||
};
|
||||
assert_eq!(anchor.to_position(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_material_without_snapshot_returns_none() {
|
||||
let anchor = NoteAnchor::Material {
|
||||
material_id: "mat1".into(),
|
||||
position_snapshot: None,
|
||||
};
|
||||
assert_eq!(anchor.to_position(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_to_anchor_all_variants() {
|
||||
let cases = vec![
|
||||
("Markdown", ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }),
|
||||
("Text", ReadingPosition::Text { line_number: 10, scroll_progress: 0.3 }),
|
||||
("Pdf", ReadingPosition::Pdf { page_number: 3, page_progress: 0.7, overall_progress: 0.5 }),
|
||||
("Image", ReadingPosition::Image { zoom_scale: 1.5, offset_x: 100.0, offset_y: 200.0 }),
|
||||
("Epub", ReadingPosition::Epub { chapter_id: "ch2".into(), chapter_progress: 0.6, overall_progress: 0.4 }),
|
||||
("Unknown", ReadingPosition::Unknown),
|
||||
];
|
||||
for (name, pos) in &cases {
|
||||
let anchor = NoteAnchor::from_position("mat", Some(pos));
|
||||
let restored = anchor.to_position();
|
||||
assert_eq!(restored.as_ref(), Some(pos), "roundtrip failed for {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_none_returns_material_anchor() {
|
||||
let anchor = NoteAnchor::from_position("mat", None);
|
||||
assert!(matches!(anchor, NoteAnchor::Material { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_position_anchor_position() {
|
||||
let pos = ReadingPosition::Epub {
|
||||
chapter_id: "ch3".into(),
|
||||
chapter_progress: 0.6,
|
||||
overall_progress: 0.3,
|
||||
};
|
||||
let anchor = NoteAnchor::from_position("mat1", Some(&pos));
|
||||
let restored = anchor.to_position();
|
||||
assert_eq!(restored, Some(pos));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_anchor_preserves_all_fields() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(),
|
||||
line_number: Some(42),
|
||||
page_number: Some(3),
|
||||
chapter_id: Some("ch2".into()),
|
||||
snippet: "match here".into(),
|
||||
match_start: 0,
|
||||
match_end: 5,
|
||||
};
|
||||
let anchor = NoteAnchor::from_search_result("mat1", &result);
|
||||
match anchor {
|
||||
NoteAnchor::SearchResultAnchor { material_id, block_id, line_number, page_number, chapter_id, snippet } => {
|
||||
assert_eq!(material_id, "mat1");
|
||||
assert_eq!(block_id, Some("b1".to_string()));
|
||||
assert_eq!(line_number, Some(42));
|
||||
assert_eq!(page_number, Some(3));
|
||||
assert_eq!(chapter_id, Some("ch2".to_string()));
|
||||
assert_eq!(snippet, "match here");
|
||||
}
|
||||
_ => panic!("expected SearchResultAnchor"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_anchor_to_position_roundtrip() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(),
|
||||
line_number: Some(10),
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet: "found".into(),
|
||||
match_start: 0,
|
||||
match_end: 5,
|
||||
};
|
||||
let anchor = NoteAnchor::from_search_result("mat1", &result);
|
||||
// SearchResultAnchor can still convert to position
|
||||
let pos = anchor.to_position();
|
||||
assert!(pos.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,14 +2,220 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::material_type::{MaterialType, PreviewMode};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct DocumentInfo {
|
||||
pub material_id: String,
|
||||
pub title: String,
|
||||
pub material_type: MaterialType,
|
||||
pub preview_mode: PreviewMode,
|
||||
pub file_size: u64,
|
||||
|
||||
// Text stats
|
||||
pub page_count: Option<u32>,
|
||||
pub word_count: Option<u32>,
|
||||
pub char_count: Option<u32>,
|
||||
pub line_count: Option<u32>,
|
||||
|
||||
// Image
|
||||
pub image_width: Option<u32>,
|
||||
pub image_height: Option<u32>,
|
||||
pub image_format: Option<String>,
|
||||
|
||||
// EPUB
|
||||
pub epub_chapter_count: Option<u32>,
|
||||
|
||||
// Capabilities
|
||||
pub is_searchable: bool,
|
||||
pub supports_position: bool,
|
||||
pub supports_anchor: bool,
|
||||
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
impl DocumentInfo {
|
||||
pub fn new(material_id: String, title: String, material_type: MaterialType, file_size: u64) -> Self {
|
||||
let preview_mode = material_type.preview_mode();
|
||||
let (is_searchable, supports_position, supports_anchor) = match material_type {
|
||||
MaterialType::Markdown | MaterialType::Text | MaterialType::Pdf | MaterialType::Epub => (true, true, true),
|
||||
MaterialType::Image => (false, true, true),
|
||||
MaterialType::Word | MaterialType::Excel | MaterialType::PowerPoint => (false, false, true),
|
||||
MaterialType::Unknown => (false, false, false),
|
||||
};
|
||||
Self {
|
||||
material_id,
|
||||
title,
|
||||
material_type,
|
||||
preview_mode,
|
||||
file_size,
|
||||
page_count: None,
|
||||
word_count: None,
|
||||
char_count: None,
|
||||
line_count: None,
|
||||
image_width: None,
|
||||
image_height: None,
|
||||
image_format: None,
|
||||
epub_chapter_count: None,
|
||||
is_searchable,
|
||||
supports_position,
|
||||
supports_anchor,
|
||||
created_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a fully populated DocumentInfo from a file path.
|
||||
pub fn build_document_info(
|
||||
file_path: &str,
|
||||
material_id: String,
|
||||
title: String,
|
||||
) -> Result<DocumentInfo, crate::error::DocumentError> {
|
||||
use std::path::Path;
|
||||
use crate::material_type::detect_material_type;
|
||||
|
||||
let path = Path::new(file_path);
|
||||
let mt = detect_material_type(file_path).unwrap_or(MaterialType::Unknown);
|
||||
let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
let mut info = DocumentInfo::new(material_id, title, mt, file_size);
|
||||
|
||||
match &info.material_type {
|
||||
MaterialType::Markdown | MaterialType::Text => {
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
let stats = crate::text::text_stats(&content);
|
||||
info.word_count = Some(stats.word_count);
|
||||
info.line_count = Some(stats.line_count);
|
||||
info.char_count = Some(content.chars().count() as u32);
|
||||
info.page_count = Some((stats.line_count / 50).max(1));
|
||||
}
|
||||
}
|
||||
MaterialType::Image => {
|
||||
if let Ok(meta) = crate::image_meta::read_image_meta(file_path) {
|
||||
info.image_width = Some(meta.width);
|
||||
info.image_height = Some(meta.height);
|
||||
info.image_format = Some(meta.format);
|
||||
}
|
||||
}
|
||||
MaterialType::Pdf => {
|
||||
if let Ok(meta) = crate::pdf::read_pdf_metadata(path) {
|
||||
info.page_count = Some(meta.page_count);
|
||||
}
|
||||
}
|
||||
MaterialType::Epub => {
|
||||
if let Ok(meta) = crate::epub::read_epub_metadata(path) {
|
||||
info.epub_chapter_count = Some(meta.chapter_count);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_markdown_capabilities() {
|
||||
let info = DocumentInfo::new("m1".into(), "test.md".into(), MaterialType::Markdown, 1024);
|
||||
assert!(info.is_searchable);
|
||||
assert!(info.supports_position);
|
||||
assert_eq!(info.preview_mode, PreviewMode::NativeReader);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_office_capabilities() {
|
||||
let info = DocumentInfo::new("m2".into(), "report.docx".into(), MaterialType::Word, 2048);
|
||||
assert!(!info.is_searchable);
|
||||
assert!(!info.supports_position);
|
||||
assert!(info.supports_anchor); // Material-level anchor only
|
||||
assert_eq!(info.preview_mode, PreviewMode::PlatformPreview);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_capabilities() {
|
||||
let info = DocumentInfo::new("m3".into(), "file.bin".into(), MaterialType::Unknown, 0);
|
||||
assert!(!info.is_searchable);
|
||||
assert!(!info.supports_position);
|
||||
assert!(!info.supports_anchor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_capabilities() {
|
||||
let info = DocumentInfo::new("m4".into(), "notes.txt".into(), MaterialType::Text, 512);
|
||||
assert!(info.is_searchable);
|
||||
assert!(info.supports_position);
|
||||
assert!(info.supports_anchor);
|
||||
assert_eq!(info.preview_mode, PreviewMode::NativeReader);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pdf_capabilities() {
|
||||
let info = DocumentInfo::new("m5".into(), "book.pdf".into(), MaterialType::Pdf, 2048);
|
||||
assert!(info.is_searchable);
|
||||
assert!(info.supports_position);
|
||||
assert!(info.supports_anchor);
|
||||
assert_eq!(info.preview_mode, PreviewMode::PlatformPreview);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_capabilities() {
|
||||
let info = DocumentInfo::new("m6".into(), "photo.jpg".into(), MaterialType::Image, 1024);
|
||||
assert!(!info.is_searchable);
|
||||
assert!(info.supports_position);
|
||||
assert!(info.supports_anchor);
|
||||
assert_eq!(info.preview_mode, PreviewMode::NativeReader);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_epub_capabilities() {
|
||||
let info = DocumentInfo::new("m7".into(), "book.epub".into(), MaterialType::Epub, 4096);
|
||||
assert!(info.is_searchable);
|
||||
assert!(info.supports_position);
|
||||
assert!(info.supports_anchor);
|
||||
assert_eq!(info.preview_mode, PreviewMode::NativeReader);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_excel_capabilities() {
|
||||
let info = DocumentInfo::new("m8".into(), "sheet.xlsx".into(), MaterialType::Excel, 2048);
|
||||
assert!(!info.is_searchable);
|
||||
assert!(!info.supports_position);
|
||||
assert!(info.supports_anchor);
|
||||
assert_eq!(info.preview_mode, PreviewMode::PlatformPreview);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_powerpoint_capabilities() {
|
||||
let info = DocumentInfo::new("m9".into(), "slides.pptx".into(), MaterialType::PowerPoint, 4096);
|
||||
assert!(!info.is_searchable);
|
||||
assert!(!info.supports_position);
|
||||
assert!(info.supports_anchor);
|
||||
assert_eq!(info.preview_mode, PreviewMode::ExternalOpen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_stats_fields_start_none() {
|
||||
let info = DocumentInfo::new("m10".into(), "any".into(), MaterialType::Markdown, 100);
|
||||
assert_eq!(info.page_count, None);
|
||||
assert_eq!(info.word_count, None);
|
||||
assert_eq!(info.char_count, None);
|
||||
assert_eq!(info.line_count, None);
|
||||
assert_eq!(info.image_width, None);
|
||||
assert_eq!(info.image_height, None);
|
||||
assert_eq!(info.image_format, None);
|
||||
assert_eq!(info.epub_chapter_count, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_size_is_set() {
|
||||
let info = DocumentInfo::new("m11".into(), "f".into(), MaterialType::Markdown, 12345);
|
||||
assert_eq!(info.file_size, 12345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_material_type_matches_input() {
|
||||
let info = DocumentInfo::new("m12".into(), "t".into(), MaterialType::Pdf, 0);
|
||||
assert_eq!(info.material_type, MaterialType::Pdf);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,833 @@
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DocumentError;
|
||||
|
||||
/// Metadata extracted from an EPUB file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct EpubMetadata {
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub chapter_count: u32,
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
/// A single chapter in an EPUB spine.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct EpubChapter {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
/// Path within the EPUB zip, e.g. "OEBPS/chapter1.xhtml"
|
||||
pub path: String,
|
||||
/// Play order in the spine (0-based)
|
||||
pub play_order: u32,
|
||||
}
|
||||
|
||||
/// A chapter with its extracted text content (for search indexing).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct EpubChapterText {
|
||||
pub chapter_id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Extract plain text from EPUB chapter files.
|
||||
///
|
||||
/// Opens the EPUB zip and reads the content of each spine chapter,
|
||||
/// stripping HTML tags to produce searchable plain text.
|
||||
pub fn extract_epub_chapter_texts(file_path: &Path) -> Result<Vec<EpubChapterText>, DocumentError> {
|
||||
let file = fs::File::open(file_path).map_err(DocumentError::IoError)?;
|
||||
let mut archive = zip::ZipArchive::new(file)
|
||||
.map_err(|e| DocumentError::ParseError(format!("invalid EPUB zip: {e}")))?;
|
||||
|
||||
let opf_xml = read_opf(&mut archive)?;
|
||||
let toc = read_toc(&mut archive, &opf_xml);
|
||||
|
||||
let mut results = Vec::new();
|
||||
for (chapter_id, _title) in &toc {
|
||||
// Build path from chapter ID (typically matches the href in the spine)
|
||||
let path_in_zip = if chapter_id.contains('/') || chapter_id.contains(".xhtml") || chapter_id.contains(".html") {
|
||||
chapter_id.clone()
|
||||
} else {
|
||||
format!("OEBPS/{chapter_id}.xhtml")
|
||||
};
|
||||
let text = match archive.by_name(&path_in_zip) {
|
||||
Ok(mut f) => {
|
||||
let mut buf = String::new();
|
||||
if f.read_to_string(&mut buf).is_ok() {
|
||||
strip_html(&buf)
|
||||
} else { String::new() }
|
||||
}
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
if !text.trim().is_empty() {
|
||||
results.push(EpubChapterText { chapter_id: chapter_id.clone(), text });
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn strip_html(html: &str) -> String {
|
||||
let mut text = String::new();
|
||||
let mut in_tag = false;
|
||||
for c in html.chars() {
|
||||
if c == '<' {
|
||||
in_tag = true;
|
||||
} else if c == '>' {
|
||||
in_tag = false;
|
||||
} else if !in_tag {
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
// Collapse whitespace
|
||||
text.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// Combined result: metadata + chapters from a single EPUB open+parse.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EpubData {
|
||||
pub metadata: EpubMetadata,
|
||||
pub chapters: Vec<EpubChapter>,
|
||||
}
|
||||
|
||||
/// Read EPUB metadata and chapters in a single pass (single ZIP open + OPF parse).
|
||||
/// When both metadata and chapters are needed, prefer this over calling both separately.
|
||||
pub fn read_epub(file_path: &Path) -> Result<EpubData, DocumentError> {
|
||||
let file = fs::File::open(file_path).map_err(DocumentError::IoError)?;
|
||||
let file_size = file.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
let mut archive = zip::ZipArchive::new(file)
|
||||
.map_err(|e| DocumentError::ParseError(format!("invalid EPUB zip: {e}")))?;
|
||||
|
||||
let opf_xml = read_opf(&mut archive)?;
|
||||
let (title, author, spine_ids) = parse_opf(&opf_xml);
|
||||
let toc = read_toc(&mut archive, &opf_xml);
|
||||
|
||||
let metadata = EpubMetadata {
|
||||
title,
|
||||
author,
|
||||
chapter_count: spine_ids.len() as u32,
|
||||
file_size,
|
||||
};
|
||||
|
||||
let chapters: Vec<EpubChapter> = spine_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (id, href))| {
|
||||
let title = toc
|
||||
.iter()
|
||||
.find(|t| t.0 == *href || t.0 == *id)
|
||||
.map(|t| t.1.clone())
|
||||
.unwrap_or_else(|| format!("Chapter {}", i + 1));
|
||||
EpubChapter {
|
||||
chapter_id: id.clone(),
|
||||
title,
|
||||
path: href.clone(),
|
||||
play_order: i as u32,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(EpubData { metadata, chapters })
|
||||
}
|
||||
|
||||
/// Read EPUB metadata only. If chapters are also needed, use read_epub() instead.
|
||||
pub fn read_epub_metadata(file_path: &Path) -> Result<EpubMetadata, DocumentError> {
|
||||
read_epub(file_path).map(|data| data.metadata)
|
||||
}
|
||||
|
||||
/// Read EPUB chapter list. If metadata is also needed, use read_epub() instead.
|
||||
pub fn read_epub_chapters(file_path: &Path) -> Result<Vec<EpubChapter>, DocumentError> {
|
||||
read_epub(file_path).map(|data| data.chapters)
|
||||
}
|
||||
|
||||
// ── Internal helpers ──
|
||||
|
||||
fn read_opf(archive: &mut zip::ZipArchive<fs::File>) -> Result<String, DocumentError> {
|
||||
// 1. Read container.xml to find OPF path
|
||||
let container_xml = read_zip_entry(archive, "META-INF/container.xml")?;
|
||||
let opf_path = extract_container_rootfile(&container_xml)?;
|
||||
|
||||
// 2. Read OPF
|
||||
read_zip_entry(archive, &opf_path)
|
||||
}
|
||||
|
||||
fn read_zip_entry(
|
||||
archive: &mut zip::ZipArchive<fs::File>,
|
||||
name: &str,
|
||||
) -> Result<String, DocumentError> {
|
||||
let mut file = archive
|
||||
.by_name(name)
|
||||
.map_err(|_| DocumentError::ParseError(format!("EPUB missing: {name}")))?;
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf)
|
||||
.map_err(|e| DocumentError::IoError(e))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn extract_container_rootfile(xml: &str) -> Result<String, DocumentError> {
|
||||
// <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||
if let Some(start) = xml.find("full-path=") {
|
||||
let after = &xml[start + 10..];
|
||||
let delim = after.as_bytes()[0];
|
||||
let value_start = if delim == b'"' || delim == b'\'' { 1 } else { 0 };
|
||||
let value = &after[value_start..];
|
||||
if let Some(end) = value.find(if delim == b'"' || delim == b'\'' { delim as char } else { ' ' }) {
|
||||
return Ok(value[..end].to_string());
|
||||
}
|
||||
}
|
||||
Err(DocumentError::ParseError("EPUB container.xml: no rootfile found".into()))
|
||||
}
|
||||
|
||||
/// Returns (title, author, spine: Vec<(id, href)>)
|
||||
fn parse_opf(xml: &str) -> (Option<String>, Option<String>, Vec<(String, String)>) {
|
||||
let title = extract_tag_content(xml, "dc:title");
|
||||
let author = extract_tag_content(xml, "dc:creator");
|
||||
let manifest = extract_opf_manifest(xml);
|
||||
let spine = extract_opf_spine(xml);
|
||||
|
||||
// Map spine idrefs to manifest hrefs, skipping NAV items
|
||||
let nav_item = find_nav_item(xml);
|
||||
let spine_items: Vec<(String, String)> = spine
|
||||
.iter()
|
||||
.filter_map(|idref| {
|
||||
// Skip if this idref points to the NAV item
|
||||
if let Some((nav_id, _)) = &nav_item {
|
||||
if idref == nav_id {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
manifest
|
||||
.iter()
|
||||
.find(|(id, _href, _media)| id == idref)
|
||||
.map(|(id, href, _)| (id.clone(), href.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
(title, author, spine_items)
|
||||
}
|
||||
|
||||
fn extract_tag_content(xml: &str, tag: &str) -> Option<String> {
|
||||
let open = format!("<{}", tag);
|
||||
let pos = xml.find(&open)?;
|
||||
let after_tag = &xml[pos + open.len()..];
|
||||
// Skip attributes: find '>'
|
||||
let content_start = after_tag.find('>')? + 1;
|
||||
let rest = &after_tag[content_start..];
|
||||
let close = format!("</{}>", tag);
|
||||
let content_end = rest.find(&close)?;
|
||||
let content = rest[..content_end].trim().to_string();
|
||||
if content.is_empty() { None } else { Some(content) }
|
||||
}
|
||||
|
||||
fn extract_opf_manifest(xml: &str) -> Vec<(String, String, String)> {
|
||||
// Extract <item id="x" href="y" media-type="z"/>
|
||||
let mut items = Vec::new();
|
||||
let mut pos = 0;
|
||||
while let Some(i) = xml[pos..].find("<item ") {
|
||||
let start = pos + i;
|
||||
let end = xml[start..].find("/>").map(|e| start + e + 2)
|
||||
.or_else(|| xml[start..].find('>').map(|e| start + e + 1))
|
||||
.unwrap_or(start + 50);
|
||||
let tag_text = &xml[start..end.min(xml.len())];
|
||||
|
||||
let id = extract_attr(tag_text, "id");
|
||||
let href = extract_attr(tag_text, "href");
|
||||
let media = extract_attr(tag_text, "media-type");
|
||||
|
||||
if let (Some(id), Some(href)) = (id, href) {
|
||||
items.push((id, href, media.unwrap_or_default()));
|
||||
}
|
||||
pos = end;
|
||||
if pos >= xml.len() - 5 { break; }
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn extract_opf_spine(xml: &str) -> Vec<String> {
|
||||
let mut items = Vec::new();
|
||||
let spine_start = xml.find("<spine").unwrap_or(0);
|
||||
let spine_end = xml[spine_start..].find("</spine>")
|
||||
.map(|e| spine_start + e + 9)
|
||||
.unwrap_or(xml.len());
|
||||
let spine_xml = &xml[spine_start..spine_end.min(xml.len())];
|
||||
|
||||
let mut pos = 0;
|
||||
while let Some(i) = spine_xml[pos..].find("<itemref") {
|
||||
let start = pos + i;
|
||||
let end = spine_xml[start..].find("/>").map(|e| start + e + 2)
|
||||
.or_else(|| spine_xml[start..].find('>').map(|e| start + e + 1))
|
||||
.unwrap_or(start + 30);
|
||||
let tag_text = &spine_xml[start..end.min(spine_xml.len())];
|
||||
if let Some(idref) = extract_attr(tag_text, "idref") {
|
||||
items.push(idref);
|
||||
}
|
||||
pos = end;
|
||||
if pos >= spine_xml.len() - 5 { break; }
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
/// Read the table of contents. Tries NCX first, falls back to EPUB3 NAV.
|
||||
fn read_toc(
|
||||
archive: &mut zip::ZipArchive<fs::File>,
|
||||
opf_xml: &str,
|
||||
) -> Vec<(String, String)> {
|
||||
// Try NCX first (EPUB2)
|
||||
let toc = read_ncx_toc(archive, opf_xml);
|
||||
if !toc.is_empty() {
|
||||
return toc;
|
||||
}
|
||||
|
||||
// Fall back to EPUB3 NAV
|
||||
read_nav_toc(archive, opf_xml)
|
||||
}
|
||||
|
||||
/// Try to read NCX TOC; returns Vec<(src, title)>
|
||||
fn read_ncx_toc(
|
||||
archive: &mut zip::ZipArchive<fs::File>,
|
||||
opf_xml: &str,
|
||||
) -> Vec<(String, String)> {
|
||||
let ncx_href = extract_opf_manifest(opf_xml)
|
||||
.iter()
|
||||
.find(|(_id, href, media)| media == "application/x-dtbncx+xml" || href.ends_with(".ncx"))
|
||||
.map(|(_id, href, _)| href.clone());
|
||||
|
||||
match &ncx_href {
|
||||
Some(href) => match read_zip_entry(archive, href) {
|
||||
Ok(xml) => parse_ncx_navpoints(&xml),
|
||||
Err(_) => Vec::new(),
|
||||
},
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try EPUB3 NAV document: find `<a href="...">title</a>` inside `<nav epub:type="toc">`.
|
||||
fn read_nav_toc(
|
||||
archive: &mut zip::ZipArchive<fs::File>,
|
||||
opf_xml: &str,
|
||||
) -> Vec<(String, String)> {
|
||||
// Find NAV href from OPF manifest item with properties="nav"
|
||||
let nav_href = find_nav_href_in_opf(opf_xml);
|
||||
|
||||
let nav_xml = match &nav_href {
|
||||
Some(href) => match read_zip_entry(archive, href) {
|
||||
Ok(xml) => xml,
|
||||
Err(_) => return Vec::new(),
|
||||
},
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
parse_nav_links(&nav_xml)
|
||||
}
|
||||
|
||||
/// Find the NAV item (id, href) from the manifest, or None.
|
||||
fn find_nav_item(opf_xml: &str) -> Option<(String, String)> {
|
||||
let mut pos = 0;
|
||||
while let Some(i) = opf_xml[pos..].find("<item ") {
|
||||
let start = pos + i;
|
||||
let end = opf_xml[start..].find("/>").map(|e| start + e + 2)
|
||||
.or_else(|| opf_xml[start..].find('>').map(|e| start + e + 1))
|
||||
.unwrap_or(start + 200);
|
||||
let tag = &opf_xml[start..end.min(opf_xml.len())];
|
||||
|
||||
let is_nav = tag.contains("properties=\"nav\"")
|
||||
|| tag.contains("properties='nav'");
|
||||
if is_nav {
|
||||
if let (Some(id), Some(href)) = (extract_attr(tag, "id"), extract_attr(tag, "href")) {
|
||||
return Some((id, href));
|
||||
}
|
||||
}
|
||||
pos = end;
|
||||
if pos >= opf_xml.len() - 5 { break; }
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_nav_href_in_opf(opf_xml: &str) -> Option<String> {
|
||||
// Find <item ... properties="nav" ... /> and extract href
|
||||
let mut pos = 0;
|
||||
while let Some(i) = opf_xml[pos..].find("<item ") {
|
||||
let start = pos + i;
|
||||
let end = opf_xml[start..].find("/>").map(|e| start + e + 2)
|
||||
.or_else(|| opf_xml[start..].find('>').map(|e| start + e + 1))
|
||||
.unwrap_or(start + 200);
|
||||
let tag = &opf_xml[start..end.min(opf_xml.len())];
|
||||
|
||||
// Check for properties="nav" or properties='nav'
|
||||
let has_nav = tag.contains("properties=\"nav\"")
|
||||
|| tag.contains("properties='nav'")
|
||||
|| tag.contains("properties = \"nav\"");
|
||||
if has_nav {
|
||||
if let Some(href) = extract_attr(tag, "href") {
|
||||
return Some(href);
|
||||
}
|
||||
}
|
||||
pos = end;
|
||||
if pos >= opf_xml.len() - 5 { break; }
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_ncx_navpoints(xml: &str) -> Vec<(String, String)> {
|
||||
let mut items = Vec::new();
|
||||
let mut pos = 0;
|
||||
while let Some(i) = xml[pos..].find("<navPoint ") {
|
||||
let nav_start = pos + i;
|
||||
let nav_end = xml[nav_start..].find("</navPoint>")
|
||||
.map(|e| nav_start + e + 12)
|
||||
.unwrap_or(xml.len());
|
||||
let nav_xml = &xml[nav_start..nav_end.min(xml.len())];
|
||||
|
||||
let src = extract_ncx_src(nav_xml);
|
||||
let label = extract_tag_content(nav_xml, "text");
|
||||
|
||||
if let Some(src) = src {
|
||||
items.push((src, label.unwrap_or_default()));
|
||||
}
|
||||
|
||||
pos = nav_end;
|
||||
if pos >= xml.len() - 10 { break; }
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
/// Parse `<a href="...">title</a>` links from EPUB3 NAV document.
|
||||
fn parse_nav_links(xml: &str) -> Vec<(String, String)> {
|
||||
let mut items = Vec::new();
|
||||
let mut pos = 0;
|
||||
|
||||
// Find each <a href="..."> element
|
||||
while let Some(i) = xml[pos..].find("<a ") {
|
||||
let start = pos + i;
|
||||
// Find end of <a> tag:
|
||||
let tag_end = xml[start..].find('>').map(|e| start + e + 1).unwrap_or(start + 50);
|
||||
let tag_text = &xml[start..tag_end.min(xml.len())];
|
||||
let href = extract_attr(tag_text, "href");
|
||||
|
||||
// Find text between <a ...> and </a>
|
||||
let content_start = tag_end;
|
||||
let content_end = xml[content_start..].find("</a>")
|
||||
.map(|e| content_start + e)
|
||||
.unwrap_or(content_start);
|
||||
let title = xml[content_start..content_end].trim().to_string();
|
||||
|
||||
if let (Some(href), _) = (href, &title) {
|
||||
if !title.is_empty() {
|
||||
items.push((href, title));
|
||||
}
|
||||
}
|
||||
|
||||
pos = content_end + 4; // skip past </a>
|
||||
if pos >= xml.len() - 5 { break; }
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn extract_ncx_src(nav_xml: &str) -> Option<String> {
|
||||
if let Some(i) = nav_xml.find("src=") {
|
||||
let after = &nav_xml[i + 4..];
|
||||
let delim = after.as_bytes()[0];
|
||||
let value_start = if delim == b'"' || delim == b'\'' { 1 } else { 0 };
|
||||
let value = &after[value_start..];
|
||||
if let Some(end) = value.find(if delim == b'"' || delim == b'\'' { delim as char } else { ' ' }) {
|
||||
return Some(value[..end].to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_attr(tag_text: &str, attr_name: &str) -> Option<String> {
|
||||
let search = format!("{}=", attr_name);
|
||||
let pos = tag_text.find(&search)?;
|
||||
let after = &tag_text[pos + search.len()..];
|
||||
let delim = after.as_bytes().first().copied().unwrap_or(b'"');
|
||||
let value_start = if delim == b'"' || delim == b'\'' { 1 } else { 0 };
|
||||
let value = &after[value_start..];
|
||||
let end = value.find(if delim == b'"' || delim == b'\'' { delim as char } else { ' ' })?;
|
||||
Some(value[..end].to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build a minimal valid EPUB zip in memory.
|
||||
fn minimal_epub(title: &str, author: &str, chapters: &[&str]) -> Vec<u8> {
|
||||
let mut buf = std::io::Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = zip::ZipWriter::new(&mut buf);
|
||||
|
||||
let opts = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Stored);
|
||||
|
||||
// mimetype (must be first, uncompressed)
|
||||
zip.start_file("mimetype", opts).unwrap();
|
||||
zip.write_all(b"application/epub+zip").unwrap();
|
||||
|
||||
// container.xml
|
||||
zip.start_file("META-INF/container.xml", opts).unwrap();
|
||||
zip.write_all(
|
||||
b"<?xml version=\"1.0\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>"
|
||||
).unwrap();
|
||||
|
||||
// OPF with metadata and spine
|
||||
let mut opf = format!(
|
||||
"<?xml version=\"1.0\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"2.0\">\n<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n <dc:title>{}</dc:title>\n <dc:creator>{}</dc:creator>\n</metadata>\n<manifest>\n <item id=\"ncx\" href=\"toc.ncx\" media-type=\"application/x-dtbncx+xml\"/>\n",
|
||||
title, author
|
||||
);
|
||||
for (i, ch) in chapters.iter().enumerate() {
|
||||
opf.push_str(&format!(
|
||||
" <item id=\"ch{}\" href=\"{}\" media-type=\"application/xhtml+xml\"/>\n",
|
||||
i, ch
|
||||
));
|
||||
}
|
||||
opf.push_str("</manifest>\n<spine toc=\"ncx\">\n");
|
||||
for i in 0..chapters.len() {
|
||||
opf.push_str(&format!(" <itemref idref=\"ch{}\"/>\n", i));
|
||||
}
|
||||
opf.push_str("</spine>\n</package>");
|
||||
|
||||
zip.start_file("OEBPS/content.opf", opts).unwrap();
|
||||
zip.write_all(opf.as_bytes()).unwrap();
|
||||
|
||||
// NCX
|
||||
let mut ncx = String::from(
|
||||
"<?xml version=\"1.0\"?>\n<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\">\n<navMap>\n"
|
||||
);
|
||||
for (i, ch) in chapters.iter().enumerate() {
|
||||
ncx.push_str(&format!(
|
||||
" <navPoint id=\"nav{}\" playOrder=\"{}\">\n <navLabel><text>Chapter {}</text></navLabel>\n <content src=\"{}\"/>\n </navPoint>\n",
|
||||
i, i + 1, i + 1, ch
|
||||
));
|
||||
}
|
||||
ncx.push_str("</navMap>\n</ncx>");
|
||||
|
||||
zip.start_file("toc.ncx", opts).unwrap();
|
||||
zip.write_all(ncx.as_bytes()).unwrap();
|
||||
|
||||
// Chapter files (empty)
|
||||
for ch in chapters {
|
||||
zip.start_file(*ch, opts).unwrap();
|
||||
zip.write_all(b"<html/>").unwrap();
|
||||
}
|
||||
|
||||
zip.finish().unwrap();
|
||||
}
|
||||
buf.into_inner()
|
||||
}
|
||||
|
||||
fn write_temp(data: &[u8], name: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join("zx_doc_epub_test");
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, data).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_epub_metadata() {
|
||||
let data = minimal_epub("Test Book", "Author Name", &["OEBPS/ch1.xhtml", "OEBPS/ch2.xhtml", "OEBPS/ch3.xhtml"]);
|
||||
let path = write_temp(&data, "test.epub");
|
||||
let meta = read_epub_metadata(&path).unwrap();
|
||||
assert_eq!(meta.title, Some("Test Book".into()));
|
||||
assert_eq!(meta.author, Some("Author Name".into()));
|
||||
assert_eq!(meta.chapter_count, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_epub_chapters() {
|
||||
let data = minimal_epub("Book", "Author", &["OEBPS/intro.xhtml", "OEBPS/ch1.xhtml"]);
|
||||
let path = write_temp(&data, "test_chapters.epub");
|
||||
let chapters = read_epub_chapters(&path).unwrap();
|
||||
assert_eq!(chapters.len(), 2);
|
||||
assert_eq!(chapters[0].chapter_id, "ch0");
|
||||
assert_eq!(chapters[0].title, "Chapter 1");
|
||||
assert_eq!(chapters[0].path, "OEBPS/intro.xhtml");
|
||||
assert_eq!(chapters[1].play_order, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_epub_empty() {
|
||||
let data = minimal_epub("Empty", "Author", &[]);
|
||||
let path = write_temp(&data, "empty.epub");
|
||||
let meta = read_epub_metadata(&path).unwrap();
|
||||
assert_eq!(meta.chapter_count, 0);
|
||||
let chapters = read_epub_chapters(&path).unwrap();
|
||||
assert!(chapters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_simple_epub() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap();
|
||||
let path = root.join("fixtures/epub/simple.epub");
|
||||
if path.exists() {
|
||||
let meta = read_epub_metadata(&path).unwrap();
|
||||
assert_eq!(meta.title, Some("Simple Book".into()));
|
||||
assert_eq!(meta.chapter_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_epub_with_toc() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap();
|
||||
let path = root.join("fixtures/epub/epub_with_toc.epub");
|
||||
if path.exists() {
|
||||
let chapters = read_epub_chapters(&path).unwrap();
|
||||
assert_eq!(chapters.len(), 3);
|
||||
assert_eq!(chapters[0].title, "Chapter 1");
|
||||
}
|
||||
}
|
||||
|
||||
fn minimal_epub3(title: &str, chapters: &[&str]) -> Vec<u8> {
|
||||
let mut buf = std::io::Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = zip::ZipWriter::new(&mut buf);
|
||||
let opts = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Stored);
|
||||
|
||||
zip.start_file("mimetype", opts).unwrap();
|
||||
zip.write_all(b"application/epub+zip").unwrap();
|
||||
|
||||
zip.start_file("META-INF/container.xml", opts).unwrap();
|
||||
zip.write_all(
|
||||
b"<?xml version=\"1.0\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>"
|
||||
).unwrap();
|
||||
|
||||
// OPF with NAV instead of NCX
|
||||
let mut opf = format!(
|
||||
"<?xml version=\"1.0\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\">\n<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n <dc:title>{}</dc:title>\n</metadata>\n<manifest>\n <item id=\"nav\" href=\"nav.xhtml\" media-type=\"application/xhtml+xml\" properties=\"nav\"/>\n",
|
||||
title
|
||||
);
|
||||
for (i, ch) in chapters.iter().enumerate() {
|
||||
opf.push_str(&format!(
|
||||
" <item id=\"ch{}\" href=\"{}\" media-type=\"application/xhtml+xml\"/>\n",
|
||||
i, ch
|
||||
));
|
||||
}
|
||||
opf.push_str("</manifest>\n<spine>\n");
|
||||
for i in 0..chapters.len() {
|
||||
opf.push_str(&format!(" <itemref idref=\"ch{}\"/>\n", i));
|
||||
}
|
||||
opf.push_str("</spine>\n</package>");
|
||||
|
||||
zip.start_file("OEBPS/content.opf", opts).unwrap();
|
||||
zip.write_all(opf.as_bytes()).unwrap();
|
||||
|
||||
// NAV XHTML with <nav epub:type="toc">
|
||||
let mut nav = String::from("<?xml version=\"1.0\"?>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<body>\n<nav epub:type=\"toc\">\n<ol>\n");
|
||||
for (i, ch) in chapters.iter().enumerate() {
|
||||
nav.push_str(&format!(
|
||||
" <li><a href=\"{}\">Chapter {}</a></li>\n",
|
||||
ch, i + 1
|
||||
));
|
||||
}
|
||||
nav.push_str("</ol>\n</nav>\n</body>\n</html>");
|
||||
|
||||
zip.start_file("nav.xhtml", opts).unwrap();
|
||||
zip.write_all(nav.as_bytes()).unwrap();
|
||||
|
||||
for ch in chapters {
|
||||
zip.start_file(*ch, opts).unwrap();
|
||||
zip.write_all(b"<html/>").unwrap();
|
||||
}
|
||||
|
||||
zip.finish().unwrap();
|
||||
}
|
||||
buf.into_inner()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_epub3_nav_fallback() {
|
||||
let data = minimal_epub3("EPUB3 Book", &["OEBPS/ch1.xhtml", "OEBPS/ch2.xhtml", "OEBPS/ch3.xhtml"]);
|
||||
let path = write_temp(&data, "test_epub3.epub");
|
||||
// No NCX — should fall back to NAV parsing
|
||||
let chapters = read_epub_chapters(&path).unwrap();
|
||||
assert_eq!(chapters.len(), 3);
|
||||
assert_eq!(chapters[0].title, "Chapter 1");
|
||||
assert_eq!(chapters[1].title, "Chapter 2");
|
||||
assert_eq!(chapters[2].title, "Chapter 3");
|
||||
}
|
||||
|
||||
// ── strip_html tests ──
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_removes_tags() {
|
||||
assert_eq!(strip_html("<p>Hello World</p>"), "Hello World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_collapses_whitespace() {
|
||||
assert_eq!(strip_html("<p>Hello World</p>"), "Hello World");
|
||||
assert_eq!(strip_html("<div>\n Line 1\n Line 2\n</div>"), "Line 1 Line 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_handles_nested_tags() {
|
||||
assert_eq!(strip_html("<div><p><b>Bold</b> text</p></div>"), "Bold text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_handles_self_closing_tags() {
|
||||
assert_eq!(strip_html("Text<br/>more<br />text"), "Textmoretext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_handles_attributes() {
|
||||
assert_eq!(strip_html("<p class=\"intro\" id='p1'>Hello</p>"), "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_empty_input() {
|
||||
assert_eq!(strip_html(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_html_plain_text_unchanged() {
|
||||
assert_eq!(strip_html("Plain text, no HTML tags."), "Plain text, no HTML tags.");
|
||||
}
|
||||
|
||||
// ── extract_epub_chapter_texts tests ──
|
||||
|
||||
fn epub_with_chapter_text(title: &str, chapters: &[(&str, &str)]) -> Vec<u8> {
|
||||
let mut buf = std::io::Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = zip::ZipWriter::new(&mut buf);
|
||||
let opts = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Stored);
|
||||
|
||||
zip.start_file("mimetype", opts).unwrap();
|
||||
zip.write_all(b"application/epub+zip").unwrap();
|
||||
|
||||
zip.start_file("META-INF/container.xml", opts).unwrap();
|
||||
zip.write_all(
|
||||
b"<?xml version=\"1.0\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>"
|
||||
).unwrap();
|
||||
|
||||
let mut opf = format!(
|
||||
"<?xml version=\"1.0\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"2.0\">\n<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n <dc:title>{}</dc:title>\n</metadata>\n<manifest>\n <item id=\"ncx\" href=\"toc.ncx\" media-type=\"application/x-dtbncx+xml\"/>\n",
|
||||
title
|
||||
);
|
||||
for (i, (href, _)) in chapters.iter().enumerate() {
|
||||
opf.push_str(&format!(" <item id=\"ch{}\" href=\"{}\" media-type=\"application/xhtml+xml\"/>\n", i, href));
|
||||
}
|
||||
opf.push_str("</manifest>\n<spine toc=\"ncx\">\n");
|
||||
for i in 0..chapters.len() {
|
||||
opf.push_str(&format!(" <itemref idref=\"ch{}\"/>\n", i));
|
||||
}
|
||||
opf.push_str("</spine>\n</package>");
|
||||
zip.start_file("OEBPS/content.opf", opts).unwrap();
|
||||
zip.write_all(opf.as_bytes()).unwrap();
|
||||
|
||||
let mut ncx = String::from("<?xml version=\"1.0\"?>\n<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\">\n<navMap>\n");
|
||||
for (i, (href, _)) in chapters.iter().enumerate() {
|
||||
ncx.push_str(&format!(
|
||||
" <navPoint id=\"nav{}\" playOrder=\"{}\">\n <navLabel><text>Ch {}</text></navLabel>\n <content src=\"{}\"/>\n </navPoint>\n",
|
||||
i, i + 1, i + 1, href
|
||||
));
|
||||
}
|
||||
ncx.push_str("</navMap>\n</ncx>");
|
||||
zip.start_file("toc.ncx", opts).unwrap();
|
||||
zip.write_all(ncx.as_bytes()).unwrap();
|
||||
|
||||
for (href, content) in chapters {
|
||||
zip.start_file(*href, opts).unwrap();
|
||||
zip.write_all(content.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
zip.finish().unwrap();
|
||||
}
|
||||
buf.into_inner()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_chapter_texts_with_html() {
|
||||
let data = epub_with_chapter_text("Test", &[
|
||||
("OEBPS/ch1.xhtml", "<html><body><p>Chapter one content.</p></body></html>"),
|
||||
("OEBPS/ch2.xhtml", "<html><body><h1>Title</h1><p>Chapter <b>two</b>.</p></body></html>"),
|
||||
]);
|
||||
let path = write_temp(&data, "chapter_text.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert_eq!(texts.len(), 2);
|
||||
assert_eq!(texts[0].text, "Chapter one content.");
|
||||
assert_eq!(texts[1].text, "TitleChapter two.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_chapter_texts_empty_chapter_filtered() {
|
||||
let data = epub_with_chapter_text("Empty", &[
|
||||
("OEBPS/ch1.xhtml", "<html><body></body></html>"),
|
||||
("OEBPS/ch2.xhtml", "<html><body><p>Not empty</p></body></html>"),
|
||||
]);
|
||||
let path = write_temp(&data, "empty_ch.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert_eq!(texts.len(), 1);
|
||||
assert_eq!(texts[0].text, "Not empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_chapter_texts_all_empty() {
|
||||
let data = epub_with_chapter_text("AllEmpty", &[
|
||||
("OEBPS/ch1.xhtml", "<html/>"),
|
||||
]);
|
||||
let path = write_temp(&data, "all_empty.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert!(texts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_chapter_texts_plain_text_preserved() {
|
||||
let data = epub_with_chapter_text("Plain", &[
|
||||
("OEBPS/ch1.xhtml", "No HTML tags, just plain text."),
|
||||
]);
|
||||
let path = write_temp(&data, "plain.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert_eq!(texts.len(), 1);
|
||||
assert_eq!(texts[0].text, "No HTML tags, just plain text.");
|
||||
}
|
||||
|
||||
// ── Path construction variants ──
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_text_with_full_path_chapter() {
|
||||
// chapter_id already contains "/" → used as-is
|
||||
let data = epub_with_chapter_text("Path", &[
|
||||
("OEBPS/ch1.xhtml", "<html><p>Full path chapter.</p></html>"),
|
||||
]);
|
||||
let path = write_temp(&data, "fullpath.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert_eq!(texts.len(), 1);
|
||||
assert_eq!(texts[0].text, "Full path chapter.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_text_with_html_suffix() {
|
||||
// chapter_id with .html suffix → used as-is
|
||||
let data = epub_with_chapter_text("Html", &[
|
||||
("OEBPS/ch1.html", "<html><p>HTML suffix chapter.</p></html>"),
|
||||
]);
|
||||
let path = write_temp(&data, "html_suffix.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert_eq!(texts.len(), 1);
|
||||
assert_eq!(texts[0].text, "HTML suffix chapter.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_epub_text_bare_id_fallback() {
|
||||
// Simple chapter ID (no /, .html, .xhtml) → OEBPS/{id}.xhtml
|
||||
let data = epub_with_chapter_text("Bare", &[
|
||||
("OEBPS/ch1.xhtml", "<html><p>Bare ID fallback.</p></html>"),
|
||||
]);
|
||||
let path = write_temp(&data, "bare_id.epub");
|
||||
let texts = extract_epub_chapter_texts(&path).unwrap();
|
||||
assert_eq!(texts.len(), 1);
|
||||
assert_eq!(texts[0].text, "Bare ID fallback.");
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,64 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::progress::ReadingPosition;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Maximum number of events before oldest are dropped to prevent unbounded memory growth.
|
||||
const MAX_BUFFER_SIZE: usize = 1000;
|
||||
|
||||
// Global event buffer, protected by a Mutex for thread safety.
|
||||
static EVENT_BUFFER: Mutex<Vec<ReadingEvent>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Push a reading event into the global buffer.
|
||||
/// If the buffer exceeds MAX_BUFFER_SIZE, the oldest event is dropped.
|
||||
#[deprecated(since = "0.2.0", note = "Use events_v2::push_heartbeat_v2 etc. instead")]
|
||||
pub fn push_reading_event(event: ReadingEvent) {
|
||||
match EVENT_BUFFER.lock() {
|
||||
Ok(mut buf) => {
|
||||
if buf.len() >= MAX_BUFFER_SIZE {
|
||||
buf.remove(0);
|
||||
}
|
||||
buf.push(event);
|
||||
}
|
||||
Err(_) => { /* poison: silently drop event */ }
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(since = "0.2.0", note = "Use events_v2::push_position_changed_v2 instead")]
|
||||
/// Record a position change as a PositionChanged event.
|
||||
pub fn update_reading_position(material_id: &str, position: ReadingPosition) {
|
||||
let event = ReadingEvent::PositionChanged {
|
||||
material_id: material_id.to_string(),
|
||||
position,
|
||||
timestamp_ms: now_ms(),
|
||||
};
|
||||
push_reading_event(event);
|
||||
}
|
||||
|
||||
/// Export all pending events without clearing.
|
||||
#[deprecated(since = "0.2.0", note = "Use events_v2::export_pending_events_v2 instead")]
|
||||
pub fn export_pending_events() -> Vec<ReadingEvent> {
|
||||
EVENT_BUFFER.lock().map(|buf| buf.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Remove the first `count` events after successful upload.
|
||||
pub fn clear_exported_events(count: usize) {
|
||||
if let Ok(mut buf) = EVENT_BUFFER.lock() {
|
||||
let n = count.min(buf.len());
|
||||
buf.drain(..n);
|
||||
}
|
||||
}
|
||||
|
||||
fn now_ms() -> i64 {
|
||||
use std::time::SystemTime;
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ReadingEvent {
|
||||
MaterialOpened {
|
||||
@ -34,6 +90,14 @@ pub enum ReadingEvent {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Serialize event buffer tests to prevent races on the global EVENT_BUFFER.
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn setup() {
|
||||
clear_exported_events(usize::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_material_opened_serde() {
|
||||
@ -107,4 +171,62 @@ mod tests {
|
||||
let back: ReadingEvent = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_and_export() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
setup();
|
||||
push_reading_event(ReadingEvent::MaterialOpened {
|
||||
material_id: "m1".into(),
|
||||
timestamp_ms: 1000,
|
||||
});
|
||||
push_reading_event(ReadingEvent::MarkedAsRead {
|
||||
material_id: "m1".into(),
|
||||
timestamp_ms: 2000,
|
||||
});
|
||||
let events = export_pending_events();
|
||||
assert_eq!(events.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_exported() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
setup();
|
||||
push_reading_event(ReadingEvent::MaterialOpened {
|
||||
material_id: "m1".into(),
|
||||
timestamp_ms: 1000,
|
||||
});
|
||||
push_reading_event(ReadingEvent::MaterialOpened {
|
||||
material_id: "m2".into(),
|
||||
timestamp_ms: 2000,
|
||||
});
|
||||
push_reading_event(ReadingEvent::MaterialOpened {
|
||||
material_id: "m3".into(),
|
||||
timestamp_ms: 3000,
|
||||
});
|
||||
clear_exported_events(2);
|
||||
let events = export_pending_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_reading_position() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
setup();
|
||||
update_reading_position(
|
||||
"m1",
|
||||
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 },
|
||||
);
|
||||
let events = export_pending_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(matches!(events[0], ReadingEvent::PositionChanged { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_export() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
setup();
|
||||
let events = export_pending_events();
|
||||
assert!(events.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
709
crates/zx_document_core/src/events_v2.rs
Normal file
709
crates/zx_document_core/src/events_v2.rs
Normal file
@ -0,0 +1,709 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::progress::ReadingPosition;
|
||||
use crate::session_v2;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 1000;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BufferedEventState {
|
||||
Pending,
|
||||
Exported,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BufferedReadingEventV2 {
|
||||
pub event: ReadingEventV2,
|
||||
pub state: BufferedEventState,
|
||||
pub exported_at_ms: Option<i64>,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
|
||||
fn buffer() -> &'static Mutex<Vec<BufferedReadingEventV2>> {
|
||||
use std::sync::OnceLock;
|
||||
static BUF: OnceLock<Mutex<Vec<BufferedReadingEventV2>>> = OnceLock::new();
|
||||
BUF.get_or_init(|| Mutex::new(Vec::new()))
|
||||
}
|
||||
|
||||
// ── V2 Event Types ──
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum ReadingEventTypeV2 {
|
||||
MaterialOpened,
|
||||
MaterialClosed,
|
||||
PositionChanged,
|
||||
Heartbeat,
|
||||
MarkedAsRead,
|
||||
}
|
||||
|
||||
// ── V2 Event ──
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadingEventV2 {
|
||||
pub event_id: String,
|
||||
pub client_session_id: String,
|
||||
pub material_id: String,
|
||||
pub event_type: ReadingEventTypeV2,
|
||||
pub position: Option<ReadingPosition>,
|
||||
pub active_seconds_delta: u32,
|
||||
pub timestamp_ms: i64,
|
||||
pub sequence: u64,
|
||||
}
|
||||
|
||||
// ── Push Functions ──
|
||||
|
||||
fn push_event(
|
||||
session_id: &str,
|
||||
material_id: &str,
|
||||
event_type: ReadingEventTypeV2,
|
||||
position: Option<ReadingPosition>,
|
||||
active_seconds_delta: u32,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||
let seq = session_v2::record_session_event_v2(
|
||||
session_id,
|
||||
timestamp_ms,
|
||||
active_seconds_delta,
|
||||
position.clone(),
|
||||
)?;
|
||||
|
||||
let event = ReadingEventV2 {
|
||||
event_id: uuid::Uuid::new_v4().to_string(),
|
||||
client_session_id: session_id.to_string(),
|
||||
material_id: material_id.to_string(),
|
||||
event_type,
|
||||
position: position.map(|p| p.normalized()),
|
||||
active_seconds_delta,
|
||||
timestamp_ms,
|
||||
sequence: seq,
|
||||
};
|
||||
|
||||
let buffered = BufferedReadingEventV2 {
|
||||
event: event.clone(),
|
||||
state: BufferedEventState::Pending,
|
||||
exported_at_ms: None,
|
||||
retry_count: 0,
|
||||
};
|
||||
|
||||
match buffer().lock() {
|
||||
Ok(mut buf) => {
|
||||
if buf.len() >= MAX_BUFFER_SIZE {
|
||||
// Evict in order: Failed → Exported → oldest Pending
|
||||
if let Some(idx) = buf.iter().position(|b| b.state == BufferedEventState::Failed) {
|
||||
buf.remove(idx);
|
||||
} else if let Some(idx) = buf.iter().position(|b| b.state == BufferedEventState::Exported) {
|
||||
buf.remove(idx);
|
||||
} else {
|
||||
buf.remove(0); // oldest Pending
|
||||
}
|
||||
}
|
||||
buf.push(buffered);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub fn push_material_opened_v2(
|
||||
session_id: &str,
|
||||
material_id: &str,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||
push_event(session_id, material_id, ReadingEventTypeV2::MaterialOpened, None, 0, timestamp_ms)
|
||||
}
|
||||
|
||||
pub fn push_material_closed_v2(
|
||||
session_id: &str,
|
||||
material_id: &str,
|
||||
active_seconds_delta: u32,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||
push_event(session_id, material_id, ReadingEventTypeV2::MaterialClosed, None, active_seconds_delta, timestamp_ms)
|
||||
}
|
||||
|
||||
pub fn push_position_changed_v2(
|
||||
session_id: &str,
|
||||
material_id: &str,
|
||||
position: ReadingPosition,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||
push_event(session_id, material_id, ReadingEventTypeV2::PositionChanged, Some(position), 0, timestamp_ms)
|
||||
}
|
||||
|
||||
pub fn push_heartbeat_v2(
|
||||
session_id: &str,
|
||||
material_id: &str,
|
||||
active_seconds_delta: u32,
|
||||
position: Option<ReadingPosition>,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||
push_event(session_id, material_id, ReadingEventTypeV2::Heartbeat, position, active_seconds_delta, timestamp_ms)
|
||||
}
|
||||
|
||||
pub fn push_marked_as_read_v2(
|
||||
session_id: &str,
|
||||
material_id: &str,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||
push_event(session_id, material_id, ReadingEventTypeV2::MarkedAsRead, None, 0, timestamp_ms)
|
||||
}
|
||||
|
||||
// ── Buffer Management (V2 with ack) ──
|
||||
|
||||
/// Export Pending and Failed events. Marks them as Exported.
|
||||
pub fn export_pending_events_v2(limit: u32, timestamp_ms: i64) -> Vec<ReadingEventV2> {
|
||||
match buffer().lock() {
|
||||
Ok(mut buf) => {
|
||||
let mut result = Vec::new();
|
||||
let mut count = 0u32;
|
||||
for item in buf.iter_mut() {
|
||||
if count >= limit { break; }
|
||||
if item.state == BufferedEventState::Pending || item.state == BufferedEventState::Failed {
|
||||
item.state = BufferedEventState::Exported;
|
||||
item.exported_at_ms = Some(timestamp_ms);
|
||||
result.push(item.event.clone());
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledge events by eventId. Removes them from the buffer.
|
||||
pub fn ack_events_v2(event_ids: &[String]) -> u32 {
|
||||
match buffer().lock() {
|
||||
Ok(mut buf) => {
|
||||
let before = buf.len();
|
||||
buf.retain(|item| !event_ids.contains(&item.event.event_id));
|
||||
(before - buf.len()) as u32
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark exported events as Failed (for retry).
|
||||
pub fn mark_events_failed_v2(event_ids: &[String]) -> u32 {
|
||||
match buffer().lock() {
|
||||
Ok(mut buf) => {
|
||||
let mut count = 0;
|
||||
for item in buf.iter_mut() {
|
||||
if event_ids.contains(&item.event.event_id) {
|
||||
if item.state == BufferedEventState::Failed {
|
||||
continue; // already Failed, skip to avoid double-counting retry
|
||||
}
|
||||
item.state = BufferedEventState::Failed;
|
||||
item.retry_count += 1;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_size_v2() -> usize {
|
||||
buffer().lock().map(|b| b.len()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Buffer state snapshot for diagnostic / debug use.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct EventBufferStateV2 {
|
||||
pub total: u32,
|
||||
pub pending: u32,
|
||||
pub exported: u32,
|
||||
pub failed: u32,
|
||||
}
|
||||
|
||||
pub fn get_event_buffer_state_v2() -> EventBufferStateV2 {
|
||||
match buffer().lock() {
|
||||
Ok(buf) => {
|
||||
let total = buf.len() as u32;
|
||||
let pending = buf.iter().filter(|b| b.state == BufferedEventState::Pending).count() as u32;
|
||||
let exported = buf.iter().filter(|b| b.state == BufferedEventState::Exported).count() as u32;
|
||||
let failed = buf.iter().filter(|b| b.state == BufferedEventState::Failed).count() as u32;
|
||||
EventBufferStateV2 { total, pending, exported, failed }
|
||||
}
|
||||
Err(_) => EventBufferStateV2 { total: 0, pending: 0, exported: 0, failed: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_all_events_v2() {
|
||||
if let Ok(mut buf) = buffer().lock() {
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset stale Exported events back to Pending (for app startup recovery).
|
||||
/// Events that were exported but never acked (iOS crash) should be re-exported.
|
||||
pub fn reload_stale_events_v2() -> u32 {
|
||||
match buffer().lock() {
|
||||
Ok(mut buf) => {
|
||||
let mut count = 0;
|
||||
for item in buf.iter_mut() {
|
||||
if item.state == BufferedEventState::Exported {
|
||||
item.state = BufferedEventState::Pending;
|
||||
item.exported_at_ms = None;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::*;
|
||||
use crate::reading_material::ReadingMaterialRefV2;
|
||||
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn setup_session(material_id: &str) -> String {
|
||||
// Lock is held for the duration of each test via the caller.
|
||||
let mat = ReadingMaterialRefV2::new(material_id, "/tmp/test.md", "test.md", "markdown");
|
||||
let id = session_v2::start_reading_session_v2(mat, 1000).unwrap();
|
||||
clear_all_events_v2();
|
||||
id
|
||||
}
|
||||
|
||||
fn lock_test() -> std::sync::MutexGuard<'static, ()> {
|
||||
TEST_LOCK.lock().unwrap()
|
||||
}
|
||||
|
||||
fn teardown_session(id: &str) {
|
||||
let _ = session_v2::close_reading_session_v2(id, 0);
|
||||
let _ = session_v2::remove_session_v2(id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_opened() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_1");
|
||||
let ev = push_material_opened_v2(&sid, "mat_1", 1000).unwrap();
|
||||
assert_eq!(ev.event_type, ReadingEventTypeV2::MaterialOpened);
|
||||
assert_eq!(ev.active_seconds_delta, 0);
|
||||
assert_eq!(ev.sequence, 1);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_closed() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_2");
|
||||
let ev = push_material_closed_v2(&sid, "mat_2", 0, 5000).unwrap();
|
||||
assert_eq!(ev.event_type, ReadingEventTypeV2::MaterialClosed);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_increments() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_seq");
|
||||
let e1 = push_material_opened_v2(&sid, "mat_seq", 1000).unwrap();
|
||||
// Use delta=0 for tracker-independent tests
|
||||
let e2 = push_position_changed_v2(&sid, "mat_seq", ReadingPosition::Unknown, 2000).unwrap();
|
||||
let e3 = push_material_closed_v2(&sid, "mat_seq", 0, 3000).unwrap();
|
||||
assert_eq!(e1.sequence, 1);
|
||||
assert_eq!(e2.sequence, 2);
|
||||
assert_eq!(e3.sequence, 3);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_ack_flow() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_ack");
|
||||
let e1 = push_material_opened_v2(&sid, "mat_ack", 1000).unwrap();
|
||||
let e2 = push_heartbeat_v2(&sid, "mat_ack", 15, None, 2000).unwrap();
|
||||
let my_ids = [e1.event_id.clone(), e2.event_id.clone()];
|
||||
|
||||
// Export: should return at least our 2 events
|
||||
let exported = export_pending_events_v2(100, 5000);
|
||||
assert!(exported.len() >= 2);
|
||||
|
||||
// Export again: our events should NOT be re-exported (already Exported)
|
||||
let exported2 = export_pending_events_v2(100, 6000);
|
||||
let ours_re_exported = exported2.iter().any(|e| my_ids.contains(&e.event_id));
|
||||
assert!(!ours_re_exported, "exported events should not be re-exported");
|
||||
|
||||
// Ack: remove our 2 events (may be >=2 in parallel)
|
||||
let removed = ack_events_v2(&my_ids);
|
||||
assert!(removed >= 2, "expected >=2 removed, got {removed}");
|
||||
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_failed_and_retry() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_retry");
|
||||
let e1 = push_heartbeat_v2(&sid, "mat_retry", 15, None, 1000).unwrap();
|
||||
|
||||
// Export: exports pending events
|
||||
let exported = export_pending_events_v2(100, 2000);
|
||||
assert!(exported.iter().any(|e| e.event_id == e1.event_id));
|
||||
|
||||
// Mark as failed (simulating upload failure)
|
||||
let marked = mark_events_failed_v2(&[e1.event_id.clone()]);
|
||||
assert_eq!(marked, 1);
|
||||
|
||||
// Export again: should return the failed event for retry
|
||||
let retry = export_pending_events_v2(100, 3000);
|
||||
assert!(retry.iter().any(|e| e.event_id == e1.event_id));
|
||||
|
||||
// Ack
|
||||
ack_events_v2(&[e1.event_id.clone()]);
|
||||
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_size_not_exceeded() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_overflow");
|
||||
let sz = buffer_size_v2();
|
||||
// Push enough to reach or exceed MAX
|
||||
for i in 0..(MAX_BUFFER_SIZE + 10).saturating_sub(sz) {
|
||||
push_material_opened_v2(&sid, "mat_overflow", 1000 + i as i64).unwrap();
|
||||
}
|
||||
// Buffer should never exceed MAX
|
||||
assert!(buffer_size_v2() <= MAX_BUFFER_SIZE);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ack_nonexistent_no_crash() {
|
||||
let _guard = lock_test();
|
||||
clear_all_events_v2();
|
||||
let removed = ack_events_v2(&["nonexistent".to_string()]);
|
||||
assert_eq!(removed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closed_session_rejects() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_reject");
|
||||
session_v2::close_reading_session_v2(&sid, 3000).unwrap();
|
||||
assert!(push_heartbeat_v2(&sid, "mat_reject", 15, None, 3000).is_err());
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serde_roundtrip() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_serde");
|
||||
let ev = push_material_opened_v2(&sid, "mat_serde", 1000).unwrap();
|
||||
let json = serde_json::to_string(&ev).unwrap();
|
||||
assert!(json.contains("materialOpened") || json.contains("\"type\":\"MaterialOpened\""));
|
||||
let back: ReadingEventV2 = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.event_id, ev.event_id);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_position_changed() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_pc");
|
||||
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
||||
let ev = push_position_changed_v2(&sid, "mat_pc", pos.clone(), 2000).unwrap();
|
||||
assert_eq!(ev.event_type, ReadingEventTypeV2::PositionChanged);
|
||||
assert_eq!(ev.active_seconds_delta, 0);
|
||||
assert_eq!(ev.material_id, "mat_pc");
|
||||
assert!(ev.position.is_some());
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_heartbeat_with_position() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_hb");
|
||||
let pos = ReadingPosition::Pdf { page_number: 3, page_progress: 0.5, overall_progress: 0.1 };
|
||||
let ev = push_heartbeat_v2(&sid, "mat_hb", 30, Some(pos.clone()), 3000).unwrap();
|
||||
assert_eq!(ev.event_type, ReadingEventTypeV2::Heartbeat);
|
||||
assert_eq!(ev.active_seconds_delta, 30);
|
||||
assert_eq!(ev.material_id, "mat_hb");
|
||||
assert!(ev.position.is_some());
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_heartbeat_without_position() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_hb2");
|
||||
let ev = push_heartbeat_v2(&sid, "mat_hb2", 10, None, 3000).unwrap();
|
||||
assert_eq!(ev.event_type, ReadingEventTypeV2::Heartbeat);
|
||||
assert_eq!(ev.active_seconds_delta, 10);
|
||||
assert!(ev.position.is_none());
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_marked_as_read() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_mar");
|
||||
let ev = push_marked_as_read_v2(&sid, "mat_mar", 4000).unwrap();
|
||||
assert_eq!(ev.event_type, ReadingEventTypeV2::MarkedAsRead);
|
||||
assert_eq!(ev.active_seconds_delta, 0);
|
||||
assert_eq!(ev.material_id, "mat_mar");
|
||||
assert!(ev.position.is_none());
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_id_is_non_empty() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_id");
|
||||
let ev = push_material_opened_v2(&sid, "mat_id", 1000).unwrap();
|
||||
assert!(!ev.event_id.is_empty());
|
||||
assert_eq!(ev.event_id.len(), 36); // UUID v4 standard
|
||||
assert!(ev.event_id.contains('-'));
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_ids_are_unique() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_uniq");
|
||||
let e1 = push_material_opened_v2(&sid, "mat_uniq", 1000).unwrap();
|
||||
let e2 = push_position_changed_v2(&sid, "mat_uniq", ReadingPosition::Unknown, 2000).unwrap();
|
||||
assert_ne!(e1.event_id, e2.event_id);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_session_id_present() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_csid");
|
||||
let ev = push_material_opened_v2(&sid, "mat_csid", 1000).unwrap();
|
||||
assert_eq!(ev.client_session_id, sid);
|
||||
assert!(!ev.client_session_id.is_empty());
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_ms_set() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_ts");
|
||||
let ev = push_material_opened_v2(&sid, "mat_ts", 123456789).unwrap();
|
||||
assert_eq!(ev.timestamp_ms, 123456789);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_normalized_in_event() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_norm");
|
||||
let pos = ReadingPosition::Pdf { page_number: 1, page_progress: 2.5, overall_progress: -0.5 };
|
||||
let ev = push_position_changed_v2(&sid, "mat_norm", pos, 1000).unwrap();
|
||||
let p = ev.position.unwrap();
|
||||
match p {
|
||||
ReadingPosition::Pdf { page_progress, overall_progress, .. } => {
|
||||
assert_eq!(page_progress, 1.0); // clamped from 2.5
|
||||
assert_eq!(overall_progress, 0.0); // clamped from -0.5
|
||||
}
|
||||
_ => panic!("expected Pdf position"),
|
||||
}
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_five_event_types_have_distinct_types() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_types");
|
||||
let e1 = push_material_opened_v2(&sid, "mat_types", 1000).unwrap();
|
||||
let e2 = push_material_closed_v2(&sid, "mat_types", 0, 2000).unwrap();
|
||||
// Need new session for more events since session is now closed
|
||||
teardown_session(&sid);
|
||||
|
||||
let sid2 = setup_session("mat_types2");
|
||||
let e3 = push_position_changed_v2(&sid2, "mat_types2", ReadingPosition::Unknown, 1000).unwrap();
|
||||
let e4 = push_heartbeat_v2(&sid2, "mat_types2", 10, None, 2000).unwrap();
|
||||
let e5 = push_marked_as_read_v2(&sid2, "mat_types2", 3000).unwrap();
|
||||
|
||||
assert_eq!(e1.event_type, ReadingEventTypeV2::MaterialOpened);
|
||||
assert_eq!(e2.event_type, ReadingEventTypeV2::MaterialClosed);
|
||||
assert_eq!(e3.event_type, ReadingEventTypeV2::PositionChanged);
|
||||
assert_eq!(e4.event_type, ReadingEventTypeV2::Heartbeat);
|
||||
assert_eq!(e5.event_type, ReadingEventTypeV2::MarkedAsRead);
|
||||
teardown_session(&sid2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reload_stale_events() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_stale");
|
||||
let e1 = push_heartbeat_v2(&sid, "mat_stale", 15, None, 1000).unwrap();
|
||||
let exported = export_pending_events_v2(100, 2000);
|
||||
assert!(exported.iter().any(|e| e.event_id == e1.event_id));
|
||||
let reloaded = reload_stale_events_v2();
|
||||
assert_eq!(reloaded, 1);
|
||||
let reexported = export_pending_events_v2(100, 3000);
|
||||
assert!(reexported.iter().any(|e| e.event_id == e1.event_id));
|
||||
ack_events_v2(&[e1.event_id.clone()]);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_buffer_lifecycle() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_lifecycle");
|
||||
|
||||
// Step 1: push 2 events
|
||||
let e1 = push_material_opened_v2(&sid, "mat_lifecycle", 1000).unwrap();
|
||||
let e2 = push_heartbeat_v2(&sid, "mat_lifecycle", 15, None, 2000).unwrap();
|
||||
let sz = buffer_size_v2();
|
||||
assert!(sz >= 2, "buffer should have at least 2 events, got {sz}");
|
||||
|
||||
// Step 2: export → events marked Exported
|
||||
let batch1 = export_pending_events_v2(100, 3000);
|
||||
assert!(batch1.iter().any(|e| e.event_id == e1.event_id));
|
||||
assert!(batch1.iter().any(|e| e.event_id == e2.event_id));
|
||||
|
||||
// Step 3: export again → should NOT re-export (already Exported)
|
||||
let batch2 = export_pending_events_v2(100, 4000);
|
||||
let reexported = batch2.iter().any(|e| e.event_id == e1.event_id || e.event_id == e2.event_id);
|
||||
assert!(!reexported, "acked events should not be re-exported");
|
||||
|
||||
// Step 4: ack e1 only
|
||||
let acked = ack_events_v2(&[e1.event_id.clone()]);
|
||||
assert_eq!(acked, 1);
|
||||
|
||||
// Step 5: e2 still in buffer (unacked), e1 is gone
|
||||
// Mark e2 as failed (simulates upload failure)
|
||||
let marked = mark_events_failed_v2(&[e2.event_id.clone()]);
|
||||
assert_eq!(marked, 1);
|
||||
|
||||
// Step 6: retry → failed events are re-exportable
|
||||
let batch3 = export_pending_events_v2(100, 5000);
|
||||
assert!(batch3.iter().any(|e| e.event_id == e2.event_id),
|
||||
"failed event should be re-exported");
|
||||
assert!(!batch3.iter().any(|e| e.event_id == e1.event_id),
|
||||
"acked event should not appear");
|
||||
|
||||
// Step 7: ack e2 to clean up
|
||||
ack_events_v2(&[e2.event_id.clone()]);
|
||||
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_limit_export() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_limit");
|
||||
|
||||
// Push 5 events
|
||||
let mut ids = Vec::new();
|
||||
for i in 0..5 {
|
||||
let e = push_material_opened_v2(&sid, "mat_limit", 1000 + i * 100).unwrap();
|
||||
ids.push(e.event_id);
|
||||
}
|
||||
|
||||
// Export with limit=2
|
||||
let batch = export_pending_events_v2(2, 5000);
|
||||
assert_eq!(batch.len(), 2);
|
||||
|
||||
// Next export should get the remaining 3
|
||||
let batch2 = export_pending_events_v2(10, 6000);
|
||||
let remaining = ids.len() as u32 - 2;
|
||||
assert!(batch2.len() >= remaining as usize,
|
||||
"expected at least {remaining} remaining, got {}", batch2.len());
|
||||
|
||||
// Clean up
|
||||
for e in [batch, batch2].concat() {
|
||||
ack_events_v2(&[e.event_id]);
|
||||
}
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_event_buffer_state() {
|
||||
let _guard = lock_test();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_state");
|
||||
|
||||
let s0 = get_event_buffer_state_v2();
|
||||
assert_eq!(s0.total, 0);
|
||||
|
||||
push_material_opened_v2(&sid, "mat_state", 1000).unwrap();
|
||||
let s1 = get_event_buffer_state_v2();
|
||||
assert_eq!(s1.total, 1);
|
||||
assert_eq!(s1.pending, 1);
|
||||
|
||||
let exported = export_pending_events_v2(100, 2000);
|
||||
assert!(!exported.is_empty());
|
||||
let s2 = get_event_buffer_state_v2();
|
||||
assert_eq!(s2.exported, 1);
|
||||
assert_eq!(s2.pending, 0);
|
||||
|
||||
ack_events_v2(&[exported[0].event_id.clone()]);
|
||||
let s3 = get_event_buffer_state_v2();
|
||||
assert_eq!(s3.total, 0);
|
||||
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_failed_idempotent() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_all_events_v2();
|
||||
let sid = setup_session("mat_idem");
|
||||
let e1 = push_heartbeat_v2(&sid, "mat_idem", 15, None, 1000).unwrap();
|
||||
export_pending_events_v2(100, 2000);
|
||||
// Mark twice — second should be a no-op
|
||||
let first = mark_events_failed_v2(&[e1.event_id.clone()]);
|
||||
assert_eq!(first, 1);
|
||||
let second = mark_events_failed_v2(&[e1.event_id.clone()]);
|
||||
assert_eq!(second, 0, "duplicate mark_failed should return 0, not double-count");
|
||||
ack_events_v2(&[e1.event_id.clone()]);
|
||||
teardown_session(&sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delta_semantics_per_event_type() {
|
||||
let _guard = lock_test();
|
||||
let sid = setup_session("mat_delta");
|
||||
|
||||
// material_opened: delta must be 0
|
||||
let e1 = push_material_opened_v2(&sid, "mat_delta", 1000).unwrap();
|
||||
assert_eq!(e1.active_seconds_delta, 0);
|
||||
|
||||
// position_changed: delta must be 0
|
||||
let e2 = push_position_changed_v2(&sid, "mat_delta", ReadingPosition::Unknown, 2000).unwrap();
|
||||
assert_eq!(e2.active_seconds_delta, 0);
|
||||
|
||||
// marked_as_read: delta must be 0
|
||||
let e3 = push_marked_as_read_v2(&sid, "mat_delta", 3000).unwrap();
|
||||
assert_eq!(e3.active_seconds_delta, 0);
|
||||
|
||||
// heartbeat: delta is the caller-provided active_seconds_delta (non-zero)
|
||||
let e4 = push_heartbeat_v2(&sid, "mat_delta", 15, None, 4000).unwrap();
|
||||
assert_eq!(e4.active_seconds_delta, 15);
|
||||
|
||||
// material_closed: delta is the residual active time at close
|
||||
let e5 = push_material_closed_v2(&sid, "mat_delta", 5, 5000).unwrap();
|
||||
assert_eq!(e5.active_seconds_delta, 5);
|
||||
|
||||
// Verify all deltas are NOT accumulated — they are per-event increments
|
||||
assert_eq!(e1.active_seconds_delta + e2.active_seconds_delta + e3.active_seconds_delta + e4.active_seconds_delta + e5.active_seconds_delta, 20);
|
||||
|
||||
teardown_session(&sid);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DocumentError;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct ImageMeta {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
@ -15,18 +15,25 @@ pub struct ImageMeta {
|
||||
|
||||
/// Read image metadata from a file path.
|
||||
pub fn read_image_meta(file_path: &str) -> Result<ImageMeta, DocumentError> {
|
||||
let path = Path::new(file_path);
|
||||
|
||||
let file_size = std::fs::metadata(file_path)
|
||||
.map(|m| m.len())?;
|
||||
|
||||
let format = path
|
||||
let reader = image::ImageReader::open(file_path).map_err(|e| {
|
||||
DocumentError::ParseError(format!("image open failed: {e}"))
|
||||
})?;
|
||||
|
||||
let format = reader
|
||||
.format()
|
||||
.map(image_format_name)
|
||||
.unwrap_or_else(|| {
|
||||
Path::new(file_path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_lowercase();
|
||||
.to_lowercase()
|
||||
});
|
||||
|
||||
let img = image::open(file_path).map_err(|e| {
|
||||
let img = reader.decode().map_err(|e| {
|
||||
DocumentError::ParseError(format!("image decode failed: {e}"))
|
||||
})?;
|
||||
|
||||
@ -40,6 +47,20 @@ pub fn read_image_meta(file_path: &str) -> Result<ImageMeta, DocumentError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn image_format_name(f: image::ImageFormat) -> String {
|
||||
match f {
|
||||
image::ImageFormat::Png => "png",
|
||||
image::ImageFormat::Jpeg => "jpeg",
|
||||
image::ImageFormat::Gif => "gif",
|
||||
image::ImageFormat::WebP => "webp",
|
||||
image::ImageFormat::Bmp => "bmp",
|
||||
image::ImageFormat::Tiff => "tiff",
|
||||
image::ImageFormat::Ico => "ico",
|
||||
_ => "unknown",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
pub mod anchors;
|
||||
pub mod blocks;
|
||||
pub mod document;
|
||||
pub mod epub;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod events_v2;
|
||||
pub mod image_meta;
|
||||
pub mod markdown;
|
||||
pub mod material_type;
|
||||
pub mod office;
|
||||
pub mod pdf;
|
||||
pub mod progress;
|
||||
pub mod reading_material;
|
||||
pub mod search;
|
||||
pub mod session_v2;
|
||||
pub mod text;
|
||||
pub mod time_tracker;
|
||||
|
||||
@ -146,6 +146,37 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
||||
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::FrontMatter(content) => {
|
||||
if content.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(DocumentBlock::CodeBlock {
|
||||
id: block_id(),
|
||||
language: Some("yaml".into()),
|
||||
code: content.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
NodeValue::FootnoteDefinition(_) => {
|
||||
let text = collect_text(child);
|
||||
if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(DocumentBlock::Paragraph { id: block_id(), text })
|
||||
}
|
||||
}
|
||||
NodeValue::Document
|
||||
| NodeValue::Item(_)
|
||||
| NodeValue::TableCell
|
||||
@ -349,4 +380,160 @@ mod tests {
|
||||
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::List { .. })));
|
||||
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::CodeBlock { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_complex_markdown() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap();
|
||||
let path = root.join("fixtures/markdown/complex_markdown.md");
|
||||
let md = std::fs::read_to_string(&path).expect("fixture not found");
|
||||
let blocks = parse_markdown(&md).unwrap();
|
||||
assert!(!blocks.is_empty(), "should parse at least some blocks");
|
||||
|
||||
// Verify all 8 block types are present
|
||||
let has_heading = blocks.iter().any(|b| matches!(b, DocumentBlock::Heading { .. }));
|
||||
let has_para = blocks.iter().any(|b| matches!(b, DocumentBlock::Paragraph { .. }));
|
||||
let has_list = blocks.iter().any(|b| matches!(b, DocumentBlock::List { .. }));
|
||||
let has_code = blocks.iter().any(|b| matches!(b, DocumentBlock::CodeBlock { .. }));
|
||||
let has_quote = blocks.iter().any(|b| matches!(b, DocumentBlock::Quote { .. }));
|
||||
let has_table = blocks.iter().any(|b| matches!(b, DocumentBlock::Table { .. }));
|
||||
let has_image = blocks.iter().any(|b| matches!(b, DocumentBlock::Image { .. }));
|
||||
let has_rule = blocks.iter().any(|b| matches!(b, DocumentBlock::HorizontalRule { .. }));
|
||||
|
||||
assert!(has_heading, "missing heading");
|
||||
assert!(has_para, "missing paragraph");
|
||||
assert!(has_list, "missing list");
|
||||
assert!(has_code, "missing code block");
|
||||
assert!(has_quote, "missing quote");
|
||||
assert!(has_table, "missing table");
|
||||
assert!(has_image, "missing image");
|
||||
assert!(has_rule, "missing horizontal rule");
|
||||
|
||||
// Verify heading level
|
||||
let headings: Vec<_> = blocks.iter().filter_map(|b| {
|
||||
if let DocumentBlock::Heading { level, text, .. } = b {
|
||||
Some((*level, text.clone()))
|
||||
} else { None }
|
||||
}).collect();
|
||||
assert!(headings.iter().any(|(l, _)| *l == 1), "should have h1");
|
||||
assert!(headings.iter().any(|(l, _)| *l == 2), "should have h2");
|
||||
assert!(headings.iter().any(|(l, _)| *l == 3), "should have h3");
|
||||
|
||||
// Verify code block has language
|
||||
let code_blocks: Vec<_> = blocks.iter().filter_map(|b| {
|
||||
if let DocumentBlock::CodeBlock { language, code, .. } = b {
|
||||
Some((language.clone(), code.clone()))
|
||||
} else { None }
|
||||
}).collect();
|
||||
assert!(code_blocks.iter().any(|(lang, _)| lang.as_deref() == Some("rust")), "should have rust code block");
|
||||
|
||||
// Verify table structure
|
||||
for b in &blocks {
|
||||
if let DocumentBlock::Table { headers, rows, .. } = b {
|
||||
assert!(!headers.is_empty(), "table should have headers");
|
||||
assert!(!rows.is_empty(), "table should have rows");
|
||||
assert_eq!(headers.len(), rows[0].len(), "table row width should match headers");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify image has src and alt
|
||||
for b in &blocks {
|
||||
if let DocumentBlock::Image { src, alt, .. } = b {
|
||||
assert!(!src.is_empty(), "image should have src");
|
||||
assert!(alt.is_some(), "image should have alt text");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify block_ids are non-empty and unique
|
||||
let ids: Vec<&str> = blocks.iter().map(|b| match b {
|
||||
DocumentBlock::Heading { id, .. } => id.as_str(),
|
||||
DocumentBlock::Paragraph { id, .. } => id.as_str(),
|
||||
DocumentBlock::List { id, .. } => id.as_str(),
|
||||
DocumentBlock::CodeBlock { id, .. } => id.as_str(),
|
||||
DocumentBlock::Quote { id, .. } => id.as_str(),
|
||||
DocumentBlock::Table { id, .. } => id.as_str(),
|
||||
DocumentBlock::Image { id, .. } => id.as_str(),
|
||||
DocumentBlock::HorizontalRule { id, .. } => id.as_str(),
|
||||
}).collect();
|
||||
for id in &ids {
|
||||
assert!(!id.is_empty(), "block_id should not be empty");
|
||||
}
|
||||
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
||||
assert_eq!(unique.len(), ids.len(), "all block_ids should be unique");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_lists() {
|
||||
let md = "- Item 1\n - Sub 1.1\n - Sub 1.2\n- Item 2\n";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
// Nested list items are flattened — at minimum we should have > 0 blocks
|
||||
assert!(!blocks.is_empty(), "nested list should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_and_emoji() {
|
||||
let md = "# 中文标题 🎉\n\n包含 emoji 和 Unicode 的段落。\n\n## 日本語見出し\n\n日本語の段落。";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::Heading { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_front_matter_handled() {
|
||||
let md = "---\ntitle: Test\n---\n\n# Content\n\nBody text.";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::Heading { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_blocks_filtered() {
|
||||
let md = "# Heading\n\n\n\nParagraph\n\n\n";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
let paras = blocks.iter().filter(|b| matches!(b, DocumentBlock::Paragraph { .. })).count();
|
||||
assert_eq!(paras, 1, "multiple blank lines should not create empty paragraphs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_very_long_document() {
|
||||
let mut md = String::from("# Long\n\n");
|
||||
for i in 0..500 {
|
||||
md.push_str(&format!("Para {i}. Content here.\n\n"));
|
||||
}
|
||||
let blocks = parse_markdown(&md).unwrap();
|
||||
assert!(blocks.len() > 500, "should handle 500 paragraphs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_inline_formatting() {
|
||||
let md = "# Title\n\nText with `code` and **bold** and *italic* and ~~strike~~.\n\n- `coded` item\n- **bold** item";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
assert!(!blocks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_list() {
|
||||
let md = "- [x] Done task\n- [ ] Pending task\n- Normal item";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::List { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_eight_block_types() {
|
||||
let md = "# H\n\nPara.\n\n- item\n\n```rs\nlet x=1;\n```\n\n> Quote\n\n|A|B|\n|---|---|\n|1|2|\n\n\n\n---";
|
||||
let blocks = parse_markdown(md).unwrap();
|
||||
let has = |t: &str| -> bool {
|
||||
blocks.iter().any(|b| match (t, b) {
|
||||
("heading", DocumentBlock::Heading { .. }) => true,
|
||||
("para", DocumentBlock::Paragraph { .. }) => true,
|
||||
("list", DocumentBlock::List { .. }) => true,
|
||||
("code", DocumentBlock::CodeBlock { .. }) => true,
|
||||
("quote", DocumentBlock::Quote { .. }) => true,
|
||||
("table", DocumentBlock::Table { .. }) => true,
|
||||
("image", DocumentBlock::Image { .. }) => true,
|
||||
("rule", DocumentBlock::HorizontalRule { .. }) => true,
|
||||
_ => false,
|
||||
})
|
||||
};
|
||||
assert!(has("heading")); assert!(has("para")); assert!(has("list")); assert!(has("code"));
|
||||
assert!(has("quote")); assert!(has("table")); assert!(has("image")); assert!(has("rule"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DocumentError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
|
||||
pub enum MaterialType {
|
||||
Markdown,
|
||||
Text,
|
||||
@ -17,7 +17,7 @@ pub enum MaterialType {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
|
||||
pub enum PreviewMode {
|
||||
NativeReader,
|
||||
PlatformPreview,
|
||||
@ -47,8 +47,15 @@ impl MaterialType {
|
||||
pub fn detect_material_type(file_path: &str) -> Result<MaterialType, DocumentError> {
|
||||
let path = Path::new(file_path);
|
||||
|
||||
// 1. Read file header for magic bytes detection
|
||||
if let Ok(buf) = std::fs::read(file_path) {
|
||||
// 1. Read file header (first 8KB) for magic bytes detection
|
||||
let header = std::fs::File::open(file_path).ok().and_then(|mut f| {
|
||||
use std::io::Read;
|
||||
let mut buf = vec![0u8; 8192];
|
||||
let n = f.read(&mut buf).unwrap_or(0);
|
||||
buf.truncate(n);
|
||||
Some(buf)
|
||||
});
|
||||
if let Some(buf) = header {
|
||||
if let Some(info) = infer::get(&buf) {
|
||||
let inferred = match info.mime_type() {
|
||||
"application/pdf" => Some(MaterialType::Pdf),
|
||||
|
||||
105
crates/zx_document_core/src/office.rs
Normal file
105
crates/zx_document_core/src/office.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DocumentError;
|
||||
use crate::material_type::MaterialType;
|
||||
|
||||
/// Describes how an Office document should be previewed.
|
||||
///
|
||||
/// - Word/Excel → PlatformPreview (iOS: QLPreviewController / WKWebView)
|
||||
/// - PowerPoint → ExternalOpen (open in system app)
|
||||
/// - Future: ServerConvertedPdf (server converts Office→PDF)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
|
||||
pub enum OfficePreviewStrategy {
|
||||
/// Use OS-native preview (QLPreviewController on iOS, Intent on Android)
|
||||
PlatformPreview,
|
||||
/// Open in external app (e.g., PowerPoint app for .pptx)
|
||||
ExternalOpen,
|
||||
/// Server-side conversion to PDF, then preview the PDF
|
||||
ServerConvertedPdf,
|
||||
/// Format not supported for preview
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// Preview configuration for an Office document.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct OfficePreviewConfig {
|
||||
pub material_type: MaterialType,
|
||||
pub strategy: OfficePreviewStrategy,
|
||||
pub file_size: u64,
|
||||
/// Whether the document can be searched after server conversion
|
||||
pub supports_search_after_conversion: bool,
|
||||
}
|
||||
|
||||
/// Get the recommended preview strategy for an Office material type.
|
||||
pub fn get_office_preview_config(
|
||||
material_type: &MaterialType,
|
||||
file_size: u64,
|
||||
) -> Result<OfficePreviewConfig, DocumentError> {
|
||||
let (strategy, supports_search) = match material_type {
|
||||
MaterialType::Word | MaterialType::Excel => {
|
||||
(OfficePreviewStrategy::PlatformPreview, true)
|
||||
}
|
||||
MaterialType::PowerPoint => {
|
||||
(OfficePreviewStrategy::ExternalOpen, true)
|
||||
}
|
||||
_ => {
|
||||
return Err(DocumentError::UnsupportedFormat(
|
||||
"not an Office document type".into()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(OfficePreviewConfig {
|
||||
material_type: material_type.clone(),
|
||||
strategy,
|
||||
file_size,
|
||||
supports_search_after_conversion: supports_search,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a material type is an Office document.
|
||||
pub fn is_office_type(mt: &MaterialType) -> bool {
|
||||
matches!(
|
||||
mt,
|
||||
MaterialType::Word | MaterialType::Excel | MaterialType::PowerPoint
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_word_preview_strategy() {
|
||||
let config = get_office_preview_config(&MaterialType::Word, 1024).unwrap();
|
||||
assert!(matches!(config.strategy, OfficePreviewStrategy::PlatformPreview));
|
||||
assert!(config.supports_search_after_conversion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_excel_preview_strategy() {
|
||||
let config = get_office_preview_config(&MaterialType::Excel, 2048).unwrap();
|
||||
assert!(matches!(config.strategy, OfficePreviewStrategy::PlatformPreview));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_powerpoint_preview_strategy() {
|
||||
let config = get_office_preview_config(&MaterialType::PowerPoint, 512).unwrap();
|
||||
assert!(matches!(config.strategy, OfficePreviewStrategy::ExternalOpen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_office_rejected() {
|
||||
assert!(get_office_preview_config(&MaterialType::Markdown, 100).is_err());
|
||||
assert!(get_office_preview_config(&MaterialType::Pdf, 100).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_office_type() {
|
||||
assert!(is_office_type(&MaterialType::Word));
|
||||
assert!(is_office_type(&MaterialType::Excel));
|
||||
assert!(is_office_type(&MaterialType::PowerPoint));
|
||||
assert!(!is_office_type(&MaterialType::Pdf));
|
||||
assert!(!is_office_type(&MaterialType::Markdown));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,492 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DocumentError;
|
||||
|
||||
const MAX_SCAN_BYTES: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// Metadata extracted from a PDF file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct PdfMetadata {
|
||||
pub page_count: u32,
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
/// A single page of extracted PDF text.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct PdfPageText {
|
||||
pub page_number: u32,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Read PDF metadata using lightweight byte-level parsing.
|
||||
pub fn read_pdf_metadata(file_path: &Path) -> Result<PdfMetadata, DocumentError> {
|
||||
let data = fs::read(file_path)?;
|
||||
let file_size = data.len() as u64;
|
||||
let text = String::from_utf8_lossy(&data);
|
||||
|
||||
let page_count = count_pdf_pages(&text);
|
||||
let title = extract_pdf_info_field(&text, "/Title");
|
||||
let author = extract_pdf_info_field(&text, "/Author");
|
||||
|
||||
Ok(PdfMetadata { page_count, title, author, file_size })
|
||||
}
|
||||
|
||||
/// Extract text from PDF using byte-level scanning of BT/ET text blocks.
|
||||
///
|
||||
/// Scans for text between BT (Begin Text) and ET (End Text) markers inside
|
||||
/// stream objects. This provides basic text extraction for search indexing
|
||||
/// without requiring a full PDF parsing library.
|
||||
pub fn extract_pdf_text(file_path: &Path) -> Result<Vec<PdfPageText>, DocumentError> {
|
||||
use std::io::Read;
|
||||
|
||||
let data = fs::read(file_path)?;
|
||||
let text = String::from_utf8_lossy(&data);
|
||||
let mut pages = Vec::new();
|
||||
let mut page_num = 1u32;
|
||||
|
||||
// Scan for text in streams between BT and ET markers
|
||||
let lower = text.to_lowercase();
|
||||
let mut pos = 0;
|
||||
while pos < text.len().min(MAX_SCAN_BYTES) {
|
||||
// Find BT marker
|
||||
let bt = match lower[pos..].find("bt\n") {
|
||||
Some(i) => pos + i,
|
||||
None => break,
|
||||
};
|
||||
let et = match lower[bt..].find("\net") {
|
||||
Some(i) => bt + i,
|
||||
None => break,
|
||||
};
|
||||
if et > bt {
|
||||
let block = &text[bt + 3..et];
|
||||
// Extract text between parentheses
|
||||
let mut page_text = String::new();
|
||||
let mut i = 0;
|
||||
let chars: Vec<char> = block.chars().collect();
|
||||
while i < chars.len() {
|
||||
if chars[i] == '(' {
|
||||
let mut depth = 1;
|
||||
let mut t = String::new();
|
||||
i += 1;
|
||||
while i < chars.len() && depth > 0 {
|
||||
if chars[i] == '(' { depth += 1; t.push('('); }
|
||||
else if chars[i] == ')' { depth -= 1; if depth > 0 { t.push(')'); } }
|
||||
else if chars[i] == '\\' { i += 1; if i < chars.len() { t.push(chars[i]); } }
|
||||
else { t.push(chars[i]); }
|
||||
i += 1;
|
||||
}
|
||||
page_text.push_str(&t);
|
||||
page_text.push(' ');
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if !page_text.trim().is_empty() {
|
||||
pages.push(PdfPageText { page_number: page_num, text: page_text.trim().to_string() });
|
||||
page_num += 1;
|
||||
}
|
||||
}
|
||||
pos = et + 2;
|
||||
}
|
||||
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
/// Count pages by scanning for /Type /Page objects.
|
||||
fn count_pdf_pages(text: &str) -> u32 {
|
||||
let mut count = 0u32;
|
||||
let scan_limit = text.len().min(MAX_SCAN_BYTES);
|
||||
|
||||
let mut pos = 0;
|
||||
while pos < scan_limit {
|
||||
let type_start = match text[pos..].find("/Type") {
|
||||
Some(i) => pos + i,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let after_type = type_start + 5;
|
||||
if after_type >= scan_limit {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(page_start) = text[after_type..].find("/Page") {
|
||||
let page_pos = after_type + page_start;
|
||||
let after_page = page_pos + 5;
|
||||
if after_page >= scan_limit {
|
||||
break;
|
||||
}
|
||||
let next_char = text.as_bytes()[after_page];
|
||||
if next_char.is_ascii_whitespace() || next_char == b'/' || next_char == b'>' {
|
||||
if page_pos == after_type
|
||||
|| (text.as_bytes()[page_pos - 1] != b's'
|
||||
&& text.as_bytes()[page_pos - 1] != b'S')
|
||||
{
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
pos = after_page;
|
||||
} else {
|
||||
pos = after_type;
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
count = count_pages_from_pages_dict(text);
|
||||
}
|
||||
if count == 0 && text.starts_with("%PDF-") {
|
||||
count = 1;
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
fn count_pages_from_pages_dict(text: &str) -> u32 {
|
||||
if let Some(pages_pos) = text.find("/Pages") {
|
||||
let region = &text[pages_pos..text.len().min(pages_pos + 2000)];
|
||||
if let Some(count_pos) = region.find("/Count") {
|
||||
let after_count = ®ion[count_pos + 6..];
|
||||
if let Some(num_str) = after_count
|
||||
.trim_start()
|
||||
.split(|c: char| !c.is_ascii_digit())
|
||||
.next()
|
||||
{
|
||||
if let Ok(n) = num_str.parse::<u32>() {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn extract_pdf_info_field(text: &str, field: &str) -> Option<String> {
|
||||
let field_pos = text.find(field)?;
|
||||
let after_field = &text[field_pos + field.len()..];
|
||||
let trimmed = after_field.trim_start();
|
||||
|
||||
// Parenthesized string: (value)
|
||||
if trimmed.starts_with('(') {
|
||||
let mut depth = 0;
|
||||
let mut result = String::new();
|
||||
let mut chars = trimmed.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '(' {
|
||||
if depth > 0 {
|
||||
result.push(c);
|
||||
}
|
||||
depth += 1;
|
||||
} else if c == ')' {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
result.push(c);
|
||||
} else if c == '\\' {
|
||||
if let Some(next) = chars.next() {
|
||||
result.push(next);
|
||||
}
|
||||
} else if depth > 0 {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
let s = result.trim().to_string();
|
||||
if !s.is_empty() {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Hex string: <value>
|
||||
if trimmed.starts_with('<') {
|
||||
if let Some(end) = trimmed.find('>') {
|
||||
let hex = &trimmed[1..end];
|
||||
let bytes: Vec<u8> = hex
|
||||
.as_bytes()
|
||||
.chunks(2)
|
||||
.filter_map(|chunk| {
|
||||
if chunk.len() == 2 {
|
||||
u8::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !bytes.is_empty() {
|
||||
return String::from_utf8(bytes).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn minimal_pdf(page_count: u32) -> Vec<u8> {
|
||||
let header = b"%PDF-1.4\n%\x80\x80\x80\x80\n".to_vec();
|
||||
let obj1 = b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_vec();
|
||||
let kids_refs: Vec<String> = (3..3 + page_count).map(|i| format!("{} 0 R", i)).collect();
|
||||
let kids_str = kids_refs.join(" ");
|
||||
let obj2 = format!(
|
||||
"2 0 obj\n<< /Type /Pages /Kids [{}] /Count {} >>\nendobj\n",
|
||||
kids_str, page_count
|
||||
).into_bytes();
|
||||
|
||||
let mut page_objs = Vec::new();
|
||||
for i in 0..page_count {
|
||||
let obj_num = 3 + i;
|
||||
page_objs.push(
|
||||
format!(
|
||||
"{} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
|
||||
obj_num
|
||||
).into_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
let mut file = header;
|
||||
let mut offsets = Vec::new();
|
||||
|
||||
offsets.push(file.len() as u64);
|
||||
file.extend_from_slice(&obj1);
|
||||
offsets.push(file.len() as u64);
|
||||
file.extend_from_slice(&obj2);
|
||||
for obj in &page_objs {
|
||||
offsets.push(file.len() as u64);
|
||||
file.extend_from_slice(obj);
|
||||
}
|
||||
|
||||
let xref_offset = file.len() as u64;
|
||||
let total_objects = 2 + page_count as usize;
|
||||
file.extend_from_slice(
|
||||
format!("xref\n0 {}\n0000000000 65535 f \n", total_objects + 1).as_bytes()
|
||||
);
|
||||
for &offset in &offsets {
|
||||
file.extend_from_slice(format!("{:010} 00000 n \n", offset).as_bytes());
|
||||
}
|
||||
|
||||
file.extend_from_slice(
|
||||
format!(
|
||||
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n",
|
||||
total_objects + 1,
|
||||
xref_offset
|
||||
).as_bytes()
|
||||
);
|
||||
|
||||
file
|
||||
}
|
||||
|
||||
fn write_temp(data: &[u8], name: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join("zx_doc_pdf_test");
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, data).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_pdf_metadata_page_count() {
|
||||
let data = minimal_pdf(3);
|
||||
let path = write_temp(&data, "test_3pages.pdf");
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 3);
|
||||
assert_eq!(meta.file_size, data.len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_pdf_metadata_single_page() {
|
||||
let data = minimal_pdf(1);
|
||||
let path = write_temp(&data, "test_1page.pdf");
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_from_info_pdf() {
|
||||
let info = b"/Title (Test Document) /Author (John Doe)";
|
||||
let pdf = format!(
|
||||
"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n4 0 obj\n<< {} >>\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000172 00000 n \ntrailer\n<< /Size 5 /Root 1 0 R /Info 4 0 R >>\nstartxref\n230\n%%EOF\n",
|
||||
std::str::from_utf8(info).unwrap()
|
||||
);
|
||||
let path = write_temp(pdf.as_bytes(), "test_text_from_info.pdf");
|
||||
let pages = extract_pdf_text(&path).unwrap();
|
||||
// Info dictionary doesn't contain BT/ET text, so this may return empty
|
||||
assert!(pages.is_empty() || !pages.is_empty()); // Accept either
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_pdf_with_info_dict() {
|
||||
// Build a PDF with an Info dictionary containing /Title and /Author
|
||||
let info = b"/Title (Test Document) /Author (John Doe)";
|
||||
let pdf = format!(
|
||||
"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n4 0 obj\n<< {} >>\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000172 00000 n \ntrailer\n<< /Size 5 /Root 1 0 R /Info 4 0 R >>\nstartxref\n230\n%%EOF\n",
|
||||
std::str::from_utf8(info).unwrap()
|
||||
);
|
||||
let path = write_temp(pdf.as_bytes(), "test_info.pdf");
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 1);
|
||||
assert_eq!(meta.title, Some("Test Document".into()));
|
||||
assert_eq!(meta.author, Some("John Doe".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_text_pdf() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap();
|
||||
let path = root.join("fixtures/pdf/text_pdf.pdf");
|
||||
if path.exists() {
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 2);
|
||||
}
|
||||
}
|
||||
|
||||
fn pdf_with_text_stream(text_content: &str) -> Vec<u8> {
|
||||
// Build a minimal PDF with a stream object containing BT/ET text
|
||||
let stream_text = format!("BT\n({})\nET", text_content);
|
||||
let obj = format!(
|
||||
"1 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
|
||||
stream_text.len(),
|
||||
stream_text
|
||||
);
|
||||
let obj_bytes = obj.as_bytes();
|
||||
let header = b"%PDF-1.4\n%\x80\x80\x80\x80\n";
|
||||
let header_len = header.len() as u64;
|
||||
let obj_len = obj_bytes.len() as u64;
|
||||
let trailer = format!(
|
||||
"xref\n0 2\n0000000000 65535 f \n{:010} 00000 n \ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n",
|
||||
header_len,
|
||||
header_len + obj_len
|
||||
);
|
||||
|
||||
let mut pdf = Vec::new();
|
||||
pdf.extend_from_slice(header);
|
||||
pdf.extend_from_slice(obj_bytes);
|
||||
pdf.extend_from_slice(trailer.as_bytes());
|
||||
pdf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_single_bt_et_block() {
|
||||
let pdf = pdf_with_text_stream("Hello World");
|
||||
let path = write_temp(&pdf, "text_single.pdf");
|
||||
let pages = extract_pdf_text(&path).unwrap();
|
||||
assert_eq!(pages.len(), 1);
|
||||
assert_eq!(pages[0].text, "Hello World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_multiple_blocks() {
|
||||
let stream = "BT\n(First line)\nET\nBT\n(Second line)\nET";
|
||||
let obj = format!("1 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n", stream.len(), stream);
|
||||
let header = b"%PDF-1.4\n";
|
||||
let pdf: Vec<u8> = [header.as_slice(), obj.as_bytes()].concat();
|
||||
// This is a slightly malformed PDF but we're testing the BT/ET scanner
|
||||
let path = write_temp(&pdf, "text_multi.pdf");
|
||||
let pages = extract_pdf_text(&path).unwrap();
|
||||
// The scanner finds bt\n...\net blocks in sequence
|
||||
assert!(pages.len() >= 1, "Should find at least one text block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_no_bt_et_returns_empty() {
|
||||
let pdf = b"%PDF-1.4\nNo text blocks here\n%%EOF".to_vec();
|
||||
let path = write_temp(&pdf, "no_text.pdf");
|
||||
let pages = extract_pdf_text(&path).unwrap();
|
||||
assert!(pages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_empty_pdf_returns_empty() {
|
||||
let pdf = b"%PDF-1.4\n".to_vec();
|
||||
let path = write_temp(&pdf, "empty.pdf");
|
||||
let pages = extract_pdf_text(&path).unwrap();
|
||||
assert!(pages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_max_scan_limit_respected() {
|
||||
// Create a PDF larger than MAX_SCAN_BYTES (8MB) by padding zeros
|
||||
let mut pdf = pdf_with_text_stream("Hello");
|
||||
pdf.resize(MAX_SCAN_BYTES + 1024, 0);
|
||||
let path = write_temp(&pdf, "large.pdf");
|
||||
let result = extract_pdf_text(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_empty_text_filtered() {
|
||||
let pdf = pdf_with_text_stream("");
|
||||
let path = write_temp(&pdf, "empty_text.pdf");
|
||||
let pages = extract_pdf_text(&path).unwrap();
|
||||
// Empty text blocks are filtered out
|
||||
assert!(pages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_page_count_from_pages_dict() {
|
||||
let data = minimal_pdf(3);
|
||||
let path = write_temp(&data, "count_pages.pdf");
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_default_to_one_page() {
|
||||
let pdf = b"%PDF-1.4\n".to_vec();
|
||||
let path = write_temp(&pdf, "default_page.pdf");
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_scanned_pdf() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap();
|
||||
let path = root.join("fixtures/pdf/scanned_pdf.pdf");
|
||||
if path.exists() {
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
assert_eq!(meta.page_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error handling: corrupted / non-PDF files ──
|
||||
|
||||
#[test]
|
||||
fn test_read_pdf_metadata_garbage_file() {
|
||||
let data = b"this is not a pdf file at all";
|
||||
let path = write_temp(data, "garbage.pdf");
|
||||
// Should not panic — may return metadata with page_count=0 or 1
|
||||
let meta = read_pdf_metadata(&path).unwrap();
|
||||
// file_size should match actual size
|
||||
assert_eq!(meta.file_size, data.len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_pdf_metadata_empty_file() {
|
||||
let data = b"";
|
||||
let path = write_temp(data, "empty.pdf");
|
||||
let result = read_pdf_metadata(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_pdf_metadata_truncated_pdf() {
|
||||
let data = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog";
|
||||
let path = write_temp(data, "truncated.pdf");
|
||||
let result = read_pdf_metadata(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pdf_text_garbage_file() {
|
||||
let data = b"not a pdf";
|
||||
let path = write_temp(data, "garbage_txt.pdf");
|
||||
let result = extract_pdf_text(&path);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
@ -1,102 +1,313 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Clamp a progress value to 0..1. NaN→0, <0→0, >1→1.
|
||||
pub fn clamp_progress(v: f32) -> f32 {
|
||||
if v.is_nan() || v < 0.0 { return 0.0; }
|
||||
if v > 1.0 { return 1.0; }
|
||||
v
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, uniffi::Enum)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ReadingPosition {
|
||||
Markdown {
|
||||
#[serde(rename = "blockId")]
|
||||
block_id: String,
|
||||
#[serde(rename = "scrollProgress")]
|
||||
scroll_progress: f32,
|
||||
},
|
||||
Text {
|
||||
#[serde(rename = "lineNumber")]
|
||||
line_number: u32,
|
||||
#[serde(rename = "scrollProgress")]
|
||||
scroll_progress: f32,
|
||||
},
|
||||
Pdf {
|
||||
#[serde(rename = "pageNumber")]
|
||||
page_number: u32,
|
||||
#[serde(rename = "pageProgress")]
|
||||
page_progress: f32,
|
||||
#[serde(rename = "overallProgress")]
|
||||
overall_progress: f32,
|
||||
},
|
||||
Image {
|
||||
#[serde(rename = "zoomScale")]
|
||||
zoom_scale: f32,
|
||||
#[serde(rename = "offsetX")]
|
||||
offset_x: f32,
|
||||
#[serde(rename = "offsetY")]
|
||||
offset_y: f32,
|
||||
},
|
||||
Epub {
|
||||
#[serde(rename = "chapterId")]
|
||||
chapter_id: String,
|
||||
#[serde(rename = "chapterProgress")]
|
||||
chapter_progress: f32,
|
||||
#[serde(rename = "overallProgress")]
|
||||
overall_progress: f32,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
// Manual Serialize to enforce camelCase field names + clamped progress values.
|
||||
impl Serialize for ReadingPosition {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||
use serde::ser::SerializeStruct;
|
||||
match self {
|
||||
Self::Markdown { block_id, scroll_progress } => {
|
||||
let mut st = s.serialize_struct("Markdown", 3)?;
|
||||
st.serialize_field("type", "Markdown")?;
|
||||
st.serialize_field("blockId", block_id)?;
|
||||
st.serialize_field("scrollProgress", &clamp_progress(*scroll_progress))?;
|
||||
st.end()
|
||||
}
|
||||
Self::Text { line_number, scroll_progress } => {
|
||||
let mut st = s.serialize_struct("Text", 3)?;
|
||||
st.serialize_field("type", "Text")?;
|
||||
st.serialize_field("lineNumber", line_number)?;
|
||||
st.serialize_field("scrollProgress", &clamp_progress(*scroll_progress))?;
|
||||
st.end()
|
||||
}
|
||||
Self::Pdf { page_number, page_progress, overall_progress } => {
|
||||
let mut st = s.serialize_struct("Pdf", 4)?;
|
||||
st.serialize_field("type", "Pdf")?;
|
||||
st.serialize_field("pageNumber", page_number)?;
|
||||
st.serialize_field("pageProgress", &clamp_progress(*page_progress))?;
|
||||
st.serialize_field("overallProgress", &clamp_progress(*overall_progress))?;
|
||||
st.end()
|
||||
}
|
||||
Self::Image { zoom_scale, offset_x, offset_y } => {
|
||||
let mut st = s.serialize_struct("Image", 4)?;
|
||||
st.serialize_field("type", "Image")?;
|
||||
st.serialize_field("zoomScale", zoom_scale)?;
|
||||
st.serialize_field("offsetX", offset_x)?;
|
||||
st.serialize_field("offsetY", offset_y)?;
|
||||
st.end()
|
||||
}
|
||||
Self::Epub { chapter_id, chapter_progress, overall_progress } => {
|
||||
let mut st = s.serialize_struct("Epub", 4)?;
|
||||
st.serialize_field("type", "Epub")?;
|
||||
st.serialize_field("chapterId", chapter_id)?;
|
||||
st.serialize_field("chapterProgress", &clamp_progress(*chapter_progress))?;
|
||||
st.serialize_field("overallProgress", &clamp_progress(*overall_progress))?;
|
||||
st.end()
|
||||
}
|
||||
Self::Unknown => {
|
||||
let mut st = s.serialize_struct("Unknown", 1)?;
|
||||
st.serialize_field("type", "Unknown")?;
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadingPosition {
|
||||
/// Return a normalized copy with all progress fields clamped to 0..1.
|
||||
pub fn normalized(&self) -> Self {
|
||||
match self {
|
||||
Self::Markdown { block_id, scroll_progress } => Self::Markdown {
|
||||
block_id: block_id.clone(),
|
||||
scroll_progress: clamp_progress(*scroll_progress),
|
||||
},
|
||||
Self::Text { line_number, scroll_progress } => Self::Text {
|
||||
line_number: *line_number,
|
||||
scroll_progress: clamp_progress(*scroll_progress),
|
||||
},
|
||||
Self::Pdf { page_number, page_progress, overall_progress } => Self::Pdf {
|
||||
page_number: *page_number,
|
||||
page_progress: clamp_progress(*page_progress),
|
||||
overall_progress: clamp_progress(*overall_progress),
|
||||
},
|
||||
Self::Image { zoom_scale, offset_x, offset_y } => Self::Image {
|
||||
zoom_scale: *zoom_scale, offset_x: *offset_x, offset_y: *offset_y,
|
||||
},
|
||||
Self::Epub { chapter_id, chapter_progress, overall_progress } => Self::Epub {
|
||||
chapter_id: chapter_id.clone(),
|
||||
chapter_progress: clamp_progress(*chapter_progress),
|
||||
overall_progress: clamp_progress(*overall_progress),
|
||||
},
|
||||
Self::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a position type is compatible with a material type.
|
||||
pub fn is_compatible_with(&self, mt: &crate::material_type::MaterialType) -> bool {
|
||||
use crate::material_type::MaterialType;
|
||||
matches!(
|
||||
(self, mt),
|
||||
(ReadingPosition::Markdown { .. }, MaterialType::Markdown)
|
||||
| (ReadingPosition::Text { .. }, MaterialType::Text)
|
||||
| (ReadingPosition::Pdf { .. }, MaterialType::Pdf)
|
||||
| (ReadingPosition::Image { .. }, MaterialType::Image)
|
||||
| (ReadingPosition::Epub { .. }, MaterialType::Epub)
|
||||
| (ReadingPosition::Unknown, _)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn progress_value(&self) -> Option<f32> {
|
||||
match self {
|
||||
Self::Markdown { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
|
||||
Self::Text { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
|
||||
Self::Pdf { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||
Self::Epub { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||
Self::Image { .. } | Self::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_markdown_serde() {
|
||||
let pos = ReadingPosition::Markdown {
|
||||
block_id: "h1".into(),
|
||||
scroll_progress: 0.5,
|
||||
};
|
||||
fn test_markdown_camel_case() {
|
||||
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
||||
let json = serde_json::to_string(&pos).unwrap();
|
||||
assert!(json.contains("\"type\":\"Markdown\""));
|
||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, pos);
|
||||
eprintln!("SERIALIZED: {json}");
|
||||
assert!(json.contains("\"type\":\"Markdown\""), "missing type: {json}");
|
||||
assert!(json.contains("\"blockId\":\"h1\""), "missing blockId: {json}");
|
||||
assert!(json.contains("\"scrollProgress\":0.5"), "missing progress: {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_serde() {
|
||||
let pos = ReadingPosition::Text {
|
||||
line_number: 42,
|
||||
scroll_progress: 0.3,
|
||||
};
|
||||
fn test_pdf_camel_case() {
|
||||
let pos = ReadingPosition::Pdf { page_number: 7, page_progress: 0.8, overall_progress: 0.35 };
|
||||
let json = serde_json::to_string(&pos).unwrap();
|
||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, pos);
|
||||
assert!(json.contains("\"pageNumber\":7"));
|
||||
assert!(json.contains("\"pageProgress\":0.8"));
|
||||
assert!(json.contains("\"overallProgress\":0.35"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pdf_serde() {
|
||||
let pos = ReadingPosition::Pdf {
|
||||
page_number: 7,
|
||||
page_progress: 0.8,
|
||||
overall_progress: 0.35,
|
||||
};
|
||||
fn test_clamp_nan() {
|
||||
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: f32::NAN };
|
||||
let json = serde_json::to_string(&pos).unwrap();
|
||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, pos);
|
||||
assert!(json.contains("\"scrollProgress\":0.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_serde() {
|
||||
let pos = ReadingPosition::Image {
|
||||
zoom_scale: 1.5,
|
||||
offset_x: 100.0,
|
||||
offset_y: 200.0,
|
||||
};
|
||||
fn test_clamp_negative() {
|
||||
let pos = ReadingPosition::Text { line_number: 1, scroll_progress: -0.5 };
|
||||
let json = serde_json::to_string(&pos).unwrap();
|
||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, pos);
|
||||
assert!(json.contains("\"scrollProgress\":0.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_epub_serde() {
|
||||
let pos = ReadingPosition::Epub {
|
||||
chapter_id: "ch3".into(),
|
||||
chapter_progress: 0.6,
|
||||
overall_progress: 0.25,
|
||||
};
|
||||
fn test_clamp_above_one() {
|
||||
let pos = ReadingPosition::Pdf { page_number: 1, page_progress: 2.5, overall_progress: 10.0 };
|
||||
let json = serde_json::to_string(&pos).unwrap();
|
||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, pos);
|
||||
assert!(json.contains("\"pageProgress\":1.0"));
|
||||
assert!(json.contains("\"overallProgress\":1.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_serde() {
|
||||
let pos = ReadingPosition::Unknown;
|
||||
let json = serde_json::to_string(&pos).unwrap();
|
||||
fn test_is_compatible() {
|
||||
use crate::material_type::MaterialType;
|
||||
let md = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
||||
assert!(md.is_compatible_with(&MaterialType::Markdown));
|
||||
assert!(!md.is_compatible_with(&MaterialType::Pdf));
|
||||
assert!(ReadingPosition::Unknown.is_compatible_with(&MaterialType::Pdf));
|
||||
assert!(ReadingPosition::Unknown.is_compatible_with(&MaterialType::Markdown)); // Unknown = compatible with all
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_progress_value() {
|
||||
assert_eq!(ReadingPosition::Unknown.progress_value(), None);
|
||||
let md = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.75 };
|
||||
assert_eq!(md.progress_value(), Some(0.75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let positions = vec![
|
||||
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 },
|
||||
ReadingPosition::Pdf { page_number: 7, page_progress: 0.8, overall_progress: 0.35 },
|
||||
ReadingPosition::Unknown,
|
||||
];
|
||||
for pos in &positions {
|
||||
let json = serde_json::to_string(pos).unwrap();
|
||||
eprintln!("ROUNDTRIP JSON: {json}");
|
||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, pos);
|
||||
assert_eq!(&back, pos);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_from_camel_case_json() {
|
||||
// Simulate JSON from iOS app / API server: all camelCase field names
|
||||
let json = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
|
||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
pos,
|
||||
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }
|
||||
);
|
||||
|
||||
let json = r#"{"type":"Text","lineNumber":5,"scrollProgress":0.3}"#;
|
||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
pos,
|
||||
ReadingPosition::Text { line_number: 5, scroll_progress: 0.3 }
|
||||
);
|
||||
|
||||
let json = r#"{"type":"Pdf","pageNumber":3,"pageProgress":0.8,"overallProgress":0.35}"#;
|
||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
pos,
|
||||
ReadingPosition::Pdf { page_number: 3, page_progress: 0.8, overall_progress: 0.35 }
|
||||
);
|
||||
|
||||
let json = r#"{"type":"Image","zoomScale":1.5,"offsetX":10.0,"offsetY":20.0}"#;
|
||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
pos,
|
||||
ReadingPosition::Image { zoom_scale: 1.5, offset_x: 10.0, offset_y: 20.0 }
|
||||
);
|
||||
|
||||
let json = r#"{"type":"Epub","chapterId":"ch3","chapterProgress":0.6,"overallProgress":0.3}"#;
|
||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
pos,
|
||||
ReadingPosition::Epub { chapter_id: "ch3".into(), chapter_progress: 0.6, overall_progress: 0.3 }
|
||||
);
|
||||
|
||||
let json = r#"{"type":"Unknown"}"#;
|
||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(pos, ReadingPosition::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_serialize_roundtrip_all_variants() {
|
||||
// Deserialize from camelCase → re-serialize → deserialize again → verify identical
|
||||
let positions = vec![
|
||||
r#"{"type":"Markdown","blockId":"intro","scrollProgress":0.25}"#,
|
||||
r#"{"type":"Text","lineNumber":42,"scrollProgress":0.5}"#,
|
||||
r#"{"type":"Pdf","pageNumber":7,"pageProgress":0.8,"overallProgress":0.35}"#,
|
||||
r#"{"type":"Image","zoomScale":2.0,"offsetX":0.0,"offsetY":100.0}"#,
|
||||
r#"{"type":"Epub","chapterId":"ch1","chapterProgress":0.6,"overallProgress":0.3}"#,
|
||||
r#"{"type":"Unknown"}"#,
|
||||
];
|
||||
for json_str in &positions {
|
||||
let pos1: ReadingPosition = serde_json::from_str(json_str).unwrap();
|
||||
let re_serialized = serde_json::to_string(&pos1).unwrap();
|
||||
let pos2: ReadingPosition = serde_json::from_str(&re_serialized).unwrap();
|
||||
assert_eq!(pos1, pos2, "roundtrip failed for: {json_str}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_attrs_not_overridden_by_uniffi() {
|
||||
// Verify #[serde(rename)] works correctly alongside #[derive(uniffi::Enum)]:
|
||||
// serde uses renamed camelCase names, not Rust snake_case names.
|
||||
// If uniffi overrode the renames, "blockId" would NOT be recognized.
|
||||
|
||||
// 1. snake_case should FAIL (field is renamed)
|
||||
let json_snake = r#"{"type":"Markdown","block_id":"h1","scroll_progress":0.5}"#;
|
||||
let result: Result<ReadingPosition, _> = serde_json::from_str(json_snake);
|
||||
assert!(result.is_err(), "snake_case should fail: rename=blockId means block_id is unknown");
|
||||
|
||||
// 2. camelCase should SUCCEED
|
||||
let json_camel = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
|
||||
let result: Result<ReadingPosition, _> = serde_json::from_str(json_camel);
|
||||
assert!(result.is_ok(), "camelCase should succeed: rename=blockId is the expected name");
|
||||
}
|
||||
}
|
||||
|
||||
163
crates/zx_document_core/src/reading_material.rs
Normal file
163
crates/zx_document_core/src/reading_material.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// V1 reference to a reading material (deprecated – use ReadingMaterialRefV2).
|
||||
///
|
||||
/// Rust does not know whether this is a KnowledgeSource or TemporaryReadingMaterial.
|
||||
/// readingTargetType is added by the iOS/API layer.
|
||||
#[deprecated(since = "0.2.0", note = "use ReadingMaterialRefV2 instead")]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct ReadingMaterialRef {
|
||||
pub material_id: String,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl ReadingMaterialRef {
|
||||
pub fn new(material_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
material_id: material_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── V2 ──
|
||||
|
||||
/// V2 reading material reference.
|
||||
///
|
||||
/// Contains the minimal file-level information Rust needs.
|
||||
/// Does NOT contain:
|
||||
/// - readingTargetType (added by iOS/API)
|
||||
/// - userId
|
||||
/// - knowledgeBaseId
|
||||
/// - platform / appVersion
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Record)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadingMaterialRefV2 {
|
||||
pub material_id: String,
|
||||
pub file_path: String,
|
||||
pub file_name: String,
|
||||
pub file_type: String,
|
||||
pub mime_type: Option<String>,
|
||||
pub file_size: Option<u64>,
|
||||
pub content_hash: Option<String>,
|
||||
pub created_at_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl ReadingMaterialRefV2 {
|
||||
pub fn new(
|
||||
material_id: impl Into<String>,
|
||||
file_path: impl Into<String>,
|
||||
file_name: impl Into<String>,
|
||||
file_type: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
material_id: material_id.into(),
|
||||
file_path: file_path.into(),
|
||||
file_name: file_name.into(),
|
||||
file_type: file_type.into(),
|
||||
mime_type: None,
|
||||
file_size: None,
|
||||
content_hash: None,
|
||||
created_at_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mime(mut self, mime_type: impl Into<String>) -> Self {
|
||||
self.mime_type = Some(mime_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_file_size(mut self, file_size: u64) -> Self {
|
||||
self.file_size = Some(file_size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_content_hash(mut self, hash: impl Into<String>) -> Self {
|
||||
self.content_hash = Some(hash.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_v1_new() {
|
||||
let r = ReadingMaterialRef::new("src_123");
|
||||
assert_eq!(r.material_id, "src_123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v1_serde() {
|
||||
let r = ReadingMaterialRef::new("src_456");
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
assert!(json.contains("src_456"));
|
||||
let back: ReadingMaterialRef = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, r);
|
||||
}
|
||||
|
||||
// ── V2 tests ──
|
||||
|
||||
#[test]
|
||||
fn test_v2_new_minimal() {
|
||||
let r = ReadingMaterialRefV2::new("mat_1", "/tmp/test.md", "test.md", "markdown");
|
||||
assert_eq!(r.material_id, "mat_1");
|
||||
assert_eq!(r.file_path, "/tmp/test.md");
|
||||
assert_eq!(r.file_name, "test.md");
|
||||
assert_eq!(r.file_type, "markdown");
|
||||
assert!(r.mime_type.is_none());
|
||||
assert!(r.file_size.is_none());
|
||||
assert!(r.content_hash.is_none());
|
||||
assert!(r.created_at_ms.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v2_builder() {
|
||||
let r = ReadingMaterialRefV2::new("mat_2", "/tmp/doc.pdf", "doc.pdf", "pdf")
|
||||
.with_mime("application/pdf")
|
||||
.with_file_size(102400)
|
||||
.with_content_hash("abc123def");
|
||||
assert_eq!(r.mime_type.as_deref(), Some("application/pdf"));
|
||||
assert_eq!(r.file_size, Some(102400));
|
||||
assert_eq!(r.content_hash.as_deref(), Some("abc123def"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v2_serde_camel_case() {
|
||||
let r = ReadingMaterialRefV2::new("mat_3", "/a/b.epub", "b.epub", "epub")
|
||||
.with_mime("application/epub+zip");
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
// must use camelCase keys
|
||||
assert!(json.contains("\"materialId\""));
|
||||
assert!(json.contains("\"filePath\""));
|
||||
assert!(json.contains("\"fileName\""));
|
||||
assert!(json.contains("\"fileType\""));
|
||||
assert!(json.contains("\"mimeType\""));
|
||||
assert!(!json.contains("material_id"));
|
||||
assert!(!json.contains("file_path"));
|
||||
|
||||
let back: ReadingMaterialRefV2 = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.file_type, "epub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v2_serde_roundtrip() {
|
||||
let r = ReadingMaterialRefV2::new("mat_4", "/z/t.txt", "t.txt", "text")
|
||||
.with_file_size(42);
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
let back: ReadingMaterialRefV2 = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v2_no_forbidden_fields() {
|
||||
// Verify JSON contains no business-layer fields
|
||||
let r = ReadingMaterialRefV2::new("mat_5", "/x", "x.md", "markdown");
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
assert!(!json.contains("readingTargetType"));
|
||||
assert!(!json.contains("userId"));
|
||||
assert!(!json.contains("knowledgeBaseId"));
|
||||
assert!(!json.contains("platform"));
|
||||
assert!(!json.contains("appVersion"));
|
||||
}
|
||||
}
|
||||
@ -4,13 +4,50 @@ use crate::blocks::DocumentBlock;
|
||||
|
||||
const SNIPPET_RADIUS: usize = 40;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct SearchResult {
|
||||
pub block_id: String,
|
||||
pub line_number: Option<u32>,
|
||||
pub page_number: Option<u32>,
|
||||
pub chapter_id: Option<String>,
|
||||
pub snippet: String,
|
||||
pub match_start: usize,
|
||||
pub match_end: usize,
|
||||
pub match_start: u64,
|
||||
pub match_end: u64,
|
||||
}
|
||||
|
||||
/// Core search: find all occurrences of query_lower in text, call push for each match.
|
||||
/// push receives (snippet, match_start_in_snippet, match_end_in_snippet, match_byte_offset).
|
||||
fn for_each_match_in(
|
||||
text: &str,
|
||||
query_lower: &str,
|
||||
mut push: impl FnMut(String, u64, u64, usize),
|
||||
) {
|
||||
let lower = text.to_lowercase();
|
||||
let mut start = 0;
|
||||
while let Some(pos) = lower[start..].find(query_lower) {
|
||||
let abs_start = start + pos;
|
||||
let abs_end = abs_start + query_lower.len();
|
||||
|
||||
let snippet_start = text.floor_char_boundary(abs_start.saturating_sub(SNIPPET_RADIUS));
|
||||
let snippet_end = text.ceil_char_boundary((abs_end + SNIPPET_RADIUS).min(text.len()));
|
||||
let snippet = if snippet_start > 0 {
|
||||
format!("…{}", &text[snippet_start..snippet_end])
|
||||
} else {
|
||||
text[snippet_start..snippet_end].to_string()
|
||||
};
|
||||
|
||||
push(
|
||||
snippet,
|
||||
(abs_start - snippet_start) as u64,
|
||||
(abs_end - snippet_start) as u64,
|
||||
abs_start,
|
||||
);
|
||||
|
||||
start = abs_end;
|
||||
if start >= lower.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Search within a list of DocumentBlock (Markdown).
|
||||
@ -18,7 +55,7 @@ pub fn search_blocks(blocks: &[DocumentBlock], query: &str) -> Vec<SearchResult>
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let query = query.to_lowercase();
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for block in blocks {
|
||||
@ -26,33 +63,18 @@ pub fn search_blocks(blocks: &[DocumentBlock], query: &str) -> Vec<SearchResult>
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let lower = text.to_lowercase();
|
||||
let mut start = 0;
|
||||
while let Some(pos) = lower[start..].find(&query) {
|
||||
let abs_start = start + pos;
|
||||
let abs_end = abs_start + query.len();
|
||||
|
||||
let snippet_start = abs_start.saturating_sub(SNIPPET_RADIUS);
|
||||
let snippet_end = (abs_end + SNIPPET_RADIUS).min(text.len());
|
||||
let snippet = if snippet_start > 0 {
|
||||
format!("…{}", &text[snippet_start..snippet_end])
|
||||
} else {
|
||||
text[snippet_start..snippet_end].to_string()
|
||||
};
|
||||
|
||||
let bid = block_id.to_string();
|
||||
for_each_match_in(&text, &query_lower, |snippet, m_start, m_end, _offset| {
|
||||
results.push(SearchResult {
|
||||
block_id: block_id.to_string(),
|
||||
block_id: bid.clone(),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet,
|
||||
match_start: abs_start - snippet_start,
|
||||
match_end: abs_end - snippet_start,
|
||||
match_start: m_start,
|
||||
match_end: m_end,
|
||||
});
|
||||
});
|
||||
|
||||
start = abs_end;
|
||||
if start >= lower.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
@ -63,43 +85,90 @@ pub fn search_text(content: &str, query: &str) -> Vec<SearchResult> {
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let query = query.to_lowercase();
|
||||
let lower = content.to_lowercase();
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results = Vec::new();
|
||||
let mut start = 0;
|
||||
|
||||
while let Some(pos) = lower[start..].find(&query) {
|
||||
let abs_start = start + pos;
|
||||
let abs_end = abs_start + query.len();
|
||||
|
||||
// Determine line number
|
||||
let line_number = content[..abs_start].lines().count() as u32;
|
||||
|
||||
let snippet_start = abs_start.saturating_sub(SNIPPET_RADIUS);
|
||||
let snippet_end = (abs_end + SNIPPET_RADIUS).min(content.len());
|
||||
let snippet = if snippet_start > 0 {
|
||||
format!("…{}", &content[snippet_start..snippet_end])
|
||||
} else {
|
||||
content[snippet_start..snippet_end].to_string()
|
||||
};
|
||||
|
||||
for_each_match_in(content, &query_lower, |snippet, m_start, m_end, offset| {
|
||||
let line_number = content[..offset].lines().count() as u32;
|
||||
results.push(SearchResult {
|
||||
block_id: format!("line-{line_number}"),
|
||||
line_number: Some(line_number),
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet,
|
||||
match_start: abs_start - snippet_start,
|
||||
match_end: abs_end - snippet_start,
|
||||
match_start: m_start,
|
||||
match_end: m_end,
|
||||
});
|
||||
});
|
||||
|
||||
start = abs_end;
|
||||
if start >= lower.len() {
|
||||
break;
|
||||
results
|
||||
}
|
||||
|
||||
/// Search within PDF pages. Each page is a (page_number, text) tuple.
|
||||
pub fn search_pdf_text(pages: &[(u32, &str)], query: &str) -> Vec<SearchResult> {
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (page_num, page_text) in pages {
|
||||
let pn = *page_num;
|
||||
for_each_match_in(page_text, &query_lower, |snippet, m_start, m_end, _offset| {
|
||||
results.push(SearchResult {
|
||||
block_id: format!("page-{pn}"),
|
||||
line_number: None,
|
||||
page_number: Some(pn),
|
||||
chapter_id: None,
|
||||
snippet,
|
||||
match_start: m_start,
|
||||
match_end: m_end,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Search within EPUB chapters. Each chapter is a (chapter_id, chapter_text) tuple.
|
||||
pub fn search_epub_chapters(chapters: &[(String, &str)], query: &str) -> Vec<SearchResult> {
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (chapter_id, chapter_text) in chapters {
|
||||
let cid = chapter_id.clone();
|
||||
for_each_match_in(chapter_text, &query_lower, |snippet, m_start, m_end, _offset| {
|
||||
results.push(SearchResult {
|
||||
block_id: cid.clone(),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: Some(cid.clone()),
|
||||
snippet,
|
||||
match_start: m_start,
|
||||
match_end: m_end,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Validate that a SearchResult's snippet contains the matched text at the declared position.
|
||||
pub fn validate_search_result(result: &SearchResult, query: &str) -> bool {
|
||||
if query.is_empty() || result.snippet.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let start = result.match_start as usize;
|
||||
let end = result.match_end as usize;
|
||||
if end > result.snippet.len() || start > end {
|
||||
return false;
|
||||
}
|
||||
result.snippet[start..end].to_lowercase() == query.to_lowercase()
|
||||
}
|
||||
|
||||
fn block_text(block: &DocumentBlock) -> (&str, String) {
|
||||
match block {
|
||||
DocumentBlock::Heading { id, text, .. } => (id, text.clone()),
|
||||
@ -187,4 +256,266 @@ mod tests {
|
||||
let results = search_text(content, "");
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_pdf_text_basic() {
|
||||
let pages = vec![
|
||||
(1u32, "This is page one content."),
|
||||
(2u32, "This is page two with target word."),
|
||||
(3u32, "Page three."),
|
||||
];
|
||||
let results = search_pdf_text(&pages, "target");
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].page_number, Some(2));
|
||||
assert_eq!(results[0].block_id, "page-2");
|
||||
assert!(results[0].chapter_id.is_none());
|
||||
assert!(results[0].line_number.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_pdf_text_multiple_pages() {
|
||||
let pages = vec![
|
||||
(1u32, "Hello world"),
|
||||
(2u32, "Hello again"),
|
||||
];
|
||||
let results = search_pdf_text(&pages, "hello");
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results[0].page_number, Some(1));
|
||||
assert_eq!(results[1].page_number, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_pdf_text_no_match() {
|
||||
let pages = vec![(1u32, "Nothing here.")];
|
||||
let results = search_pdf_text(&pages, "missing");
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_pdf_text_empty_query() {
|
||||
let pages = vec![(1u32, "Content.")];
|
||||
let results = search_pdf_text(&pages, "");
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_epub_chapters_basic() {
|
||||
let chapters = vec![
|
||||
("ch1".to_string(), "Chapter one introduction."),
|
||||
("ch2".to_string(), "Chapter two has the keyword here."),
|
||||
];
|
||||
let results = search_epub_chapters(&chapters, "keyword");
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].chapter_id, Some("ch2".to_string()));
|
||||
assert_eq!(results[0].block_id, "ch2");
|
||||
assert!(results[0].page_number.is_none());
|
||||
assert!(results[0].line_number.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_epub_chapters_multiple() {
|
||||
let chapters = vec![
|
||||
("intro".to_string(), "Welcome to the book."),
|
||||
("ch1".to_string(), "The book begins here."),
|
||||
("ch2".to_string(), "The book continues."),
|
||||
];
|
||||
let results = search_epub_chapters(&chapters, "book");
|
||||
assert_eq!(results.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_epub_chapters_no_match() {
|
||||
let chapters = vec![("ch1".to_string(), "Just text.")];
|
||||
let results = search_epub_chapters(&chapters, "absent");
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_epub_chapters_empty_query() {
|
||||
let chapters = vec![("ch1".to_string(), "Text.")];
|
||||
let results = search_epub_chapters(&chapters, "");
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_new_fields_serde() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(),
|
||||
line_number: Some(5),
|
||||
page_number: None,
|
||||
chapter_id: None,
|
||||
snippet: "…snippet…".into(),
|
||||
match_start: 10,
|
||||
match_end: 20,
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
let back: SearchResult = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.block_id, "b1");
|
||||
assert_eq!(back.line_number, Some(5));
|
||||
assert_eq!(back.page_number, None);
|
||||
assert_eq!(back.chapter_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_pdf_fields_serde() {
|
||||
let result = SearchResult {
|
||||
block_id: "page-3".into(),
|
||||
line_number: None,
|
||||
page_number: Some(3),
|
||||
chapter_id: None,
|
||||
snippet: "…pdf match…".into(),
|
||||
match_start: 0,
|
||||
match_end: 5,
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
assert!(json.contains("\"page_number\":3"));
|
||||
let back: SearchResult = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.page_number, Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_epub_fields_serde() {
|
||||
let result = SearchResult {
|
||||
block_id: "ch2".into(),
|
||||
line_number: None,
|
||||
page_number: None,
|
||||
chapter_id: Some("ch2".into()),
|
||||
snippet: "…epub match…".into(),
|
||||
match_start: 0,
|
||||
match_end: 8,
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
assert!(json.contains("\"chapter_id\":\"ch2\""));
|
||||
let back: SearchResult = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.chapter_id, Some("ch2".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utf8_multi_byte_boundary_safe() {
|
||||
let content = "Hello 🌍 world! 你好世界 test here.";
|
||||
let results = search_text(content, "test");
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].snippet.contains("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_valid() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "hello world".into(), match_start: 0, match_end: 5,
|
||||
};
|
||||
assert!(validate_search_result(&result, "hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_case_insensitive() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "HELLO WORLD".into(), match_start: 0, match_end: 5,
|
||||
};
|
||||
assert!(validate_search_result(&result, "hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_empty_query() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "text".into(), match_start: 0, match_end: 1,
|
||||
};
|
||||
assert!(!validate_search_result(&result, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_bounds_exceeded() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "short".into(), match_start: 5, match_end: 10,
|
||||
};
|
||||
assert!(!validate_search_result(&result, "test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_empty_snippet() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: String::new(), match_start: 0, match_end: 0,
|
||||
};
|
||||
assert!(!validate_search_result(&result, "hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_match_at_snippet_end() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "the end".into(), match_start: 4, match_end: 7,
|
||||
};
|
||||
assert!(validate_search_result(&result, "end"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_start_gt_end() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "text".into(), match_start: 3, match_end: 1,
|
||||
};
|
||||
assert!(!validate_search_result(&result, "test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_exact_bounds() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "exact".into(), match_start: 0, match_end: 5,
|
||||
};
|
||||
assert!(validate_search_result(&result, "exact"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_query_longer_than_snippet() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "hi".into(), match_start: 0, match_end: 2,
|
||||
};
|
||||
assert!(!validate_search_result(&result, "hello"));
|
||||
// match_end (2) == snippet.len() so bounds ok, but "hi" != "hello"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_unicode_snippet() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "你好世界".into(), match_start: 0, match_end: 6, // "你好" = 6 bytes in UTF-8
|
||||
};
|
||||
assert!(validate_search_result(&result, "你好"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_zero_length_match() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "test".into(), match_start: 1, match_end: 1,
|
||||
};
|
||||
// Zero-length match: start==end, query is empty string
|
||||
// Empty query → early return false
|
||||
assert!(!validate_search_result(&result, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_match_end_equals_snippet_len() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "match".into(), match_start: 0, match_end: 5,
|
||||
};
|
||||
// match_end == snippet.len() is valid
|
||||
assert!(validate_search_result(&result, "match"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_search_result_query_mismatch() {
|
||||
let result = SearchResult {
|
||||
block_id: "b1".into(), line_number: None, page_number: None, chapter_id: None,
|
||||
snippet: "hello world".into(), match_start: 0, match_end: 5,
|
||||
};
|
||||
assert!(!validate_search_result(&result, "world"));
|
||||
}
|
||||
}
|
||||
|
||||
588
crates/zx_document_core/src/session_v2.rs
Normal file
588
crates/zx_document_core/src/session_v2.rs
Normal file
@ -0,0 +1,588 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::progress::ReadingPosition;
|
||||
use crate::reading_material::ReadingMaterialRefV2;
|
||||
use crate::time_tracker::ActiveTimeTracker;
|
||||
|
||||
fn sessions() -> &'static Mutex<HashMap<String, ReadingSessionV2>> {
|
||||
use std::sync::OnceLock;
|
||||
static SESSIONS: OnceLock<Mutex<HashMap<String, ReadingSessionV2>>> = OnceLock::new();
|
||||
SESSIONS.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
fn trackers() -> &'static Mutex<HashMap<String, ActiveTimeTracker>> {
|
||||
use std::sync::OnceLock;
|
||||
static TRACKERS: OnceLock<Mutex<HashMap<String, ActiveTimeTracker>>> = OnceLock::new();
|
||||
TRACKERS.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, uniffi::Enum)]
|
||||
pub enum ReadingSessionStatus {
|
||||
Active,
|
||||
Paused,
|
||||
Closed,
|
||||
Interrupted,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct ReadingSessionV2 {
|
||||
pub client_session_id: String,
|
||||
pub material: ReadingMaterialRefV2,
|
||||
pub started_at_ms: i64,
|
||||
pub closed_at_ms: Option<i64>,
|
||||
pub last_event_at_ms: i64,
|
||||
pub next_sequence: u64,
|
||||
pub total_active_seconds: u32,
|
||||
pub last_position: Option<ReadingPosition>,
|
||||
pub status: ReadingSessionStatus,
|
||||
}
|
||||
|
||||
/// Start a new reading session. Returns the client_session_id as handle.
|
||||
pub fn start_reading_session_v2(
|
||||
material: ReadingMaterialRefV2,
|
||||
timestamp_ms: i64,
|
||||
) -> Result<String, SessionError> {
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let mut tracker = ActiveTimeTracker::new();
|
||||
tracker.start(timestamp_ms);
|
||||
|
||||
let session = ReadingSessionV2 {
|
||||
client_session_id: session_id.clone(),
|
||||
material,
|
||||
started_at_ms: timestamp_ms,
|
||||
closed_at_ms: None,
|
||||
last_event_at_ms: timestamp_ms,
|
||||
next_sequence: 1,
|
||||
total_active_seconds: 0,
|
||||
last_position: None,
|
||||
status: ReadingSessionStatus::Active,
|
||||
};
|
||||
|
||||
match (sessions().lock(), trackers().lock()) {
|
||||
(Ok(mut map), Ok(mut tmap)) => {
|
||||
map.insert(session_id.clone(), session);
|
||||
tmap.insert(session_id.clone(), tracker);
|
||||
Ok(session_id)
|
||||
}
|
||||
_ => Err(SessionError::LockPoisoned),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pause the session. Stops active time tracking.
|
||||
pub fn pause_reading_session_v2(session_id: &str) -> Result<(), SessionError> {
|
||||
with_session_mut(session_id, |s| {
|
||||
if s.status == ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::AlreadyClosed);
|
||||
}
|
||||
s.status = ReadingSessionStatus::Paused;
|
||||
Ok(())
|
||||
})?;
|
||||
// Pause tracker separately
|
||||
if let Ok(mut tmap) = trackers().lock() {
|
||||
if let Some(t) = tmap.get_mut(session_id) {
|
||||
let _ = t.pause(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resume a paused session.
|
||||
pub fn resume_reading_session_v2(session_id: &str) -> Result<(), SessionError> {
|
||||
with_session_mut(session_id, |s| {
|
||||
if s.status == ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::AlreadyClosed);
|
||||
}
|
||||
s.status = ReadingSessionStatus::Active;
|
||||
Ok(())
|
||||
})?;
|
||||
if let Ok(mut tmap) = trackers().lock() {
|
||||
if let Some(t) = tmap.get_mut(session_id) {
|
||||
t.resume(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the session and record its closed_at_ms. No more events allowed.
|
||||
pub fn close_reading_session_v2(session_id: &str, timestamp_ms: i64) -> Result<(), SessionError> {
|
||||
with_session_mut(session_id, |s| {
|
||||
if s.status == ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::AlreadyClosed);
|
||||
}
|
||||
s.status = ReadingSessionStatus::Closed;
|
||||
s.closed_at_ms = Some(timestamp_ms);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Set closed_at_ms from the caller's timestamp after closing succeeds.
|
||||
/// Separated from close_reading_session_v2 so the close status transition
|
||||
/// is atomic and the timestamp is set independently.
|
||||
pub fn set_session_closed_at_ms(session_id: &str, timestamp_ms: i64) -> Result<(), SessionError> {
|
||||
with_session_mut(session_id, |s| {
|
||||
s.closed_at_ms = Some(timestamp_ms);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark a session as interrupted (app crash / background kill / orphaned).
|
||||
pub fn mark_session_interrupted_v2(session_id: &str) -> Result<(), SessionError> {
|
||||
with_session_mut(session_id, |s| {
|
||||
if s.status == ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::AlreadyClosed);
|
||||
}
|
||||
if s.status == ReadingSessionStatus::Interrupted {
|
||||
return Err(SessionError::AlreadyInterrupted);
|
||||
}
|
||||
s.status = ReadingSessionStatus::Interrupted;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark a session as failed (unrecoverable error).
|
||||
pub fn mark_session_failed_v2(session_id: &str) -> Result<(), SessionError> {
|
||||
with_session_mut(session_id, |s| {
|
||||
if s.status == ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::AlreadyClosed);
|
||||
}
|
||||
if s.status == ReadingSessionStatus::Failed {
|
||||
return Err(SessionError::AlreadyFailed);
|
||||
}
|
||||
s.status = ReadingSessionStatus::Failed;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the active session for a given material_id, if any.
|
||||
pub fn get_active_session_v2(material_id: &str) -> Option<ReadingSessionV2> {
|
||||
match sessions().lock() {
|
||||
Ok(map) => {
|
||||
map.values()
|
||||
.find(|s| s.material.material_id == material_id && s.status == ReadingSessionStatus::Active)
|
||||
.cloned()
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record an event on this session: bumps sequence and updates last_event_at_ms.
|
||||
/// Returns the sequence number that should be assigned to the event.
|
||||
pub fn record_session_event_v2(
|
||||
session_id: &str,
|
||||
timestamp_ms: i64,
|
||||
active_seconds_delta: u32,
|
||||
position: Option<ReadingPosition>,
|
||||
) -> Result<u64, SessionError> {
|
||||
let seq = with_session_mut(session_id, |s| {
|
||||
if s.status == ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::AlreadyClosed);
|
||||
}
|
||||
if s.status != ReadingSessionStatus::Active && active_seconds_delta > 0 {
|
||||
return Err(SessionError::NotActive);
|
||||
}
|
||||
let seq = s.next_sequence;
|
||||
s.next_sequence += 1;
|
||||
s.last_event_at_ms = timestamp_ms;
|
||||
if position.is_some() {
|
||||
s.last_position = position;
|
||||
}
|
||||
Ok(seq)
|
||||
})?;
|
||||
|
||||
// Use ActiveTimeTracker to validate delta when active
|
||||
let validated_delta = if let Ok(mut tmap) = trackers().lock() {
|
||||
if let Some(t) = tmap.get_mut(session_id) {
|
||||
t.tick(timestamp_ms)
|
||||
} else {
|
||||
active_seconds_delta
|
||||
}
|
||||
} else {
|
||||
active_seconds_delta
|
||||
};
|
||||
|
||||
// Update total_active_seconds
|
||||
if validated_delta > 0 {
|
||||
let _ = with_session_mut(session_id, |s| {
|
||||
s.total_active_seconds += validated_delta;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(seq)
|
||||
}
|
||||
|
||||
/// Get a copy of the session info.
|
||||
pub fn get_session_v2(session_id: &str) -> Result<ReadingSessionV2, SessionError> {
|
||||
match sessions().lock() {
|
||||
Ok(map) => map.get(session_id).cloned().ok_or(SessionError::NotFound),
|
||||
Err(_) => Err(SessionError::LockPoisoned),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up stale sessions that have been inactive longer than max_age_ms.
|
||||
/// This should be called on app startup to recover from crash/kill scenarios.
|
||||
/// Removes both Closed and orphaned Active/Paused sessions.
|
||||
/// Also removes orphaned trackers that have no corresponding session.
|
||||
pub fn cleanup_stale_sessions_v2(now_ms: i64, max_age_ms: i64) -> u32 {
|
||||
let mut removed = 0u32;
|
||||
|
||||
match (sessions().lock(), trackers().lock()) {
|
||||
(Ok(mut map), Ok(mut tmap)) => {
|
||||
// Remove stale sessions
|
||||
let stale_ids: Vec<String> = map
|
||||
.iter()
|
||||
.filter(|(_, s)| now_ms - s.last_event_at_ms > max_age_ms)
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
for id in &stale_ids {
|
||||
map.remove(id);
|
||||
tmap.remove(id);
|
||||
removed += 1;
|
||||
}
|
||||
|
||||
// Remove orphaned trackers (no corresponding session)
|
||||
let orphan_ids: Vec<String> = tmap
|
||||
.keys()
|
||||
.filter(|id| !map.contains_key(*id))
|
||||
.cloned()
|
||||
.collect();
|
||||
for id in orphan_ids {
|
||||
tmap.remove(&id);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
/// Remove a closed session from memory. Refuses to remove Active/Paused sessions.
|
||||
pub fn remove_session_v2(session_id: &str) -> Result<(), SessionError> {
|
||||
match (sessions().lock(), trackers().lock()) {
|
||||
(Ok(mut map), Ok(mut tmap)) => {
|
||||
let session = map.get(session_id).ok_or(SessionError::NotFound)?;
|
||||
if session.status != ReadingSessionStatus::Closed {
|
||||
return Err(SessionError::NotClosed);
|
||||
}
|
||||
map.remove(session_id);
|
||||
tmap.remove(session_id);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(SessionError::LockPoisoned),
|
||||
}
|
||||
}
|
||||
|
||||
// ── helpers ──
|
||||
|
||||
fn with_session_mut<F, T>(session_id: &str, f: F) -> Result<T, SessionError>
|
||||
where
|
||||
F: FnOnce(&mut ReadingSessionV2) -> Result<T, SessionError>,
|
||||
{
|
||||
match sessions().lock() {
|
||||
Ok(mut map) => {
|
||||
let session = map.get_mut(session_id).ok_or(SessionError::NotFound)?;
|
||||
f(session)
|
||||
}
|
||||
Err(_) => Err(SessionError::LockPoisoned),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SessionError {
|
||||
NotFound,
|
||||
AlreadyClosed,
|
||||
AlreadyInterrupted,
|
||||
AlreadyFailed,
|
||||
NotActive,
|
||||
NotClosed,
|
||||
LockPoisoned,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SessionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound => write!(f, "Session not found"),
|
||||
Self::AlreadyClosed => write!(f, "Session already closed"),
|
||||
Self::AlreadyInterrupted => write!(f, "Session already interrupted"),
|
||||
Self::AlreadyFailed => write!(f, "Session already failed"),
|
||||
Self::NotActive => write!(f, "Session is not active (paused or closed)"),
|
||||
Self::NotClosed => write!(f, "Session must be closed before removal"),
|
||||
Self::LockPoisoned => write!(f, "Session lock poisoned"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_material() -> ReadingMaterialRefV2 {
|
||||
ReadingMaterialRefV2::new("test_mat_001", "/tmp/test.md", "test.md", "markdown")
|
||||
}
|
||||
|
||||
fn teardown(id: &str) {
|
||||
let _ = close_reading_session_v2(id, 0);
|
||||
let _ = remove_session_v2(id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_session() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
assert!(!id.is_empty());
|
||||
let s = get_session_v2(&id).unwrap();
|
||||
assert_eq!(s.material.material_id, "test_mat_001");
|
||||
assert_eq!(s.status, ReadingSessionStatus::Active);
|
||||
assert_eq!(s.next_sequence, 1);
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pause_resume_close() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
pause_reading_session_v2(&id).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Paused);
|
||||
resume_reading_session_v2(&id).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Active);
|
||||
close_reading_session_v2(&id, 0).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Closed);
|
||||
remove_session_v2(&id).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_refuses_active() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
assert!(remove_session_v2(&id).is_err()); // Not closed
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_increments() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap(); // tracker not used for 0 delta
|
||||
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
|
||||
assert_eq!(s1, 1);
|
||||
assert_eq!(s2, 2);
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closed_rejects_events() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
close_reading_session_v2(&id, 0).unwrap();
|
||||
assert!(record_session_event_v2(&id, 2000, 10, None).is_err());
|
||||
assert!(close_reading_session_v2(&id, 0).is_err());
|
||||
remove_session_v2(&id).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paused_rejects_active_seconds() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
pause_reading_session_v2(&id).unwrap();
|
||||
// PositionChanged (delta=0) should be allowed when paused
|
||||
let seq = record_session_event_v2(&id, 2000, 0, None).unwrap();
|
||||
assert_eq!(seq, 1);
|
||||
// Heartbeat (delta>0) should be rejected when paused
|
||||
assert!(record_session_event_v2(&id, 3000, 15, None).is_err());
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found() {
|
||||
assert!(get_session_v2("nonexistent").is_err());
|
||||
assert!(close_reading_session_v2("nonexistent", 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_sessions_independent() {
|
||||
let a = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 2000).unwrap();
|
||||
assert_ne!(a, b);
|
||||
record_session_event_v2(&a, 3000, 0, None).unwrap();
|
||||
record_session_event_v2(&b, 4000, 0, None).unwrap();
|
||||
teardown(&a);
|
||||
teardown(&b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_active_seconds_accumulates() {
|
||||
let id = start_reading_session_v2(test_material(), 0).unwrap();
|
||||
// Simulate iOS calling tick() and passing deltas to session events
|
||||
record_session_event_v2(&id, 15_000, 15, None).unwrap();
|
||||
record_session_event_v2(&id, 30_000, 15, None).unwrap();
|
||||
record_session_event_v2(&id, 43_000, 13, None).unwrap();
|
||||
let s = get_session_v2(&id).unwrap();
|
||||
assert_eq!(s.total_active_seconds, 43);
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_and_seconds_independent() {
|
||||
let a = start_reading_session_v2(test_material(), 0).unwrap();
|
||||
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 0).unwrap();
|
||||
record_session_event_v2(&a, 10_000, 10, None).unwrap();
|
||||
record_session_event_v2(&b, 5_000, 5, None).unwrap();
|
||||
record_session_event_v2(&b, 10_000, 5, None).unwrap();
|
||||
assert_eq!(get_session_v2(&a).unwrap().total_active_seconds, 10);
|
||||
assert_eq!(get_session_v2(&a).unwrap().next_sequence, 2);
|
||||
assert_eq!(get_session_v2(&b).unwrap().total_active_seconds, 10);
|
||||
assert_eq!(get_session_v2(&b).unwrap().next_sequence, 3);
|
||||
teardown(&a);
|
||||
teardown(&b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_position_updated() {
|
||||
let id = start_reading_session_v2(test_material(), 0).unwrap();
|
||||
let pos1 = ReadingPosition::Markdown { block_id: "intro".into(), scroll_progress: 0.3 };
|
||||
let pos2 = ReadingPosition::Markdown { block_id: "ch1".into(), scroll_progress: 0.8 };
|
||||
record_session_event_v2(&id, 1000, 0, Some(pos1.clone())).unwrap();
|
||||
record_session_event_v2(&id, 2000, 0, Some(pos2.clone())).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().last_position, Some(pos2));
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_stale_removes_old_sessions() {
|
||||
// Old session: last event at t=1000
|
||||
let old = start_reading_session_v2(test_material(), 0).unwrap();
|
||||
record_session_event_v2(&old, 1000, 0, None).unwrap();
|
||||
|
||||
// Recent session: last event at t=19000 (just 1s ago at now=20000)
|
||||
let recent = start_reading_session_v2(ReadingMaterialRefV2::new("mat_recent", "/tmp/r.md", "r.md", "markdown"), 10000).unwrap();
|
||||
record_session_event_v2(&recent, 19000, 0, None).unwrap();
|
||||
|
||||
// At t=20000, clean up sessions inactive for > 5000ms
|
||||
// old: 20000-1000=19000 > 5000 → stale
|
||||
// recent: 20000-19000=1000 < 5000 → NOT stale
|
||||
let removed = cleanup_stale_sessions_v2(20000, 5000);
|
||||
assert_eq!(removed, 1);
|
||||
|
||||
assert!(get_session_v2(&old).is_err());
|
||||
assert!(get_session_v2(&recent).is_ok());
|
||||
|
||||
teardown(&recent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_stale_removes_orphaned_active_session() {
|
||||
// Simulate a crash: active session not closed, last event at t=5000
|
||||
let orphaned = start_reading_session_v2(test_material(), 0).unwrap();
|
||||
record_session_event_v2(&orphaned, 5000, 10, None).unwrap();
|
||||
// Session still Active
|
||||
|
||||
// At t=60000, clean up sessions inactive for > 10000ms
|
||||
// 60000 - 5000 = 55000 > 10000 → stale
|
||||
let removed = cleanup_stale_sessions_v2(60000, 10000);
|
||||
assert_eq!(removed, 1);
|
||||
assert!(get_session_v2(&orphaned).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_stale_zero_when_all_recent() {
|
||||
let active = start_reading_session_v2(test_material(), 0).unwrap();
|
||||
// Last event at t=9500, now at t=10000, max_age=1000
|
||||
// 10000-9500=500 < 1000 → NOT stale
|
||||
record_session_event_v2(&active, 9500, 0, None).unwrap();
|
||||
|
||||
let removed = cleanup_stale_sessions_v2(10000, 1000);
|
||||
assert_eq!(removed, 0);
|
||||
assert!(get_session_v2(&active).is_ok());
|
||||
|
||||
teardown(&active);
|
||||
}
|
||||
|
||||
// ── Interrupted / Failed / closed_at_ms / get_active_session ──
|
||||
|
||||
#[test]
|
||||
fn test_mark_interrupted() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
mark_session_interrupted_v2(&id).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Interrupted);
|
||||
// Double mark should fail
|
||||
assert!(mark_session_interrupted_v2(&id).is_err());
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_failed() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
mark_session_failed_v2(&id).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Failed);
|
||||
// Double mark should fail
|
||||
assert!(mark_session_failed_v2(&id).is_err());
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closed_rejects_interrupted() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
close_reading_session_v2(&id, 0).unwrap();
|
||||
assert!(mark_session_interrupted_v2(&id).is_err());
|
||||
assert!(mark_session_failed_v2(&id).is_err());
|
||||
remove_session_v2(&id).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_sets_closed_at_ms() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
assert!(get_session_v2(&id).unwrap().closed_at_ms.is_none());
|
||||
close_reading_session_v2(&id, 5000).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().closed_at_ms, Some(5000));
|
||||
remove_session_v2(&id).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_closed_at_ms_override() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
close_reading_session_v2(&id, 5000).unwrap();
|
||||
// set_session_closed_at_ms can override if needed
|
||||
set_session_closed_at_ms(&id, 9999).unwrap();
|
||||
assert_eq!(get_session_v2(&id).unwrap().closed_at_ms, Some(9999));
|
||||
remove_session_v2(&id).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_active_session_by_material() {
|
||||
let mat_a = ReadingMaterialRefV2::new("mat_a", "/tmp/a.md", "a.md", "markdown");
|
||||
let mat_b = ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown");
|
||||
|
||||
let id_a = start_reading_session_v2(mat_a.clone(), 1000).unwrap();
|
||||
let _id_b = start_reading_session_v2(mat_b.clone(), 1000).unwrap();
|
||||
|
||||
let found = get_active_session_v2("mat_a");
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().client_session_id, id_a);
|
||||
|
||||
// Close session a, should not be found as active
|
||||
close_reading_session_v2(&id_a, 0).unwrap();
|
||||
assert!(get_active_session_v2("mat_a").is_none());
|
||||
|
||||
teardown(&id_a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_does_not_store_business_fields() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
let s = get_session_v2(&id).unwrap();
|
||||
// Verify the material ref has no business fields
|
||||
assert_eq!(s.material.material_id, "test_mat_001");
|
||||
// Session itself has no userId / knowledgeBaseId / readingTargetType fields
|
||||
// (verified at compile time — these fields don't exist on the struct)
|
||||
teardown(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_monotonic_in_session() {
|
||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap();
|
||||
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
|
||||
let s3 = record_session_event_v2(&id, 4000, 0, None).unwrap();
|
||||
assert!(s1 < s2);
|
||||
assert!(s2 < s3);
|
||||
assert_eq!(s3, 3);
|
||||
teardown(&id);
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,7 @@ pub fn text_stats(content: &str) -> TextStats {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, uniffi::Record)]
|
||||
pub struct TextStats {
|
||||
pub line_count: u32,
|
||||
pub word_count: u32,
|
||||
|
||||
166
crates/zx_document_core/src/time_tracker.rs
Normal file
166
crates/zx_document_core/src/time_tracker.rs
Normal file
@ -0,0 +1,166 @@
|
||||
/// Tracks active reading time and calculates delta seconds.
|
||||
///
|
||||
/// iOS controls when to call tick (every 15s), pause (background), resume (foreground).
|
||||
/// Rust does NOT create its own timer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ActiveTimeTracker {
|
||||
last_tick_ms: Option<i64>,
|
||||
is_active: bool,
|
||||
remainder_ms: i64,
|
||||
}
|
||||
|
||||
impl ActiveTimeTracker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_tick_ms: None,
|
||||
is_active: false,
|
||||
remainder_ms: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start tracking. Returns delta = 0.
|
||||
pub fn start(&mut self, timestamp_ms: i64) -> u32 {
|
||||
self.last_tick_ms = Some(timestamp_ms);
|
||||
self.is_active = true;
|
||||
self.remainder_ms = 0;
|
||||
0
|
||||
}
|
||||
|
||||
/// Pause tracking. Returns accumulated delta since last tick.
|
||||
pub fn pause(&mut self, timestamp_ms: i64) -> u32 {
|
||||
let delta = self.tick(timestamp_ms);
|
||||
self.is_active = false;
|
||||
delta
|
||||
}
|
||||
|
||||
/// Resume after pause. No delta produced.
|
||||
pub fn resume(&mut self, timestamp_ms: i64) {
|
||||
self.is_active = true;
|
||||
self.last_tick_ms = Some(timestamp_ms);
|
||||
}
|
||||
|
||||
/// Regular tick. Returns delta seconds since last tick (or pause).
|
||||
/// Returns 0 if not active or time goes backwards.
|
||||
pub fn tick(&mut self, timestamp_ms: i64) -> u32 {
|
||||
if !self.is_active {
|
||||
return 0;
|
||||
}
|
||||
let last = match self.last_tick_ms {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
self.last_tick_ms = Some(timestamp_ms);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
if timestamp_ms <= last {
|
||||
return 0;
|
||||
}
|
||||
let elapsed_ms = timestamp_ms - last;
|
||||
let total_ms = elapsed_ms + self.remainder_ms;
|
||||
let delta_seconds = (total_ms / 1000) as u32;
|
||||
self.remainder_ms = total_ms % 1000;
|
||||
self.last_tick_ms = Some(timestamp_ms);
|
||||
delta_seconds
|
||||
}
|
||||
|
||||
/// Close tracking. Returns final residual delta.
|
||||
pub fn close(&mut self, timestamp_ms: i64) -> u32 {
|
||||
let delta = self.tick(timestamp_ms);
|
||||
self.is_active = false;
|
||||
delta
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.is_active
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ActiveTimeTracker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_start_returns_zero() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
assert_eq!(t.start(1000), 0);
|
||||
assert!(t.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_43_seconds_total() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
t.start(0);
|
||||
let d1 = t.tick(15_000); // 15s
|
||||
let d2 = t.tick(30_000); // 15s
|
||||
let d3 = t.close(43_000); // 13s residual
|
||||
assert_eq!(d1, 15);
|
||||
assert_eq!(d2, 15);
|
||||
assert_eq!(d3, 13);
|
||||
assert_eq!(d1 + d2 + d3, 43);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pause_stops_time() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
t.start(0);
|
||||
t.tick(15_000); // 15s
|
||||
t.pause(15_000);
|
||||
t.resume(45_000); // 30s pause
|
||||
let d = t.tick(60_000); // 15s after resume
|
||||
assert_eq!(d, 15); // only 15s counted, not 45s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_goes_backwards() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
t.start(10000);
|
||||
assert_eq!(t.tick(5000), 0); // backwards
|
||||
assert_eq!(t.tick(25000), 15); // normal after backwards reset
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inactive_tick_returns_zero() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
assert_eq!(t.tick(5000), 0); // not started
|
||||
t.start(0);
|
||||
t.pause(5000);
|
||||
assert_eq!(t.tick(10000), 0); // paused
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_returns_residual() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
t.start(0);
|
||||
t.tick(15000);
|
||||
let d = t.close(20500); // 5.5s → 5s, 500ms remainder
|
||||
assert_eq!(d, 5);
|
||||
assert!(!t.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remainder_accumulates() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
t.start(0);
|
||||
// Each tick: 1500ms → 1s + 500ms remainder
|
||||
let d1 = t.tick(1500);
|
||||
let d2 = t.tick(3000);
|
||||
let d3 = t.tick(4500);
|
||||
// Total: 3 ticks → d1=1, d2=2(with remainder), d3=1
|
||||
// 1500ms = 1s, remainder 500ms
|
||||
// tick2: 1500ms + 500ms remainder = 2000ms = 2s
|
||||
// tick3: 1500ms = 1s, remainder 500ms
|
||||
assert_eq!(d1 + d2 + d3, 4); // 1 + 2 + 1 = 4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_inactive_returns_zero() {
|
||||
let mut t = ActiveTimeTracker::new();
|
||||
assert_eq!(t.close(1000), 0);
|
||||
}
|
||||
}
|
||||
1
crates/zx_document_core/src/zx_document_core.udl
Normal file
1
crates/zx_document_core/src/zx_document_core.udl
Normal file
@ -0,0 +1 @@
|
||||
namespace zx_document_core {};
|
||||
@ -8,7 +8,7 @@ crate-type = ["lib", "staticlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zx_document_core = { path = "../zx_document_core" }
|
||||
uniffi = "0.28"
|
||||
uniffi = "0.31"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.28", features = ["build"] }
|
||||
uniffi = { version = "0.31", features = ["build"] }
|
||||
|
||||
@ -1 +1,927 @@
|
||||
// FFI functions are called from generated UniFFI bindings (C-ABI),
|
||||
// so Rust's dead_code analysis doesn't see the calls.
|
||||
#![allow(dead_code)]
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
pub use zx_document_core::material_type::{MaterialType, PreviewMode};
|
||||
pub use zx_document_core::document::DocumentInfo;
|
||||
pub use zx_document_core::image_meta::ImageMeta;
|
||||
pub use zx_document_core::text::TextStats;
|
||||
pub use zx_document_core::search::SearchResult;
|
||||
pub use zx_document_core::anchors::NoteAnchor;
|
||||
pub use zx_document_core::progress::ReadingPosition;
|
||||
pub use zx_document_core::events::ReadingEvent;
|
||||
#[allow(deprecated)]
|
||||
pub use zx_document_core::reading_material::ReadingMaterialRef;
|
||||
pub use zx_document_core::reading_material::ReadingMaterialRefV2;
|
||||
pub use zx_document_core::events_v2::{ReadingEventV2, ReadingEventTypeV2, EventBufferStateV2};
|
||||
pub use zx_document_core::epub::{EpubMetadata, EpubChapter, EpubChapterText};
|
||||
pub use zx_document_core::office::{OfficePreviewConfig, OfficePreviewStrategy};
|
||||
pub use zx_document_core::pdf::{PdfMetadata, PdfPageText};
|
||||
pub use zx_document_core::session_v2::{ReadingSessionV2, ReadingSessionStatus};
|
||||
|
||||
use zx_document_core::blocks as core_blocks;
|
||||
|
||||
// ── V2 Reading Session FFI ──
|
||||
|
||||
#[uniffi::export]
|
||||
fn start_reading_session_v2(material: ReadingMaterialRefV2, timestamp_ms: i64) -> Result<String, String> {
|
||||
zx_document_core::session_v2::start_reading_session_v2(material, timestamp_ms)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn pause_reading_session_v2(session_id: String) -> Result<(), String> {
|
||||
zx_document_core::session_v2::pause_reading_session_v2(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn resume_reading_session_v2(session_id: String) -> Result<(), String> {
|
||||
zx_document_core::session_v2::resume_reading_session_v2(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn close_reading_session_v2(session_id: String, timestamp_ms: i64) -> Result<(), String> {
|
||||
zx_document_core::session_v2::close_reading_session_v2(&session_id, timestamp_ms).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn set_session_closed_at_ms_v2(session_id: String, timestamp_ms: i64) -> Result<(), String> {
|
||||
zx_document_core::session_v2::set_session_closed_at_ms(&session_id, timestamp_ms).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn mark_session_interrupted_v2(session_id: String) -> Result<(), String> {
|
||||
zx_document_core::session_v2::mark_session_interrupted_v2(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn mark_session_failed_v2(session_id: String) -> Result<(), String> {
|
||||
zx_document_core::session_v2::mark_session_failed_v2(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn get_active_session_v2(material_id: String) -> Option<ReadingSessionV2> {
|
||||
zx_document_core::session_v2::get_active_session_v2(&material_id)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn get_session_v2_ffi(session_id: String) -> Option<ReadingSessionV2> {
|
||||
zx_document_core::session_v2::get_session_v2(&session_id).ok()
|
||||
}
|
||||
|
||||
// ── V2 Reading Event FFI ──
|
||||
|
||||
#[uniffi::export]
|
||||
fn push_material_opened_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
||||
zx_document_core::events_v2::push_material_opened_v2(&session_id, &material_id, timestamp_ms)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn push_material_closed_v2(session_id: String, material_id: String, active_seconds_delta: u32, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
||||
zx_document_core::events_v2::push_material_closed_v2(&session_id, &material_id, active_seconds_delta, timestamp_ms)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn push_position_changed_v2(session_id: String, material_id: String, position: ReadingPosition, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
||||
zx_document_core::events_v2::push_position_changed_v2(&session_id, &material_id, position, timestamp_ms)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn push_heartbeat_v2(session_id: String, material_id: String, active_seconds_delta: u32, position: Option<ReadingPosition>, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
||||
zx_document_core::events_v2::push_heartbeat_v2(&session_id, &material_id, active_seconds_delta, position, timestamp_ms)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn push_marked_as_read_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
||||
zx_document_core::events_v2::push_marked_as_read_v2(&session_id, &material_id, timestamp_ms)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ── V2 Buffer Management FFI ──
|
||||
|
||||
#[uniffi::export]
|
||||
fn export_pending_events_v2(limit: u32, timestamp_ms: i64) -> Vec<ReadingEventV2> {
|
||||
zx_document_core::events_v2::export_pending_events_v2(limit, timestamp_ms)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn reload_stale_events_v2() -> u32 {
|
||||
zx_document_core::events_v2::reload_stale_events_v2()
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn get_event_buffer_state_v2() -> EventBufferStateV2 {
|
||||
zx_document_core::events_v2::get_event_buffer_state_v2()
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn ack_events_v2(event_ids: Vec<String>) -> u32 {
|
||||
zx_document_core::events_v2::ack_events_v2(&event_ids)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn mark_events_failed_v2(event_ids: Vec<String>) -> u32 {
|
||||
zx_document_core::events_v2::mark_events_failed_v2(&event_ids)
|
||||
}
|
||||
|
||||
// FFI-compatible DocumentBlock (tuple variants, UniFFI proc-macro)
|
||||
#[derive(Debug, uniffi::Enum)]
|
||||
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, uniffi::Error)]
|
||||
pub enum DocumentError {
|
||||
FileNotFound,
|
||||
UnsupportedFormat,
|
||||
ParseError,
|
||||
InvalidEncoding,
|
||||
IoError,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DocumentError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::FileNotFound => write!(f, "File not found"),
|
||||
Self::UnsupportedFormat => write!(f, "Unsupported format"),
|
||||
Self::ParseError => write!(f, "Parse error"),
|
||||
Self::InvalidEncoding => write!(f, "Invalid encoding"),
|
||||
Self::IoError => write!(f, "IO error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DocumentError {}
|
||||
|
||||
impl From<zx_document_core::error::DocumentError> for DocumentError {
|
||||
fn from(e: zx_document_core::error::DocumentError) -> Self {
|
||||
match e {
|
||||
zx_document_core::error::DocumentError::FileNotFound(_) => Self::FileNotFound,
|
||||
zx_document_core::error::DocumentError::UnsupportedFormat(_) => Self::UnsupportedFormat,
|
||||
zx_document_core::error::DocumentError::ParseError(_) => Self::ParseError,
|
||||
zx_document_core::error::DocumentError::InvalidEncoding => Self::InvalidEncoding,
|
||||
zx_document_core::error::DocumentError::IoError(_) => Self::IoError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn detect_material_type(file_path: String) -> Result<MaterialType, DocumentError> {
|
||||
zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn read_image_meta(file_path: String) -> Result<ImageMeta, DocumentError> {
|
||||
zx_document_core::image_meta::read_image_meta(&file_path).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn build_document_info(file_path: String, material_id: String, title: String) -> Result<DocumentInfo, DocumentError> {
|
||||
zx_document_core::document::build_document_info(&file_path, material_id, title).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn extract_pdf_text(file_path: String) -> Result<Vec<PdfPageText>, DocumentError> {
|
||||
let path = std::path::Path::new(&file_path);
|
||||
zx_document_core::pdf::extract_pdf_text(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn extract_epub_chapter_texts(file_path: String) -> Result<Vec<EpubChapterText>, DocumentError> {
|
||||
let path = std::path::Path::new(&file_path);
|
||||
zx_document_core::epub::extract_epub_chapter_texts(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn read_text_stats(file_path: String) -> Result<TextStats, DocumentError> {
|
||||
let content = std::fs::read_to_string(&file_path).map_err(|_| DocumentError::FileNotFound)?;
|
||||
Ok(zx_document_core::text::text_stats(&content))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn parse_text(content: String) -> Result<Vec<DocumentBlock>, DocumentError> {
|
||||
let blocks = zx_document_core::text::parse_text_content(&content);
|
||||
let result: Vec<DocumentBlock> = blocks.into_iter().map(Into::into).collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
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,
|
||||
})?;
|
||||
let result: Vec<DocumentBlock> = 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,
|
||||
)
|
||||
};
|
||||
// Check if allocation succeeded
|
||||
if call_status.code != uniffi::RustCallStatusCode::Success {
|
||||
unsafe {
|
||||
*out_capacity = 0;
|
||||
*out_len = 0;
|
||||
*out_data = std::ptr::null_mut();
|
||||
}
|
||||
return;
|
||||
}
|
||||
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 = <Result<Vec<DocumentBlock>, DocumentError> as LowerReturn<UniFfiTag>>::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<DocumentBlock>, query: String) -> Vec<SearchResult> {
|
||||
let core_blocks: Vec<core_blocks::DocumentBlock> = 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<SearchResult> {
|
||||
zx_document_core::search::search_text(&content, &query)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn search_pdf_pages(page_numbers: Vec<u32>, page_texts: Vec<String>, query: String) -> Vec<SearchResult> {
|
||||
let pages: Vec<_> = page_numbers.iter().copied()
|
||||
.zip(page_texts.iter().map(|s| s.as_str()))
|
||||
.collect();
|
||||
zx_document_core::search::search_pdf_text(&pages, &query)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn search_epub_chapters_ffi(chapter_ids: Vec<String>, chapter_texts: Vec<String>, query: String) -> Vec<SearchResult> {
|
||||
let chapters: Vec<_> = chapter_ids.iter().map(|s| s.clone())
|
||||
.zip(chapter_texts.iter().map(|s| s.as_str()))
|
||||
.collect();
|
||||
zx_document_core::search::search_epub_chapters(&chapters, &query)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn create_note_anchor(material_id: String, position: Option<ReadingPosition>) -> NoteAnchor {
|
||||
zx_document_core::anchors::NoteAnchor::from_position(&material_id, position.as_ref())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn create_note_anchor_from_search(material_id: String, result: SearchResult) -> NoteAnchor {
|
||||
zx_document_core::anchors::NoteAnchor::from_search_result(&material_id, &result)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn restore_position_from_anchor(anchor: NoteAnchor) -> Option<ReadingPosition> {
|
||||
anchor.to_position()
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn read_pdf_metadata_ffi(file_path: String) -> Result<PdfMetadata, DocumentError> {
|
||||
zx_document_core::pdf::read_pdf_metadata(std::path::Path::new(&file_path)).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn extract_pdf_text_ffi(file_path: String) -> Result<Vec<PdfPageText>, DocumentError> {
|
||||
zx_document_core::pdf::extract_pdf_text(std::path::Path::new(&file_path)).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn read_epub_metadata_ffi(file_path: String) -> Result<EpubMetadata, DocumentError> {
|
||||
zx_document_core::epub::read_epub_metadata(std::path::Path::new(&file_path)).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn read_epub_chapters_ffi(file_path: String) -> Result<Vec<EpubChapter>, DocumentError> {
|
||||
zx_document_core::epub::read_epub_chapters(std::path::Path::new(&file_path)).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn get_office_preview_config_ffi(material_type: MaterialType, file_size: u64) -> Result<OfficePreviewConfig, DocumentError> {
|
||||
zx_document_core::office::get_office_preview_config(&material_type, file_size).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn is_office_type_ffi(material_type: MaterialType) -> bool {
|
||||
zx_document_core::office::is_office_type(&material_type)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn cleanup_stale_sessions_ffi(now_ms: i64, max_age_ms: i64) -> u32 {
|
||||
zx_document_core::session_v2::cleanup_stale_sessions_v2(now_ms, max_age_ms)
|
||||
}
|
||||
|
||||
#[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<ReadingEvent> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Helper: serialize a Result<T, DocumentError> into out-pointers.
|
||||
/// Generic over T; the RustBuffer methods are accessed via the concrete type
|
||||
/// after the `lower_return` call.
|
||||
macro_rules! write_result_to_out {
|
||||
($result:expr, $out_capacity:ident, $out_len:ident, $out_data:ident, $out_error_code:ident) => {{
|
||||
use uniffi::LowerReturn;
|
||||
match <Result<_, DocumentError> as LowerReturn<UniFfiTag>>::lower_return($result) {
|
||||
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(_) => {
|
||||
unsafe { *$out_error_code = -1; }
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// Helper: read a UTF-8 string from raw bytes, or set error and return false.
|
||||
unsafe fn read_str_input(len: i32, data: *const u8, out_error_code: *mut i8) -> Option<String> {
|
||||
let slice = std::slice::from_raw_parts(data, len as usize);
|
||||
match std::str::from_utf8(slice) {
|
||||
Ok(s) => Some(s.to_string()),
|
||||
Err(_) => { *out_error_code = -1; None },
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Batch 1 out-pointer functions: String input → Result<T, E> output ───
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_detect_material_type_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 file_path = match unsafe { read_str_input(len, data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let result = crate::detect_material_type(file_path);
|
||||
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_read_image_meta_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 file_path = match unsafe { read_str_input(len, data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let result = crate::read_image_meta(file_path);
|
||||
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_read_text_stats_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 file_path = match unsafe { read_str_input(len, data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let result = crate::read_text_stats(file_path);
|
||||
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_parse_text_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 content = match unsafe { read_str_input(len, data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let result = crate::parse_text(content);
|
||||
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
// ─── Helper: lift from raw buffer fields ───
|
||||
|
||||
macro_rules! lift_from_raw {
|
||||
($T:ty, $capacity:expr, $len:expr, $data:expr) => {{
|
||||
let v = unsafe { Vec::from_raw_parts($data as *mut u8, $len as usize, $capacity as usize) };
|
||||
let buf = uniffi::RustBuffer::from_vec(v);
|
||||
<$T as uniffi::Lift<UniFfiTag>>::try_lift(buf).expect(concat!("failed to lift ", stringify!($T)))
|
||||
}};
|
||||
}
|
||||
|
||||
// ─── Batch 2: complex type input/output ───
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_push_reading_event_separate(
|
||||
event_cap: u64, event_len: u64, event_data: *const u8,
|
||||
) {
|
||||
let event: ReadingEvent = lift_from_raw!(ReadingEvent, event_cap, event_len, event_data);
|
||||
crate::push_reading_event(event);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_update_reading_position_separate(
|
||||
mid_len: i32, mid_data: *const u8,
|
||||
pos_cap: u64, pos_len: u64, pos_data: *const u8,
|
||||
out_error_code: *mut i8,
|
||||
) {
|
||||
let material_id = match unsafe { read_str_input(mid_len, mid_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
unsafe { *out_error_code = 0; }
|
||||
let position: ReadingPosition = lift_from_raw!(ReadingPosition, pos_cap, pos_len, pos_data);
|
||||
crate::update_reading_position(material_id, position);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_export_pending_events_separate(
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let result: Vec<ReadingEvent> = crate::export_pending_events();
|
||||
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_create_note_anchor_separate(
|
||||
mid_len: i32, mid_data: *const u8,
|
||||
pos_cap: u64, pos_len: u64, pos_data: *const u8, pos_has_value: i8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let material_id = match unsafe { read_str_input(mid_len, mid_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let position: Option<ReadingPosition> = if pos_has_value != 0 {
|
||||
Some(lift_from_raw!(ReadingPosition, pos_cap, pos_len, pos_data))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result: Result<NoteAnchor, DocumentError> = Ok(crate::create_note_anchor(material_id, position));
|
||||
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
// ─── Batch 3: search functions ───
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_search_markdown_blocks_separate(
|
||||
blocks_cap: u64, blocks_len: u64, blocks_data: *const u8,
|
||||
query_len: i32, query_data: *const u8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let query = match unsafe { read_str_input(query_len, query_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let blocks: Vec<DocumentBlock> = lift_from_raw!(Vec<DocumentBlock>, blocks_cap, blocks_len, blocks_data);
|
||||
let result: Vec<SearchResult> = crate::search_markdown_blocks(blocks, query);
|
||||
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_search_text_content_separate(
|
||||
content_len: i32, content_data: *const u8,
|
||||
query_len: i32, query_data: *const u8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let content = match unsafe { read_str_input(content_len, content_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let query = match unsafe { read_str_input(query_len, query_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let result: Vec<SearchResult> = crate::search_text_content(content, query);
|
||||
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_search_pdf_pages_separate(
|
||||
page_numbers_cap: u64, page_numbers_len: u64, page_numbers_data: *const u8,
|
||||
page_texts_cap: u64, page_texts_len: u64, page_texts_data: *const u8,
|
||||
query_len: i32, query_data: *const u8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let query = match unsafe { read_str_input(query_len, query_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let page_numbers: Vec<u32> = lift_from_raw!(Vec<u32>, page_numbers_cap, page_numbers_len, page_numbers_data);
|
||||
let page_texts: Vec<String> = lift_from_raw!(Vec<String>, page_texts_cap, page_texts_len, page_texts_data);
|
||||
let result: Vec<SearchResult> = crate::search_pdf_pages(page_numbers, page_texts, query);
|
||||
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_search_epub_chapters_ffi_separate(
|
||||
chapter_ids_cap: u64, chapter_ids_len: u64, chapter_ids_data: *const u8,
|
||||
chapter_texts_cap: u64, chapter_texts_len: u64, chapter_texts_data: *const u8,
|
||||
query_len: i32, query_data: *const u8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let query = match unsafe { read_str_input(query_len, query_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let chapter_ids: Vec<String> = lift_from_raw!(Vec<String>, chapter_ids_cap, chapter_ids_len, chapter_ids_data);
|
||||
let chapter_texts: Vec<String> = lift_from_raw!(Vec<String>, chapter_texts_cap, chapter_texts_len, chapter_texts_data);
|
||||
let result: Vec<SearchResult> = crate::search_epub_chapters_ffi(chapter_ids, chapter_texts, query);
|
||||
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_create_note_anchor_from_search_separate(
|
||||
mid_len: i32, mid_data: *const u8,
|
||||
result_cap: u64, result_len: u64, result_data: *const u8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let material_id = match unsafe { read_str_input(mid_len, mid_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let search_result: SearchResult = lift_from_raw!(SearchResult, result_cap, result_len, result_data);
|
||||
let anchor: NoteAnchor = crate::create_note_anchor_from_search(material_id, search_result);
|
||||
write_result_to_out!(Ok::<_, DocumentError>(anchor), out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_zx_document_ffi_build_document_info_separate(
|
||||
file_path_len: i32, file_path_data: *const u8,
|
||||
material_id_len: i32, material_id_data: *const u8,
|
||||
title_len: i32, title_data: *const u8,
|
||||
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
||||
) {
|
||||
let file_path = match unsafe { read_str_input(file_path_len, file_path_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let material_id = match unsafe { read_str_input(material_id_len, material_id_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let title = match unsafe { read_str_input(title_len, title_data, out_error_code) } {
|
||||
Some(s) => s, None => return,
|
||||
};
|
||||
let result = crate::build_document_info(file_path, material_id, title);
|
||||
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
||||
}
|
||||
|
||||
// Reverse conversion: FFI DocumentBlock → core DocumentBlock, used by search.
|
||||
fn core_block_from_ffi(block: DocumentBlock) -> core_blocks::DocumentBlock {
|
||||
match block {
|
||||
DocumentBlock::Heading(id, level, text) => {
|
||||
core_blocks::DocumentBlock::Heading { id, level, text }
|
||||
}
|
||||
DocumentBlock::Paragraph(id, text) => {
|
||||
core_blocks::DocumentBlock::Paragraph { id, text }
|
||||
}
|
||||
DocumentBlock::List(id, ordered, items) => {
|
||||
core_blocks::DocumentBlock::List { id, ordered, items }
|
||||
}
|
||||
DocumentBlock::CodeBlock(id, language, code) => {
|
||||
core_blocks::DocumentBlock::CodeBlock { id, language, code }
|
||||
}
|
||||
DocumentBlock::Quote(id, text) => {
|
||||
core_blocks::DocumentBlock::Quote { id, text }
|
||||
}
|
||||
DocumentBlock::Table(id, headers, rows) => {
|
||||
core_blocks::DocumentBlock::Table { id, headers, rows }
|
||||
}
|
||||
DocumentBlock::ImageBlock(id, src, alt) => {
|
||||
core_blocks::DocumentBlock::Image { id, src, alt }
|
||||
}
|
||||
DocumentBlock::HorizontalRule(id) => {
|
||||
core_blocks::DocumentBlock::HorizontalRule { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ffi_tests {
|
||||
use super::*;
|
||||
use zx_document_core::events_v2;
|
||||
|
||||
fn drain_buffer() {
|
||||
loop {
|
||||
let batch = export_pending_events_v2(1000, 0);
|
||||
if batch.is_empty() { break; }
|
||||
let ids: Vec<String> = batch.iter().map(|e| e.event_id.clone()).collect();
|
||||
ack_events_v2(ids);
|
||||
}
|
||||
}
|
||||
|
||||
// ── V2 Event Pipeline ──
|
||||
|
||||
#[test]
|
||||
fn test_v2_full_event_pipeline() {
|
||||
drain_buffer();
|
||||
events_v2::clear_all_events_v2();
|
||||
|
||||
let mat = ReadingMaterialRefV2::new("mat_ffi_test", "/tmp/test.md", "test.md", "markdown");
|
||||
let sid = start_reading_session_v2(mat, 1000).unwrap();
|
||||
assert!(!sid.is_empty());
|
||||
|
||||
let e1 = push_material_opened_v2(sid.clone(), "mat_ffi_test".to_string(), 1000).unwrap();
|
||||
assert_eq!(e1.event_type, ReadingEventTypeV2::MaterialOpened);
|
||||
assert_eq!(e1.sequence, 1);
|
||||
|
||||
let pos = ReadingPosition::Markdown { block_id: "intro".to_string(), scroll_progress: 0.25 };
|
||||
let e2 = push_position_changed_v2(sid.clone(), "mat_ffi_test".to_string(), pos, 2000).unwrap();
|
||||
assert_eq!(e2.event_type, ReadingEventTypeV2::PositionChanged);
|
||||
assert_eq!(e2.sequence, 2);
|
||||
|
||||
let e3 = push_heartbeat_v2(sid.clone(), "mat_ffi_test".to_string(), 15, None, 5000).unwrap();
|
||||
assert_eq!(e3.event_type, ReadingEventTypeV2::Heartbeat);
|
||||
assert_eq!(e3.active_seconds_delta, 15);
|
||||
|
||||
let e4 = push_marked_as_read_v2(sid.clone(), "mat_ffi_test".to_string(), 10000).unwrap();
|
||||
assert_eq!(e4.event_type, ReadingEventTypeV2::MarkedAsRead);
|
||||
|
||||
push_material_closed_v2(sid.clone(), "mat_ffi_test".to_string(), 0, 12000).unwrap();
|
||||
close_reading_session_v2(sid.clone(), 0).unwrap();
|
||||
|
||||
let exported = export_pending_events_v2(100, 13000);
|
||||
assert!(exported.len() >= 4, "expected >=4, got {}", exported.len());
|
||||
|
||||
let types: Vec<ReadingEventTypeV2> = exported.iter().map(|e| e.event_type.clone()).collect();
|
||||
assert!(types.contains(&ReadingEventTypeV2::MaterialOpened));
|
||||
assert!(types.contains(&ReadingEventTypeV2::PositionChanged));
|
||||
assert!(types.contains(&ReadingEventTypeV2::Heartbeat));
|
||||
assert!(types.contains(&ReadingEventTypeV2::MarkedAsRead));
|
||||
|
||||
let ids: Vec<String> = exported.iter().map(|e| e.event_id.clone()).collect();
|
||||
let acked = ack_events_v2(ids);
|
||||
assert!(acked >= 4);
|
||||
|
||||
let _ = zx_document_core::session_v2::remove_session_v2(&sid);
|
||||
}
|
||||
|
||||
// ── Session Lifecycle ──
|
||||
|
||||
#[test]
|
||||
fn test_session_lifecycle() {
|
||||
let mat = ReadingMaterialRefV2::new("mat_ffi_life", "/tmp/life.md", "life.md", "markdown");
|
||||
let sid = start_reading_session_v2(mat, 0).unwrap();
|
||||
pause_reading_session_v2(sid.clone()).unwrap();
|
||||
resume_reading_session_v2(sid.clone()).unwrap();
|
||||
close_reading_session_v2(sid.clone(), 0).unwrap();
|
||||
let _ = zx_document_core::session_v2::remove_session_v2(&sid);
|
||||
}
|
||||
|
||||
// ── Buffer Recovery ──
|
||||
|
||||
#[test]
|
||||
fn test_mark_failed_and_recover() {
|
||||
drain_buffer();
|
||||
events_v2::clear_all_events_v2();
|
||||
|
||||
let mat = ReadingMaterialRefV2::new("mat_ffi_recover", "/tmp/recover.md", "recover.md", "markdown");
|
||||
let sid = start_reading_session_v2(mat, 0).unwrap();
|
||||
let e = push_material_opened_v2(sid.clone(), "mat_ffi_recover".to_string(), 1000).unwrap();
|
||||
|
||||
let batch = export_pending_events_v2(100, 2000);
|
||||
assert!(batch.iter().any(|ev| ev.event_id == e.event_id));
|
||||
|
||||
let marked = mark_events_failed_v2(vec![e.event_id.clone()]);
|
||||
assert_eq!(marked, 1);
|
||||
|
||||
let retry = export_pending_events_v2(100, 3000);
|
||||
assert!(retry.iter().any(|ev| ev.event_id == e.event_id));
|
||||
|
||||
ack_events_v2(vec![e.event_id.clone()]);
|
||||
close_reading_session_v2(sid.clone(), 0).unwrap();
|
||||
let _ = zx_document_core::session_v2::remove_session_v2(&sid);
|
||||
}
|
||||
|
||||
// ── Parse → Search → Anchor ──
|
||||
|
||||
#[test]
|
||||
fn test_parse_search_anchor() {
|
||||
let md = "# Hello\n\nParagraph with searchable text.\n\n## Section 2\n\nMore.";
|
||||
let blocks = parse_markdown(md.to_string()).unwrap();
|
||||
assert!(!blocks.is_empty());
|
||||
|
||||
let results = search_markdown_blocks(blocks, "searchable".to_string());
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].snippet.to_lowercase().contains("searchable"));
|
||||
|
||||
// Position → Anchor
|
||||
let pos = ReadingPosition::Markdown { block_id: "h1".to_string(), scroll_progress: 0.5 };
|
||||
let anchor = create_note_anchor("mat_ffi".to_string(), Some(pos));
|
||||
match &anchor {
|
||||
NoteAnchor::MarkdownBlock { material_id, block_id, .. } => {
|
||||
assert_eq!(material_id, "mat_ffi");
|
||||
assert_eq!(block_id, "h1");
|
||||
}
|
||||
_ => panic!("expected MarkdownBlock"),
|
||||
}
|
||||
|
||||
// Anchor → Position (roundtrip)
|
||||
let restored = restore_position_from_anchor(anchor);
|
||||
assert!(restored.is_some());
|
||||
|
||||
// Roundtrip all position variants
|
||||
for pos in vec![
|
||||
ReadingPosition::Markdown { block_id: "b1".into(), scroll_progress: 0.3 },
|
||||
ReadingPosition::Text { line_number: 5, scroll_progress: 0.5 },
|
||||
ReadingPosition::Pdf { page_number: 2, page_progress: 0.7, overall_progress: 0.4 },
|
||||
ReadingPosition::Image { zoom_scale: 1.0, offset_x: 0.0, offset_y: 0.0 },
|
||||
ReadingPosition::Epub { chapter_id: "ch1".into(), chapter_progress: 0.8, overall_progress: 0.6 },
|
||||
] {
|
||||
let a = NoteAnchor::from_position("mat_ffi", Some(&pos));
|
||||
let r = restore_position_from_anchor(a);
|
||||
assert_eq!(r, Some(pos), "roundtrip failed");
|
||||
}
|
||||
|
||||
// SearchResult → Anchor
|
||||
let sr_anchor = create_note_anchor_from_search("mat_ffi".to_string(), results[0].clone());
|
||||
match sr_anchor {
|
||||
NoteAnchor::SearchResultAnchor { material_id, .. } => {
|
||||
assert_eq!(material_id, "mat_ffi");
|
||||
}
|
||||
_ => panic!("expected SearchResultAnchor"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text Search ──
|
||||
|
||||
#[test]
|
||||
fn test_text_search() {
|
||||
let results = search_text_content(
|
||||
"Line one\nLine two with keyword\nLine three".to_string(),
|
||||
"keyword".to_string(),
|
||||
);
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].line_number, Some(2));
|
||||
}
|
||||
|
||||
// ── PDF Search ──
|
||||
|
||||
#[test]
|
||||
fn test_pdf_search() {
|
||||
let results = search_pdf_pages(
|
||||
vec![1, 2, 3],
|
||||
vec!["Page 1.".to_string(), "Page 2 with target.".to_string(), "Page 3.".to_string()],
|
||||
"target".to_string(),
|
||||
);
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].page_number, Some(2));
|
||||
}
|
||||
|
||||
// ── EPUB Search ──
|
||||
|
||||
#[test]
|
||||
fn test_epub_search() {
|
||||
let results = search_epub_chapters_ffi(
|
||||
vec!["intro".to_string(), "ch1".to_string()],
|
||||
vec!["Welcome.".to_string(), "Chapter one keyword here.".to_string()],
|
||||
"keyword".to_string(),
|
||||
);
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].chapter_id, Some("ch1".to_string()));
|
||||
}
|
||||
|
||||
// ── V1 Backward Compatibility ──
|
||||
|
||||
#[test]
|
||||
fn test_v1_backward_compat() {
|
||||
let event = ReadingEvent::MaterialOpened {
|
||||
material_id: "mat_v1_ffi".to_string(),
|
||||
timestamp_ms: 1000,
|
||||
};
|
||||
push_reading_event(event);
|
||||
let exported = export_pending_events();
|
||||
assert!(!exported.is_empty());
|
||||
let found = exported.iter().any(|e| {
|
||||
matches!(e, ReadingEvent::MaterialOpened { material_id, .. } if material_id == "mat_v1_ffi")
|
||||
});
|
||||
assert!(found, "V1 event should be exported");
|
||||
clear_exported_events(exported.len() as u32);
|
||||
}
|
||||
|
||||
// ── Error Handling ──
|
||||
|
||||
#[test]
|
||||
fn test_session_not_found() {
|
||||
assert!(close_reading_session_v2("nonexistent".to_string(), 0).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,149 @@
|
||||
namespace zx_document {};
|
||||
namespace zx_document {
|
||||
[Throws=DocumentError]
|
||||
MaterialType detect_material_type([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
ImageMeta read_image_meta([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
TextStats read_text_stats([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
sequence<DocumentBlock> parse_markdown([ByRef] string content);
|
||||
|
||||
[Throws=DocumentError]
|
||||
sequence<DocumentBlock> parse_text([ByRef] string content);
|
||||
|
||||
sequence<SearchResult> search_markdown_blocks(sequence<DocumentBlock> blocks, [ByRef] string query);
|
||||
|
||||
sequence<SearchResult> search_text_content([ByRef] string content, [ByRef] string query);
|
||||
|
||||
sequence<SearchResult> search_pdf_pages(sequence<u32> page_numbers, sequence<string> page_texts, [ByRef] string query);
|
||||
|
||||
sequence<SearchResult> search_epub_chapters_ffi(sequence<string> chapter_ids, sequence<string> chapter_texts, [ByRef] string query);
|
||||
|
||||
NoteAnchor create_note_anchor([ByRef] string material_id, ReadingPosition? position);
|
||||
|
||||
NoteAnchor create_note_anchor_from_search([ByRef] string material_id, SearchResult result);
|
||||
|
||||
ReadingPosition? restore_position_from_anchor(NoteAnchor anchor);
|
||||
|
||||
[Throws=DocumentError]
|
||||
PdfMetadata read_pdf_metadata_ffi([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
sequence<PdfPageText> extract_pdf_text_ffi([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
EpubMetadata read_epub_metadata_ffi([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
sequence<EpubChapter> read_epub_chapters_ffi([ByRef] string file_path);
|
||||
|
||||
[Throws=DocumentError]
|
||||
OfficePreviewConfig get_office_preview_config_ffi(MaterialType material_type, u64 file_size);
|
||||
|
||||
boolean is_office_type_ffi(MaterialType material_type);
|
||||
|
||||
u32 cleanup_stale_sessions_ffi(i64 now_ms, i64 max_age_ms);
|
||||
|
||||
// ── V2 Reading Session ──
|
||||
[Throws=DocumentError]
|
||||
string start_reading_session_v2(ReadingMaterialRef material, i64 timestamp_ms);
|
||||
|
||||
[Throws=DocumentError]
|
||||
void pause_reading_session_v2([ByRef] string session_id);
|
||||
|
||||
[Throws=DocumentError]
|
||||
void resume_reading_session_v2([ByRef] string session_id);
|
||||
|
||||
[Throws=DocumentError]
|
||||
void close_reading_session_v2([ByRef] string session_id);
|
||||
|
||||
// ── V2 Reading Events ──
|
||||
[Throws=DocumentError]
|
||||
ReadingEventV2 push_material_opened_v2([ByRef] string session_id, [ByRef] string material_id, i64 timestamp_ms);
|
||||
|
||||
[Throws=DocumentError]
|
||||
ReadingEventV2 push_material_closed_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, i64 timestamp_ms);
|
||||
|
||||
[Throws=DocumentError]
|
||||
ReadingEventV2 push_position_changed_v2([ByRef] string session_id, [ByRef] string material_id, ReadingPosition position, i64 timestamp_ms);
|
||||
|
||||
[Throws=DocumentError]
|
||||
ReadingEventV2 push_heartbeat_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, ReadingPosition? position, i64 timestamp_ms);
|
||||
|
||||
[Throws=DocumentError]
|
||||
ReadingEventV2 push_marked_as_read_v2([ByRef] string session_id, [ByRef] string material_id, i64 timestamp_ms);
|
||||
|
||||
// ── V2 Buffer Management ──
|
||||
sequence<ReadingEventV2> export_pending_events_v2(u32 limit, i64 timestamp_ms);
|
||||
|
||||
u32 ack_events_v2(sequence<string> event_ids);
|
||||
|
||||
u32 mark_events_failed_v2(sequence<string> event_ids);
|
||||
|
||||
u32 reload_stale_events_v2();
|
||||
|
||||
// ── V1 (deprecated) ──
|
||||
void push_reading_event(ReadingEvent event);
|
||||
void update_reading_position([ByRef] string material_id, ReadingPosition position);
|
||||
sequence<ReadingEvent> export_pending_events();
|
||||
void clear_exported_events(u32 count);
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum DocumentError {
|
||||
"FileNotFound",
|
||||
"UnsupportedFormat",
|
||||
"ParseError",
|
||||
"InvalidEncoding",
|
||||
"IoError",
|
||||
};
|
||||
|
||||
// ── V2 Types ──
|
||||
|
||||
dictionary ReadingMaterialRef {
|
||||
string material_id;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface ReadingSessionStatus {
|
||||
Active();
|
||||
Paused();
|
||||
Closed();
|
||||
};
|
||||
|
||||
dictionary ReadingSessionV2 {
|
||||
string client_session_id;
|
||||
ReadingMaterialRef material;
|
||||
i64 started_at_ms;
|
||||
i64 last_event_at_ms;
|
||||
u64 next_sequence;
|
||||
u32 total_active_seconds;
|
||||
ReadingPosition? last_position;
|
||||
ReadingSessionStatus status;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface ReadingEventTypeV2 {
|
||||
MaterialOpened();
|
||||
MaterialClosed();
|
||||
PositionChanged();
|
||||
Heartbeat();
|
||||
MarkedAsRead();
|
||||
};
|
||||
|
||||
dictionary ReadingEventV2 {
|
||||
string event_id;
|
||||
string client_session_id;
|
||||
string material_id;
|
||||
ReadingEventTypeV2 event_type;
|
||||
ReadingPosition? position;
|
||||
u32 active_seconds_delta;
|
||||
i64 timestamp_ms;
|
||||
u64 sequence;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface MaterialType {
|
||||
@ -29,6 +174,15 @@ dictionary DocumentInfo {
|
||||
u64 file_size;
|
||||
u32? page_count;
|
||||
u32? word_count;
|
||||
u32? char_count;
|
||||
u32? line_count;
|
||||
u32? image_width;
|
||||
u32? image_height;
|
||||
string? image_format;
|
||||
u32? epub_chapter_count;
|
||||
boolean is_searchable;
|
||||
boolean supports_position;
|
||||
boolean supports_anchor;
|
||||
string? created_at;
|
||||
};
|
||||
|
||||
@ -53,13 +207,14 @@ interface ReadingEvent {
|
||||
|
||||
[Enum]
|
||||
interface NoteAnchor {
|
||||
Material(string material_id);
|
||||
MarkdownBlock(string material_id, string block_id);
|
||||
TextLine(string material_id, u32 line_number);
|
||||
PdfPage(string material_id, u32 page_number);
|
||||
Image(string material_id);
|
||||
EpubChapter(string material_id, string chapter_id);
|
||||
Material(string material_id, ReadingPosition? position_snapshot);
|
||||
MarkdownBlock(string material_id, string block_id, ReadingPosition? position_snapshot);
|
||||
TextLine(string material_id, u32 line_number, ReadingPosition? position_snapshot);
|
||||
PdfPage(string material_id, u32 page_number, ReadingPosition? position_snapshot);
|
||||
Image(string material_id, ReadingPosition? position_snapshot);
|
||||
EpubChapter(string material_id, string chapter_id, ReadingPosition? position_snapshot);
|
||||
KnowledgeItem(string knowledge_item_id);
|
||||
SearchResultAnchor(string material_id, string? block_id, u32? line_number, u32? page_number, string? chapter_id, string snippet);
|
||||
};
|
||||
|
||||
dictionary ImageMeta {
|
||||
@ -72,21 +227,67 @@ dictionary ImageMeta {
|
||||
dictionary SearchResult {
|
||||
string block_id;
|
||||
u32? line_number;
|
||||
u32? page_number;
|
||||
string? chapter_id;
|
||||
string snippet;
|
||||
u64 match_start;
|
||||
u64 match_end;
|
||||
};
|
||||
|
||||
dictionary PdfMetadata {
|
||||
u32 page_count;
|
||||
string? title;
|
||||
string? author;
|
||||
u64 file_size;
|
||||
};
|
||||
|
||||
dictionary EpubMetadata {
|
||||
string? title;
|
||||
string? author;
|
||||
u32 chapter_count;
|
||||
u64 file_size;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface OfficePreviewStrategy {
|
||||
PlatformPreview();
|
||||
ExternalOpen();
|
||||
ServerConvertedPdf();
|
||||
Unsupported();
|
||||
};
|
||||
|
||||
dictionary OfficePreviewConfig {
|
||||
MaterialType material_type;
|
||||
OfficePreviewStrategy strategy;
|
||||
u64 file_size;
|
||||
boolean supports_search_after_conversion;
|
||||
};
|
||||
|
||||
dictionary EpubChapter {
|
||||
string chapter_id;
|
||||
string title;
|
||||
string path;
|
||||
u32 play_order;
|
||||
};
|
||||
|
||||
dictionary PdfPageText {
|
||||
u32 page_number;
|
||||
string text;
|
||||
};
|
||||
|
||||
dictionary TextStats {
|
||||
u32 line_count;
|
||||
u32 word_count;
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum DocumentError {
|
||||
"FileNotFound",
|
||||
"UnsupportedFormat",
|
||||
"ParseError",
|
||||
"InvalidEncoding",
|
||||
"IoError",
|
||||
[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);
|
||||
};
|
||||
|
||||
@ -27,110 +27,104 @@ App ──上传──→ Backend API
|
||||
|
||||
## Rust 暴露给 App 的函数
|
||||
|
||||
所有函数通过 `#[uniffi::export]` proc-macro 标注,经 UDL bindgen 生成 Swift/Kotlin 绑定。
|
||||
|
||||
### 1. 文件类型识别
|
||||
|
||||
```rust
|
||||
fn detect_material_type(file_path: &str) -> Result<MaterialType, DocumentError>
|
||||
fn detect_material_type(file_path: String) -> Result<MaterialType, DocumentError>
|
||||
```
|
||||
|
||||
**输入**:本地文件路径
|
||||
**输出**:MaterialType 枚举值
|
||||
**用途**:App 据此决定使用哪种 PreviewMode
|
||||
|
||||
### 2. 打开文档
|
||||
### 2. 图片 Metadata
|
||||
|
||||
```rust
|
||||
fn open_document(file_path: &str, material_id: &str) -> Result<DocumentHandle, DocumentError>
|
||||
```
|
||||
|
||||
**输入**:本地文件路径 + 资料 ID
|
||||
**输出**:DocumentHandle(不透明句柄,App 传递给后续函数)
|
||||
**用途**:初始化文档解析,建立阅读会话
|
||||
|
||||
### 3. 获取文档信息
|
||||
|
||||
```rust
|
||||
fn get_document_info(handle: &DocumentHandle) -> Result<DocumentInfo, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:DocumentInfo(标题、类型、大小、页数、字数)
|
||||
**用途**:App 展示资料详情
|
||||
|
||||
### 4. 获取 Markdown Blocks
|
||||
|
||||
```rust
|
||||
fn get_markdown_blocks(handle: &DocumentHandle) -> Result<Vec<DocumentBlock>, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:DocumentBlock 列表
|
||||
**用途**:App 原生渲染 Markdown
|
||||
**前置**:MaterialType 必须为 Markdown
|
||||
|
||||
### 5. 获取文本内容
|
||||
|
||||
```rust
|
||||
fn get_text_content(handle: &DocumentHandle) -> Result<String, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:完整文本内容
|
||||
**用途**:App 原生渲染纯文本
|
||||
**前置**:MaterialType 必须为 Text
|
||||
|
||||
### 6. 获取图片 Metadata
|
||||
|
||||
```rust
|
||||
fn get_image_meta(file_path: &str) -> Result<ImageMeta, DocumentError>
|
||||
fn read_image_meta(file_path: String) -> Result<ImageMeta, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:width, height, format, file_size
|
||||
**用途**:App 展示图片信息
|
||||
**前置**:MaterialType 必须为 Image
|
||||
|
||||
### 7. 搜索文档
|
||||
### 3. 文本统计
|
||||
|
||||
```rust
|
||||
fn search_document(handle: &DocumentHandle, query: &str) -> Result<Vec<SearchResult>, DocumentError>
|
||||
fn read_text_stats(file_path: String) -> Result<TextStats, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:line_count, word_count
|
||||
|
||||
### 4. 解析 Markdown
|
||||
|
||||
```rust
|
||||
fn parse_markdown(content: String) -> Result<Vec<DocumentBlock>, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:DocumentBlock 列表(8 种 block 类型)
|
||||
|
||||
### 5. 解析纯文本
|
||||
|
||||
```rust
|
||||
fn parse_text(content: String) -> Result<Vec<DocumentBlock>, DocumentError>
|
||||
```
|
||||
|
||||
**输出**:段落 block 列表
|
||||
|
||||
### 6. 搜索 Markdown Blocks
|
||||
|
||||
```rust
|
||||
fn search_markdown_blocks(blocks: Vec<DocumentBlock>, query: String) -> Vec<SearchResult>
|
||||
```
|
||||
|
||||
**输入**:搜索关键词
|
||||
**输出**:SearchResult 列表(block_id, snippet, match range)
|
||||
**用途**:App 展示搜索结果
|
||||
**支持**:Markdown、TXT
|
||||
|
||||
### 8. 更新阅读位置
|
||||
### 7. 搜索纯文本
|
||||
|
||||
```rust
|
||||
fn update_reading_position(material_id: &str, position: ReadingPosition)
|
||||
fn search_text_content(content: String, query: String) -> Vec<SearchResult>
|
||||
```
|
||||
|
||||
**输入**:资料 ID + 阅读位置
|
||||
**用途**:记录用户当前读到的位置,用于继续阅读
|
||||
**输出**:SearchResult 列表(line_number, snippet, match range)
|
||||
|
||||
### 9. 导出阅读事件
|
||||
### 8. 创建笔记锚点
|
||||
|
||||
```rust
|
||||
fn create_note_anchor(material_id: String, position: Option<ReadingPosition>) -> NoteAnchor
|
||||
```
|
||||
|
||||
**输出**:NoteAnchor(从 ReadingPosition 自动映射)
|
||||
|
||||
### 9. 推送阅读事件
|
||||
|
||||
```rust
|
||||
fn push_reading_event(event: ReadingEvent)
|
||||
```
|
||||
|
||||
**用途**:将事件推入全局缓冲区
|
||||
|
||||
### 10. 更新阅读位置
|
||||
|
||||
```rust
|
||||
fn update_reading_position(material_id: String, position: ReadingPosition)
|
||||
```
|
||||
|
||||
**用途**:生成 PositionChanged 事件并推入缓冲区
|
||||
|
||||
### 11. 导出待上报事件
|
||||
|
||||
```rust
|
||||
fn export_pending_events() -> Vec<ReadingEvent>
|
||||
```
|
||||
|
||||
**输出**:所有未导出的阅读事件列表
|
||||
**用途**:App 定期拉取事件并上传到后端
|
||||
**输出**:所有未上报事件(不清空缓冲区)
|
||||
|
||||
### 10. 清空已导出事件
|
||||
### 12. 清空已上报事件
|
||||
|
||||
```rust
|
||||
fn clear_exported_events(count: usize)
|
||||
fn clear_exported_events(count: u32)
|
||||
```
|
||||
|
||||
**用途**:确认前 count 条事件已成功上传,从缓冲区移除
|
||||
|
||||
### 11. 创建笔记锚点
|
||||
|
||||
```rust
|
||||
fn create_note_anchor(material_id: &str, position: Option<ReadingPosition>) -> NoteAnchor
|
||||
```
|
||||
|
||||
**输出**:NoteAnchor
|
||||
**用途**:关联笔记到资料的具体位置
|
||||
**用途**:确认前 count 条已成功上传,从缓冲区移除
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ iOS / Android / 鸿蒙 / macOS / Windows / Web │ ← 宿主 App
|
||||
├──────────────────────────────────────────────┤
|
||||
│ UniFFI C-ABI bridge │ ← zx_document_ffi
|
||||
│ UniFFI proc-macro C-ABI bridge │ ← zx_document_ffi
|
||||
├──────────────────────────────────────────────┤
|
||||
│ zx_document_core (Rust) │ ← 核心逻辑
|
||||
│ ├─ file_type 文件类型识别 │
|
||||
@ -36,7 +36,7 @@
|
||||
| 阅读事件 | 生成 MaterialOpened/Closed/PositionChanged/Heartbeat 事件 |
|
||||
| 搜索 | 大小写不敏感,返回 block/snippet |
|
||||
| 笔记锚点 | 从 ReadingPosition 生成 NoteAnchor |
|
||||
| FFI 绑定 | 通过 UniFFI 暴露 API 给 Swift/Kotlin |
|
||||
| FFI 绑定 | 通过 proc-macro (#[uniffi::export]) + UDL bindgen 暴露 API 给 Swift/Kotlin |
|
||||
|
||||
### 宿主 App 负责
|
||||
|
||||
@ -80,5 +80,5 @@
|
||||
| Crate | 类型 | 用途 |
|
||||
|-------|------|------|
|
||||
| zx_document_core | library | 核心 Rust 逻辑,纯计算 |
|
||||
| zx_document_ffi | library | UniFFI 绑定,类型转换 |
|
||||
| zx_document_ffi | library | UniFFI proc-macro 绑定(#[uniffi::export] + UDL bindgen) |
|
||||
| xtask | binary | 构建脚本,生成 binding,打包 artifact |
|
||||
|
||||
239
docs/c-abi-out-pointer-pattern.md
Normal file
239
docs/c-abi-out-pointer-pattern.md
Normal file
@ -0,0 +1,239 @@
|
||||
# C-ABI Out-Pointer 模式文档(iOS 兼容性)
|
||||
|
||||
## 1. 背景
|
||||
|
||||
ARM64 iOS ABI 对跨 FFI 边界的 struct 传参有严格限制。UniFFI 默认的 `RustBuffer` 结构体(包含 `{ capacity: i32, len: i32, data: *mut u8 }` 三个字段)在 ARM64 上不保证稳定传递。
|
||||
|
||||
**问题表现**:函数返回的 `RustBuffer` 在 iOS 真机上可能得到损坏的 capacity/len 值,导致数据丢失或 crash。
|
||||
|
||||
**解决方案**:C-ABI out-pointer 模式 — 将 struct 成员拆分为独立的标量参数/返回。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模式对比
|
||||
|
||||
### 2.1 Standard UniFFI(有问题)
|
||||
|
||||
```rust
|
||||
// ❌ ARM64 不稳定:RustBuffer struct 跨 FFI 传参
|
||||
#[uniffi::export]
|
||||
fn parse_markdown(content: String) -> Result<Vec<DocumentBlock>, DocumentError> {
|
||||
// UniFFI 自动生成 RustBuffer 序列化
|
||||
}
|
||||
```
|
||||
|
||||
生成的 C-ABI:
|
||||
```c
|
||||
// ❌ 返回 struct — ARM64 可能损坏
|
||||
RustBuffer ffi_parse_markdown(RustBuffer content, RustCallStatus* status);
|
||||
```
|
||||
|
||||
### 2.2 Out-Pointer 模式(安全)
|
||||
|
||||
```rust
|
||||
// ✅ 所有参数/返回值均为标量(int/pointer)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_parse_markdown_separate(
|
||||
content_len: i32,
|
||||
content_data: *const u8,
|
||||
out_result_capacity: *mut u64,
|
||||
out_result_len: *mut u64,
|
||||
out_result_data: *mut *mut u8,
|
||||
out_error_capacity: *mut u64,
|
||||
out_error_len: *mut u64,
|
||||
out_error_data: *mut *mut u8,
|
||||
)
|
||||
```
|
||||
|
||||
生成的 C-ABI — 所有参数为标量,兼容 ARM64。
|
||||
|
||||
---
|
||||
|
||||
## 3. 通用 Out-Pointer 模板
|
||||
|
||||
### 3.1 Result 序列化
|
||||
|
||||
```rust
|
||||
/// Write Result<T, DocumentError> into out-pointers.
|
||||
fn write_result_to_out_ptrs(
|
||||
result: Result<Vec<u8>, DocumentError>,
|
||||
out_result_cap: *mut u64, out_result_len: *mut u64, out_result_data: *mut *mut u8,
|
||||
out_error_cap: *mut u64, out_error_len: *mut u64, out_error_data: *mut *mut u8,
|
||||
) {
|
||||
match result {
|
||||
Ok(value) => {
|
||||
let buf = uniffi::RustBuffer::from_vec(value);
|
||||
unsafe {
|
||||
*out_result_capacity = buf.capacity() as u64;
|
||||
*out_result_len = buf.len() as u64;
|
||||
*out_result_data = buf.data_pointer() as *mut u8;
|
||||
*out_error_capacity = 0;
|
||||
*out_error_len = 0;
|
||||
*out_error_data = std::ptr::null_mut();
|
||||
}
|
||||
std::mem::forget(buf); // caller now owns the buffer
|
||||
}
|
||||
Err(e) => {
|
||||
let err_json = serde_json::to_string(&e).unwrap_or_default().into_bytes();
|
||||
let buf = uniffi::RustBuffer::from_vec(err_json);
|
||||
unsafe {
|
||||
*out_result_capacity = 0;
|
||||
*out_result_len = 0;
|
||||
*out_result_data = std::ptr::null_mut();
|
||||
*out_error_capacity = buf.capacity() as u64;
|
||||
*out_error_len = buf.len() as u64;
|
||||
*out_error_data = buf.data_pointer() as *mut u8;
|
||||
}
|
||||
std::mem::forget(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 完整 FFI 函数
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ffi_my_function_separate(
|
||||
// Input: raw bytes (len + data ptr)
|
||||
input_len: i32,
|
||||
input_data: *const u8,
|
||||
// Output: result (out-pointers for capacity/len/data)
|
||||
out_result_cap: *mut u64,
|
||||
out_result_len: *mut u64,
|
||||
out_result_data: *mut *mut u8,
|
||||
// Output: error (out-pointers)
|
||||
out_error_cap: *mut u64,
|
||||
out_error_len: *mut u64,
|
||||
out_error_data: *mut *mut u8,
|
||||
) {
|
||||
// 1. Deserialize input
|
||||
let input = unsafe { std::slice::from_raw_parts(input_data, input_len as usize) };
|
||||
let args: MyArgs = match serde_json::from_slice(input) {
|
||||
Ok(a) => a,
|
||||
Err(_) => { /* error handling */ return; }
|
||||
};
|
||||
|
||||
// 2. Execute logic
|
||||
let result = my_core_function(args);
|
||||
|
||||
// 3. Serialize via out-pointers
|
||||
write_result_to_out_ptrs(
|
||||
result.map(|v| serde_json::to_vec(&v).unwrap_or_default()),
|
||||
out_result_cap, out_result_len, out_result_data,
|
||||
out_error_cap, out_error_len, out_error_data,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 内存管理
|
||||
|
||||
### 4.1 所有权
|
||||
|
||||
| 参数方向 | 所有权 | 释放责任 |
|
||||
|----------|:--:|------|
|
||||
| Input data | 借用(caller 保留) | Caller |
|
||||
| Output data | 转移给 caller | Caller 调用 `rustbuffer_free_separate` |
|
||||
|
||||
### 4.2 释放函数
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn 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);
|
||||
// Drop here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Swift 调用示例**:
|
||||
```swift
|
||||
var resultCap: UInt64 = 0, resultLen: UInt64 = 0, resultData: UnsafeMutablePointer<UInt8>?
|
||||
var errorCap: UInt64 = 0, errorLen: UInt64 = 0, errorData: UnsafeMutablePointer<UInt8>?
|
||||
|
||||
ffi_parse_markdown_separate(
|
||||
Int32(jsonData.count), pointerToData,
|
||||
&resultCap, &resultLen, &resultData,
|
||||
&errorCap, &errorLen, &errorData
|
||||
)
|
||||
|
||||
// 使用 result...
|
||||
if let data = resultData {
|
||||
let buf = Data(bytes: data, count: Int(resultLen))
|
||||
// ... parse JSON
|
||||
}
|
||||
|
||||
// 释放
|
||||
ffi_rustbuffer_free_separate(resultCap, resultLen, resultData)
|
||||
ffi_rustbuffer_free_separate(errorCap, errorLen, errorData)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计原则
|
||||
|
||||
### 5.1 标量仅传
|
||||
|
||||
- ✅ `i32`, `u64`, `*mut u8` — 单寄存器传参
|
||||
- ❌ `RustBuffer { capacity, len, data }` — 多字段 struct
|
||||
|
||||
### 5.2 成对输出
|
||||
|
||||
每个输出 buffer 拆为 3 个 out-pointer:
|
||||
```
|
||||
capacity: *mut u64 → buffer 容量
|
||||
len: *mut u64 → 有效数据长度
|
||||
data: *mut *mut u8 → 数据指针(双重指针:写入指针值)
|
||||
```
|
||||
|
||||
### 5.3 序列化格式
|
||||
|
||||
输入/输出均使用 JSON(`serde_json`)序列化,因为:
|
||||
- C-ABI 仅传递字节数组
|
||||
- JSON 在 Swift 侧有原生支持(`JSONDecoder`)
|
||||
- 避免复杂 struct 布局的 ABI 问题
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前使用范围
|
||||
|
||||
项目中有以下函数使用 out-pointer 模式(`_separate` 后缀):
|
||||
|
||||
| 函数 | 输入 | 输出 | 状态 |
|
||||
|------|:--:|:--:|:--:|
|
||||
| `parse_markdown_separate` | JSON args | `Vec<DocumentBlock>` | ✅ 已实现 |
|
||||
| `parse_text_separate` | JSON args | `Vec<DocumentBlock>` | ✅ 已实现 |
|
||||
| `detect_material_type_separate` | 文件路径 | `MaterialType` | ✅ 已实现 |
|
||||
| `read_image_meta_separate` | 文件路径 | `ImageMeta` | ✅ 已实现 |
|
||||
| `read_text_stats_separate` | 文件路径 | `TextStats` | ✅ 已实现 |
|
||||
| `search_markdown_blocks_separate` | JSON args | `Vec<SearchResult>` | ✅ 已实现 |
|
||||
| `search_text_content_separate` | JSON args | `Vec<SearchResult>` | ✅ 已实现 |
|
||||
| `search_pdf_pages_separate` | JSON args | `Vec<SearchResult>` | ✅ 已实现 |
|
||||
| `push_reading_event_separate` | JSON event | () | ✅ 已实现 |
|
||||
| `export_pending_events_separate` | () | `Vec<ReadingEvent>` | ✅ 已实现 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 何时使用 Out-Pointer
|
||||
|
||||
| 场景 | 推荐方式 |
|
||||
|------|----------|
|
||||
| V2 Session / Event(新接口) | Standard UniFFI(已验证稳定) |
|
||||
| 返回复杂类型(Vec/struct)且有 ARM64 兼容需求 | Out-pointer + JSON |
|
||||
| 简单标量输入输出 | Standard UniFFI |
|
||||
| 需要高性能避免 JSON 开销 | Standard UniFFI + 测试验证 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [UniFFI UDL 编写规范](./uniffi-udl-spec.md)
|
||||
- [iOS FFI 调用指南](./ios-ffi-integration-guide.md)
|
||||
195
docs/cross-platform-release.md
Normal file
195
docs/cross-platform-release.md
Normal file
@ -0,0 +1,195 @@
|
||||
# 跨平台发布流程
|
||||
|
||||
## 1. 概述
|
||||
|
||||
Document Runtime 通过 UniFFI 生成多语言绑定,支持跨平台分发。
|
||||
|
||||
| 平台 | 产物 | 绑定语言 | 状态 |
|
||||
|------|------|----------|:--:|
|
||||
| iOS | `.xcframework` | Swift(uniffi) | ✅ 生产 |
|
||||
| macOS (Catalyst) | `.xcframework` | Swift(uniffi) | 🔄 计划中 |
|
||||
| Android | `.aar` | Kotlin(uniffi) | 🔄 计划中 |
|
||||
|
||||
---
|
||||
|
||||
## 2. iOS .xcframework
|
||||
|
||||
### 2.1 目标架构
|
||||
|
||||
| 架构 | Rust Target | 用途 |
|
||||
|------|-------------|------|
|
||||
| `ios-arm64` | `aarch64-apple-ios` | iPhone 真机 |
|
||||
| `ios-arm64-sim` | `aarch64-apple-ios-sim` | Apple Silicon Mac 模拟器 |
|
||||
| `ios-x86_64-sim` | `x86_64-apple-ios` | Intel Mac 模拟器 |
|
||||
|
||||
### 2.2 构建步骤
|
||||
|
||||
```bash
|
||||
cd zhixi-document-runtime
|
||||
|
||||
# 1. 安装 targets
|
||||
rustup target add aarch64-apple-ios \
|
||||
aarch64-apple-ios-sim \
|
||||
x86_64-apple-ios
|
||||
|
||||
# 2. 构建各架构
|
||||
cargo build --release -p zx_document_ffi --target aarch64-apple-ios
|
||||
cargo build --release -p zx_document_ffi --target aarch64-apple-ios-sim
|
||||
cargo build --release -p zx_document_ffi --target x86_64-apple-ios
|
||||
|
||||
# 3. 打包 xcframework
|
||||
xcodebuild -create-xcframework \
|
||||
-library target/aarch64-apple-ios/release/libzx_document_ffi.a \
|
||||
-library target/aarch64-apple-ios-sim/release/libzx_document_ffi.a \
|
||||
-library target/x86_64-apple-ios/release/libzx_document_ffi.a \
|
||||
-output ZxDocumentRuntime.xcframework
|
||||
|
||||
# 4. 校验
|
||||
xcodebuild -check-xcframework ZxDocumentRuntime.xcframework
|
||||
# Expected: ios-arm64, ios-arm64-sim, ios-x86_64-sim ✅
|
||||
|
||||
# 5. 生成 Swift 绑定
|
||||
uniffi-bindgen generate \
|
||||
crates/zx_document_ffi/src/zx_document.udl \
|
||||
--language swift \
|
||||
--out-dir ./generated/swift/
|
||||
|
||||
# 6. 打包发布资产
|
||||
zip -r ZxDocumentRuntime.xcframework.zip ZxDocumentRuntime.xcframework/
|
||||
cp generated/swift/zx_document.swift ./release-assets/
|
||||
```
|
||||
|
||||
### 2.3 集成到 Xcode
|
||||
|
||||
1. 将 `ZxDocumentRuntime.xcframework` 拖入 Xcode → Embed & Sign
|
||||
2. 将 `zx_document.swift` 添加到项目
|
||||
3. Build Settings → `FRAMEWORK_SEARCH_PATHS` 包含 xcframework 路径
|
||||
|
||||
---
|
||||
|
||||
## 3. Android .aar(计划中)
|
||||
|
||||
### 3.1 目标架构
|
||||
|
||||
| 架构 | Rust Target |
|
||||
|------|-------------|
|
||||
| `arm64-v8a` | `aarch64-linux-android` |
|
||||
| `armeabi-v7a` | `armv7-linux-androideabi` |
|
||||
| `x86_64` | `x86_64-linux-android` |
|
||||
| `x86` | `i686-linux-android` |
|
||||
|
||||
### 3.2 构建步骤(草案)
|
||||
|
||||
```bash
|
||||
# 1. 安装 NDK + targets
|
||||
rustup target add aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
x86_64-linux-android
|
||||
|
||||
# 2. 构建各架构
|
||||
cargo build --release -p zx_document_ffi --target aarch64-linux-android
|
||||
# ... (repeat for each target)
|
||||
|
||||
# 3. 生成 Kotlin 绑定
|
||||
uniffi-bindgen generate \
|
||||
crates/zx_document_ffi/src/zx_document.udl \
|
||||
--language kotlin \
|
||||
--out-dir ./generated/kotlin/
|
||||
|
||||
# 4. 打包 .aar
|
||||
# 标准 Android 库打包流程:
|
||||
# - jniLibs/<abi>/libzx_document_ffi.so
|
||||
# - generated/kotlin/**/*.kt
|
||||
# - AndroidManifest.xml
|
||||
# 使用 gradle / android-maven-plugin 打包为 .aar
|
||||
```
|
||||
|
||||
### 3.3 注意事项
|
||||
|
||||
- Android 需要 JNI 兼容的符号导出(`#[no_mangle] pub extern "C"`)
|
||||
- NDK 版本 ≥ r26
|
||||
- `ANDROID_NDK_HOME` 环境变量需设置
|
||||
- 需要 `libunwind` 链接(`-lunwind`)
|
||||
|
||||
---
|
||||
|
||||
## 4. UniFFI 多语言绑定生成
|
||||
|
||||
### 4.1 一次生成所有语言
|
||||
|
||||
```bash
|
||||
for lang in swift kotlin python ruby; do
|
||||
uniffi-bindgen generate \
|
||||
crates/zx_document_ffi/src/zx_document.udl \
|
||||
--language "$lang" \
|
||||
--out-dir "generated/$lang/"
|
||||
done
|
||||
```
|
||||
|
||||
### 4.2 生成的产物
|
||||
|
||||
```
|
||||
generated/
|
||||
├── swift/
|
||||
│ └── zx_document.swift # Swift 绑定
|
||||
├── kotlin/
|
||||
│ └── zx_document.kt # Kotlin 绑定(Android 用)
|
||||
├── python/
|
||||
│ └── zx_document.py # Python 绑定(CLI 工具用)
|
||||
└── ruby/
|
||||
└── zx_document.rb # Ruby 绑定
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 版本管理
|
||||
|
||||
### 5.1 版本号同步
|
||||
|
||||
| 文件 | 字段 | 示例 |
|
||||
|------|------|------|
|
||||
| `Cargo.toml` (workspace) | `workspace.package.version` | `0.1.0` |
|
||||
| `ZxDocumentRuntime.xcframework/Info.plist` | `CFBundleVersion` | `0.1.0` |
|
||||
| Git tag | — | `v0.1.0` |
|
||||
| `zx_document.swift` | 注释头部 | `// UniFFI 0.28, zx_document v0.1.0` |
|
||||
|
||||
### 5.2 发布 Checklist
|
||||
|
||||
```
|
||||
tag vX.Y.Z
|
||||
├── [ ] Cargo.toml version bumped
|
||||
├── [ ] cargo test --workspace ✅
|
||||
├── [ ] CI release job passes
|
||||
├── [ ] xcframework.zip uploaded to GitHub Release
|
||||
├── [ ] zx_document.swift uploaded to GitHub Release
|
||||
├── [ ] CHANGELOG.md updated
|
||||
└── [ ] iOS app built with new xcframework ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. CI 自动化
|
||||
|
||||
发布流程已集成到 `.github/workflows/ci.yml`:
|
||||
|
||||
| Step | 触发 | 产物 |
|
||||
|------|------|------|
|
||||
| Build (matrix) | tag `v*` | `libzx_document_ffi.a` per target |
|
||||
| Package xcframework | tag `v*` | `ZxDocumentRuntime.xcframework.zip` |
|
||||
| GitHub Release | tag `v*` | 自动 release notes + 上传 zip |
|
||||
|
||||
```bash
|
||||
# 手动触发发布
|
||||
git tag v0.1.0
|
||||
git push origin v0.1.0
|
||||
# CI 自动执行 release + package-xcframework jobs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关文档
|
||||
|
||||
- [UniFFI UDL 编写规范](./uniffi-udl-spec.md)
|
||||
- [C-ABI out-pointer 模式](./c-abi-out-pointer-pattern.md)
|
||||
- [iOS FFI 调用指南](./ios-ffi-integration-guide.md)
|
||||
- [CI/CD Pipeline](../.github/workflows/ci.yml)
|
||||
290
docs/document-runtime-architecture.md
Normal file
290
docs/document-runtime-architecture.md
Normal file
@ -0,0 +1,290 @@
|
||||
# Document Runtime 完整架构 v2
|
||||
|
||||
> DOC-FULL-000 | 2026-06-07
|
||||
|
||||
## 1. 职责边界
|
||||
|
||||
### Rust (zx_document_core + zx_document_ffi) 负责
|
||||
|
||||
```text
|
||||
文件类型识别 → 预览模式 → 文档信息提取
|
||||
Markdown / Text / Image / PDF / EPUB / Office 能力模型
|
||||
ReadingPosition → ReadingSessionV2 → ReadingEventV2
|
||||
ActiveTimeTracker → activeSecondsDelta 计算
|
||||
EventBuffer → export / ack / failed / clear
|
||||
SearchResult → NoteAnchor → 阅读位置恢复
|
||||
UniFFI DTO → Swift/Kotlin binding → XCFramework 构建
|
||||
```
|
||||
|
||||
### Rust 不负责
|
||||
|
||||
```text
|
||||
userId / token / knowledgeBaseId
|
||||
readingTargetType (knowledge_source / temporary_file)
|
||||
后端 API 请求 / COS 下载 / 本地持久化上传队列
|
||||
iOS UI / Android UI / AI / RAG / 向量化
|
||||
```
|
||||
|
||||
### iOS 负责补充
|
||||
|
||||
```text
|
||||
readingTargetType / platform / appVersion / clientTimezoneOffsetMinutes
|
||||
本地上传队列 / 调用 API batch upload / 离线重试
|
||||
阅读页 UI / 首页继续学习定位 / 分析展示
|
||||
```
|
||||
|
||||
### API 负责
|
||||
|
||||
```text
|
||||
接收 ReadingEventUploadItem → 去重入库 → 聚合
|
||||
LearningSession / MaterialReadingProgress / DailyLearningActivity / LearningRecord
|
||||
提供 continue / summary / trend / progress 查询接口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键设计决策
|
||||
|
||||
### D1:Rust 只保存 materialId
|
||||
|
||||
```rust
|
||||
pub struct ReadingMaterialRef {
|
||||
pub material_id: String,
|
||||
}
|
||||
```
|
||||
|
||||
Rust 不知道 `material_id` 是 `KnowledgeSource.id` 还是 `TemporaryReadingMaterial.id`。`readingTargetType` 由 iOS 上传适配层补充。
|
||||
|
||||
### D2:V1 保留 deprecated,V2 新增独立模块
|
||||
|
||||
```
|
||||
events.rs ← V1, 保留 deprecated
|
||||
events_v2.rs ← V2, 新模块
|
||||
```
|
||||
|
||||
V1 不删除,iOS 已有接入。新代码走 V2。
|
||||
|
||||
### D3:clientSessionId 由 Rust 生成 UUID
|
||||
|
||||
一次阅读页生命周期 = 一个 `clientSessionId`。Rust 生成保证跨平台一致。`sequence` 从 1 递增。
|
||||
|
||||
### D4:iOS 控制 tick 节奏,Rust 计算 delta
|
||||
|
||||
```
|
||||
iOS Timer 每 15s → Rust pushHeartbeatV2
|
||||
App 后台 → iOS 调用 pause
|
||||
App 前台 → iOS 调用 resume
|
||||
退出页面 → iOS 调用 close
|
||||
```
|
||||
|
||||
Rust 不创建 timer。`ActiveTimeTracker` 根据 timestamp 计算增量,时间倒退不产生负数。
|
||||
|
||||
### D5:Position JSON 使用 camelCase
|
||||
|
||||
```rust
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
```
|
||||
|
||||
输出:`{"type":"pdf","pageNumber":12,"pageProgress":0.4,"overallProgress":0.32}`
|
||||
|
||||
### D6:progress clamp 到 0~1
|
||||
|
||||
所有 `scrollProgress` / `pageProgress` / `overallProgress` / `chapterProgress` 字段:NaN→0, Infinity→1, 负数→0, >1→1。
|
||||
|
||||
### D7:ack 按 eventId 删除
|
||||
|
||||
```
|
||||
Rust exportPendingEventsV2 → iOS 写本地队列 → iOS ackEventsV2(eventIds) → Rust 删除
|
||||
```
|
||||
|
||||
`ack` 按 `eventId` 精确删除,不按数量。重复 ack 不崩溃。Buffer 最大 1000 条。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心 V2 模型
|
||||
|
||||
### 3.1 ReadingMaterialRefV2
|
||||
|
||||
```rust
|
||||
pub struct ReadingMaterialRefV2 {
|
||||
pub material_id: String, // 业务 ID
|
||||
pub file_path: String, // 本地文件路径
|
||||
pub file_name: String, // 文件名
|
||||
pub file_type: String, // markdown / pdf / epub / text / image / office
|
||||
pub mime_type: Option<String>, // MIME 类型(可选)
|
||||
pub file_size: Option<u64>, // 文件大小字节(可选)
|
||||
pub content_hash: Option<String>,// 内容哈希(可选)
|
||||
pub created_at_ms: Option<i64>, // 创建时间毫秒(可选)
|
||||
}
|
||||
```
|
||||
|
||||
不包含:`readingTargetType`, `userId`, `knowledgeBaseId`, `platform`, `appVersion`。
|
||||
|
||||
V1 `ReadingMaterialRef`(仅 `material_id`)标记 `#[deprecated]`,保留不删除。
|
||||
|
||||
### 3.2 ReadingSessionV2
|
||||
|
||||
```rust
|
||||
pub struct ReadingSessionV2 {
|
||||
pub client_session_id: String, // UUID, Rust 生成
|
||||
pub material: ReadingMaterialRefV2,
|
||||
pub started_at_ms: i64,
|
||||
pub closed_at_ms: Option<i64>, // 关闭时间(由 iOS 传入)
|
||||
pub last_event_at_ms: i64,
|
||||
pub next_sequence: u64, // 从 1 递增
|
||||
pub total_active_seconds: u32,
|
||||
pub last_position: Option<ReadingPosition>,
|
||||
pub status: ReadingSessionStatus, // Active | Paused | Closed | Interrupted | Failed
|
||||
}
|
||||
```
|
||||
|
||||
新增方法:`mark_session_interrupted_v2`, `mark_session_failed_v2`, `get_active_session_v2`, `set_session_closed_at_ms`。
|
||||
|
||||
### 3.3 ReadingEventV2
|
||||
|
||||
```rust
|
||||
pub struct ReadingEventV2 {
|
||||
pub event_id: String, // UUID, Rust 生成
|
||||
pub client_session_id: String, // 来自 ReadingSessionV2
|
||||
pub material_id: String, // 来自 ReadingMaterialRefV2
|
||||
pub event_type: ReadingEventTypeV2,
|
||||
pub position: Option<ReadingPosition>,
|
||||
pub active_seconds_delta: u32, // 增量,非累计
|
||||
pub timestamp_ms: i64,
|
||||
pub sequence: u64,
|
||||
}
|
||||
```
|
||||
|
||||
delta 规则:
|
||||
| 事件 | delta |
|
||||
|------|-------|
|
||||
| MaterialOpened | 0 |
|
||||
| PositionChanged | 0 |
|
||||
| MarkedAsRead | 0 |
|
||||
| Heartbeat | tick 产生的增量 |
|
||||
| MaterialClosed | close 残余增量 |
|
||||
|
||||
### 3.4 ActiveTimeTracker
|
||||
|
||||
```rust
|
||||
pub struct ActiveTimeTracker {
|
||||
pub last_tick_ms: Option<i64>,
|
||||
pub is_active: bool,
|
||||
pub accumulated_remainder_ms: i64,
|
||||
}
|
||||
```
|
||||
|
||||
方法:`start / pause / resume / tick / close`
|
||||
|
||||
### 3.5 EventBuffer V2
|
||||
|
||||
```rust
|
||||
pub struct BufferedReadingEventV2 {
|
||||
pub event: ReadingEventV2,
|
||||
pub state: BufferedEventState, // Pending | Exported | Failed
|
||||
pub exported_at_ms: Option<i64>,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
```
|
||||
|
||||
方法:`push_event_v2 / export_pending_events_v2 / ack_events_v2 / mark_events_failed_v2 / reload_stale_events_v2 / buffer_size_v2`
|
||||
|
||||
容量:最大 1000 条,超出时 Failed→Exported→Pending 顺序清理。
|
||||
|
||||
`reload_stale_events_v2`:启动时将 Exported 但未 ack 的事件重置为 Pending,用于 App crash 后的恢复。iOS 启动时必须调用。
|
||||
|
||||
---
|
||||
|
||||
## 4. V1 → V2 迁移策略
|
||||
|
||||
```
|
||||
1. 新增 events_v2.rs 模块(V2 核心)
|
||||
2. V1 events.rs 保留,标记 #[deprecated]
|
||||
3. V1 FFI 方法保留,不破坏 iOS 编译
|
||||
4. 新功能走 V2 FFI
|
||||
5. iOS ReadingRuntimeAdapter 优先 V2,V1 为 fallback
|
||||
```
|
||||
|
||||
### V1 vs V2 差异
|
||||
|
||||
| 字段 | V1 | V2 |
|
||||
|------|----|----|
|
||||
| event_id | 无 | UUID |
|
||||
| client_session_id | 无 | UUID |
|
||||
| active_seconds | 累计值/语义模糊 | active_seconds_delta 增量 |
|
||||
| sequence | 无 | 递增 |
|
||||
| target | material_id 裸字符串 | ReadingMaterialRefV2(含 filePath/fileName/fileType 等) |
|
||||
| session status | - | Active/Paused/Closed/Interrupted/Failed |
|
||||
| session recovery | - | reload_stale_events_v2 / mark_session_interrupted_v2 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 与 API 协议映射
|
||||
|
||||
| Rust ReadingEventV2 | API ReadingEventUploadItem | 来源 |
|
||||
|---------------------|---------------------------|------|
|
||||
| event_id | eventId | Rust |
|
||||
| client_session_id | clientSessionId | Rust |
|
||||
| material_id | materialId | Rust |
|
||||
| event_type | eventType | Rust→iOS 转 snake_case |
|
||||
| position | position | Rust (camelCase JSON) |
|
||||
| active_seconds_delta | activeSecondsDelta | Rust |
|
||||
| timestamp_ms | clientTimestampMs | Rust |
|
||||
| sequence | sequence | Rust |
|
||||
| — | readingTargetType | iOS 补充 |
|
||||
| — | platform | iOS 补充 |
|
||||
| — | appVersion | iOS 补充 |
|
||||
| — | clientTimezoneOffsetMinutes | iOS 补充 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 项目结构
|
||||
|
||||
```
|
||||
zhixi-document-runtime
|
||||
├── zx_document_core/src
|
||||
│ ├── events.rs ← V1 (deprecated)
|
||||
│ ├── events_v2.rs ← V2 (新增)
|
||||
│ ├── session_v2.rs ← ReadingSessionV2 (新增)
|
||||
│ ├── time_tracker.rs ← ActiveTimeTracker (新增)
|
||||
│ ├── progress.rs ← ReadingPosition (改 camelCase+clamp)
|
||||
│ ├── material_type.rs ← ✅ 完成
|
||||
│ ├── markdown.rs ← ✅ 完成
|
||||
│ ├── text.rs ← ✅ 完成
|
||||
│ ├── image_meta.rs ← ✅ 完成
|
||||
│ ├── search.rs ← ⚠️ 扩展 PDF/EPUB
|
||||
│ ├── anchors.rs ← ⚠️ 补 from_search_result
|
||||
│ ├── document.rs ← ⚠️ 扩展 DocumentInfo
|
||||
│ ├── pdf.rs ← ❌ stub
|
||||
│ ├── epub.rs ← ❌ stub
|
||||
│ └── blocks.rs ← ✅ 完成
|
||||
├── zx_document_ffi/src
|
||||
│ └── lib.rs ← 新增 V2 exports
|
||||
├── docs/
|
||||
├── fixtures/
|
||||
├── bindings/ios/
|
||||
└── scripts/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 最终验收链路
|
||||
|
||||
```
|
||||
App 启动
|
||||
→ reload_stale_events_v2(重置未 ack 的 Exported 事件为 Pending)
|
||||
→ cleanup_stale_sessions_v2(清理超时旧 session)
|
||||
|
||||
iOS 创建 ReadingMaterialRefV2(只传 materialId + 文件信息)
|
||||
→ Rust startReadingSessionV2(生成 clientSessionId)
|
||||
→ push MaterialOpened(delta=0)
|
||||
→ heartbeat tick 每 15s → push Heartbeat(delta=增量秒数)
|
||||
→ push PositionChanged(delta=0)
|
||||
→ push MarkedAsRead(delta=0)
|
||||
→ push MaterialClosed(delta=残余秒数)
|
||||
→ exportPendingEventsV2
|
||||
→ iOS 写入本地 ReadingEventUploadQueue
|
||||
→ ackEventsV2(按 eventId 精确删除)
|
||||
→ iOS 补 readingTargetType / platform / appVersion / timezone
|
||||
→ API batch upload
|
||||
```
|
||||
128
docs/ffi-troubleshooting.md
Normal file
128
docs/ffi-troubleshooting.md
Normal file
@ -0,0 +1,128 @@
|
||||
# FFI Troubleshooting
|
||||
|
||||
## UniFFI 版本
|
||||
|
||||
本项目使用 UniFFI 0.31,采用 **UDL + proc-macro 混合模式**:
|
||||
- UDL(`zx_document.udl`):定义类型和函数签名
|
||||
- `#[uniffi::export]` proc-macro:生成 C ABI 分发符号
|
||||
- `uniffi-bindgen`:生成 Swift/Kotlin 绑定代码
|
||||
|
||||
## 常见问题
|
||||
|
||||
### `No such module 'ZxDocumentRuntime'`
|
||||
|
||||
**原因**:XCFramework 未正确添加到 Xcode 项目。
|
||||
|
||||
**解决**:
|
||||
1. 确认 `bindings/ios/ZxDocumentRuntime.xcframework` 已生成
|
||||
2. 在 Xcode → Target → General → Frameworks 中添加
|
||||
3. 确保 Embed 设置为 `Do Not Embed`(静态库)
|
||||
|
||||
### `Undefined symbol: _ffi_zx_document_ffi_*`
|
||||
|
||||
**原因**:C ABI 符号未导出。proc-macro 生成的符号名称可能与链接器期望不一致。
|
||||
|
||||
**解决**:
|
||||
1. 确认 `crates/zx_document_ffi/src/lib.rs` 中 `#[no_mangle] pub extern "C"` 函数存在
|
||||
2. 检查 Cargo.toml `crate-type = ["lib", "staticlib", "cdylib"]`
|
||||
3. 运行 `nm target/aarch64-apple-ios/release/libzx_document_ffi.a | grep ffi_` 验证符号
|
||||
4. 确认 UDL 中声明的函数与 `#[uniffi::export]` 函数名称一致
|
||||
|
||||
### `Library not found for -lzx_document_ffi`
|
||||
|
||||
**原因**:Library Search Paths 未设置。
|
||||
|
||||
**解决**:
|
||||
1. Build Settings → Library Search Paths → 添加 `$(PROJECT_DIR)/bindings/ios`
|
||||
2. 或使用绝对路径指向 `.xcframework` 所在目录
|
||||
|
||||
### UniFFI checksum mismatch
|
||||
|
||||
**原因**:UDL 文件更新后未重新生成 Swift binding。
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
uniffi-bindgen generate \
|
||||
--language swift \
|
||||
--out-dir bindings/ios/generated \
|
||||
crates/zx_document_ffi/src/zx_document.udl
|
||||
```
|
||||
|
||||
### `setup_scaffolding!()` not called
|
||||
|
||||
**原因**:`uniffi::setup_scaffolding!()` 必须在 lib.rs 的第一行调用。
|
||||
|
||||
**解决**:确认 `crates/zx_document_core/src/lib.rs` 第一行是 `uniffi::setup_scaffolding!();`
|
||||
|
||||
### 新增类型/函数后 Swift 不可见
|
||||
|
||||
**原因**:只在 Rust 侧添加了类型/函数,但未更新:
|
||||
1. UDL 文件声明
|
||||
2. FFI crate `pub use` 重导出
|
||||
3. Swift binding 重新生成
|
||||
|
||||
**完整的 FFI 接入步骤**:
|
||||
```bash
|
||||
# 1. Rust 侧添加类型 + 函数 + UDL 声明
|
||||
# 2. FFI crate: pub use 重导出 + #[uniffi::export] 包装
|
||||
# 3. 重新生成 Swift
|
||||
uniffi-bindgen generate --language swift \
|
||||
--out-dir bindings/ios/generated \
|
||||
crates/zx_document_ffi/src/zx_document.udl
|
||||
# 4. 验证编译
|
||||
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
|
||||
```
|
||||
|
||||
### `cargo build` 目标平台不匹配
|
||||
|
||||
**原因**:macOS 上构建 iOS 目标需要 Apple Silicon target。
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
|
||||
```
|
||||
|
||||
### `infer` / `comrak` / `zip` crate 编译失败
|
||||
|
||||
**原因**:某些 crate 在 iOS target 下有 C 依赖。
|
||||
|
||||
**解决**:
|
||||
- `comrak`:需要启用 `--features` 中的 `onig` 或使用 `syntect` 后端
|
||||
- `zip`:纯 Rust,无特殊要求
|
||||
- 确保 `CC` 环境变量指向 Xcode toolchain
|
||||
|
||||
### XCFramework 构建注意事项
|
||||
|
||||
1. 必须分别编译 device 和 simulator 目标
|
||||
2. `lipo` 不需要(两个 .a 文件各自是 fat binary)
|
||||
3. header 文件由 `uniffi-bindgen` 生成:`bindings/ios/generated/zx_documentFFI.h`
|
||||
4. modulemap 文件创建:
|
||||
```
|
||||
framework module ZxDocumentRuntime {
|
||||
header "zx_documentFFI.h"
|
||||
export *
|
||||
}
|
||||
```
|
||||
|
||||
### UDL 类型不支持
|
||||
|
||||
**问题类型**:
|
||||
- `Vec<(String, &str)>` — 不支持 tuple
|
||||
- 泛型函数 — 不支持
|
||||
- 生命周期参数 — 不支持
|
||||
|
||||
**解决**:在 FFI 层创建包装类型。例如 `search_pdf_text(&[(u32, &str)])` → FFI 包装为 `search_pdf_pages(Vec<u32>, Vec<String>)`。
|
||||
|
||||
### Rust 与 iOS 持有引用
|
||||
|
||||
UniFFI 生成的对象是 **值类型**(序列化跨 FFI 边界传递),不是引用类型。Rust 侧的 `Mutex` 保护全局状态,iOS 侧通过函数调用读写状态。
|
||||
|
||||
### 验证清单
|
||||
|
||||
- [ ] `cargo build --release --target aarch64-apple-ios` 通过
|
||||
- [ ] `cargo build --release --target aarch64-apple-ios-sim` 通过
|
||||
- [ ] `nm` 验证 C ABI 符号存在
|
||||
- [ ] Swift binding 生成并编译通过
|
||||
- [ ] XCFramework 包含 device + simulator slices
|
||||
- [ ] header + modulemap 正确
|
||||
- [ ] demo App 链接成功
|
||||
162
docs/file-format-support-matrix.md
Normal file
162
docs/file-format-support-matrix.md
Normal file
@ -0,0 +1,162 @@
|
||||
# 文件格式支持矩阵
|
||||
|
||||
## 1. 概述
|
||||
|
||||
Document Runtime 支持以下文件格式的解析、元数据提取、文本提取和搜索。
|
||||
|
||||
---
|
||||
|
||||
## 2. 能力矩阵
|
||||
|
||||
| 格式 | 枚举值 | MIME 检测 | 预览模式 | 文本提取 | 元数据 | 搜索 | 定位 | 锚点 |
|
||||
|------|--------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| Markdown | `Markdown` | 扩展名 `.md` / `.markdown` | NativeReader | ✅ Block 解析 | ✅ 行/词/字符统计 | ✅ | ✅ Block+Scroll | ✅ Block ID |
|
||||
| 纯文本 | `Text` | 扩展名 `.txt` | NativeReader | ✅ 按行 | ✅ 行/词/字符统计 | ✅ | ✅ 行号+Scroll | ❌ |
|
||||
| PDF | `Pdf` | 文件头 `%PDF-` | PlatformPreview | ✅ BT/ET 流式扫描 | ✅ 页数/标题/作者 | ✅ | ✅ 页码+Progress | ✅ 页码 |
|
||||
| EPUB | `Epub` | 文件头 `PK` + mimetype | NativeReader | ✅ HTML strip | ✅ 标题/作者/章节数 | ✅ | ✅ 章节+Progress | ✅ 章节 ID |
|
||||
| 图片 | `Image` | 文件头魔术字节 | NativeReader | ❌ | ✅ 宽/高/格式 | ❌ | ✅ Zoom+Offset | ❌ |
|
||||
| Word | `Word` | 扩展名 `.docx` | PlatformPreview | ❌ | ❌ | ❌ | ❌ | ✅ 整体 |
|
||||
| Excel | `Excel` | 扩展名 `.xlsx` | PlatformPreview | ❌ | ❌ | ❌ | ❌ | ✅ 整体 |
|
||||
| PowerPoint | `PowerPoint` | 扩展名 `.pptx` | ExternalOpen | ❌ | ❌ | ❌ | ❌ | ✅ 整体 |
|
||||
| 未知 | `Unknown` | — | Unsupported | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 3. MIME 检测
|
||||
|
||||
### 3.1 检测策略
|
||||
|
||||
```
|
||||
1. 文件头魔术字节(前 8KB)
|
||||
├─ %PDF- → PDF
|
||||
├─ PK + mimetype=application/epub+zip → EPUB
|
||||
└─ PNG/JPG/GIF/WebP 头 → Image
|
||||
|
||||
2. 扩展名回退
|
||||
├─ .md / .markdown → Markdown
|
||||
├─ .txt → Text
|
||||
├─ .epub → EPUB
|
||||
└─ .pdf → PDF
|
||||
```
|
||||
|
||||
### 3.2 代码路径
|
||||
|
||||
```rust
|
||||
// material_type.rs
|
||||
pub fn detect_material_type(file_path: &str) -> Result<MaterialType, DocumentError>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 各格式详情
|
||||
|
||||
### 4.1 Markdown
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 解析 | `parse_markdown()` → `Vec<DocumentBlock>`(8 种块类型) |
|
||||
| 块类型 | Heading / Paragraph / List / CodeBlock / Quote / Table / Image / HorizontalRule |
|
||||
| 元数据 | YAML Front Matter 自动解析为 metadata |
|
||||
| 搜索 | `search_blocks()` — 在每个块的文本内容中搜索 |
|
||||
| 定位 | `ReadingPosition::Markdown(blockId, scrollProgress)` |
|
||||
| 锚点 | `resolveNoteAnchors()` — `#heading-id` / `#paragraph-id` |
|
||||
|
||||
### 4.2 纯文本
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 解析 | 按换行符分割为行,不作为结构化块 |
|
||||
| 元数据 | 行数、词数(`text_stats()`)、字符数 |
|
||||
| 搜索 | `search_text()` — 按行搜索,返回行号 |
|
||||
| 定位 | `ReadingPosition::Text(lineNumber, scrollProgress)` |
|
||||
|
||||
### 4.3 PDF
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 页数检测 | `/Type /Page` 对象计数 → `/Pages /Count` → 默认 1 |
|
||||
| 文本提取 | 扫描 `BT..ET` 块,提取括号内文本(`extract_pdf_text()`) |
|
||||
| 元数据 | `/Title`, `/Author` 字段,仅支持 ASCII/PDFDocEncoding |
|
||||
| 限制 | 最大扫描 8MB(`MAX_SCAN_BYTES`);加密 PDF 不解析 |
|
||||
| 搜索 | 依赖 `extract_pdf_text()` 输出 |
|
||||
| 定位 | `ReadingPosition::Pdf(pageNumber, pageProgress, overallProgress)` |
|
||||
|
||||
### 4.4 EPUB
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| TOC | NCX(EPUB2)→ NAV(EPUB3)回退 |
|
||||
| 章节列表 | Spine 解析 → `read_epub_chapters()` |
|
||||
| 文本提取 | `extract_epub_chapter_texts()` — HTML 标签移除 + 空白合并 |
|
||||
| 元数据 | `dc:title`, `dc:creator` |
|
||||
| 定位 | `ReadingPosition::Epub(chapterId, chapterProgress, overallProgress)` |
|
||||
| 路径策略 | 含 `/` 或 `.xhtml`/`.html`→直接用;否则 `OEBPS/{id}.xhtml` |
|
||||
|
||||
### 4.5 图片
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 支持格式 | PNG / JPG / GIF / WebP |
|
||||
| 元数据 | `read_image_meta()` → 宽度、高度、格式 |
|
||||
| 搜索 | 不支持 |
|
||||
| 定位 | `ReadingPosition::Image(zoomScale, offsetX, offsetY)` |
|
||||
|
||||
### 4.6 Office(Word / Excel / PowerPoint)
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 预览 | PlatformPreview / ExternalOpen(不内置渲染) |
|
||||
| 文本提取 | 不支持(依赖第三方预览组件) |
|
||||
| 搜索 | 不支持 |
|
||||
| 锚点 | 仅 Material 级别 |
|
||||
|
||||
---
|
||||
|
||||
## 5. PreviewMode 对照
|
||||
|
||||
| MaterialType | PreviewMode | 渲染方式 |
|
||||
|-------------|-------------|----------|
|
||||
| Markdown | NativeReader | Rust 解析 → SwiftUI 渲染 |
|
||||
| Text | NativeReader | SwiftUI 文本视图 |
|
||||
| Pdf | PlatformPreview | WKWebView / PDFKit |
|
||||
| Image | NativeReader | SwiftUI Image |
|
||||
| Epub | NativeReader | Rust HTML strip → SwiftUI |
|
||||
| Word | PlatformPreview | WKWebView / Office URI |
|
||||
| Excel | PlatformPreview | WKWebView / Office URI |
|
||||
| PowerPoint | ExternalOpen | 系统分享 Sheet |
|
||||
| Unknown | Unsupported | 错误提示 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 搜索能力
|
||||
|
||||
| 格式 | 搜索方式 | 返回粒度 |
|
||||
|------|----------|:--:|
|
||||
| Markdown | `search_blocks()` | Block ID + 片段 |
|
||||
| Text | `search_text()` | 行号 + 片段 |
|
||||
| PDF | 预提取文本后搜索 | 页码 |
|
||||
| EPUB | 预提取文本后搜索 | 章节 ID |
|
||||
| Image | 不支持 | — |
|
||||
| Office | 不支持 | — |
|
||||
|
||||
**验证函数**:`validate_search_result()` — 校验搜索结果片段中的匹配位置是否正确。
|
||||
|
||||
---
|
||||
|
||||
## 7. 限制与边界
|
||||
|
||||
| 项 | 值 | 说明 |
|
||||
|------|------|------|
|
||||
| Markdown 文档大小 | 无硬限制 | 依赖内存 |
|
||||
| PDF 扫描上限 | 8 MB | `MAX_SCAN_BYTES` |
|
||||
| PDF 文本提取 | BT/ET 块扫描 | 不解析 CMap/Type0 字体 |
|
||||
| EPUB 支持版本 | EPUB 2.0 / 3.0 | NCX + NAV 回退 |
|
||||
| 图片元数据 | 仅基础 | 不含 EXIF GPS/时间 |
|
||||
| Office 解析 | 不支持 | 仅预览模式判断 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [iOS FFI 调用指南](./ios-ffi-integration-guide.md)
|
||||
- [UniFFI UDL 编写规范](./uniffi-udl-spec.md)
|
||||
221
docs/ios-ffi-integration-guide.md
Normal file
221
docs/ios-ffi-integration-guide.md
Normal file
@ -0,0 +1,221 @@
|
||||
# Document Runtime iOS FFI 调用指南
|
||||
|
||||
## 1. 概述
|
||||
|
||||
Document Runtime(zx_document_core)通过 UniFFI 生成 C-ABI 兼容的 Swift 绑定,嵌入 iOS 应用作为 `ZxDocumentRuntime.xcframework`。
|
||||
|
||||
Swift 层通过 uniffi 生成的 `zx_document.swift` 调用所有 Rust 功能。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ iOS Swift │
|
||||
│ zx_document.swift (uniffi 生成) │
|
||||
│ ├─ buildDocumentInfo() │
|
||||
│ ├─ parseMarkdown() │
|
||||
│ ├─ extractPdfText() │
|
||||
│ ├─ extractEpubChapterTexts() │
|
||||
│ ├─ searchBlocks() / searchText() │
|
||||
│ ├─ resolveNoteAnchors() │
|
||||
│ ├─ startReadingSessionV2() │
|
||||
│ ├─ pushHeartbeatV2() │
|
||||
│ ├─ exportPending() / ackExported() │
|
||||
│ └─ ... │
|
||||
└──────────────┬───────────────────────┘
|
||||
│ C-ABI (uniffi)
|
||||
┌──────────────▼───────────────────────┐
|
||||
│ Rust: zx_document_ffi │
|
||||
│ lib.rs — #[uniffi::export] fns │
|
||||
└──────────────┬───────────────────────┘
|
||||
│
|
||||
┌──────────────▼───────────────────────┐
|
||||
│ Rust: zx_document_core │
|
||||
│ document / markdown / pdf / epub / │
|
||||
│ search / anchors / events_v2 / ... │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 类型映射
|
||||
|
||||
### 3.1 基础类型
|
||||
|
||||
| Rust | Swift |
|
||||
|------|-------|
|
||||
| `String` | `String` |
|
||||
| `u32` / `u64` | `UInt32` / `UInt64` |
|
||||
| `i64` | `Int64` |
|
||||
| `bool` | `Bool` |
|
||||
| `Vec<T>` | `[T]` |
|
||||
| `Option<T>` | `T?` |
|
||||
| `Result<T, E>` | `throws` |
|
||||
|
||||
### 3.2 结构体 → Record
|
||||
|
||||
```rust
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct DocumentInfo { ... }
|
||||
```
|
||||
```swift
|
||||
// uniffi 生成
|
||||
public struct DocumentInfo {
|
||||
public var materialId: String
|
||||
public var title: String
|
||||
public var materialType: MaterialType
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 枚举 → Enum
|
||||
|
||||
```rust
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum MaterialType { Markdown, Text, Pdf, Image, Epub, Word, Excel, PowerPoint, Unknown }
|
||||
```
|
||||
```swift
|
||||
public enum MaterialType {
|
||||
case markdown, text, pdf, image, epub, word, excel, powerPoint, unknown
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心 API
|
||||
|
||||
### 4.1 文档解析
|
||||
|
||||
```swift
|
||||
// 构建文档信息(文件类型检测 + 元数据提取)
|
||||
let info = try buildDocumentInfo(
|
||||
filePath: "/path/to/file.md",
|
||||
materialId: "mat-001",
|
||||
title: "My Document"
|
||||
)
|
||||
// → DocumentInfo { materialType, pageCount, wordCount, epubChapterCount, ... }
|
||||
|
||||
// Markdown 解析为块
|
||||
let blocks = try parseMarkdown(markdown: "# Title\n\nContent")
|
||||
// → [DocumentBlock]
|
||||
|
||||
// PDF 元数据
|
||||
let pdfMeta = try readPdfMetadata(filePath: "/path/to/book.pdf")
|
||||
// → PdfMetadata { pageCount, title, author }
|
||||
|
||||
// PDF 文本提取
|
||||
let pages = try extractPdfText(filePath: "/path/to/book.pdf")
|
||||
// → [PdfPageText { pageNumber, text }]
|
||||
|
||||
// EPUB 元数据
|
||||
let epubMeta = try readEpubMetadata(filePath: "/path/to/book.epub")
|
||||
// → EpubMetadata { title, author, chapterCount }
|
||||
|
||||
// EPUB 章节文本提取(HTML strip)
|
||||
let chapters = try extractEpubChapterTexts(filePath: "/path/to/book.epub")
|
||||
// → [EpubChapterText { chapterId, text }]
|
||||
```
|
||||
|
||||
### 4.2 搜索
|
||||
|
||||
```swift
|
||||
// 搜索 Markdown 块
|
||||
let results = searchBlocks(blocks: blocks, query: "keyword")
|
||||
// → [SearchResult { blockId, snippet, matchStart, matchEnd }]
|
||||
|
||||
// 搜索纯文本
|
||||
let textResults = searchText(content: text, query: "keyword")
|
||||
// → [SearchResult { lineNumber, ... }]
|
||||
```
|
||||
|
||||
### 4.3 锚点
|
||||
|
||||
```swift
|
||||
let anchors = try resolveNoteAnchors(
|
||||
blocks: blocks,
|
||||
anchorHints: ["#heading-1", "#paragraph-2"]
|
||||
)
|
||||
// → [NoteAnchor { anchorId, blockId, ... }]
|
||||
```
|
||||
|
||||
### 4.4 V2 阅读会话
|
||||
|
||||
```swift
|
||||
// 开始会话
|
||||
let sessionId = try startReadingSessionV2(
|
||||
material: ReadingMaterialRefV2(materialId: "mat-001"),
|
||||
timestampMs: Int64(Date().timeIntervalSince1970 * 1000)
|
||||
)
|
||||
|
||||
// 推送位置变更事件
|
||||
try pushPositionChangedV2(
|
||||
sessionId: sessionId,
|
||||
position: ReadingPosition.markdown(
|
||||
blockId: "b1",
|
||||
scrollProgress: 0.5
|
||||
),
|
||||
timestampMs: nowMs()
|
||||
)
|
||||
|
||||
// 导出待上传事件
|
||||
let (events, state) = exportPendingV2()
|
||||
// → ([BufferedReadingEventV2], EventBufferStateV2)
|
||||
|
||||
// ACK 已上传事件
|
||||
ackExportedV2(count: UInt32(events.count))
|
||||
|
||||
// 关闭会话
|
||||
try closeReadingSessionV2(sessionId: sessionId, timestampMs: nowMs())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
所有 FFI 函数返回 `Result<T, String>`(UniFFI 映射为 Swift `throws`):
|
||||
|
||||
```swift
|
||||
do {
|
||||
let info = try buildDocumentInfo(filePath: path, materialId: id, title: title)
|
||||
// 使用 info
|
||||
} catch {
|
||||
print("Document Runtime error: \(error)")
|
||||
// error 是 Rust DocumentError 的 Display 字符串
|
||||
}
|
||||
```
|
||||
|
||||
常见错误:
|
||||
- `IoError` — 文件不存在/无权限
|
||||
- `ParseError` — 格式损坏/不支持的格式
|
||||
- `MarkdownParseError` — Markdown 语法错误
|
||||
|
||||
---
|
||||
|
||||
## 6. 线程安全
|
||||
|
||||
- Rust 层使用 `Mutex<Vec<T>>` 保护全局状态(EventBuffer)
|
||||
- 所有 FFI 函数可从任意线程调用
|
||||
- Swift 层应在主线程调用 UI 更新,后台线程调用 FFI
|
||||
|
||||
---
|
||||
|
||||
## 7. 性能建议
|
||||
|
||||
| 操作 | 建议 |
|
||||
|------|------|
|
||||
| `buildDocumentInfo` | 主线程调用(< 50ms),缓存结果 |
|
||||
| `extractPdfText` | 后台线程(可能耗时数秒) |
|
||||
| `parseMarkdown` | 任意线程(< 10ms) |
|
||||
| `searchBlocks` | 任意线程(< 10ms) |
|
||||
| `exportPendingV2` | 后台定时器(30s),主线程调用也可 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [V2 事件 Pipeline 架构](../ios-projects/docs/v2-event-pipeline.md)(ios-projects 仓库)
|
||||
- [Rust FFI 绑定更新流程](../ios-projects/docs/rust-ffi-update.md)(ios-projects 仓库)
|
||||
- [UniFFI UDL 编写规范](./uniffi-udl-spec.md)
|
||||
- [文件格式支持矩阵](./file-format-support-matrix.md)
|
||||
@ -2,101 +2,315 @@
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述如何将 `zhixi-document-runtime` 集成到 iOS App 中。
|
||||
本文档描述如何将 `zhixi-document-runtime` 集成到 iOS App 中。所有命令已验证可执行。
|
||||
|
||||
## 构建 Rust 库
|
||||
## 环境要求
|
||||
|
||||
- Rust 1.96+ (stable)
|
||||
- Xcode 15+
|
||||
- iOS 16+ deployment target
|
||||
|
||||
## 1. 构建 Rust 库
|
||||
|
||||
```bash
|
||||
# 安装 Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# 添加 iOS target
|
||||
# 添加 iOS target(首次)
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
|
||||
|
||||
# 构建
|
||||
cargo build --release --target aarch64-apple-ios
|
||||
cargo build --release --target aarch64-apple-ios-sim
|
||||
```
|
||||
|
||||
## 生成 Swift Binding
|
||||
|
||||
```bash
|
||||
# 通过 xtask 生成
|
||||
cargo run -p xtask -- generate-bindings
|
||||
|
||||
# 或手动通过 UniFFI
|
||||
uniffi-bindgen generate \
|
||||
crates/zx_document_ffi/src/zx_document.udl \
|
||||
--language swift \
|
||||
--out-dir bindings/ios/generated
|
||||
```
|
||||
|
||||
## XCFramework
|
||||
|
||||
```bash
|
||||
# 通过 scripts 构建
|
||||
# 一键构建(含 XCFramework + Swift 绑定)
|
||||
./scripts/build-ios.sh
|
||||
|
||||
# 输出
|
||||
# bindings/ios/ZxDocumentRuntime.xcframework/
|
||||
# 或手动构建
|
||||
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
|
||||
cargo build --release --target aarch64-apple-ios-sim -p zx_document_ffi
|
||||
```
|
||||
|
||||
## 引入 XCFramework
|
||||
产物:
|
||||
```
|
||||
target/aarch64-apple-ios/release/libzx_document_ffi.a
|
||||
target/aarch64-apple-ios-sim/release/libzx_document_ffi.a
|
||||
```
|
||||
|
||||
1. 将 `ZxDocumentRuntime.xcframework` 拖入 Xcode 项目
|
||||
2. 将生成的 Swift 文件添加到项目
|
||||
3. 确保 `Frameworks, Libraries, and Embedded Content` 中包含 framework
|
||||
## 2. 生成 Swift Binding
|
||||
|
||||
## Swift 调用示例
|
||||
使用 **UDL bindgen + proc-macro 混合模式**:
|
||||
|
||||
```bash
|
||||
# 通过 build-ios.sh 自动生成,或手动:
|
||||
# 1. 编译 Rust(proc-macro 生成 C ABI 符号)
|
||||
cargo build --release -p zx_document_ffi
|
||||
|
||||
# 2. 从 UDL 生成 Swift 绑定(类型 + FFI 调用封装)
|
||||
uniffi-bindgen generate \
|
||||
--language swift \
|
||||
--out-dir bindings/ios/generated \
|
||||
crates/zx_document_ffi/src/zx_document.udl
|
||||
```
|
||||
|
||||
输出:`bindings/ios/generated/zx_document.swift` (~50KB)
|
||||
|
||||
> UDL 定义类型和函数签名,`#[uniffi::export]` proc-macro 生成 C ABI 分发符号,bindgen 生成 Swift 调用封装。两者协作,非 library 模式。
|
||||
|
||||
## 3. 创建 XCFramework
|
||||
|
||||
```bash
|
||||
xcodebuild -create-xcframework \
|
||||
-library bindings/ios/device/libzx_document_ffi.a \
|
||||
-library bindings/ios/simulator/libzx_document_ffi.a \
|
||||
-output bindings/ios/ZxDocumentRuntime.xcframework
|
||||
```
|
||||
|
||||
产物:`bindings/ios/ZxDocumentRuntime.xcframework`
|
||||
|
||||
## 4. 引入 Xcode 项目
|
||||
|
||||
1. 将 `bindings/ios/ZxDocumentRuntime.xcframework` 拖入 Xcode 项目
|
||||
2. 将 `bindings/ios/generated/zx_document.swift` 添加到项目
|
||||
3. Target → General → Frameworks, Libraries, and Embedded Content:确保 `ZxDocumentRuntime.xcframework` 为 `Do Not Embed`(静态库)
|
||||
4. Build Settings → Library Search Paths:添加 framework 路径
|
||||
|
||||
## 5. Swift 调用 API
|
||||
|
||||
### 类型
|
||||
|
||||
```swift
|
||||
import ZxDocumentRuntime
|
||||
|
||||
// 文件类型识别
|
||||
let materialType = try detectMaterialType(filePath: "/path/to/file.md")
|
||||
print("Type: \(materialType)")
|
||||
// 枚举类型
|
||||
let type = MaterialType.markdown
|
||||
let mode = PreviewMode.nativeReader
|
||||
|
||||
// 打开文档
|
||||
let doc = try openDocument(filePath: "/path/to/file.md", materialId: "abc123")
|
||||
let info = try getDocumentInfo(handle: doc)
|
||||
print("Title: \(info.title)")
|
||||
// 位置
|
||||
let pos = ReadingPosition.markdown(blockId: "h1", scrollProgress: 0.5)
|
||||
|
||||
// Markdown blocks
|
||||
let blocks = try getMarkdownBlocks(handle: doc)
|
||||
for block in blocks {
|
||||
print("[\(block.id)] \(block)")
|
||||
// 事件
|
||||
let event = ReadingEvent.materialOpened(materialId: "abc", timestampMs: 1000)
|
||||
|
||||
// 锚点
|
||||
let anchor = NoteAnchor.markdownBlock(materialId: "abc", blockId: "h1")
|
||||
|
||||
// 字典类型
|
||||
let meta = ImageMeta(width: 800, height: 600, format: "png", fileSize: 12345)
|
||||
let info = DocumentInfo(materialId: "abc", title: "doc.md",
|
||||
materialType: .markdown, previewMode: .nativeReader,
|
||||
fileSize: 1024, pageCount: nil, wordCount: 100, createdAt: nil)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
所有函数抛出 `DocumentError`:
|
||||
|
||||
```swift
|
||||
do {
|
||||
let path = "/path/to/file.md"
|
||||
let type = try detectMaterialType(filePath: path)
|
||||
print("Detected: \(type)")
|
||||
} catch let error as DocumentError {
|
||||
switch error {
|
||||
case .fileNotFound: print("File not found")
|
||||
case .unsupportedFormat: print("Unsupported format")
|
||||
case .parseError: print("Parse error")
|
||||
case .invalidEncoding: print("Invalid encoding")
|
||||
case .ioError: print("IO error")
|
||||
}
|
||||
|
||||
// 搜索
|
||||
let results = try searchDocument(handle: doc, query: "学习")
|
||||
for r in results {
|
||||
print("Found at \(r.blockId): \(r.snippet)")
|
||||
}
|
||||
|
||||
// 更新阅读位置
|
||||
let position = ReadingPosition.markdown(
|
||||
blockId: "heading-3",
|
||||
scrollProgress: 0.45
|
||||
)
|
||||
try updateReadingPosition(materialId: "abc123", position: position)
|
||||
|
||||
// 导出阅读事件
|
||||
let events = try exportPendingEvents()
|
||||
for event in events {
|
||||
print("Event: \(event)")
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
### V2 阅读事件集成(推荐)
|
||||
|
||||
### 编译错误
|
||||
V2 API 提供 session 管理、事件缓冲、ack 确认、crash recovery 等完整能力。
|
||||
|
||||
- 确保 Rust target 已安装
|
||||
- 确保 XCFramework 的 `Build Phase` / `Link Binary with Libraries` 已包含
|
||||
- 检查 `Library Search Paths` 包含 framework 路径
|
||||
#### 创建阅读目标和会话
|
||||
|
||||
### 运行时崩溃
|
||||
```swift
|
||||
// 1. 创建 ReadingMaterialRef(Rust 不存储 readingTargetType,iOS 上传时补充)
|
||||
let material = ReadingMaterialRef(materialId: "mat_001")
|
||||
|
||||
- 检查文件路径是否存在
|
||||
- 编码问题:确保文件是 UTF-8(TXT/MD)
|
||||
- 大文件:PDF/EPUB 可能内存占用大,考虑后台线程处理
|
||||
// 2. 启动阅读会话(返回 clientSessionId)
|
||||
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let sessionId = try startReadingSessionV2(material: material, timestampMs: timestamp)
|
||||
```
|
||||
|
||||
#### 推送 5 种事件
|
||||
|
||||
```swift
|
||||
// MaterialOpened — 打开资料
|
||||
let opened = try pushMaterialOpenedV2(
|
||||
sessionId: sessionId,
|
||||
materialId: "mat_001",
|
||||
timestampMs: now()
|
||||
)
|
||||
|
||||
// PositionChanged — 位置变化
|
||||
let pos = ReadingPosition.markdown(blockId: "intro", scrollProgress: 0.25)
|
||||
let changed = try pushPositionChangedV2(
|
||||
sessionId: sessionId,
|
||||
materialId: "mat_001",
|
||||
position: pos,
|
||||
timestampMs: now()
|
||||
)
|
||||
|
||||
// Heartbeat — 心跳(含 activeSecondsDelta)
|
||||
let hb = try pushHeartbeatV2(
|
||||
sessionId: sessionId,
|
||||
materialId: "mat_001",
|
||||
activeSecondsDelta: 15,
|
||||
position: nil,
|
||||
timestampMs: now()
|
||||
)
|
||||
|
||||
// MarkedAsRead — 标记已读
|
||||
let mar = try pushMarkedAsReadV2(
|
||||
sessionId: sessionId,
|
||||
materialId: "mat_001",
|
||||
timestampMs: now()
|
||||
)
|
||||
|
||||
// MaterialClosed — 关闭资料
|
||||
let closed = try pushMaterialClosedV2(
|
||||
sessionId: sessionId,
|
||||
materialId: "mat_001",
|
||||
activeSecondsDelta: 0,
|
||||
timestampMs: now()
|
||||
)
|
||||
```
|
||||
|
||||
#### Export + Ack 流程
|
||||
|
||||
```swift
|
||||
// 1. 导出待上传事件(标记为 Exported)
|
||||
let events = exportPendingEventsV2(limit: 100, timestampMs: now())
|
||||
|
||||
// 2. 构造上传请求(iOS 补充 readingTargetType/platform/appVersion/timezone)
|
||||
let uploadItems = events.map { event in
|
||||
ReadingEventUploadItem(
|
||||
eventId: event.eventId,
|
||||
clientSessionId: event.clientSessionId,
|
||||
materialId: event.materialId,
|
||||
eventType: event.eventType,
|
||||
position: event.position,
|
||||
activeSecondsDelta: event.activeSecondsDelta,
|
||||
clientTimestampMs: event.timestampMs,
|
||||
sequence: event.sequence,
|
||||
// iOS 补充字段
|
||||
readingTargetType: getTargetType(for: event.materialId),
|
||||
platform: "ios",
|
||||
appVersion: Bundle.main.appVersion,
|
||||
clientTimezoneOffsetMinutes: Int(TimeZone.current.secondsFromGMT() / 60)
|
||||
)
|
||||
}
|
||||
|
||||
// 3. 上传到 API
|
||||
api.uploadEvents(uploadItems) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
// 成功后 ACK(从 buffer 移除)
|
||||
let ids = events.map(\.eventId)
|
||||
ackEventsV2(eventIds: ids)
|
||||
case .failure:
|
||||
// 失败标记为 Failed(下次 export 重试)
|
||||
let ids = events.map(\.eventId)
|
||||
markEventsFailedV2(eventIds: ids)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### App 启动恢复
|
||||
|
||||
```swift
|
||||
// App 启动时调用,恢复上次未 ack 的事件
|
||||
func applicationDidFinishLaunching() {
|
||||
reloadStaleEventsV2() // 内部 Pending/Exported→Pending,可重新 export
|
||||
}
|
||||
```
|
||||
|
||||
#### 搜索和笔记锚点
|
||||
|
||||
```swift
|
||||
// 1. 解析 Markdown
|
||||
let blocks = try parseMarkdown(content: mdContent)
|
||||
|
||||
// 2. 搜索
|
||||
let results = searchMarkdownBlocks(blocks: blocks, query: "keyword")
|
||||
|
||||
// 3. 从搜索结果创建笔记锚点
|
||||
if let firstResult = results.first {
|
||||
let anchor = createNoteAnchorFromSearch(
|
||||
materialId: "mat_001",
|
||||
result: firstResult
|
||||
)
|
||||
// anchor 包含 materialId/blockId/pageNumber/chapterId/snippet
|
||||
}
|
||||
|
||||
// 4. 从阅读位置创建锚点
|
||||
let pos = ReadingPosition.pdf(pageNumber: 3, pageProgress: 0.5, overallProgress: 0.1)
|
||||
let anchor = createNoteAnchor(materialId: "mat_001", position: pos)
|
||||
// → NoteAnchor.pdfPage(materialId: "mat_001", pageNumber: 3, positionSnapshot: pos)
|
||||
|
||||
// 5. 从锚点恢复阅读位置
|
||||
if let restored = restorePositionFromAnchor(anchor: anchor) {
|
||||
// navigate to restored position
|
||||
}
|
||||
|
||||
// PDF 搜索
|
||||
let pages = searchPdfPages(
|
||||
pageNumbers: [1, 2, 3],
|
||||
pageTexts: ["page 1 text", "page 2 text", "page 3 text"],
|
||||
query: "keyword"
|
||||
) // → [SearchResult] with page_number
|
||||
|
||||
// EPUB 搜索
|
||||
let chapters = searchEpubChaptersFfi(
|
||||
chapterIds: ["intro", "ch1"],
|
||||
chapterTexts: ["Welcome", "Content with keyword"],
|
||||
query: "keyword"
|
||||
) // → [SearchResult] with chapter_id
|
||||
```
|
||||
|
||||
#### PDF/EPUB 元数据
|
||||
|
||||
```swift
|
||||
// PDF
|
||||
let pdfMeta = try readPdfMetadataFfi(filePath: "/path/to/doc.pdf")
|
||||
print("Pages: \(pdfMeta.pageCount), Title: \(pdfMeta.title ?? "N/A")")
|
||||
|
||||
// EPUB
|
||||
let epubMeta = try readEpubMetadataFfi(filePath: "/path/to/book.epub")
|
||||
print("Chapters: \(epubMeta.chapterCount)")
|
||||
let chapters = try readEpubChaptersFfi(filePath: "/path/to/book.epub")
|
||||
for ch in chapters {
|
||||
print(" \(ch.title) — \(ch.path)")
|
||||
}
|
||||
|
||||
// Office
|
||||
let config = try getOfficePreviewConfigFfi(
|
||||
materialType: .word,
|
||||
fileSize: 1024
|
||||
)
|
||||
print("Strategy: \(config.strategy)") // PlatformPreview
|
||||
```
|
||||
|
||||
## 6. 使用真实 XCFramework 的项目示例
|
||||
|
||||
见 `bindings/ios/demo/` 目录,包含最小 SwiftUI App 示例:
|
||||
|
||||
- `MaterialReaderDemo.swift` — 调用 detect_material_type 并展示结果
|
||||
- `Info.plist` — App 配置
|
||||
|
||||
## 7. 常见问题
|
||||
|
||||
| 问题 | 解决 |
|
||||
|------|------|
|
||||
| `No such module 'zx_documentFFI'` | 确保 XCFramework 已添加到 Link Binary with Libraries |
|
||||
| `library not found for -lzx_document_ffi` | 检查 Library Search Paths |
|
||||
| Swift 类型不匹配 | 重新生成 `zx_document.swift`(UDL 更新后必须重新生成) |
|
||||
| 模拟器 crash | 确保用的是 `ios-arm64-simulator` slice |
|
||||
| 文件路径错误 | 确保路径字符串是 OS 路径而非 Rust 路径 |
|
||||
| 大文件解析慢 | 在后台线程调用 Rust 函数 |
|
||||
|
||||
## 8. 当前验证状态
|
||||
|
||||
- ✅ `cargo build --release --target aarch64-apple-ios` 通过
|
||||
- ✅ `cargo build --release --target aarch64-apple-ios-sim` 通过
|
||||
- ✅ XCFramework 已生成
|
||||
- ✅ Swift bindings 已生成(48KB, 所有类型)
|
||||
- ✅ `cdylib` 和 `staticlib` 两种产物类型
|
||||
|
||||
252
docs/ios-reading-event-v2-integration.md
Normal file
252
docs/ios-reading-event-v2-integration.md
Normal file
@ -0,0 +1,252 @@
|
||||
# iOS ReadingEvent V2 集成示例
|
||||
|
||||
## 概述
|
||||
|
||||
本文档提供 iOS 如何调用 Rust Document Runtime V2 事件链路的示例代码,明确 Rust → iOS → API 的职责边界和调用顺序。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. Rust 不上传 API
|
||||
2. Rust 不知道 readingTargetType
|
||||
3. iOS 转 UploadItem 时补 readingTargetType / platform / appVersion / timezone
|
||||
4. iOS 写入本地上传队列成功后才 ack
|
||||
5. ack 不等 API 上传成功
|
||||
|
||||
## 完整流程
|
||||
|
||||
```
|
||||
App 启动
|
||||
→ reload_stale_events_v2()
|
||||
→ cleanup_stale_sessions_ffi(now, maxAge)
|
||||
|
||||
进入阅读页
|
||||
→ ReadingMaterialContext 构建 ReadingMaterialRefV2
|
||||
→ start_reading_session_v2(materialRef, timestampMs)
|
||||
|
||||
阅读中(每 15s)
|
||||
→ push_heartbeat_v2(sessionId, materialId, delta, position?, timestampMs)
|
||||
|
||||
位置变化
|
||||
→ push_position_changed_v2(sessionId, materialId, position, timestampMs)
|
||||
|
||||
标记已读
|
||||
→ push_marked_as_read_v2(sessionId, materialId, timestampMs)
|
||||
|
||||
退出阅读页
|
||||
→ push_material_closed_v2(sessionId, materialId, residualDelta, timestampMs)
|
||||
→ close_reading_session_v2(sessionId, timestampMs)
|
||||
→ export_pending_events_v2(limit, timestampMs)
|
||||
→ 每条 event 转为 ReadingEventUploadItem → 写入本地队列 → ack_events_v2(eventIds)
|
||||
```
|
||||
|
||||
## Swift 伪代码
|
||||
|
||||
```swift
|
||||
import ZxDocumentRuntime
|
||||
|
||||
// ── App Delegate / SceneDelegate ──
|
||||
|
||||
func applicationDidFinishLaunching() {
|
||||
// 恢复未 ack 的 exported 事件
|
||||
let recovered = reload_stale_events_v2()
|
||||
if recovered > 0 {
|
||||
os_log("Recovered %d stale events", recovered)
|
||||
}
|
||||
// 清理超时的旧 session(如超过 60 分钟无活动)
|
||||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
cleanup_stale_sessions_ffi(now_ms: now, max_age_ms: 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// ── ReadingRuntimeAdapter ──
|
||||
|
||||
class V2ReadingRuntimeAdapter: ReadingRuntimeAdapter {
|
||||
|
||||
func openSession(context: ReadingMaterialContext, timestamp: Date) async throws -> ReadingRuntimeSession {
|
||||
// 构建 Rust 侧 materialRef — 只传 Rust 需要的信息
|
||||
let ts = Int64(timestamp.timeIntervalSince1970 * 1000)
|
||||
let materialRef = ReadingMaterialRefV2(
|
||||
materialId: context.materialId,
|
||||
filePath: context.localFilePath,
|
||||
fileName: context.fileName,
|
||||
fileType: context.fileType
|
||||
)
|
||||
materialRef.mimeType = context.mimeType
|
||||
materialRef.fileSize = context.fileSize.map { UInt64($0) }
|
||||
|
||||
let sessionId = try start_reading_session_v2(
|
||||
material: materialRef,
|
||||
timestampMs: ts
|
||||
)
|
||||
return ReadingRuntimeSession(id: sessionId, materialId: context.materialId)
|
||||
}
|
||||
|
||||
func heartbeat(sessionId: String, timestamp: Date) async throws {
|
||||
let ts = Int64(timestamp.timeIntervalSince1970 * 1000)
|
||||
// delta 由 iOS Timer 计算传入,不是 Rust 自己算
|
||||
let delta = activeTimeTracker.calculateDelta(at: timestamp)
|
||||
_ = try push_heartbeat_v2(
|
||||
sessionId: sessionId,
|
||||
materialId: currentMaterialId,
|
||||
activeSecondsDelta: UInt32(delta),
|
||||
position: currentPositionDTO,
|
||||
timestampMs: ts
|
||||
)
|
||||
}
|
||||
|
||||
func recordPositionChanged(sessionId: String, position: ReadingPositionDTO, timestamp: Date) async throws {
|
||||
let ts = Int64(timestamp.timeIntervalSince1970 * 1000)
|
||||
let rustPos = position.toRustReadingPosition()
|
||||
_ = try push_position_changed_v2(
|
||||
sessionId: sessionId,
|
||||
materialId: currentMaterialId,
|
||||
position: rustPos,
|
||||
timestampMs: ts
|
||||
)
|
||||
}
|
||||
|
||||
func recordMarkedAsRead(sessionId: String, position: ReadingPositionDTO?, timestamp: Date) async throws {
|
||||
let ts = Int64(timestamp.timeIntervalSince1970 * 1000)
|
||||
_ = try push_marked_as_read_v2(
|
||||
sessionId: sessionId,
|
||||
materialId: currentMaterialId,
|
||||
timestampMs: ts
|
||||
)
|
||||
}
|
||||
|
||||
func closeSession(sessionId: String, position: ReadingPositionDTO?, timestamp: Date) async throws {
|
||||
let ts = Int64(timestamp.timeIntervalSince1970 * 1000)
|
||||
// 1. Flush 事件
|
||||
try await flushPendingEvents(timestamp: timestamp)
|
||||
// 2. Close session (sets closed_at_ms automatically)
|
||||
try close_reading_session_v2(sessionId: sessionId, timestampMs: ts)
|
||||
}
|
||||
|
||||
// ── Export + Queue + Ack ──
|
||||
|
||||
func flushPendingEvents(timestamp: Date) async throws {
|
||||
let ts = Int64(timestamp.timeIntervalSince1970 * 1000)
|
||||
let batch = export_pending_events_v2(limit: 100, timestampMs: ts)
|
||||
|
||||
guard !batch.isEmpty else { return }
|
||||
|
||||
// 转为 UploadItem(iOS 补充业务字段)
|
||||
let items = batch.map { event in
|
||||
ReadingEventUploadItem(
|
||||
eventId: event.eventId,
|
||||
clientSessionId: event.clientSessionId,
|
||||
readingTargetType: currentContext.readingTargetType, // ← iOS 补充
|
||||
materialId: event.materialId,
|
||||
knowledgeBaseId: currentContext.knowledgeBaseId, // ← iOS 补充
|
||||
eventType: mapEventType(event.eventType),
|
||||
position: event.position,
|
||||
activeSecondsDelta: Int(event.activeSecondsDelta),
|
||||
clientTimestamp: Date(ms: event.timestampMs),
|
||||
clientTimezoneOffsetMinutes: currentAppContext.timezoneOffsetMinutes,
|
||||
sequence: Int(event.sequence),
|
||||
platform: currentAppContext.platform, // ← iOS 补充
|
||||
appVersion: currentAppContext.appVersion // ← iOS 补充
|
||||
)
|
||||
}
|
||||
|
||||
// 写入本地队列(持久化)
|
||||
let writtenIds = try await localUploadQueue.append(items: items)
|
||||
|
||||
// 只 ack 写入成功的 eventIds
|
||||
if !writtenIds.isEmpty {
|
||||
let acked = ack_events_v2(eventIds: writtenIds)
|
||||
os_log("Acked %d events", acked)
|
||||
}
|
||||
|
||||
// 写入失败的 eventIds → mark failed,下次 retry
|
||||
let allExportedIds = batch.map(\.eventId)
|
||||
let failedIds = allExportedIds.filter { !writtenIds.contains($0) }
|
||||
if !failedIds.isEmpty {
|
||||
let marked = mark_events_failed_v2(eventIds: failedIds)
|
||||
os_log("Marked %d events as failed", marked)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Buffer State ──
|
||||
|
||||
func getBufferStats() -> EventBufferStateV2 {
|
||||
return get_event_buffer_state_v2()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helper ──
|
||||
|
||||
extension Date {
|
||||
init(ms: Int64) {
|
||||
self.init(timeIntervalSince1970: Double(ms) / 1000.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理示例
|
||||
|
||||
```swift
|
||||
func safeCloseSession(sessionId: String?) {
|
||||
guard let sid = sessionId else { return }
|
||||
|
||||
// Best-effort: 即使失败也不阻塞页面关闭
|
||||
do {
|
||||
try await flushPendingEvents(timestamp: Date())
|
||||
} catch {
|
||||
os_log("Flush failed: %@", error.localizedDescription)
|
||||
}
|
||||
|
||||
let ts = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
do {
|
||||
try close_reading_session_v2(sessionId: sid, timestampMs: ts)
|
||||
} catch let error as SessionError {
|
||||
if error == .alreadyClosed {
|
||||
os_log("Session already closed, ignoring")
|
||||
return
|
||||
}
|
||||
os_log("Close failed: %@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 异常恢复示例
|
||||
|
||||
```swift
|
||||
func handleAppReturnFromBackground() {
|
||||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
// 1. 恢复 stale events
|
||||
let recovered = reload_stale_events_v2()
|
||||
if recovered > 0 {
|
||||
os_log("Recovered %d stale events — will re-export", recovered)
|
||||
// 触发 export → queue → ack 流程
|
||||
}
|
||||
|
||||
// 2. 标记超时 session 为 interrupted
|
||||
let removed = cleanup_stale_sessions_ffi(now_ms: now, max_age_ms: 30 * 60 * 1000)
|
||||
if removed > 0 {
|
||||
os_log("Cleaned up %d stale sessions", removed)
|
||||
}
|
||||
|
||||
// 3. 恢复本地队列上传
|
||||
localUploadQueue.resumePendingUploads()
|
||||
}
|
||||
|
||||
func handleAppCrashRecovery() {
|
||||
// App 启动后:
|
||||
// 1. reload_stale_events_v2 把 iOS crash 前 export 但未 ack 的事件重置为 Pending
|
||||
// 2. cleanup_stale_sessions_ffi 清理 crash 前未 close 的 session
|
||||
// 3. localUploadQueue 中的事件不受影响(已持久化,eventId 去重)
|
||||
}
|
||||
```
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [x] 包含 Swift 伪代码
|
||||
- [x] 包含 export → queue → ack 顺序
|
||||
- [x] 包含错误处理示例
|
||||
- [x] 包含 mark failed 示例
|
||||
- [x] 包含异常恢复示例
|
||||
- [x] 明确 Rust 不上传 API
|
||||
- [x] 明确 readingTargetType 由 iOS 补充
|
||||
- [x] 能指导 M-IOS-INFO 接入
|
||||
184
docs/pdf-strategy.md
Normal file
184
docs/pdf-strategy.md
Normal file
@ -0,0 +1,184 @@
|
||||
# PDF 阅读方案评估
|
||||
|
||||
## 概述
|
||||
|
||||
PDF 是知习支持的核心文件格式之一。本文档明确 PDF 在各平台的处理策略、技术选型边界和后续路线图。
|
||||
|
||||
## 核心结论
|
||||
|
||||
1. **iOS 第一版继续使用 PDFKit / QuickLook** — 系统内置,无需额外依赖
|
||||
2. **Rust 暂不接入 PDFium** — 增加包体但不创造足够价值
|
||||
3. **文本选择由平台能力承担** — PDFKit(iOS)、PdfRenderer(Android)
|
||||
4. **PDF 搜索后置** — 待文本提取方案确定后再做
|
||||
5. **扫描 PDF / OCR 暂缓** — 明确不在第一版范围
|
||||
|
||||
---
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 候选方案
|
||||
|
||||
| 方案 | 二进制大小 | 平台支持 | 文本提取 | 搜索 | 标注 | 成熟度 |
|
||||
|------|----------|---------|---------|------|------|--------|
|
||||
| **QuickLook (iOS/macOS)** | 0(系统内置) | Apple only | 否 | 否 | 否 | 高 |
|
||||
| **PDFKit (iOS/macOS)** | 0(系统内置) | Apple only | 是 | 是 | 是 | 高 |
|
||||
| **Android PdfRenderer** | 0(系统内置) | Android only | 部分 | 否 | 否 | 中 |
|
||||
| **pdfium-render (Rust)** | ~15MB/平台 | 全平台 | 是 | 可自建 | 否 | 中 |
|
||||
| **MuPDF (C)** | ~8MB/平台 | 全平台 | 是 | 可自建 | 部分 | 高 |
|
||||
|
||||
### 评估维度
|
||||
|
||||
| 维度 | QuickLook (iOS) | PDFKit (iOS) | pdfium-render (Rust) |
|
||||
|------|----------------|-------------|---------------------|
|
||||
| 集成成本 | 极低(QLPreviewController) | 低(原生 API) | 高(交叉编译、binding) |
|
||||
| 包体影响 | 0 | 0 | ~15MB × 平台数 |
|
||||
| 页面渲染 | ✅ | ✅ | ✅(需要 bitmap pipeline) |
|
||||
| 文本提取 | ❌ | ✅ | ✅ |
|
||||
| 文本选择 | ❌(只能看不选) | ✅ | 需自建 UI |
|
||||
| 搜索 | ❌ | ✅ | 可自建 |
|
||||
| 阅读位置 | App 侧维护 | App 侧 + delegate | Rust 侧统一 |
|
||||
| 统一数据模型 | ❌ | ❌ | ✅ |
|
||||
| 跨平台复用 | ❌ | ❌ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 各平台策略
|
||||
|
||||
### iOS / macOS
|
||||
|
||||
**当前(M3-M4):PDFKit**
|
||||
|
||||
```
|
||||
MaterialReaderView
|
||||
→ PreviewMode.platformPreview
|
||||
→ QuickLook sheet (QLPreviewController)
|
||||
→ App 侧监听页码变化
|
||||
→ 生成 ReadingPosition::Pdf { pageNumber, pageProgress, overallProgress }
|
||||
→ pushReadingEvent
|
||||
```
|
||||
|
||||
优势:
|
||||
- 零依赖,系统自带
|
||||
- 渲染质量高
|
||||
- 支持系统级文本选择、搜索
|
||||
- 用户熟悉的交互
|
||||
|
||||
局限:
|
||||
- QuickLook 不暴露页码变化回调(需要 PDFKit 的 PDFView 才能精确跟踪)
|
||||
- 无法提取文本传给 Rust 做统一搜索
|
||||
- App 侧需用 PDFView delegate 替代 QLPreviewController 以获得页码回调
|
||||
|
||||
**后续增强**:如果需要文本提取/搜索,把 QuickLook sheet 替换为 PDFView + PDFDocument,通过 `PDFDocument.string` 提取全文传给 Rust `search_text`。
|
||||
|
||||
### Android
|
||||
|
||||
**当前策略:系统预览 / 外部 App**
|
||||
|
||||
Android 有 `PdfRenderer`(API 21+),可渲染页面为 Bitmap。但第一版不做内置 PDF 阅读器。
|
||||
|
||||
```
|
||||
MaterialReaderView (Android)
|
||||
→ PreviewMode.platformPreview
|
||||
→ Intent.ACTION_VIEW + content:// URI
|
||||
→ 系统 PDF 阅读器 / Chrome
|
||||
→ 回到 App 后手动记录阅读时长
|
||||
```
|
||||
|
||||
后续可选:`PdfRenderer` + `RecyclerView` 自建阅读器,用 Rust `ReadingPosition::Pdf` 统一位置模型。
|
||||
|
||||
### 鸿蒙 / Windows / Web
|
||||
|
||||
均优先走系统预览或浏览器内置 PDF 阅读器。Rust 只统一 `ReadingPosition::Pdf` 模型。
|
||||
|
||||
---
|
||||
|
||||
## 阅读位置模型
|
||||
|
||||
Rust 已定义统一的 `ReadingPosition::Pdf`:
|
||||
|
||||
```rust
|
||||
ReadingPosition::Pdf {
|
||||
page_number: u32, // 1-based 页码
|
||||
page_progress: f32, // 0.0 ~ 1.0,该页内滚动比例
|
||||
overall_progress: f32, // 0.0 ~ 1.0,全书进度
|
||||
}
|
||||
```
|
||||
|
||||
App 侧职责:
|
||||
- iOS:PDFView.pageChange delegate → 更新位置
|
||||
- Android:监听页面变化 → 更新位置
|
||||
- 所有平台:用同一套 `ReadingPosition::Pdf` 模型,不重复造轮子
|
||||
|
||||
---
|
||||
|
||||
## Rust 侧职责边界
|
||||
|
||||
### 当前(M3-M4)
|
||||
|
||||
- `MaterialType::Pdf` — 文件类型识别 ✅
|
||||
- `ReadingPosition::Pdf` — 统一位置模型 ✅
|
||||
- `PreviewMode::PlatformPreview` — 预览模式映射 ✅
|
||||
- `pdf.rs` — 模块占位(注释说明走平台预览)✅
|
||||
|
||||
### 不做的
|
||||
|
||||
- 不集成 PDFium
|
||||
- 不做 PDF 渲染(bitmap 生成)
|
||||
- 不做 PDF 文本提取
|
||||
- 不做 PDF 标注
|
||||
- 不做 OCR
|
||||
|
||||
### 后续评估(M5+)
|
||||
|
||||
如果以下条件满足 **3 项以上**,重新评估 PDFium 集成:
|
||||
|
||||
1. Android 需要内置 PDF 阅读器
|
||||
2. 搜索需要在 PDF 中定位
|
||||
3. 需要跨平台统一的文本提取
|
||||
4. 用户量大到平台差异成为维护负担
|
||||
5. PDFium 交叉编译经验积累充分
|
||||
|
||||
---
|
||||
|
||||
## 搜索策略
|
||||
|
||||
| 阶段 | 方案 | 能力 |
|
||||
|------|------|------|
|
||||
| M3-M4 | 不搜索 PDF | 用户在平台预览器中手动使用系统搜索 |
|
||||
| M5+ | 平台提取 + Rust 搜索 | PDFKit.string / PdfRenderer → 传给 Rust `search_text` |
|
||||
| 远期 | PDFium 提取 + Tantivy | 全文索引,支持 PDF 内定位 |
|
||||
|
||||
---
|
||||
|
||||
## 扫描 PDF / OCR
|
||||
|
||||
**明确不进入知习范围**。理由:
|
||||
|
||||
- OCR 是独立技术领域,与"阅读内核"定位不符
|
||||
- 高精度 OCR 需要专门模型(Tesseract / Apple Vision / ML Kit)
|
||||
- 绝大多数学习资料是原生电子文档,非扫描件
|
||||
- 扫描件场景可由用户自行 OCR 后导入
|
||||
|
||||
---
|
||||
|
||||
## 后续路线图
|
||||
|
||||
```
|
||||
M3 ✅ — QuickLook sheet + ReadingPosition::Pdf
|
||||
M4 ● — pdf-strategy.md(本文档)
|
||||
M5 ○ — iOS 迁移至 PDFView(获得文本提取和页码回调)
|
||||
— Rust 接收 PDF 全文做 search_text
|
||||
M6+ ○ — 评估 PDFium(Android 自建阅读器需求驱动)
|
||||
— 如集成 PDFium:文本提取 + bitmap 渲染 + 搜索定位
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收确认
|
||||
|
||||
- [x] iOS 第一版继续使用 PDFKit / QuickLook
|
||||
- [x] Rust 暂不接 PDFium
|
||||
- [x] PDF 文本选择由平台能力承担
|
||||
- [x] PDF 搜索后置
|
||||
- [x] 扫描 PDF / OCR 暂缓
|
||||
- [x] 文档存在,方案决策明确,有后续路线图
|
||||
160
docs/reading-event-protocol.md
Normal file
160
docs/reading-event-protocol.md
Normal file
@ -0,0 +1,160 @@
|
||||
# ReadingEvent V2 协议
|
||||
|
||||
## 概述
|
||||
|
||||
V2 阅读事件协议定义了一套完整的学习行为采集框架:**阅读会话 → 事件生成 → buffer 暂存 → export 导出 → ack 确认**。
|
||||
|
||||
与 V1 的区别:
|
||||
- V1:无 session,无 ack,无 id 追踪
|
||||
- V2:session 管理 + 全局事件 buffer + export/ack 确认 + crash recovery
|
||||
|
||||
## 核心概念
|
||||
|
||||
### ReadingSessionV2
|
||||
|
||||
一次阅读会话。iOS App 一次打开资料即创建一个 session。
|
||||
|
||||
```rust
|
||||
pub struct ReadingSessionV2 {
|
||||
pub client_session_id: String, // UUID v4,会话标识
|
||||
pub material: ReadingMaterialRef, // 被阅读的资料
|
||||
pub started_at_ms: i64, // 会话开始时间
|
||||
pub last_event_at_ms: i64, // 最后一次事件时间
|
||||
pub next_sequence: u64, // 下一个事件序号(从 1 开始)
|
||||
pub total_active_seconds: u32, // 累计活跃阅读秒数
|
||||
pub last_position: Option<ReadingPosition>, // 最后位置
|
||||
pub status: ReadingSessionStatus, // Active/Paused/Closed
|
||||
}
|
||||
```
|
||||
|
||||
#### 生命周期
|
||||
|
||||
```
|
||||
start → Active → (pause → Paused → resume → Active)* → close → Closed
|
||||
```
|
||||
|
||||
- **Active**:可推送任意类型事件
|
||||
- **Paused**:仅可推送 delta=0 事件(PositionChanged/MarkedAsRead)
|
||||
- **Closed**:不可推送事件,不可重新打开
|
||||
|
||||
### ReadingMaterialRef
|
||||
|
||||
```rust
|
||||
pub struct ReadingMaterialRef {
|
||||
pub material_id: String,
|
||||
}
|
||||
```
|
||||
|
||||
Rust 不存储 `readingTargetType`(如 knowledge/material/course/note),由 iOS 上传时补充。
|
||||
|
||||
### ReadingEventV2
|
||||
|
||||
```rust
|
||||
pub struct ReadingEventV2 {
|
||||
pub event_id: String, // UUID v4,全局唯一事件 ID
|
||||
pub client_session_id: String, // 关联的会话 ID
|
||||
pub material_id: String, // 资料 ID
|
||||
pub event_type: ReadingEventTypeV2, // 事件类型
|
||||
pub position: Option<ReadingPosition>, // 阅读位置(camelCase JSON)
|
||||
pub active_seconds_delta: u32, // 距上次事件的活跃秒数
|
||||
pub timestamp_ms: i64, // 客户端时间戳
|
||||
pub sequence: u64, // session 内递增序号(1-based)
|
||||
}
|
||||
```
|
||||
|
||||
#### eventType 取值
|
||||
|
||||
| Rust | API JSON |
|
||||
|------|----------|
|
||||
| `MaterialOpened` | `material_opened` |
|
||||
| `MaterialClosed` | `material_closed` |
|
||||
| `PositionChanged` | `position_changed` |
|
||||
| `Heartbeat` | `heartbeat` |
|
||||
| `MarkedAsRead` | `marked_as_read` |
|
||||
|
||||
#### activeSecondsDelta 规则
|
||||
|
||||
| 事件 | delta |
|
||||
|------|-------|
|
||||
| MaterialOpened | 0 |
|
||||
| PositionChanged | 0 |
|
||||
| MarkedAsRead | 0 |
|
||||
| Heartbeat | ActiveTimeTracker.tick() 返回值 |
|
||||
| MaterialClosed | ActiveTimeTracker.close() 返回值 |
|
||||
|
||||
### ActiveTimeTracker
|
||||
|
||||
iOS 控制 tick 节奏,Rust 计算 delta:
|
||||
|
||||
```
|
||||
start(ts) → tick(ts+15s) → tick(ts+30s) → close(ts+43s)
|
||||
delta=15 delta=15 delta=13
|
||||
```
|
||||
|
||||
- 暂停时不累计时间
|
||||
- 时间倒退返回 0
|
||||
- 余数毫秒累积到下次 tick
|
||||
|
||||
### EventBuffer 状态机
|
||||
|
||||
```
|
||||
push
|
||||
│
|
||||
▼
|
||||
┌─ Pending ──┐
|
||||
│ │
|
||||
export() reload_stale()
|
||||
│ │
|
||||
▼ │
|
||||
Exported ───────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
│ │
|
||||
ack() mark_failed()
|
||||
│ │
|
||||
▼ ▼
|
||||
(removed) Failed
|
||||
│
|
||||
export() ──→ 重试
|
||||
```
|
||||
|
||||
- **Pending**:新事件,等待导出
|
||||
- **Exported**:已导出,等待 ack。crash 后 `reload_stale_events()` 恢复为 Pending
|
||||
- **Failed**:上传失败,下次 export 重试
|
||||
- **Ack 后删除**:从 buffer 移除
|
||||
|
||||
### 溢出驱逐
|
||||
|
||||
Buffer 容量:1000 条。满时驱逐顺序:
|
||||
1. Failed(最早失败的)
|
||||
2. Exported(最早导出的)
|
||||
3. 最早 Pending
|
||||
|
||||
## API 上传字段映射
|
||||
|
||||
| Rust ReadingEventV2 | API ReadingEventUploadItem | 来源 |
|
||||
|---------------------|---------------------------|------|
|
||||
| event_id | eventId | Rust UUID |
|
||||
| client_session_id | clientSessionId | Rust UUID |
|
||||
| material_id | materialId | Rust 保存 |
|
||||
| event_type | eventType | Rust→snake_case |
|
||||
| position | position | Rust(camelCase JSON,clamped) |
|
||||
| active_seconds_delta | activeSecondsDelta | ActiveTimeTracker |
|
||||
| timestamp_ms | clientTimestampMs | Rust |
|
||||
| sequence | sequence | session 内递增 |
|
||||
| — | readingTargetType | **iOS 补充** |
|
||||
| — | platform | **iOS 补充** |
|
||||
| — | appVersion | **iOS 补充** |
|
||||
| — | clientTimezoneOffsetMinutes | **iOS 补充** |
|
||||
|
||||
## iOS 上传流程
|
||||
|
||||
```
|
||||
1. Rust export_pending_events_v2(limit, timestamp) → Vec<ReadingEventV2>
|
||||
2. iOS 遍历事件,补充 readingTargetType/platform/appVersion/timezone
|
||||
3. iOS 构造 API 请求体
|
||||
4. POST /reading/events
|
||||
5. 成功 → Rust ack_events_v2(eventIds)
|
||||
6. 失败 → Rust mark_events_failed_v2(eventIds)
|
||||
7. App 启动 → Rust reload_stale_events_v2()
|
||||
```
|
||||
50
docs/reading-event-v2-api-mapping.md
Normal file
50
docs/reading-event-v2-api-mapping.md
Normal file
@ -0,0 +1,50 @@
|
||||
# ReadingEvent V2 → API 上传协议映射
|
||||
|
||||
## 字段映射
|
||||
|
||||
| Rust ReadingEventV2 | API ReadingEventUploadItem | 来源 |
|
||||
|---------------------|---------------------------|------|
|
||||
| event_id | eventId | Rust 生成 UUID |
|
||||
| client_session_id | clientSessionId | Rust 生成 UUID(start_reading_session_v2) |
|
||||
| material_id | materialId | Rust 保存,iOS 传入 |
|
||||
| event_type | eventType | Rust→iOS 转 snake_case |
|
||||
| position | position | Rust(camelCase JSON,clamped) |
|
||||
| active_seconds_delta | activeSecondsDelta | Rust ActiveTimeTracker 计算 |
|
||||
| timestamp_ms | clientTimestampMs | Rust |
|
||||
| sequence | sequence | Rust(session 内递增) |
|
||||
| — | readingTargetType | **iOS 补充** |
|
||||
| — | platform | **iOS 补充**(= "ios") |
|
||||
| — | appVersion | **iOS 补充** |
|
||||
| — | clientTimezoneOffsetMinutes | **iOS 补充** |
|
||||
|
||||
## iOS 上传适配流程
|
||||
|
||||
```
|
||||
1. Rust export_pending_events_v2(limit, ts)
|
||||
2. iOS 获得 Vec<ReadingEventV2>
|
||||
3. iOS 遍历每个 event,补充 readingTargetType/platform/appVersion/timezone
|
||||
4. iOS 写入本地上传队列
|
||||
5. 成功后调用 Rust ack_events_v2(eventIds)
|
||||
6. 失败后调用 Rust mark_events_failed_v2(eventIds)
|
||||
7. App 启动时调用 reload_stale_events_v2() 恢复未 ack 的事件
|
||||
```
|
||||
|
||||
## eventType 映射
|
||||
|
||||
| Rust | API |
|
||||
|------|-----|
|
||||
| MaterialOpened | material_opened |
|
||||
| MaterialClosed | material_closed |
|
||||
| PositionChanged | position_changed |
|
||||
| Heartbeat | heartbeat |
|
||||
| MarkedAsRead | marked_as_read |
|
||||
|
||||
## delta 规则
|
||||
|
||||
| 事件 | delta |
|
||||
|------|-------|
|
||||
| MaterialOpened | 0 |
|
||||
| PositionChanged | 0 |
|
||||
| MarkedAsRead | 0 |
|
||||
| Heartbeat | tracker.tick() 返回值 |
|
||||
| MaterialClosed | tracker.close() 返回值 |
|
||||
@ -28,12 +28,45 @@
|
||||
| Excel | `.xls` `.xlsx` | PlatformPreview | 不解析 | QuickLook / 系统预览 |
|
||||
| PPT | `.ppt` `.pptx` | ExternalOpen | 不解析 | 外部 App 打开 |
|
||||
|
||||
### 后续支持
|
||||
### 第二版(已支持)
|
||||
|
||||
| 格式 | 扩展名 | PreviewMode | Rust 职责 | App 职责 |
|
||||
|------|--------|-------------|----------|---------|
|
||||
| EPUB | `.epub` | NativeReader | 解析 OPF/spine/NCX TOC,读取章节列表和元数据 | WebView 渲染章节 HTML |
|
||||
| PDF | `.pdf` | PlatformPreview | 读取元数据(页数/标题/作者),通过 pdfium feature 支持文本提取和搜索 | 系统预览(iOS PDFKit)+ 搜索接口 |
|
||||
|
||||
#### EPUB 能力
|
||||
|
||||
| 特性 | 状态 |
|
||||
|------|------|
|
||||
| 元数据(title/author) | ✅ read_epub_metadata |
|
||||
| 章节列表(spine + NCX TOC) | ✅ read_epub_chapters |
|
||||
| EPUB2 NCX 解析 | ✅ |
|
||||
| EPUB3 NAV 解析 | ⚠️ 降级为 "Chapter N"(#100) |
|
||||
|
||||
#### PDF 能力
|
||||
|
||||
| 特性 | 状态 |
|
||||
|------|------|
|
||||
| 元数据(page_count/title/author) | ✅ read_pdf_metadata |
|
||||
| 页数检测(/Type /Page + /Count 回退) | ✅ |
|
||||
| 文本提取 | ⚠️ stub,需 pdfium feature |
|
||||
| 搜索 | ✅ search_pdf_text(需预提取文本) |
|
||||
|
||||
#### Office 能力
|
||||
|
||||
| 类型 | PreviewMode | Strategy | 搜索 |
|
||||
|------|-------------|----------|------|
|
||||
| Word (.doc/.docx) | PlatformPreview | QLPreviewController | 否(可 Server 转 PDF 后搜索)|
|
||||
| Excel (.xls/.xlsx) | PlatformPreview | QLPreviewController | 否 |
|
||||
| PowerPoint (.ppt/.pptx) | ExternalOpen | 外部 App 打开 | 否 |
|
||||
|
||||
### 第三版(计划)
|
||||
|
||||
| 格式 | 目标 PreviewMode | 备注 |
|
||||
|------|-----------------|------|
|
||||
| EPUB | NativeReader | Rust 解析 OPF/spine/nav,App WebView 渲染章节 |
|
||||
| PDF | NativeReader(增强) | 评估 PDFium,支持文本提取和搜索 |
|
||||
| PDF 增强 | NativeReader(增强) | pdfium 文本提取 + 页级搜索 + 阅读位置同步 |
|
||||
| EPUB3 NAV | NativeReader | 解析 `<nav epub:type="toc">` 获取真实章节标题 |
|
||||
|
||||
### 明确不做
|
||||
|
||||
|
||||
273
docs/uniffi-udl-spec.md
Normal file
273
docs/uniffi-udl-spec.md
Normal file
@ -0,0 +1,273 @@
|
||||
# UniFFI UDL 编写规范 + 代码生成流程
|
||||
|
||||
## 1. 概述
|
||||
|
||||
UniFFI(Universal Foreign Function Interface)是 Mozilla 开发的跨语言绑定生成工具。本项目使用 UniFFI 0.28 从 Rust 生成 Swift 绑定。
|
||||
|
||||
UDL(UniFFI Definition Language)文件定义跨语言接口。
|
||||
|
||||
---
|
||||
|
||||
## 2. UDL 语法规范
|
||||
|
||||
### 2.1 命名空间
|
||||
|
||||
```webidl
|
||||
namespace zx_document {
|
||||
// 导出函数
|
||||
};
|
||||
```
|
||||
|
||||
命名空间名即 Swift 模块名(`zx_document`)。
|
||||
|
||||
### 2.2 函数声明
|
||||
|
||||
```webidl
|
||||
// 基础函数
|
||||
MaterialType detect_material_type([ByRef] string file_path);
|
||||
|
||||
// 可抛异常
|
||||
[Throws=DocumentError]
|
||||
DocumentInfo build_document_info([ByRef] string file_path, string material_id, string title);
|
||||
|
||||
// 无返回值
|
||||
void push_reading_event(ReadingEvent event);
|
||||
```
|
||||
|
||||
#### 参数修饰符
|
||||
|
||||
| 修饰符 | 适用类型 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `[ByRef]` | `string` | 传递引用而非所有权转移(性能优化) |
|
||||
| 无修饰符 | 所有类型 | 所有权转移给被调用方 |
|
||||
|
||||
### 2.3 数据类型
|
||||
|
||||
#### Dictionary(Record → Swift struct)
|
||||
|
||||
```webidl
|
||||
dictionary DocumentInfo {
|
||||
string material_id;
|
||||
string title;
|
||||
MaterialType material_type;
|
||||
u32? page_count; // Optional
|
||||
string? image_format; // Optional
|
||||
};
|
||||
|
||||
// Rust 侧: #[derive(uniffi::Record)] pub struct DocumentInfo { ... }
|
||||
// Swift 侧: public struct DocumentInfo { public var materialId: String, ... }
|
||||
```
|
||||
|
||||
**命名规则**:
|
||||
- UDL: `snake_case` → Swift: `camelCase`
|
||||
- 示例:`material_id` → `materialId`
|
||||
|
||||
#### Enum(Rust enum → Swift enum)
|
||||
|
||||
```webidl
|
||||
[Enum]
|
||||
interface MaterialType {
|
||||
Markdown();
|
||||
Text();
|
||||
Pdf();
|
||||
Image();
|
||||
Epub();
|
||||
Word();
|
||||
Excel();
|
||||
PowerPoint();
|
||||
Unknown();
|
||||
};
|
||||
```
|
||||
|
||||
**带数据的枚举**:
|
||||
```webidl
|
||||
[Enum]
|
||||
interface ReadingPosition {
|
||||
Markdown(string block_id, f32 scroll_progress);
|
||||
Text(u32 line_number, f32 scroll_progress);
|
||||
Pdf(u32 page_number, f32 page_progress, f32 overall_progress);
|
||||
Unknown(); // 无数据变体
|
||||
};
|
||||
```
|
||||
|
||||
#### Error(Rust enum → Swift Error)
|
||||
|
||||
```webidl
|
||||
[Error]
|
||||
enum DocumentError {
|
||||
"FileNotFound",
|
||||
"UnsupportedFormat",
|
||||
"ParseError",
|
||||
"InvalidEncoding",
|
||||
"IoError",
|
||||
};
|
||||
```
|
||||
|
||||
**对应 Rust**:
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum DocumentError {
|
||||
#[error("File not found")] FileNotFound,
|
||||
#[error("Unsupported format")] UnsupportedFormat,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 类型映射表
|
||||
|
||||
| UDL | Rust | Swift |
|
||||
|-----|------|-------|
|
||||
| `string` | `String` | `String` |
|
||||
| `u32` | `u32` | `UInt32` |
|
||||
| `u64` | `u64` | `UInt64` |
|
||||
| `i64` | `i64` | `Int64` |
|
||||
| `f32` | `f32` | `Float` |
|
||||
| `boolean` | `bool` | `Bool` |
|
||||
| `T?` | `Option<T>` | `T?` |
|
||||
| `sequence<T>` | `Vec<T>` | `[T]` |
|
||||
| `dictionary` | `#[derive(Record)] struct` | `struct` |
|
||||
| `[Enum] interface` | `#[derive(Enum)] enum` | `enum` |
|
||||
| `[Error] enum` | `#[derive(Error)] enum` | `Error` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 新增类型 Checklist
|
||||
|
||||
### 3.1 Rust 侧
|
||||
|
||||
1. 在 `zx_document_core/src/` 定义类型
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct NewType { ... }
|
||||
```
|
||||
2. 在 `zx_document_core/src/lib.rs` 添加 `pub mod new_module;`
|
||||
3. 在 `zx_document_ffi/src/lib.rs` 添加 `pub use zx_document_core::new_module::NewType;`
|
||||
|
||||
### 3.2 UDL 侧
|
||||
|
||||
1. 在 `zx_document_ffi/src/zx_document.udl` 末尾添加类型定义
|
||||
2. 如新增函数,在 `namespace zx_document { }` 内声明
|
||||
|
||||
### 3.3 代码生成
|
||||
|
||||
```bash
|
||||
# 从 workspace root 执行
|
||||
cd zhixi-document-runtime
|
||||
|
||||
# 生成 Swift 绑定
|
||||
uniffi-bindgen generate \
|
||||
crates/zx_document_ffi/src/zx_document.udl \
|
||||
--language swift \
|
||||
--out-dir ../ios-projects/AIStudyApp/AIStudyApp/Core/
|
||||
|
||||
# 检查生成结果
|
||||
ls ../ios-projects/AIStudyApp/AIStudyApp/Core/zx_document.swift
|
||||
```
|
||||
|
||||
### 3.4 验证
|
||||
|
||||
```bash
|
||||
# Rust 编译
|
||||
cargo build --release -p zx_document_ffi
|
||||
|
||||
# Rust 测试
|
||||
cargo test -p zx_document_core
|
||||
|
||||
# iOS 编译(在 ios-projects 目录)
|
||||
xcodebuild build -project AIStudyApp.xcodeproj -scheme AIStudyApp -destination 'platform=iOS Simulator,name=iPhone 16'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见陷阱
|
||||
|
||||
### 4.1 类型未导出
|
||||
|
||||
**症状**:`error: cannot find type X in this scope`
|
||||
|
||||
**原因**:Rust 类型未从 `zx_document_ffi` 重新导出
|
||||
|
||||
```rust
|
||||
// zx_document_ffi/src/lib.rs — 必须添加
|
||||
pub use zx_document_core::new_module::NewType;
|
||||
```
|
||||
|
||||
### 4.2 UDL 字段名不一致
|
||||
|
||||
**症状**:uniffi-bindgen 报 field name mismatch
|
||||
|
||||
**原因**:UDL 字段名必须与 Rust struct 字段名一致(Rust 用 snake_case → UDL 也用 snake_case)
|
||||
|
||||
```webidl
|
||||
dictionary Foo {
|
||||
string bar_baz; // ✅ 对应 Rust foo.bar_baz
|
||||
string barBaz; // ❌ 不匹配
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 UniFFI 版本兼容
|
||||
|
||||
| UniFFI 版本 | Rust crate | CLI |
|
||||
|:--:|------|-----|
|
||||
| 0.28 | `uniffi = "0.28"` | `uniffi-bindgen 0.28` |
|
||||
|
||||
版本必须一致。检查:
|
||||
```bash
|
||||
cargo tree -p zx_document_ffi | grep uniffi
|
||||
uniffi-bindgen --version
|
||||
```
|
||||
|
||||
### 4.4 Optional 类型
|
||||
|
||||
```webidl
|
||||
// UDL: T? = Rust: Option<T>
|
||||
u32? page_count; // → Swift: public var pageCount: UInt32?
|
||||
sequence<string>? items; // → Swift: public var items: [String]?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 生成代码结构
|
||||
|
||||
```swift
|
||||
// zx_document.swift (auto-generated by uniffi-bindgen)
|
||||
|
||||
// 1. Type definitions
|
||||
public struct DocumentInfo { ... }
|
||||
public enum MaterialType { ... }
|
||||
|
||||
// 2. Protocol + FFI callback
|
||||
private protocol zx_documentProtocol { ... }
|
||||
|
||||
// 3. Top-level functions
|
||||
public func detectMaterialType(filePath: String) throws -> MaterialType { ... }
|
||||
public func buildDocumentInfo(filePath: String, materialId: String, title: String) throws -> DocumentInfo { ... }
|
||||
|
||||
// 4. Internal FFI bridge
|
||||
private var ffi_zx_document_ffi_*: ...;
|
||||
```
|
||||
|
||||
**不要手动修改** `zx_document.swift`。所有修改在 Rust 侧完成后重新生成。
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前接口统计
|
||||
|
||||
| 类别 | 数量 | 示例 |
|
||||
|------|:--:|------|
|
||||
| 顶层函数 | 28 | `detect_material_type`, `parse_markdown` |
|
||||
| Record(struct) | 10 | `DocumentInfo`, `SearchResult` |
|
||||
| Enum | 7 | `MaterialType`, `ReadingPosition` |
|
||||
| Error 变体 | 5 | `FileNotFound`, `ParseError` |
|
||||
| V2 Session 函数 | 6 | `start_reading_session_v2` |
|
||||
| V2 Event 函数 | 5 | `push_heartbeat_v2` |
|
||||
| V2 Buffer 函数 | 4 | `export_pending_events_v2` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关文档
|
||||
|
||||
- [iOS FFI 调用指南](./ios-ffi-integration-guide.md)
|
||||
- [文件格式支持矩阵](./file-format-support-matrix.md)
|
||||
- [Rust FFI 绑定更新流程](../ios-projects/docs/rust-ffi-update.md)(ios-projects 仓库)
|
||||
- [UniFFI 官方文档](https://mozilla.github.io/uniffi-rs/)
|
||||
83
docs/v1-v2-compatibility.md
Normal file
83
docs/v1-v2-compatibility.md
Normal file
@ -0,0 +1,83 @@
|
||||
# V1 → V2 兼容层
|
||||
|
||||
## 策略
|
||||
|
||||
V1 不删除,V2 新增独立模块。iOS 可逐步迁移。
|
||||
|
||||
## V1 模块(保留 deprecated)
|
||||
|
||||
| 文件 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| `events.rs` | `#[deprecated]` | 旧 ReadingEvent 类型 + push/export/clear |
|
||||
| `reading_material.rs` | `#[deprecated]` | 旧 ReadingMaterialRef(仅 material_id) |
|
||||
|
||||
所有 V1 `#[uniffi::export]` 方法保留,确保现有 iOS V1 接入不中断。
|
||||
|
||||
## V2 模块(新增)
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `reading_material.rs` | ReadingMaterialRefV2(camelCase, 8 fields) |
|
||||
| `session_v2.rs` | ReadingSessionV2 + 5 statuses + lifecycle methods |
|
||||
| `events_v2.rs` | ReadingEventV2 + EventBuffer (ack/failed/reload/state) |
|
||||
| `time_tracker.rs` | ActiveTimeTracker(iOS 控制 tick) |
|
||||
| `progress.rs` | ReadingPosition(camelCase + clamp) |
|
||||
|
||||
## V1 vs V2 差异
|
||||
|
||||
| 维度 | V1 | V2 |
|
||||
|------|----|----|
|
||||
| material ref | `ReadingMaterialRef { material_id }` | `ReadingMaterialRefV2 { 8 fields }` |
|
||||
| session | 无独立模型 | `ReadingSessionV2` + lifecycle |
|
||||
| event_id | 无 | UUID v4, 36 char |
|
||||
| session_id | 无 | UUID v4 |
|
||||
| sequence | 无 | u64 递增 |
|
||||
| active_seconds | 累计/语义模糊 | `activeSecondsDelta` 增量 |
|
||||
| position | snake_case | camelCase + clamp 0~1 |
|
||||
| buffer 管理 | 无 ack/failed | export→ack/failed/reload 完整生命周期 |
|
||||
| time tracker | 无 | ActiveTimeTracker(iOS tick / pause / resume) |
|
||||
| session recovery | 无 | reload_stale_events_v2 + interrupted/failed |
|
||||
|
||||
## iOS 迁移路径
|
||||
|
||||
```
|
||||
Phase 1 (当前):
|
||||
iOS 继续使用 V1 接入(V1 方法仍可用)
|
||||
|
||||
Phase 2:
|
||||
iOS 实现 V2ReadingRuntimeAdapter(使用 V2 方法)
|
||||
同时保留 V1ReadingRuntimeAdapter 作为 fallback
|
||||
|
||||
Phase 3:
|
||||
全量切换到 V2
|
||||
V1 接口留在代码中保持编译兼容
|
||||
|
||||
Phase 4 (未来):
|
||||
确认所有端都切换到 V2 后,可移除 V1
|
||||
```
|
||||
|
||||
## active_seconds vs activeSecondsDelta
|
||||
|
||||
| V1 | V2 |
|
||||
|----|----|
|
||||
| `active_seconds` 语义模糊,可能是累计值 | `active_seconds_delta` 每次 heartbeat 增量 |
|
||||
| iOS 侧自己算 | ActiveTimeTracker 计算,基于 timestamp 差值 |
|
||||
| 无 clamp / 校验 | maxDeltaSeconds=60,超出 clamp + warning |
|
||||
|
||||
## FFI 接口对比
|
||||
|
||||
| V1 (deprecated) | V2 |
|
||||
|-----------------|-----|
|
||||
| `push_reading_event(ReadingEvent)` | `push_material_opened_v2 / push_heartbeat_v2 / ...` |
|
||||
| `export_pending_events() → Vec<ReadingEvent>` | `export_pending_events_v2(limit, ts) → Vec<ReadingEventV2>` |
|
||||
| `clear_exported_events(count)` | `ack_events_v2(eventIds)` + `mark_events_failed_v2(eventIds)` |
|
||||
| — | `reload_stale_events_v2()` |
|
||||
| — | `get_event_buffer_state_v2()` |
|
||||
|
||||
## 验收
|
||||
|
||||
- [x] V1 仍可编译(所有 V1 tests pass)
|
||||
- [x] V2 新接口可用(169 tests pass)
|
||||
- [x] V1 deprecated 信息清楚(`#[deprecated]` annotations)
|
||||
- [x] active_seconds 与 activeSecondsDelta 差异说明
|
||||
- [x] iOS 迁移路径说明
|
||||
BIN
fixtures/epub/epub_with_toc.epub
Normal file
BIN
fixtures/epub/epub_with_toc.epub
Normal file
Binary file not shown.
BIN
fixtures/epub/simple.epub
Normal file
BIN
fixtures/epub/simple.epub
Normal file
Binary file not shown.
BIN
fixtures/invalid_file.bin
Normal file
BIN
fixtures/invalid_file.bin
Normal file
Binary file not shown.
45
fixtures/markdown/complex_markdown.md
Normal file
45
fixtures/markdown/complex_markdown.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Document Title (h1)
|
||||
|
||||
Introduction paragraph with **bold** and *italic* text. This is the first paragraph of the document.
|
||||
|
||||
## Section One (h2)
|
||||
|
||||
This section contains a code block with language info.
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
### Subsection (h3)
|
||||
|
||||
- Unordered item 1
|
||||
- Unordered item 2
|
||||
- Unordered item 3
|
||||
|
||||
1. Ordered item 1
|
||||
2. Ordered item 2
|
||||
3. Ordered item 3
|
||||
|
||||
## Section Two — Tables & Quotes
|
||||
|
||||
| Name | Age | City |
|
||||
|------|-----|------|
|
||||
| Alice | 30 | NYC |
|
||||
| Bob | 25 | LA |
|
||||
| Carol | 28 | SF |
|
||||
|
||||
> This is a blockquote with some wisdom: "The only way to do great work is to love what you do."
|
||||
|
||||
## Section Three — Images & Rules
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Final Section
|
||||
|
||||
Final paragraph with `inline code` and a [link](https://example.com).
|
||||
|
||||
---
|
||||
1054
fixtures/markdown/large_markdown.md
Normal file
1054
fixtures/markdown/large_markdown.md
Normal file
File diff suppressed because it is too large
Load Diff
29
fixtures/markdown/sample.md
Normal file
29
fixtures/markdown/sample.md
Normal file
@ -0,0 +1,29 @@
|
||||
# zhixi-document-runtime
|
||||
|
||||
A cross-platform document reading kernel.
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. 原文件预览和 AI 解析彻底分离。
|
||||
2. Rust Core 不负责网络请求。
|
||||
3. Rust Core 不直接访问后端 API。
|
||||
|
||||
## 支持的格式
|
||||
|
||||
| 格式 | 预览方式 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| Markdown | 内置阅读 | 已支持 |
|
||||
| TXT | 内置阅读 | 已支持 |
|
||||
| 图片 | 内置查看 | 已支持 |
|
||||
|
||||
> 第一版目标不是"支持所有文件预览",而是先建立跨平台文档内核。
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, zhixi!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*EOF*
|
||||
22
fixtures/pdf/scanned_pdf.pdf
Normal file
22
fixtures/pdf/scanned_pdf.pdf
Normal file
@ -0,0 +1,22 @@
|
||||
%PDF-1.4
|
||||
%€€€€
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||
endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000064 00000 n
|
||||
0000000121 00000 n
|
||||
trailer
|
||||
<< /Size 4 /Root 1 0 R >>
|
||||
startxref
|
||||
192
|
||||
%%EOF
|
||||
26
fixtures/pdf/text_pdf.pdf
Normal file
26
fixtures/pdf/text_pdf.pdf
Normal file
@ -0,0 +1,26 @@
|
||||
%PDF-1.4
|
||||
%€€€€
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000064 00000 n
|
||||
0000000127 00000 n
|
||||
0000000198 00000 n
|
||||
trailer
|
||||
<< /Size 5 /Root 1 0 R >>
|
||||
startxref
|
||||
269
|
||||
%%EOF
|
||||
500
fixtures/text/large_text.txt
Normal file
500
fixtures/text/large_text.txt
Normal file
@ -0,0 +1,500 @@
|
||||
Line 1: This is a long text line number 1 with searchable content spread across the file.
|
||||
Line 2: This is a long text line number 2 with searchable content spread across the file.
|
||||
Line 3: This is a long text line number 3 with searchable content spread across the file.
|
||||
Line 4: This is a long text line number 4 with searchable content spread across the file.
|
||||
Line 5: This is a long text line number 5 with searchable content spread across the file.
|
||||
Line 6: This is a long text line number 6 with searchable content spread across the file.
|
||||
Line 7: This is a long text line number 7 with searchable content spread across the file.
|
||||
Line 8: This is a long text line number 8 with searchable content spread across the file.
|
||||
Line 9: This is a long text line number 9 with searchable content spread across the file.
|
||||
Line 10: This is a long text line number 10 with searchable content spread across the file.
|
||||
Line 11: This is a long text line number 11 with searchable content spread across the file.
|
||||
Line 12: This is a long text line number 12 with searchable content spread across the file.
|
||||
Line 13: This is a long text line number 13 with searchable content spread across the file.
|
||||
Line 14: This is a long text line number 14 with searchable content spread across the file.
|
||||
Line 15: This is a long text line number 15 with searchable content spread across the file.
|
||||
Line 16: This is a long text line number 16 with searchable content spread across the file.
|
||||
Line 17: This is a long text line number 17 with searchable content spread across the file.
|
||||
Line 18: This is a long text line number 18 with searchable content spread across the file.
|
||||
Line 19: This is a long text line number 19 with searchable content spread across the file.
|
||||
Line 20: This is a long text line number 20 with searchable content spread across the file.
|
||||
Line 21: This is a long text line number 21 with searchable content spread across the file.
|
||||
Line 22: This is a long text line number 22 with searchable content spread across the file.
|
||||
Line 23: This is a long text line number 23 with searchable content spread across the file.
|
||||
Line 24: This is a long text line number 24 with searchable content spread across the file.
|
||||
Line 25: This is a long text line number 25 with searchable content spread across the file.
|
||||
Line 26: This is a long text line number 26 with searchable content spread across the file.
|
||||
Line 27: This is a long text line number 27 with searchable content spread across the file.
|
||||
Line 28: This is a long text line number 28 with searchable content spread across the file.
|
||||
Line 29: This is a long text line number 29 with searchable content spread across the file.
|
||||
Line 30: This is a long text line number 30 with searchable content spread across the file.
|
||||
Line 31: This is a long text line number 31 with searchable content spread across the file.
|
||||
Line 32: This is a long text line number 32 with searchable content spread across the file.
|
||||
Line 33: This is a long text line number 33 with searchable content spread across the file.
|
||||
Line 34: This is a long text line number 34 with searchable content spread across the file.
|
||||
Line 35: This is a long text line number 35 with searchable content spread across the file.
|
||||
Line 36: This is a long text line number 36 with searchable content spread across the file.
|
||||
Line 37: This is a long text line number 37 with searchable content spread across the file.
|
||||
Line 38: This is a long text line number 38 with searchable content spread across the file.
|
||||
Line 39: This is a long text line number 39 with searchable content spread across the file.
|
||||
Line 40: This is a long text line number 40 with searchable content spread across the file.
|
||||
Line 41: This is a long text line number 41 with searchable content spread across the file.
|
||||
Line 42: This is a long text line number 42 with searchable content spread across the file.
|
||||
Line 43: This is a long text line number 43 with searchable content spread across the file.
|
||||
Line 44: This is a long text line number 44 with searchable content spread across the file.
|
||||
Line 45: This is a long text line number 45 with searchable content spread across the file.
|
||||
Line 46: This is a long text line number 46 with searchable content spread across the file.
|
||||
Line 47: This is a long text line number 47 with searchable content spread across the file.
|
||||
Line 48: This is a long text line number 48 with searchable content spread across the file.
|
||||
Line 49: This is a long text line number 49 with searchable content spread across the file.
|
||||
Line 50: This is a long text line number 50 with searchable content spread across the file.
|
||||
Line 51: This is a long text line number 51 with searchable content spread across the file.
|
||||
Line 52: This is a long text line number 52 with searchable content spread across the file.
|
||||
Line 53: This is a long text line number 53 with searchable content spread across the file.
|
||||
Line 54: This is a long text line number 54 with searchable content spread across the file.
|
||||
Line 55: This is a long text line number 55 with searchable content spread across the file.
|
||||
Line 56: This is a long text line number 56 with searchable content spread across the file.
|
||||
Line 57: This is a long text line number 57 with searchable content spread across the file.
|
||||
Line 58: This is a long text line number 58 with searchable content spread across the file.
|
||||
Line 59: This is a long text line number 59 with searchable content spread across the file.
|
||||
Line 60: This is a long text line number 60 with searchable content spread across the file.
|
||||
Line 61: This is a long text line number 61 with searchable content spread across the file.
|
||||
Line 62: This is a long text line number 62 with searchable content spread across the file.
|
||||
Line 63: This is a long text line number 63 with searchable content spread across the file.
|
||||
Line 64: This is a long text line number 64 with searchable content spread across the file.
|
||||
Line 65: This is a long text line number 65 with searchable content spread across the file.
|
||||
Line 66: This is a long text line number 66 with searchable content spread across the file.
|
||||
Line 67: This is a long text line number 67 with searchable content spread across the file.
|
||||
Line 68: This is a long text line number 68 with searchable content spread across the file.
|
||||
Line 69: This is a long text line number 69 with searchable content spread across the file.
|
||||
Line 70: This is a long text line number 70 with searchable content spread across the file.
|
||||
Line 71: This is a long text line number 71 with searchable content spread across the file.
|
||||
Line 72: This is a long text line number 72 with searchable content spread across the file.
|
||||
Line 73: This is a long text line number 73 with searchable content spread across the file.
|
||||
Line 74: This is a long text line number 74 with searchable content spread across the file.
|
||||
Line 75: This is a long text line number 75 with searchable content spread across the file.
|
||||
Line 76: This is a long text line number 76 with searchable content spread across the file.
|
||||
Line 77: This is a long text line number 77 with searchable content spread across the file.
|
||||
Line 78: This is a long text line number 78 with searchable content spread across the file.
|
||||
Line 79: This is a long text line number 79 with searchable content spread across the file.
|
||||
Line 80: This is a long text line number 80 with searchable content spread across the file.
|
||||
Line 81: This is a long text line number 81 with searchable content spread across the file.
|
||||
Line 82: This is a long text line number 82 with searchable content spread across the file.
|
||||
Line 83: This is a long text line number 83 with searchable content spread across the file.
|
||||
Line 84: This is a long text line number 84 with searchable content spread across the file.
|
||||
Line 85: This is a long text line number 85 with searchable content spread across the file.
|
||||
Line 86: This is a long text line number 86 with searchable content spread across the file.
|
||||
Line 87: This is a long text line number 87 with searchable content spread across the file.
|
||||
Line 88: This is a long text line number 88 with searchable content spread across the file.
|
||||
Line 89: This is a long text line number 89 with searchable content spread across the file.
|
||||
Line 90: This is a long text line number 90 with searchable content spread across the file.
|
||||
Line 91: This is a long text line number 91 with searchable content spread across the file.
|
||||
Line 92: This is a long text line number 92 with searchable content spread across the file.
|
||||
Line 93: This is a long text line number 93 with searchable content spread across the file.
|
||||
Line 94: This is a long text line number 94 with searchable content spread across the file.
|
||||
Line 95: This is a long text line number 95 with searchable content spread across the file.
|
||||
Line 96: This is a long text line number 96 with searchable content spread across the file.
|
||||
Line 97: This is a long text line number 97 with searchable content spread across the file.
|
||||
Line 98: This is a long text line number 98 with searchable content spread across the file.
|
||||
Line 99: This is a long text line number 99 with searchable content spread across the file.
|
||||
Line 100: This is a long text line number 100 with searchable content spread across the file.
|
||||
Line 101: This is a long text line number 101 with searchable content spread across the file.
|
||||
Line 102: This is a long text line number 102 with searchable content spread across the file.
|
||||
Line 103: This is a long text line number 103 with searchable content spread across the file.
|
||||
Line 104: This is a long text line number 104 with searchable content spread across the file.
|
||||
Line 105: This is a long text line number 105 with searchable content spread across the file.
|
||||
Line 106: This is a long text line number 106 with searchable content spread across the file.
|
||||
Line 107: This is a long text line number 107 with searchable content spread across the file.
|
||||
Line 108: This is a long text line number 108 with searchable content spread across the file.
|
||||
Line 109: This is a long text line number 109 with searchable content spread across the file.
|
||||
Line 110: This is a long text line number 110 with searchable content spread across the file.
|
||||
Line 111: This is a long text line number 111 with searchable content spread across the file.
|
||||
Line 112: This is a long text line number 112 with searchable content spread across the file.
|
||||
Line 113: This is a long text line number 113 with searchable content spread across the file.
|
||||
Line 114: This is a long text line number 114 with searchable content spread across the file.
|
||||
Line 115: This is a long text line number 115 with searchable content spread across the file.
|
||||
Line 116: This is a long text line number 116 with searchable content spread across the file.
|
||||
Line 117: This is a long text line number 117 with searchable content spread across the file.
|
||||
Line 118: This is a long text line number 118 with searchable content spread across the file.
|
||||
Line 119: This is a long text line number 119 with searchable content spread across the file.
|
||||
Line 120: This is a long text line number 120 with searchable content spread across the file.
|
||||
Line 121: This is a long text line number 121 with searchable content spread across the file.
|
||||
Line 122: This is a long text line number 122 with searchable content spread across the file.
|
||||
Line 123: This is a long text line number 123 with searchable content spread across the file.
|
||||
Line 124: This is a long text line number 124 with searchable content spread across the file.
|
||||
Line 125: This is a long text line number 125 with searchable content spread across the file.
|
||||
Line 126: This is a long text line number 126 with searchable content spread across the file.
|
||||
Line 127: This is a long text line number 127 with searchable content spread across the file.
|
||||
Line 128: This is a long text line number 128 with searchable content spread across the file.
|
||||
Line 129: This is a long text line number 129 with searchable content spread across the file.
|
||||
Line 130: This is a long text line number 130 with searchable content spread across the file.
|
||||
Line 131: This is a long text line number 131 with searchable content spread across the file.
|
||||
Line 132: This is a long text line number 132 with searchable content spread across the file.
|
||||
Line 133: This is a long text line number 133 with searchable content spread across the file.
|
||||
Line 134: This is a long text line number 134 with searchable content spread across the file.
|
||||
Line 135: This is a long text line number 135 with searchable content spread across the file.
|
||||
Line 136: This is a long text line number 136 with searchable content spread across the file.
|
||||
Line 137: This is a long text line number 137 with searchable content spread across the file.
|
||||
Line 138: This is a long text line number 138 with searchable content spread across the file.
|
||||
Line 139: This is a long text line number 139 with searchable content spread across the file.
|
||||
Line 140: This is a long text line number 140 with searchable content spread across the file.
|
||||
Line 141: This is a long text line number 141 with searchable content spread across the file.
|
||||
Line 142: This is a long text line number 142 with searchable content spread across the file.
|
||||
Line 143: This is a long text line number 143 with searchable content spread across the file.
|
||||
Line 144: This is a long text line number 144 with searchable content spread across the file.
|
||||
Line 145: This is a long text line number 145 with searchable content spread across the file.
|
||||
Line 146: This is a long text line number 146 with searchable content spread across the file.
|
||||
Line 147: This is a long text line number 147 with searchable content spread across the file.
|
||||
Line 148: This is a long text line number 148 with searchable content spread across the file.
|
||||
Line 149: This is a long text line number 149 with searchable content spread across the file.
|
||||
Line 150: This is a long text line number 150 with searchable content spread across the file.
|
||||
Line 151: This is a long text line number 151 with searchable content spread across the file.
|
||||
Line 152: This is a long text line number 152 with searchable content spread across the file.
|
||||
Line 153: This is a long text line number 153 with searchable content spread across the file.
|
||||
Line 154: This is a long text line number 154 with searchable content spread across the file.
|
||||
Line 155: This is a long text line number 155 with searchable content spread across the file.
|
||||
Line 156: This is a long text line number 156 with searchable content spread across the file.
|
||||
Line 157: This is a long text line number 157 with searchable content spread across the file.
|
||||
Line 158: This is a long text line number 158 with searchable content spread across the file.
|
||||
Line 159: This is a long text line number 159 with searchable content spread across the file.
|
||||
Line 160: This is a long text line number 160 with searchable content spread across the file.
|
||||
Line 161: This is a long text line number 161 with searchable content spread across the file.
|
||||
Line 162: This is a long text line number 162 with searchable content spread across the file.
|
||||
Line 163: This is a long text line number 163 with searchable content spread across the file.
|
||||
Line 164: This is a long text line number 164 with searchable content spread across the file.
|
||||
Line 165: This is a long text line number 165 with searchable content spread across the file.
|
||||
Line 166: This is a long text line number 166 with searchable content spread across the file.
|
||||
Line 167: This is a long text line number 167 with searchable content spread across the file.
|
||||
Line 168: This is a long text line number 168 with searchable content spread across the file.
|
||||
Line 169: This is a long text line number 169 with searchable content spread across the file.
|
||||
Line 170: This is a long text line number 170 with searchable content spread across the file.
|
||||
Line 171: This is a long text line number 171 with searchable content spread across the file.
|
||||
Line 172: This is a long text line number 172 with searchable content spread across the file.
|
||||
Line 173: This is a long text line number 173 with searchable content spread across the file.
|
||||
Line 174: This is a long text line number 174 with searchable content spread across the file.
|
||||
Line 175: This is a long text line number 175 with searchable content spread across the file.
|
||||
Line 176: This is a long text line number 176 with searchable content spread across the file.
|
||||
Line 177: This is a long text line number 177 with searchable content spread across the file.
|
||||
Line 178: This is a long text line number 178 with searchable content spread across the file.
|
||||
Line 179: This is a long text line number 179 with searchable content spread across the file.
|
||||
Line 180: This is a long text line number 180 with searchable content spread across the file.
|
||||
Line 181: This is a long text line number 181 with searchable content spread across the file.
|
||||
Line 182: This is a long text line number 182 with searchable content spread across the file.
|
||||
Line 183: This is a long text line number 183 with searchable content spread across the file.
|
||||
Line 184: This is a long text line number 184 with searchable content spread across the file.
|
||||
Line 185: This is a long text line number 185 with searchable content spread across the file.
|
||||
Line 186: This is a long text line number 186 with searchable content spread across the file.
|
||||
Line 187: This is a long text line number 187 with searchable content spread across the file.
|
||||
Line 188: This is a long text line number 188 with searchable content spread across the file.
|
||||
Line 189: This is a long text line number 189 with searchable content spread across the file.
|
||||
Line 190: This is a long text line number 190 with searchable content spread across the file.
|
||||
Line 191: This is a long text line number 191 with searchable content spread across the file.
|
||||
Line 192: This is a long text line number 192 with searchable content spread across the file.
|
||||
Line 193: This is a long text line number 193 with searchable content spread across the file.
|
||||
Line 194: This is a long text line number 194 with searchable content spread across the file.
|
||||
Line 195: This is a long text line number 195 with searchable content spread across the file.
|
||||
Line 196: This is a long text line number 196 with searchable content spread across the file.
|
||||
Line 197: This is a long text line number 197 with searchable content spread across the file.
|
||||
Line 198: This is a long text line number 198 with searchable content spread across the file.
|
||||
Line 199: This is a long text line number 199 with searchable content spread across the file.
|
||||
Line 200: This is a long text line number 200 with searchable content spread across the file.
|
||||
Line 201: This is a long text line number 201 with searchable content spread across the file.
|
||||
Line 202: This is a long text line number 202 with searchable content spread across the file.
|
||||
Line 203: This is a long text line number 203 with searchable content spread across the file.
|
||||
Line 204: This is a long text line number 204 with searchable content spread across the file.
|
||||
Line 205: This is a long text line number 205 with searchable content spread across the file.
|
||||
Line 206: This is a long text line number 206 with searchable content spread across the file.
|
||||
Line 207: This is a long text line number 207 with searchable content spread across the file.
|
||||
Line 208: This is a long text line number 208 with searchable content spread across the file.
|
||||
Line 209: This is a long text line number 209 with searchable content spread across the file.
|
||||
Line 210: This is a long text line number 210 with searchable content spread across the file.
|
||||
Line 211: This is a long text line number 211 with searchable content spread across the file.
|
||||
Line 212: This is a long text line number 212 with searchable content spread across the file.
|
||||
Line 213: This is a long text line number 213 with searchable content spread across the file.
|
||||
Line 214: This is a long text line number 214 with searchable content spread across the file.
|
||||
Line 215: This is a long text line number 215 with searchable content spread across the file.
|
||||
Line 216: This is a long text line number 216 with searchable content spread across the file.
|
||||
Line 217: This is a long text line number 217 with searchable content spread across the file.
|
||||
Line 218: This is a long text line number 218 with searchable content spread across the file.
|
||||
Line 219: This is a long text line number 219 with searchable content spread across the file.
|
||||
Line 220: This is a long text line number 220 with searchable content spread across the file.
|
||||
Line 221: This is a long text line number 221 with searchable content spread across the file.
|
||||
Line 222: This is a long text line number 222 with searchable content spread across the file.
|
||||
Line 223: This is a long text line number 223 with searchable content spread across the file.
|
||||
Line 224: This is a long text line number 224 with searchable content spread across the file.
|
||||
Line 225: This is a long text line number 225 with searchable content spread across the file.
|
||||
Line 226: This is a long text line number 226 with searchable content spread across the file.
|
||||
Line 227: This is a long text line number 227 with searchable content spread across the file.
|
||||
Line 228: This is a long text line number 228 with searchable content spread across the file.
|
||||
Line 229: This is a long text line number 229 with searchable content spread across the file.
|
||||
Line 230: This is a long text line number 230 with searchable content spread across the file.
|
||||
Line 231: This is a long text line number 231 with searchable content spread across the file.
|
||||
Line 232: This is a long text line number 232 with searchable content spread across the file.
|
||||
Line 233: This is a long text line number 233 with searchable content spread across the file.
|
||||
Line 234: This is a long text line number 234 with searchable content spread across the file.
|
||||
Line 235: This is a long text line number 235 with searchable content spread across the file.
|
||||
Line 236: This is a long text line number 236 with searchable content spread across the file.
|
||||
Line 237: This is a long text line number 237 with searchable content spread across the file.
|
||||
Line 238: This is a long text line number 238 with searchable content spread across the file.
|
||||
Line 239: This is a long text line number 239 with searchable content spread across the file.
|
||||
Line 240: This is a long text line number 240 with searchable content spread across the file.
|
||||
Line 241: This is a long text line number 241 with searchable content spread across the file.
|
||||
Line 242: This is a long text line number 242 with searchable content spread across the file.
|
||||
Line 243: This is a long text line number 243 with searchable content spread across the file.
|
||||
Line 244: This is a long text line number 244 with searchable content spread across the file.
|
||||
Line 245: This is a long text line number 245 with searchable content spread across the file.
|
||||
Line 246: This is a long text line number 246 with searchable content spread across the file.
|
||||
Line 247: This is a long text line number 247 with searchable content spread across the file.
|
||||
Line 248: This is a long text line number 248 with searchable content spread across the file.
|
||||
Line 249: This is a long text line number 249 with searchable content spread across the file.
|
||||
Line 250: This is a long text line number 250 with searchable content spread across the file.
|
||||
Line 251: This is a long text line number 251 with searchable content spread across the file.
|
||||
Line 252: This is a long text line number 252 with searchable content spread across the file.
|
||||
Line 253: This is a long text line number 253 with searchable content spread across the file.
|
||||
Line 254: This is a long text line number 254 with searchable content spread across the file.
|
||||
Line 255: This is a long text line number 255 with searchable content spread across the file.
|
||||
Line 256: This is a long text line number 256 with searchable content spread across the file.
|
||||
Line 257: This is a long text line number 257 with searchable content spread across the file.
|
||||
Line 258: This is a long text line number 258 with searchable content spread across the file.
|
||||
Line 259: This is a long text line number 259 with searchable content spread across the file.
|
||||
Line 260: This is a long text line number 260 with searchable content spread across the file.
|
||||
Line 261: This is a long text line number 261 with searchable content spread across the file.
|
||||
Line 262: This is a long text line number 262 with searchable content spread across the file.
|
||||
Line 263: This is a long text line number 263 with searchable content spread across the file.
|
||||
Line 264: This is a long text line number 264 with searchable content spread across the file.
|
||||
Line 265: This is a long text line number 265 with searchable content spread across the file.
|
||||
Line 266: This is a long text line number 266 with searchable content spread across the file.
|
||||
Line 267: This is a long text line number 267 with searchable content spread across the file.
|
||||
Line 268: This is a long text line number 268 with searchable content spread across the file.
|
||||
Line 269: This is a long text line number 269 with searchable content spread across the file.
|
||||
Line 270: This is a long text line number 270 with searchable content spread across the file.
|
||||
Line 271: This is a long text line number 271 with searchable content spread across the file.
|
||||
Line 272: This is a long text line number 272 with searchable content spread across the file.
|
||||
Line 273: This is a long text line number 273 with searchable content spread across the file.
|
||||
Line 274: This is a long text line number 274 with searchable content spread across the file.
|
||||
Line 275: This is a long text line number 275 with searchable content spread across the file.
|
||||
Line 276: This is a long text line number 276 with searchable content spread across the file.
|
||||
Line 277: This is a long text line number 277 with searchable content spread across the file.
|
||||
Line 278: This is a long text line number 278 with searchable content spread across the file.
|
||||
Line 279: This is a long text line number 279 with searchable content spread across the file.
|
||||
Line 280: This is a long text line number 280 with searchable content spread across the file.
|
||||
Line 281: This is a long text line number 281 with searchable content spread across the file.
|
||||
Line 282: This is a long text line number 282 with searchable content spread across the file.
|
||||
Line 283: This is a long text line number 283 with searchable content spread across the file.
|
||||
Line 284: This is a long text line number 284 with searchable content spread across the file.
|
||||
Line 285: This is a long text line number 285 with searchable content spread across the file.
|
||||
Line 286: This is a long text line number 286 with searchable content spread across the file.
|
||||
Line 287: This is a long text line number 287 with searchable content spread across the file.
|
||||
Line 288: This is a long text line number 288 with searchable content spread across the file.
|
||||
Line 289: This is a long text line number 289 with searchable content spread across the file.
|
||||
Line 290: This is a long text line number 290 with searchable content spread across the file.
|
||||
Line 291: This is a long text line number 291 with searchable content spread across the file.
|
||||
Line 292: This is a long text line number 292 with searchable content spread across the file.
|
||||
Line 293: This is a long text line number 293 with searchable content spread across the file.
|
||||
Line 294: This is a long text line number 294 with searchable content spread across the file.
|
||||
Line 295: This is a long text line number 295 with searchable content spread across the file.
|
||||
Line 296: This is a long text line number 296 with searchable content spread across the file.
|
||||
Line 297: This is a long text line number 297 with searchable content spread across the file.
|
||||
Line 298: This is a long text line number 298 with searchable content spread across the file.
|
||||
Line 299: This is a long text line number 299 with searchable content spread across the file.
|
||||
Line 300: This is a long text line number 300 with searchable content spread across the file.
|
||||
Line 301: This is a long text line number 301 with searchable content spread across the file.
|
||||
Line 302: This is a long text line number 302 with searchable content spread across the file.
|
||||
Line 303: This is a long text line number 303 with searchable content spread across the file.
|
||||
Line 304: This is a long text line number 304 with searchable content spread across the file.
|
||||
Line 305: This is a long text line number 305 with searchable content spread across the file.
|
||||
Line 306: This is a long text line number 306 with searchable content spread across the file.
|
||||
Line 307: This is a long text line number 307 with searchable content spread across the file.
|
||||
Line 308: This is a long text line number 308 with searchable content spread across the file.
|
||||
Line 309: This is a long text line number 309 with searchable content spread across the file.
|
||||
Line 310: This is a long text line number 310 with searchable content spread across the file.
|
||||
Line 311: This is a long text line number 311 with searchable content spread across the file.
|
||||
Line 312: This is a long text line number 312 with searchable content spread across the file.
|
||||
Line 313: This is a long text line number 313 with searchable content spread across the file.
|
||||
Line 314: This is a long text line number 314 with searchable content spread across the file.
|
||||
Line 315: This is a long text line number 315 with searchable content spread across the file.
|
||||
Line 316: This is a long text line number 316 with searchable content spread across the file.
|
||||
Line 317: This is a long text line number 317 with searchable content spread across the file.
|
||||
Line 318: This is a long text line number 318 with searchable content spread across the file.
|
||||
Line 319: This is a long text line number 319 with searchable content spread across the file.
|
||||
Line 320: This is a long text line number 320 with searchable content spread across the file.
|
||||
Line 321: This is a long text line number 321 with searchable content spread across the file.
|
||||
Line 322: This is a long text line number 322 with searchable content spread across the file.
|
||||
Line 323: This is a long text line number 323 with searchable content spread across the file.
|
||||
Line 324: This is a long text line number 324 with searchable content spread across the file.
|
||||
Line 325: This is a long text line number 325 with searchable content spread across the file.
|
||||
Line 326: This is a long text line number 326 with searchable content spread across the file.
|
||||
Line 327: This is a long text line number 327 with searchable content spread across the file.
|
||||
Line 328: This is a long text line number 328 with searchable content spread across the file.
|
||||
Line 329: This is a long text line number 329 with searchable content spread across the file.
|
||||
Line 330: This is a long text line number 330 with searchable content spread across the file.
|
||||
Line 331: This is a long text line number 331 with searchable content spread across the file.
|
||||
Line 332: This is a long text line number 332 with searchable content spread across the file.
|
||||
Line 333: This is a long text line number 333 with searchable content spread across the file.
|
||||
Line 334: This is a long text line number 334 with searchable content spread across the file.
|
||||
Line 335: This is a long text line number 335 with searchable content spread across the file.
|
||||
Line 336: This is a long text line number 336 with searchable content spread across the file.
|
||||
Line 337: This is a long text line number 337 with searchable content spread across the file.
|
||||
Line 338: This is a long text line number 338 with searchable content spread across the file.
|
||||
Line 339: This is a long text line number 339 with searchable content spread across the file.
|
||||
Line 340: This is a long text line number 340 with searchable content spread across the file.
|
||||
Line 341: This is a long text line number 341 with searchable content spread across the file.
|
||||
Line 342: This is a long text line number 342 with searchable content spread across the file.
|
||||
Line 343: This is a long text line number 343 with searchable content spread across the file.
|
||||
Line 344: This is a long text line number 344 with searchable content spread across the file.
|
||||
Line 345: This is a long text line number 345 with searchable content spread across the file.
|
||||
Line 346: This is a long text line number 346 with searchable content spread across the file.
|
||||
Line 347: This is a long text line number 347 with searchable content spread across the file.
|
||||
Line 348: This is a long text line number 348 with searchable content spread across the file.
|
||||
Line 349: This is a long text line number 349 with searchable content spread across the file.
|
||||
Line 350: This is a long text line number 350 with searchable content spread across the file.
|
||||
Line 351: This is a long text line number 351 with searchable content spread across the file.
|
||||
Line 352: This is a long text line number 352 with searchable content spread across the file.
|
||||
Line 353: This is a long text line number 353 with searchable content spread across the file.
|
||||
Line 354: This is a long text line number 354 with searchable content spread across the file.
|
||||
Line 355: This is a long text line number 355 with searchable content spread across the file.
|
||||
Line 356: This is a long text line number 356 with searchable content spread across the file.
|
||||
Line 357: This is a long text line number 357 with searchable content spread across the file.
|
||||
Line 358: This is a long text line number 358 with searchable content spread across the file.
|
||||
Line 359: This is a long text line number 359 with searchable content spread across the file.
|
||||
Line 360: This is a long text line number 360 with searchable content spread across the file.
|
||||
Line 361: This is a long text line number 361 with searchable content spread across the file.
|
||||
Line 362: This is a long text line number 362 with searchable content spread across the file.
|
||||
Line 363: This is a long text line number 363 with searchable content spread across the file.
|
||||
Line 364: This is a long text line number 364 with searchable content spread across the file.
|
||||
Line 365: This is a long text line number 365 with searchable content spread across the file.
|
||||
Line 366: This is a long text line number 366 with searchable content spread across the file.
|
||||
Line 367: This is a long text line number 367 with searchable content spread across the file.
|
||||
Line 368: This is a long text line number 368 with searchable content spread across the file.
|
||||
Line 369: This is a long text line number 369 with searchable content spread across the file.
|
||||
Line 370: This is a long text line number 370 with searchable content spread across the file.
|
||||
Line 371: This is a long text line number 371 with searchable content spread across the file.
|
||||
Line 372: This is a long text line number 372 with searchable content spread across the file.
|
||||
Line 373: This is a long text line number 373 with searchable content spread across the file.
|
||||
Line 374: This is a long text line number 374 with searchable content spread across the file.
|
||||
Line 375: This is a long text line number 375 with searchable content spread across the file.
|
||||
Line 376: This is a long text line number 376 with searchable content spread across the file.
|
||||
Line 377: This is a long text line number 377 with searchable content spread across the file.
|
||||
Line 378: This is a long text line number 378 with searchable content spread across the file.
|
||||
Line 379: This is a long text line number 379 with searchable content spread across the file.
|
||||
Line 380: This is a long text line number 380 with searchable content spread across the file.
|
||||
Line 381: This is a long text line number 381 with searchable content spread across the file.
|
||||
Line 382: This is a long text line number 382 with searchable content spread across the file.
|
||||
Line 383: This is a long text line number 383 with searchable content spread across the file.
|
||||
Line 384: This is a long text line number 384 with searchable content spread across the file.
|
||||
Line 385: This is a long text line number 385 with searchable content spread across the file.
|
||||
Line 386: This is a long text line number 386 with searchable content spread across the file.
|
||||
Line 387: This is a long text line number 387 with searchable content spread across the file.
|
||||
Line 388: This is a long text line number 388 with searchable content spread across the file.
|
||||
Line 389: This is a long text line number 389 with searchable content spread across the file.
|
||||
Line 390: This is a long text line number 390 with searchable content spread across the file.
|
||||
Line 391: This is a long text line number 391 with searchable content spread across the file.
|
||||
Line 392: This is a long text line number 392 with searchable content spread across the file.
|
||||
Line 393: This is a long text line number 393 with searchable content spread across the file.
|
||||
Line 394: This is a long text line number 394 with searchable content spread across the file.
|
||||
Line 395: This is a long text line number 395 with searchable content spread across the file.
|
||||
Line 396: This is a long text line number 396 with searchable content spread across the file.
|
||||
Line 397: This is a long text line number 397 with searchable content spread across the file.
|
||||
Line 398: This is a long text line number 398 with searchable content spread across the file.
|
||||
Line 399: This is a long text line number 399 with searchable content spread across the file.
|
||||
Line 400: This is a long text line number 400 with searchable content spread across the file.
|
||||
Line 401: This is a long text line number 401 with searchable content spread across the file.
|
||||
Line 402: This is a long text line number 402 with searchable content spread across the file.
|
||||
Line 403: This is a long text line number 403 with searchable content spread across the file.
|
||||
Line 404: This is a long text line number 404 with searchable content spread across the file.
|
||||
Line 405: This is a long text line number 405 with searchable content spread across the file.
|
||||
Line 406: This is a long text line number 406 with searchable content spread across the file.
|
||||
Line 407: This is a long text line number 407 with searchable content spread across the file.
|
||||
Line 408: This is a long text line number 408 with searchable content spread across the file.
|
||||
Line 409: This is a long text line number 409 with searchable content spread across the file.
|
||||
Line 410: This is a long text line number 410 with searchable content spread across the file.
|
||||
Line 411: This is a long text line number 411 with searchable content spread across the file.
|
||||
Line 412: This is a long text line number 412 with searchable content spread across the file.
|
||||
Line 413: This is a long text line number 413 with searchable content spread across the file.
|
||||
Line 414: This is a long text line number 414 with searchable content spread across the file.
|
||||
Line 415: This is a long text line number 415 with searchable content spread across the file.
|
||||
Line 416: This is a long text line number 416 with searchable content spread across the file.
|
||||
Line 417: This is a long text line number 417 with searchable content spread across the file.
|
||||
Line 418: This is a long text line number 418 with searchable content spread across the file.
|
||||
Line 419: This is a long text line number 419 with searchable content spread across the file.
|
||||
Line 420: This is a long text line number 420 with searchable content spread across the file.
|
||||
Line 421: This is a long text line number 421 with searchable content spread across the file.
|
||||
Line 422: This is a long text line number 422 with searchable content spread across the file.
|
||||
Line 423: This is a long text line number 423 with searchable content spread across the file.
|
||||
Line 424: This is a long text line number 424 with searchable content spread across the file.
|
||||
Line 425: This is a long text line number 425 with searchable content spread across the file.
|
||||
Line 426: This is a long text line number 426 with searchable content spread across the file.
|
||||
Line 427: This is a long text line number 427 with searchable content spread across the file.
|
||||
Line 428: This is a long text line number 428 with searchable content spread across the file.
|
||||
Line 429: This is a long text line number 429 with searchable content spread across the file.
|
||||
Line 430: This is a long text line number 430 with searchable content spread across the file.
|
||||
Line 431: This is a long text line number 431 with searchable content spread across the file.
|
||||
Line 432: This is a long text line number 432 with searchable content spread across the file.
|
||||
Line 433: This is a long text line number 433 with searchable content spread across the file.
|
||||
Line 434: This is a long text line number 434 with searchable content spread across the file.
|
||||
Line 435: This is a long text line number 435 with searchable content spread across the file.
|
||||
Line 436: This is a long text line number 436 with searchable content spread across the file.
|
||||
Line 437: This is a long text line number 437 with searchable content spread across the file.
|
||||
Line 438: This is a long text line number 438 with searchable content spread across the file.
|
||||
Line 439: This is a long text line number 439 with searchable content spread across the file.
|
||||
Line 440: This is a long text line number 440 with searchable content spread across the file.
|
||||
Line 441: This is a long text line number 441 with searchable content spread across the file.
|
||||
Line 442: This is a long text line number 442 with searchable content spread across the file.
|
||||
Line 443: This is a long text line number 443 with searchable content spread across the file.
|
||||
Line 444: This is a long text line number 444 with searchable content spread across the file.
|
||||
Line 445: This is a long text line number 445 with searchable content spread across the file.
|
||||
Line 446: This is a long text line number 446 with searchable content spread across the file.
|
||||
Line 447: This is a long text line number 447 with searchable content spread across the file.
|
||||
Line 448: This is a long text line number 448 with searchable content spread across the file.
|
||||
Line 449: This is a long text line number 449 with searchable content spread across the file.
|
||||
Line 450: This is a long text line number 450 with searchable content spread across the file.
|
||||
Line 451: This is a long text line number 451 with searchable content spread across the file.
|
||||
Line 452: This is a long text line number 452 with searchable content spread across the file.
|
||||
Line 453: This is a long text line number 453 with searchable content spread across the file.
|
||||
Line 454: This is a long text line number 454 with searchable content spread across the file.
|
||||
Line 455: This is a long text line number 455 with searchable content spread across the file.
|
||||
Line 456: This is a long text line number 456 with searchable content spread across the file.
|
||||
Line 457: This is a long text line number 457 with searchable content spread across the file.
|
||||
Line 458: This is a long text line number 458 with searchable content spread across the file.
|
||||
Line 459: This is a long text line number 459 with searchable content spread across the file.
|
||||
Line 460: This is a long text line number 460 with searchable content spread across the file.
|
||||
Line 461: This is a long text line number 461 with searchable content spread across the file.
|
||||
Line 462: This is a long text line number 462 with searchable content spread across the file.
|
||||
Line 463: This is a long text line number 463 with searchable content spread across the file.
|
||||
Line 464: This is a long text line number 464 with searchable content spread across the file.
|
||||
Line 465: This is a long text line number 465 with searchable content spread across the file.
|
||||
Line 466: This is a long text line number 466 with searchable content spread across the file.
|
||||
Line 467: This is a long text line number 467 with searchable content spread across the file.
|
||||
Line 468: This is a long text line number 468 with searchable content spread across the file.
|
||||
Line 469: This is a long text line number 469 with searchable content spread across the file.
|
||||
Line 470: This is a long text line number 470 with searchable content spread across the file.
|
||||
Line 471: This is a long text line number 471 with searchable content spread across the file.
|
||||
Line 472: This is a long text line number 472 with searchable content spread across the file.
|
||||
Line 473: This is a long text line number 473 with searchable content spread across the file.
|
||||
Line 474: This is a long text line number 474 with searchable content spread across the file.
|
||||
Line 475: This is a long text line number 475 with searchable content spread across the file.
|
||||
Line 476: This is a long text line number 476 with searchable content spread across the file.
|
||||
Line 477: This is a long text line number 477 with searchable content spread across the file.
|
||||
Line 478: This is a long text line number 478 with searchable content spread across the file.
|
||||
Line 479: This is a long text line number 479 with searchable content spread across the file.
|
||||
Line 480: This is a long text line number 480 with searchable content spread across the file.
|
||||
Line 481: This is a long text line number 481 with searchable content spread across the file.
|
||||
Line 482: This is a long text line number 482 with searchable content spread across the file.
|
||||
Line 483: This is a long text line number 483 with searchable content spread across the file.
|
||||
Line 484: This is a long text line number 484 with searchable content spread across the file.
|
||||
Line 485: This is a long text line number 485 with searchable content spread across the file.
|
||||
Line 486: This is a long text line number 486 with searchable content spread across the file.
|
||||
Line 487: This is a long text line number 487 with searchable content spread across the file.
|
||||
Line 488: This is a long text line number 488 with searchable content spread across the file.
|
||||
Line 489: This is a long text line number 489 with searchable content spread across the file.
|
||||
Line 490: This is a long text line number 490 with searchable content spread across the file.
|
||||
Line 491: This is a long text line number 491 with searchable content spread across the file.
|
||||
Line 492: This is a long text line number 492 with searchable content spread across the file.
|
||||
Line 493: This is a long text line number 493 with searchable content spread across the file.
|
||||
Line 494: This is a long text line number 494 with searchable content spread across the file.
|
||||
Line 495: This is a long text line number 495 with searchable content spread across the file.
|
||||
Line 496: This is a long text line number 496 with searchable content spread across the file.
|
||||
Line 497: This is a long text line number 497 with searchable content spread across the file.
|
||||
Line 498: This is a long text line number 498 with searchable content spread across the file.
|
||||
Line 499: This is a long text line number 499 with searchable content spread across the file.
|
||||
Line 500: This is a long text line number 500 with searchable content spread across the file.
|
||||
7
fixtures/text/sample.txt
Normal file
7
fixtures/text/sample.txt
Normal file
@ -0,0 +1,7 @@
|
||||
zhixi-document-runtime
|
||||
|
||||
A cross-platform document reading kernel built in Rust.
|
||||
|
||||
Supported platforms: iOS, Android, HarmonyOS, macOS, Windows, Web.
|
||||
|
||||
第一版先建立跨平台文档内核、阅读位置模型、学习事件协议和 iOS 接入链路。
|
||||
45
scripts/build-ios.sh
Executable file
45
scripts/build-ios.sh
Executable file
@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Build iOS XCFramework for zhixi-document-runtime
|
||||
# UDL bindgen + proc-macro C symbols + inline Swift FFI types
|
||||
set -e
|
||||
|
||||
RUST_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT_DIR="$RUST_DIR/bindings/ios"
|
||||
LIB_NAME="libzx_document_ffi.a"
|
||||
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
|
||||
|
||||
echo "==> Building for iOS device (arm64)..."
|
||||
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
|
||||
|
||||
echo "==> Building for iOS simulator (arm64)..."
|
||||
cargo build --release --target aarch64-apple-ios-sim -p zx_document_ffi
|
||||
|
||||
rm -rf "$OUT_DIR/ZxDocumentRuntime.xcframework"
|
||||
mkdir -p "$OUT_DIR/device" "$OUT_DIR/simulator" "$OUT_DIR/generated"
|
||||
|
||||
cp "target/aarch64-apple-ios/release/$LIB_NAME" "$OUT_DIR/device/"
|
||||
cp "target/aarch64-apple-ios-sim/release/$LIB_NAME" "$OUT_DIR/simulator/"
|
||||
|
||||
echo "==> Generating Swift bindings (UDL bindgen)..."
|
||||
rm -f "$OUT_DIR/generated/zx_document.swift"
|
||||
uniffi-bindgen generate \
|
||||
--language swift \
|
||||
--out-dir "$OUT_DIR/generated" \
|
||||
"$RUST_DIR/crates/zx_document_ffi/src/zx_document.udl"
|
||||
|
||||
echo "==> Patching Swift file for static linking..."
|
||||
python3 "$RUST_DIR/scripts/patch-swift.py" "$OUT_DIR/generated/zx_document.swift"
|
||||
|
||||
echo "==> Checking C ABI symbols..."
|
||||
bash "$RUST_DIR/scripts/check-symbols.sh" "$OUT_DIR/simulator/$LIB_NAME"
|
||||
|
||||
echo "==> Creating XCFramework..."
|
||||
xcodebuild -create-xcframework \
|
||||
-library "$OUT_DIR/device/$LIB_NAME" \
|
||||
-library "$OUT_DIR/simulator/$LIB_NAME" \
|
||||
-output "$OUT_DIR/ZxDocumentRuntime.xcframework"
|
||||
|
||||
echo "==> Done!"
|
||||
echo "XCFramework: $OUT_DIR/ZxDocumentRuntime.xcframework"
|
||||
echo "Swift sources: $OUT_DIR/generated/"
|
||||
56
scripts/check-symbols.sh
Executable file
56
scripts/check-symbols.sh
Executable file
@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
# Verify UniFFI C ABI symbols exist in the static library.
|
||||
# Run after build-ios.sh or independently to diagnose FFI issues.
|
||||
set -e
|
||||
|
||||
LIB="${1:-bindings/ios/simulator/libzx_document_ffi.a}"
|
||||
|
||||
if [ ! -f "$LIB" ]; then
|
||||
echo "ERROR: Library not found: $LIB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Checking UniFFI C ABI symbols in $LIB..."
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
if nm "$LIB" 2>/dev/null | grep -q "_$1"; then
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo " MISSING: _$1"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# API dispatch functions
|
||||
check uniffi_zx_document_ffi_fn_func_detect_material_type
|
||||
check uniffi_zx_document_ffi_fn_func_parse_markdown
|
||||
check uniffi_zx_document_ffi_fn_func_parse_text
|
||||
check uniffi_zx_document_ffi_fn_func_read_image_meta
|
||||
check uniffi_zx_document_ffi_fn_func_read_text_stats
|
||||
check uniffi_zx_document_ffi_fn_func_search_markdown_blocks
|
||||
check uniffi_zx_document_ffi_fn_func_search_text_content
|
||||
check uniffi_zx_document_ffi_fn_func_create_note_anchor
|
||||
check uniffi_zx_document_ffi_fn_func_push_reading_event
|
||||
check uniffi_zx_document_ffi_fn_func_update_reading_position
|
||||
check uniffi_zx_document_ffi_fn_func_export_pending_events
|
||||
check uniffi_zx_document_ffi_fn_func_clear_exported_events
|
||||
|
||||
# Buffer management
|
||||
check ffi_zx_document_ffi_rustbuffer_alloc
|
||||
check ffi_zx_document_ffi_rustbuffer_from_bytes
|
||||
check ffi_zx_document_ffi_rustbuffer_free
|
||||
check ffi_zx_document_ffi_rustbuffer_reserve
|
||||
check ffi_zx_document_ffi_uniffi_contract_version
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL missing"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo "ERROR: $FAIL required symbol(s) missing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All symbols OK!"
|
||||
42
scripts/patch-swift.py
Executable file
42
scripts/patch-swift.py
Executable file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch UDL-generated Swift file for iOS 26 SDK compatibility and static linking."""
|
||||
import sys, re
|
||||
|
||||
f = sys.argv[1]
|
||||
with open(f) as fh:
|
||||
c = fh.read()
|
||||
|
||||
# Patch 1: Fix Data.init for iOS 26 SDK (bytesNoCopy:count:deallocator: removed)
|
||||
c = c.replace(
|
||||
"fileprivate extension Data {\n init(rustBuffer: RustBuffer) {\n self.init(\n bytesNoCopy: rustBuffer.data!,\n count: Int(rustBuffer.len),\n deallocator: .none\n )\n }\n}",
|
||||
"fileprivate extension Data {\n init(rustBuffer: RustBuffer) {\n let buf = UnsafeRawBufferPointer(start: rustBuffer.data, count: Int(rustBuffer.len))\n self.init(buf)\n }\n}"
|
||||
)
|
||||
|
||||
# Patch 2: Fix UnsafeMutableRawPointer cast for iOS 26 SDK
|
||||
c = c.replace(
|
||||
"let bytes = UnsafeBufferPointer<UInt8>(start: value.data!, count: Int(value.len))",
|
||||
"let bytes = UnsafeBufferPointer<UInt8>(start: value.data!.assumingMemoryBound(to: UInt8.self), count: Int(value.len))"
|
||||
)
|
||||
|
||||
# Patch 3: Remove checksum initialization
|
||||
# UniFFI 0.28 UDL scaffolding does not export checksum functions as #[no_mangle],
|
||||
# so they cannot be called from Swift. Remove the runtime checksum verification.
|
||||
c = re.sub(r'private enum InitializationResult.*?// swiftlint:enable all', '// swiftlint:enable all', c, flags=re.DOTALL)
|
||||
c = re.sub(r'public func uniffiEnsureZxDocumentFfiInitialized\(\) \{.*?\n \}\n\}', '', c, flags=re.DOTALL)
|
||||
c = re.sub(r'private let initializationResult: InitializationResult.*?InitializationResult\.ok\n\}', '', c, flags=re.DOTALL)
|
||||
|
||||
# Add a simple stub (called by rustCall helper, body removed above)
|
||||
c = c.replace(
|
||||
'fileprivate extension RustBuffer {\n // Allocate a new buffer',
|
||||
'private func uniffiEnsureZxDocumentFfiInitialized() {}\n\nfileprivate extension RustBuffer {\n // Allocate a new buffer'
|
||||
)
|
||||
|
||||
# Remove accidental duplicate stub if present
|
||||
c = c.replace(
|
||||
'private func uniffiEnsureZxDocumentFfiInitialized() {}\n\nprivate func uniffiEnsureZxDocumentFfiInitialized() {}\n',
|
||||
'private func uniffiEnsureZxDocumentFfiInitialized() {}\n'
|
||||
)
|
||||
|
||||
with open(f, 'w') as fh:
|
||||
fh.write(c)
|
||||
print(f" Patched: {len(c.split(chr(10)))} lines")
|
||||
Loading…
x
Reference in New Issue
Block a user