mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-04 07:41:50 +08:00
feat(editor): migrate typst mermaid to native (#14499)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Native/WASM Mermaid and Typst SVG preview rendering on desktop and mobile, plus cross-platform Preview plugin integrations. * **Improvements** * Centralized, sanitized rendering bridge with automatic Typst font-directory handling and configurable native renderer selection. * More consistent and robust error serialization and worker-backed preview flows for improved stability and performance. * **Tests** * Extensive unit and integration tests for preview rendering, font discovery, sanitization, and error serialization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -34,6 +34,7 @@ class AFFiNEViewController: CAPBridgeViewController {
|
||||
NavigationGesturePlugin(),
|
||||
NbStorePlugin(),
|
||||
PayWallPlugin(associatedController: self),
|
||||
PreviewPlugin(),
|
||||
]
|
||||
plugins.forEach { bridge?.registerPluginInstance($0) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
private func resolveLocalFontDir(from fontURL: String) -> String? {
|
||||
let path: String
|
||||
if fontURL.hasPrefix("file://") {
|
||||
guard let url = URL(string: fontURL), url.isFileURL else {
|
||||
return nil
|
||||
}
|
||||
path = url.path
|
||||
} else {
|
||||
let candidate = (fontURL as NSString).standardizingPath
|
||||
guard candidate.hasPrefix("/") else {
|
||||
return nil
|
||||
}
|
||||
path = candidate
|
||||
}
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory),
|
||||
isDirectory.boolValue
|
||||
{
|
||||
return path
|
||||
}
|
||||
|
||||
let directory = (path as NSString).deletingLastPathComponent
|
||||
return directory.isEmpty ? nil : directory
|
||||
}
|
||||
|
||||
private func resolveTypstFontDirs(from options: [AnyHashable: Any]?) throws -> [String]? {
|
||||
guard let rawFontUrls = options?["fontUrls"] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let fontUrls = rawFontUrls as? [Any] else {
|
||||
throw NSError(
|
||||
domain: "PreviewPlugin",
|
||||
code: 1,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Typst preview fontUrls must be an array of strings."
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var seenFontDirs = Set<String>()
|
||||
var orderedFontDirs = [String]()
|
||||
orderedFontDirs.reserveCapacity(fontUrls.count)
|
||||
|
||||
for fontUrl in fontUrls {
|
||||
guard let fontURL = fontUrl as? String else {
|
||||
throw NSError(
|
||||
domain: "PreviewPlugin",
|
||||
code: 1,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Typst preview fontUrls must be strings."
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
guard let fontDir = resolveLocalFontDir(from: fontURL) else {
|
||||
throw NSError(
|
||||
domain: "PreviewPlugin",
|
||||
code: 1,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Typst preview on mobile only supports local font file URLs or absolute font directories."
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if seenFontDirs.insert(fontDir).inserted {
|
||||
orderedFontDirs.append(fontDir)
|
||||
}
|
||||
}
|
||||
|
||||
return orderedFontDirs
|
||||
}
|
||||
|
||||
@objc(PreviewPlugin)
|
||||
public class PreviewPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "PreviewPlugin"
|
||||
public let jsName = "Preview"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "renderMermaidSvg", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "renderTypstSvg", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
@objc func renderMermaidSvg(_ call: CAPPluginCall) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
let code = try call.getStringEnsure("code")
|
||||
let options = call.getObject("options")
|
||||
let svg = try renderMermaidPreviewSvg(
|
||||
code: code,
|
||||
theme: options?["theme"] as? String,
|
||||
fontFamily: options?["fontFamily"] as? String,
|
||||
fontSize: (options?["fontSize"] as? NSNumber)?.doubleValue
|
||||
)
|
||||
call.resolve(["svg": svg])
|
||||
} catch {
|
||||
call.reject("Failed to render Mermaid preview, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func renderTypstSvg(_ call: CAPPluginCall) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
let code = try call.getStringEnsure("code")
|
||||
let options = call.getObject("options")
|
||||
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path
|
||||
let fontDirs = try resolveTypstFontDirs(from: options)
|
||||
let svg = try renderTypstPreviewSvg(code: code, fontDirs: fontDirs, cacheDir: cacheDir)
|
||||
call.resolve(["svg": svg])
|
||||
} catch {
|
||||
call.reject("Failed to render Typst preview, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2265,6 +2265,30 @@ fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
fileprivate struct FfiConverterOptionDouble: FfiConverterRustBuffer {
|
||||
typealias SwiftType = Double?
|
||||
|
||||
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
|
||||
guard let value = value else {
|
||||
writeInt(&buf, Int8(0))
|
||||
return
|
||||
}
|
||||
writeInt(&buf, Int8(1))
|
||||
FfiConverterDouble.write(value, into: &buf)
|
||||
}
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
|
||||
switch try readInt(&buf) as Int8 {
|
||||
case 0: return nil
|
||||
case 1: return try FfiConverterDouble.read(from: &buf)
|
||||
default: throw UniffiInternalError.unexpectedOptionalTag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
@@ -2644,6 +2668,25 @@ public func newDocStoragePool() -> DocStoragePool {
|
||||
)
|
||||
})
|
||||
}
|
||||
public func renderMermaidPreviewSvg(code: String, theme: String?, fontFamily: String?, fontSize: Double?)throws -> String {
|
||||
return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeUniffiError_lift) {
|
||||
uniffi_affine_mobile_native_fn_func_render_mermaid_preview_svg(
|
||||
FfiConverterString.lower(code),
|
||||
FfiConverterOptionString.lower(theme),
|
||||
FfiConverterOptionString.lower(fontFamily),
|
||||
FfiConverterOptionDouble.lower(fontSize),$0
|
||||
)
|
||||
})
|
||||
}
|
||||
public func renderTypstPreviewSvg(code: String, fontDirs: [String]?, cacheDir: String?)throws -> String {
|
||||
return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeUniffiError_lift) {
|
||||
uniffi_affine_mobile_native_fn_func_render_typst_preview_svg(
|
||||
FfiConverterString.lower(code),
|
||||
FfiConverterOptionSequenceString.lower(fontDirs),
|
||||
FfiConverterOptionString.lower(cacheDir),$0
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private enum InitializationResult {
|
||||
case ok
|
||||
@@ -2666,6 +2709,12 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool() != 32882) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_affine_mobile_native_checksum_func_render_mermaid_preview_svg() != 54334) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_affine_mobile_native_checksum_func_render_typst_preview_svg() != 42796) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks() != 51151) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -450,6 +450,16 @@ RustBuffer uniffi_affine_mobile_native_fn_func_hashcash_mint(RustBuffer resource
|
||||
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_NEW_DOC_STORAGE_POOL
|
||||
void*_Nonnull uniffi_affine_mobile_native_fn_func_new_doc_storage_pool(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_RENDER_MERMAID_PREVIEW_SVG
|
||||
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_RENDER_MERMAID_PREVIEW_SVG
|
||||
RustBuffer uniffi_affine_mobile_native_fn_func_render_mermaid_preview_svg(RustBuffer code, RustBuffer theme, RustBuffer font_family, RustBuffer font_size, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_RENDER_TYPST_PREVIEW_SVG
|
||||
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_RENDER_TYPST_PREVIEW_SVG
|
||||
RustBuffer uniffi_affine_mobile_native_fn_func_render_typst_preview_svg(RustBuffer code, RustBuffer font_dirs, RustBuffer cache_dir, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUSTBUFFER_ALLOC
|
||||
@@ -742,6 +752,18 @@ uint16_t uniffi_affine_mobile_native_checksum_func_hashcash_mint(void
|
||||
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_NEW_DOC_STORAGE_POOL
|
||||
uint16_t uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_RENDER_MERMAID_PREVIEW_SVG
|
||||
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_RENDER_MERMAID_PREVIEW_SVG
|
||||
uint16_t uniffi_affine_mobile_native_checksum_func_render_mermaid_preview_svg(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_RENDER_TYPST_PREVIEW_SVG
|
||||
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_RENDER_TYPST_PREVIEW_SVG
|
||||
uint16_t uniffi_affine_mobile_native_checksum_func_render_typst_preview_svg(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS
|
||||
|
||||
Reference in New Issue
Block a user