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:
@@ -14,8 +14,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Recouse/EventSource",
|
||||
"state" : {
|
||||
"revision" : "7b2f4f585d3927876bd76eaede9fdff779eff102",
|
||||
"version" : "0.1.5"
|
||||
"revision" : "713f8c0a0270a80a968c007ddc0d6067e80a5393",
|
||||
"version" : "0.1.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -41,8 +41,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/Litext",
|
||||
"state" : {
|
||||
"revision" : "c7e83f2f580ce34a102ca9ba9d2bb24e507dccd9",
|
||||
"version" : "0.5.6"
|
||||
"revision" : "a2ed9b63ae623a20591effc72f9db7d04e41a64c",
|
||||
"version" : "1.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,8 +77,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios-spm.git",
|
||||
"state" : {
|
||||
"revision" : "8f5df97653eb361a2097119479332afccf0aa816",
|
||||
"version" : "5.58.0"
|
||||
"revision" : "2913a336eb37dc06795cdbaa5b5de330b6707669",
|
||||
"version" : "5.65.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,4 +8,18 @@
|
||||
import ApolloAPI
|
||||
|
||||
/// The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
public typealias JSON = String
|
||||
public struct JSON: CustomScalarType, Hashable, ExpressibleByDictionaryLiteral {
|
||||
public let value: JSONValue
|
||||
|
||||
public init(_jsonValue value: JSONValue) throws {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public init(dictionaryLiteral elements: (String, JSONValue)...) {
|
||||
value = ApolloAPI.JSONObject(uniqueKeysWithValues: elements) as JSONValue
|
||||
}
|
||||
|
||||
public var _jsonValue: JSONValue {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,22 @@
|
||||
import ApolloAPI
|
||||
|
||||
/// The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
public typealias JSONObject = String
|
||||
public struct JSONObject: CustomScalarType, Hashable, ExpressibleByDictionaryLiteral {
|
||||
public let object: ApolloAPI.JSONObject
|
||||
|
||||
public init(_jsonValue value: JSONValue) throws {
|
||||
object = try ApolloAPI.JSONObject(_jsonValue: value)
|
||||
}
|
||||
|
||||
public init(_ object: ApolloAPI.JSONObject) {
|
||||
self.object = object
|
||||
}
|
||||
|
||||
public init(dictionaryLiteral elements: (String, JSONValue)...) {
|
||||
object = ApolloAPI.JSONObject(uniqueKeysWithValues: elements)
|
||||
}
|
||||
|
||||
public var _jsonValue: JSONValue {
|
||||
object
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ private extension InputBoxData {
|
||||
}
|
||||
|
||||
public extension ChatManager {
|
||||
@MainActor
|
||||
func startUserRequest(editorData: InputBoxData, sessionId: String) {
|
||||
append(sessionId: sessionId, UserMessageCellViewModel(
|
||||
id: .init(),
|
||||
@@ -163,7 +164,7 @@ private extension ChatManager {
|
||||
assert(!Thread.isMainThread)
|
||||
print("[+] starting copilot response for session: \(sessionId)")
|
||||
|
||||
let messageParameters: [String: AnyHashable] = [
|
||||
let messageParameters: AffineGraphQL.JSON = [
|
||||
// packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx
|
||||
"docs": editorData.documentAttachments.map(\.documentID), // affine doc
|
||||
"files": [String](), // attachment in context, keep nil for now
|
||||
@@ -193,18 +194,14 @@ private extension ChatManager {
|
||||
},
|
||||
].flatMap(\.self)
|
||||
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
|
||||
guard let input = try? CreateChatMessageInput(
|
||||
let input = CreateChatMessageInput(
|
||||
attachments: [],
|
||||
blob: attachmentCount == 1 ? "" : .none,
|
||||
blobs: attachmentCount > 1 && attachmentCount != 0 ? .some([]) : .none,
|
||||
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
|
||||
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
|
||||
params: .some(messageParameters),
|
||||
sessionId: sessionId
|
||||
) else {
|
||||
report(sessionId, ChatError.unknownError)
|
||||
assertionFailure() // very unlikely to happen
|
||||
return
|
||||
}
|
||||
)
|
||||
let mutation = CreateCopilotMessageMutation(options: input)
|
||||
QLService.shared.client.upload(operation: mutation, files: uploadableAttachments) { result in
|
||||
print("[*] createCopilotMessage result: \(result)")
|
||||
@@ -277,7 +274,7 @@ private extension ChatManager {
|
||||
let eventSource = EventSource()
|
||||
let dataTask = eventSource.dataTask(for: request)
|
||||
var document = ""
|
||||
self.writeMarkdownContent(document + loadingIndicator, sessionId: sessionId, vmId: vmId)
|
||||
await self.writeMarkdownContent(document + loadingIndicator, sessionId: sessionId, vmId: vmId)
|
||||
for await event in dataTask.events() {
|
||||
switch event {
|
||||
case .open:
|
||||
@@ -287,7 +284,7 @@ private extension ChatManager {
|
||||
case let .event(event):
|
||||
guard let data = event.data else { continue }
|
||||
document += data
|
||||
self.writeMarkdownContent(
|
||||
await self.writeMarkdownContent(
|
||||
document + loadingIndicator,
|
||||
sessionId: sessionId,
|
||||
vmId: vmId
|
||||
@@ -297,13 +294,13 @@ private extension ChatManager {
|
||||
print("[*] connection closed")
|
||||
}
|
||||
}
|
||||
self.writeMarkdownContent(document, sessionId: sessionId, vmId: vmId)
|
||||
await self.writeMarkdownContent(document, sessionId: sessionId, vmId: vmId)
|
||||
self.closeAll()
|
||||
}))
|
||||
self.closable.append(closable)
|
||||
}
|
||||
|
||||
private func writeMarkdownContent(
|
||||
@MainActor private func writeMarkdownContent(
|
||||
_ document: String,
|
||||
sessionId: SessionID,
|
||||
vmId: UUID
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
import MarkdownView
|
||||
|
||||
extension IntelligentContext {
|
||||
func prepareMarkdownViewThemes() {
|
||||
@MainActor func prepareMarkdownViewThemes() {
|
||||
MarkdownTheme.default.colors.body = .affineTextPrimary
|
||||
MarkdownTheme.default.colors.highlight = .affineTextLink
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
|
||||
|
||||
var preprocessedContent: MarkdownTextView.PreprocessedContent
|
||||
|
||||
init(
|
||||
@MainActor init(
|
||||
id: UUID,
|
||||
content: String,
|
||||
timestamp: Date,
|
||||
|
||||
@@ -45,13 +45,13 @@ EXTERNAL SOURCES:
|
||||
:path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: 12914e6f1b7835e161a74ebd19cb361efa37a7dd
|
||||
CapacitorApp: 63b237168fc869e758481dba283315a85743ee78
|
||||
CapacitorBrowser: b98aa3db018a2ce4c68242d27e596c344f3b81b3
|
||||
Capacitor: a5bf59e09f9dd82694fdcca4d107b4d215ac470f
|
||||
CapacitorApp: 3ddbd30ac18c321531c3da5e707b60873d89dd60
|
||||
CapacitorBrowser: 66aa8ff09cdca2a327ce464b113b470e6f667753
|
||||
CapacitorCordova: 31bbe4466000c6b86d9b7f1181ee286cff0205aa
|
||||
CapacitorHaptics: ce15be8f287fa2c61c7d2d9e958885b90cf0bebc
|
||||
CapacitorKeyboard: 5660c760113bfa48962817a785879373cf5339c3
|
||||
CapacitorPluginAppTrackingTransparency: 92ae9c1cfb5cf477753db9269689332a686f675a
|
||||
CapacitorHaptics: d17da7dd984cae34111b3f097ccd3e21f9feec62
|
||||
CapacitorKeyboard: 45cae3956a6f4fb1753f9a4df3e884aeaed8fe82
|
||||
CapacitorPluginAppTrackingTransparency: 2a2792623a5a72795f2e8f9ab3f1147573732fd8
|
||||
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
|
||||
|
||||
PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SubscriptionService,
|
||||
ValidatorProvider,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { registerNativePreviewHandlers } from '@affine/core/modules/code-block-preview-renderer';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
@@ -71,6 +72,7 @@ import { Auth } from './plugins/auth';
|
||||
import { Hashcash } from './plugins/hashcash';
|
||||
import { NbStoreNativeDBApis } from './plugins/nbstore';
|
||||
import { PayWall } from './plugins/paywall';
|
||||
import { Preview } from './plugins/preview';
|
||||
import { writeEndpointToken } from './proxy';
|
||||
import { enableNavigationGesture$ } from './web-navigation-control';
|
||||
|
||||
@@ -215,6 +217,11 @@ framework.impl(NativePaywallProvider, {
|
||||
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
registerNativePreviewHandlers({
|
||||
renderMermaidSvg: request => Preview.renderMermaidSvg(request),
|
||||
renderTypstSvg: request => Preview.renderTypstSvg(request),
|
||||
});
|
||||
|
||||
// ------ some apis for native ------
|
||||
(window as any).getCurrentServerBaseUrl = () => {
|
||||
const globalContextService = frameworkProvider.get(GlobalContextService);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface PreviewPlugin {
|
||||
renderMermaidSvg(options: {
|
||||
code: string;
|
||||
options?: {
|
||||
theme?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
};
|
||||
}): Promise<{ svg: string }>;
|
||||
renderTypstSvg(options: {
|
||||
code: string;
|
||||
options?: {
|
||||
fontUrls?: string[];
|
||||
};
|
||||
}): Promise<{ svg: string }>;
|
||||
}
|
||||
8
packages/frontend/apps/ios/src/plugins/preview/index.ts
Normal file
8
packages/frontend/apps/ios/src/plugins/preview/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
import type { PreviewPlugin } from './definitions';
|
||||
|
||||
const Preview = registerPlugin<PreviewPlugin>('Preview');
|
||||
|
||||
export * from './definitions';
|
||||
export { Preview };
|
||||
Reference in New Issue
Block a user