mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-22 23:30:36 +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:
2703
Cargo.lock
generated
2703
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -36,7 +36,7 @@ resolver = "3"
|
||||
criterion2 = { version = "3", default-features = false }
|
||||
crossbeam-channel = "0.5"
|
||||
dispatch2 = "0.3"
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser", rev = "380beea" }
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.28", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
@@ -59,6 +59,7 @@ resolver = "3"
|
||||
lru = "0.16"
|
||||
matroska = "0.30"
|
||||
memory-indexer = "0.3.0"
|
||||
mermaid-rs-renderer = { git = "https://github.com/toeverything/mermaid-rs-renderer", rev = "fba9097", default-features = false }
|
||||
mimalloc = "0.1"
|
||||
mp4parse = "0.17"
|
||||
nanoid = "0.4"
|
||||
@@ -122,6 +123,14 @@ resolver = "3"
|
||||
tree-sitter-rust = { version = "0.24" }
|
||||
tree-sitter-scala = { version = "0.24" }
|
||||
tree-sitter-typescript = { version = "0.23" }
|
||||
typst = "0.14.2"
|
||||
typst-as-lib = { version = "0.15.4", default-features = false, features = [
|
||||
"packages",
|
||||
"typst-kit-embed-fonts",
|
||||
"typst-kit-fonts",
|
||||
"ureq",
|
||||
] }
|
||||
typst-svg = "0.14.2"
|
||||
uniffi = "0.29"
|
||||
url = { version = "2.5" }
|
||||
uuid = "1.8"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"vite": "^7.2.7",
|
||||
"vite-plugin-istanbul": "^7.2.1",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
|
||||
48
deny.toml
Normal file
48
deny.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[graph]
|
||||
all-features = true
|
||||
exclude-dev = true
|
||||
targets = [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"aarch64-linux-android",
|
||||
"aarch64-apple-ios",
|
||||
"aarch64-apple-ios-sim",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
]
|
||||
confidence-threshold = 0.93
|
||||
unused-allowed-license = "allow"
|
||||
version = 2
|
||||
|
||||
[[licenses.exceptions]]
|
||||
allow = ["AGPL-3.0-only"]
|
||||
crate = "llm_adapter"
|
||||
|
||||
[[licenses.exceptions]]
|
||||
allow = ["AGPL-3.0-or-later"]
|
||||
crate = "memory-indexer"
|
||||
|
||||
[[licenses.exceptions]]
|
||||
allow = ["AGPL-3.0-or-later"]
|
||||
crate = "path-ext"
|
||||
|
||||
[licenses.private]
|
||||
ignore = true
|
||||
@@ -2,6 +2,7 @@
|
||||
edition = "2024"
|
||||
license-file = "LICENSE"
|
||||
name = "affine_server_native"
|
||||
publish = false
|
||||
version = "1.0.0"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -10,6 +10,7 @@ interface TestOps extends OpSchema {
|
||||
add: [{ a: number; b: number }, number];
|
||||
bin: [Uint8Array, Uint8Array];
|
||||
sub: [Uint8Array, number];
|
||||
init: [{ fastText?: boolean } | undefined, { ok: true }];
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
@@ -84,6 +85,55 @@ describe('op client', () => {
|
||||
expect(data.byteLength).toBe(0);
|
||||
});
|
||||
|
||||
it('should send optional payload call with abort signal', async ctx => {
|
||||
const abortController = new AbortController();
|
||||
const result = ctx.producer.call(
|
||||
'init',
|
||||
{ fastText: true },
|
||||
abortController.signal
|
||||
);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "init:1",
|
||||
"name": "init",
|
||||
"payload": {
|
||||
"fastText": true,
|
||||
},
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'init:1',
|
||||
data: { ok: true },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('should send undefined payload for optional input call', async ctx => {
|
||||
const result = ctx.producer.call('init', undefined);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "init:1",
|
||||
"name": "init",
|
||||
"payload": undefined,
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'init:1',
|
||||
data: { ok: true },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('should cancel call', async ctx => {
|
||||
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
||||
|
||||
|
||||
@@ -40,18 +40,14 @@ describe('op consumer', () => {
|
||||
it('should throw if no handler registered', async ctx => {
|
||||
ctx.handlers.call({ type: 'call', id: 'add:1', name: 'add', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"message": "Handler for operation [add] is not registered.",
|
||||
"name": "Error",
|
||||
},
|
||||
"id": "add:1",
|
||||
"type": "return",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(ctx.postMessage.mock.lastCall?.[0]).toMatchObject({
|
||||
type: 'return',
|
||||
id: 'add:1',
|
||||
error: {
|
||||
message: 'Handler for operation [add] is not registered.',
|
||||
name: 'Error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle call message', async ctx => {
|
||||
@@ -73,6 +69,38 @@ describe('op consumer', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should serialize string errors with message', async ctx => {
|
||||
ctx.consumer.register('any', () => {
|
||||
throw 'worker panic';
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchObject({
|
||||
type: 'return',
|
||||
id: 'any:1',
|
||||
error: {
|
||||
name: 'Error',
|
||||
message: 'worker panic',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize plain object errors with fallback message', async ctx => {
|
||||
ctx.consumer.register('any', () => {
|
||||
throw { reason: 'panic', code: 'E_PANIC' };
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
|
||||
const message = ctx.postMessage.mock.calls[0][0]?.error?.message;
|
||||
expect(typeof message).toBe('string');
|
||||
expect(message).toContain('"reason":"panic"');
|
||||
expect(message).toContain('"code":"E_PANIC"');
|
||||
});
|
||||
|
||||
it('should handle cancel message', async ctx => {
|
||||
ctx.consumer.register('add', ({ a, b }, { signal }) => {
|
||||
const { reject, resolve, promise } = Promise.withResolvers<number>();
|
||||
|
||||
@@ -16,6 +16,96 @@ import {
|
||||
} from './message';
|
||||
import type { OpInput, OpNames, OpOutput, OpSchema } from './types';
|
||||
|
||||
const SERIALIZABLE_ERROR_FIELDS = [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
] as const;
|
||||
|
||||
type SerializableErrorShape = Partial<
|
||||
Record<(typeof SERIALIZABLE_ERROR_FIELDS)[number], unknown>
|
||||
> & {
|
||||
name?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function getFallbackErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof error === 'number' ||
|
||||
typeof error === 'boolean' ||
|
||||
typeof error === 'bigint' ||
|
||||
typeof error === 'symbol'
|
||||
) {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
if (error === null || error === undefined) {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonMessage = JSON.stringify(error);
|
||||
if (jsonMessage && jsonMessage !== '{}') {
|
||||
return jsonMessage;
|
||||
}
|
||||
} catch {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
function serializeError(error: unknown): Error {
|
||||
const valueToPick =
|
||||
error && typeof error === 'object'
|
||||
? error
|
||||
: ({} as Record<string, unknown>);
|
||||
const serialized = pick(
|
||||
valueToPick,
|
||||
SERIALIZABLE_ERROR_FIELDS
|
||||
) as SerializableErrorShape;
|
||||
|
||||
if (!serialized.message || typeof serialized.message !== 'string') {
|
||||
serialized.message = getFallbackErrorMessage(error);
|
||||
}
|
||||
|
||||
if (!serialized.name || typeof serialized.name !== 'string') {
|
||||
if (error instanceof Error && error.name) {
|
||||
serialized.name = error.name;
|
||||
} else if (error && typeof error === 'object') {
|
||||
const constructorName = error.constructor?.name;
|
||||
serialized.name =
|
||||
typeof constructorName === 'string' && constructorName.length > 0
|
||||
? constructorName
|
||||
: 'Error';
|
||||
} else {
|
||||
serialized.name = 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!serialized.stacktrace &&
|
||||
error instanceof Error &&
|
||||
typeof error.stack === 'string'
|
||||
) {
|
||||
serialized.stacktrace = error.stack;
|
||||
}
|
||||
|
||||
return serialized as Error;
|
||||
}
|
||||
|
||||
interface OpCallContext {
|
||||
signal: AbortSignal;
|
||||
}
|
||||
@@ -71,15 +161,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
this.port.postMessage({
|
||||
type: 'return',
|
||||
id: msg.id,
|
||||
error: pick(error, [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
]),
|
||||
error: serializeError(error),
|
||||
} satisfies ReturnMessage);
|
||||
},
|
||||
complete: () => {
|
||||
@@ -109,15 +191,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
this.port.postMessage({
|
||||
type: 'error',
|
||||
id: msg.id,
|
||||
error: pick(error, [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
]),
|
||||
error: serializeError(error),
|
||||
} satisfies SubscriptionErrorMessage);
|
||||
},
|
||||
complete: () => {
|
||||
|
||||
@@ -12,7 +12,16 @@ export interface OpSchema {
|
||||
[key: string]: [any, any?];
|
||||
}
|
||||
|
||||
type RequiredInput<In> = In extends void ? [] : In extends never ? [] : [In];
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false;
|
||||
|
||||
type RequiredInput<In> =
|
||||
IsAny<In> extends true
|
||||
? [In]
|
||||
: [In] extends [never]
|
||||
? []
|
||||
: [In] extends [void]
|
||||
? []
|
||||
: [In];
|
||||
|
||||
export type OpNames<T extends OpSchema> = ValuesOf<KeyToKey<T>>;
|
||||
export type OpInput<
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
edition = "2024"
|
||||
license-file = "LICENSE"
|
||||
name = "affine_common"
|
||||
publish = false
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -19,6 +19,7 @@ import app.affine.pro.plugin.AFFiNEThemePlugin
|
||||
import app.affine.pro.plugin.AuthPlugin
|
||||
import app.affine.pro.plugin.HashCashPlugin
|
||||
import app.affine.pro.plugin.NbStorePlugin
|
||||
import app.affine.pro.plugin.PreviewPlugin
|
||||
import app.affine.pro.service.GraphQLService
|
||||
import app.affine.pro.service.SSEService
|
||||
import app.affine.pro.service.WebService
|
||||
@@ -52,6 +53,7 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AFFiNEThemePlugi
|
||||
AuthPlugin::class.java,
|
||||
HashCashPlugin::class.java,
|
||||
NbStorePlugin::class.java,
|
||||
PreviewPlugin::class.java,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.affine.pro.ai.chat
|
||||
|
||||
import com.affine.pro.graphql.GetCopilotHistoriesQuery
|
||||
import com.affine.pro.graphql.fragment.CopilotChatHistory
|
||||
import com.affine.pro.graphql.fragment.CopilotChatMessage
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
@@ -53,7 +51,7 @@ data class ChatMessage(
|
||||
createAt = Clock.System.now(),
|
||||
)
|
||||
|
||||
fun from(message: CopilotChatMessage) = ChatMessage(
|
||||
fun from(message: CopilotChatHistory.Message) = ChatMessage(
|
||||
id = message.id,
|
||||
role = Role.fromValue(message.role),
|
||||
content = message.content,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package app.affine.pro.plugin
|
||||
|
||||
import android.net.Uri
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
import uniffi.affine_mobile_native.renderMermaidPreviewSvg
|
||||
import uniffi.affine_mobile_native.renderTypstPreviewSvg
|
||||
import java.io.File
|
||||
|
||||
private fun JSObject.getOptionalString(key: String): String? {
|
||||
return if (has(key) && !isNull(key)) getString(key) else null
|
||||
}
|
||||
|
||||
private fun JSObject.getOptionalDouble(key: String): Double? {
|
||||
return if (has(key) && !isNull(key)) getDouble(key) else null
|
||||
}
|
||||
|
||||
private fun resolveLocalFontDir(fontUrl: String): String? {
|
||||
val uri = Uri.parse(fontUrl)
|
||||
val path = when {
|
||||
uri.scheme == null -> {
|
||||
val file = File(fontUrl)
|
||||
if (!file.isAbsolute) {
|
||||
return null
|
||||
}
|
||||
file.path
|
||||
}
|
||||
uri.scheme == "file" -> uri.path
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
||||
val file = File(path)
|
||||
val directory = if (file.isDirectory) file else file.parentFile ?: return null
|
||||
return directory.absolutePath
|
||||
}
|
||||
|
||||
private fun JSObject.resolveTypstFontDirs(): List<String>? {
|
||||
if (!has("fontUrls") || isNull("fontUrls")) {
|
||||
return null
|
||||
}
|
||||
|
||||
val fontUrls = optJSONArray("fontUrls")
|
||||
?: throw IllegalArgumentException("Typst preview fontUrls must be an array of strings.")
|
||||
val fontDirs = buildList(fontUrls.length()) {
|
||||
repeat(fontUrls.length()) { index ->
|
||||
val fontUrl = fontUrls.optString(index, null)
|
||||
?: throw IllegalArgumentException("Typst preview fontUrls must be strings.")
|
||||
val fontDir = resolveLocalFontDir(fontUrl)
|
||||
?: throw IllegalArgumentException("Typst preview on mobile only supports local font file URLs or absolute font directories.")
|
||||
add(fontDir)
|
||||
}
|
||||
}
|
||||
return fontDirs.distinct()
|
||||
}
|
||||
|
||||
@CapacitorPlugin(name = "Preview")
|
||||
class PreviewPlugin : Plugin() {
|
||||
|
||||
@PluginMethod
|
||||
fun renderMermaidSvg(call: PluginCall) {
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
val code = call.getStringEnsure("code")
|
||||
val options = call.getObject("options")
|
||||
val svg = renderMermaidPreviewSvg(
|
||||
code = code,
|
||||
theme = options?.getOptionalString("theme"),
|
||||
fontFamily = options?.getOptionalString("fontFamily"),
|
||||
fontSize = options?.getOptionalDouble("fontSize"),
|
||||
)
|
||||
call.resolve(JSObject().apply {
|
||||
put("svg", svg)
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to render Mermaid preview.")
|
||||
call.reject("Failed to render Mermaid preview.", null, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun renderTypstSvg(call: PluginCall) {
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
val code = call.getStringEnsure("code")
|
||||
val options = call.getObject("options")
|
||||
val svg = renderTypstPreviewSvg(
|
||||
code = code,
|
||||
fontDirs = options?.resolveTypstFontDirs(),
|
||||
cacheDir = context.cacheDir.absolutePath,
|
||||
)
|
||||
call.resolve(JSObject().apply {
|
||||
put("svg", svg)
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to render Typst preview.")
|
||||
call.reject("Failed to render Typst preview.", null, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class GraphQLService @Inject constructor() {
|
||||
).mapCatching { data ->
|
||||
data.currentUser?.copilot?.chats?.paginatedCopilotChats?.edges?.map { item -> item.node.copilotChatHistory }?.firstOrNull { history ->
|
||||
history.sessionId == sessionId
|
||||
}?.messages?.map { msg -> msg.copilotChatMessage } ?: emptyList()
|
||||
}?.messages ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getCopilotHistoryIds(
|
||||
|
||||
@@ -792,6 +792,10 @@ internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -816,6 +820,10 @@ internal interface IntegrityCheckingUniffiLib : Library {
|
||||
): Short
|
||||
fun uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool(
|
||||
): Short
|
||||
fun uniffi_affine_mobile_native_checksum_func_render_mermaid_preview_svg(
|
||||
): Short
|
||||
fun uniffi_affine_mobile_native_checksum_func_render_typst_preview_svg(
|
||||
): Short
|
||||
fun uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks(
|
||||
): Short
|
||||
fun uniffi_affine_mobile_native_checksum_method_docstoragepool_connect(
|
||||
@@ -1017,6 +1025,10 @@ fun uniffi_affine_mobile_native_fn_func_hashcash_mint(`resource`: RustBuffer.ByV
|
||||
): RustBuffer.ByValue
|
||||
fun uniffi_affine_mobile_native_fn_func_new_doc_storage_pool(uniffi_out_err: UniffiRustCallStatus,
|
||||
): Pointer
|
||||
fun uniffi_affine_mobile_native_fn_func_render_mermaid_preview_svg(`code`: RustBuffer.ByValue,`theme`: RustBuffer.ByValue,`fontFamily`: RustBuffer.ByValue,`fontSize`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus,
|
||||
): RustBuffer.ByValue
|
||||
fun uniffi_affine_mobile_native_fn_func_render_typst_preview_svg(`code`: RustBuffer.ByValue,`fontDirs`: RustBuffer.ByValue,`cacheDir`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus,
|
||||
): RustBuffer.ByValue
|
||||
fun ffi_affine_mobile_native_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus,
|
||||
): RustBuffer.ByValue
|
||||
fun ffi_affine_mobile_native_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus,
|
||||
@@ -1149,6 +1161,12 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
|
||||
if (lib.uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool() != 32882.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_affine_mobile_native_checksum_func_render_mermaid_preview_svg() != 54334.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_affine_mobile_native_checksum_func_render_typst_preview_svg() != 42796.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks() != 51151.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
@@ -3178,6 +3196,38 @@ public object FfiConverterOptionalLong: FfiConverterRustBuffer<kotlin.Long?> {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
public object FfiConverterOptionalDouble: FfiConverterRustBuffer<kotlin.Double?> {
|
||||
override fun read(buf: ByteBuffer): kotlin.Double? {
|
||||
if (buf.get().toInt() == 0) {
|
||||
return null
|
||||
}
|
||||
return FfiConverterDouble.read(buf)
|
||||
}
|
||||
|
||||
override fun allocationSize(value: kotlin.Double?): ULong {
|
||||
if (value == null) {
|
||||
return 1UL
|
||||
} else {
|
||||
return 1UL + FfiConverterDouble.allocationSize(value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(value: kotlin.Double?, buf: ByteBuffer) {
|
||||
if (value == null) {
|
||||
buf.put(0)
|
||||
} else {
|
||||
buf.put(1)
|
||||
FfiConverterDouble.write(value, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@@ -3584,4 +3634,24 @@ public object FfiConverterSequenceTypeSearchHit: FfiConverterRustBuffer<List<Sea
|
||||
}
|
||||
|
||||
|
||||
@Throws(UniffiException::class) fun `renderMermaidPreviewSvg`(`code`: kotlin.String, `theme`: kotlin.String?, `fontFamily`: kotlin.String?, `fontSize`: kotlin.Double?): kotlin.String {
|
||||
return FfiConverterString.lift(
|
||||
uniffiRustCallWithError(UniffiException) { _status ->
|
||||
UniffiLib.INSTANCE.uniffi_affine_mobile_native_fn_func_render_mermaid_preview_svg(
|
||||
FfiConverterString.lower(`code`),FfiConverterOptionalString.lower(`theme`),FfiConverterOptionalString.lower(`fontFamily`),FfiConverterOptionalDouble.lower(`fontSize`),_status)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Throws(UniffiException::class) fun `renderTypstPreviewSvg`(`code`: kotlin.String, `fontDirs`: List<kotlin.String>?, `cacheDir`: kotlin.String?): kotlin.String {
|
||||
return FfiConverterString.lift(
|
||||
uniffiRustCallWithError(UniffiException) { _status ->
|
||||
UniffiLib.INSTANCE.uniffi_affine_mobile_native_fn_func_render_typst_preview_svg(
|
||||
FfiConverterString.lower(`code`),FfiConverterOptionalSequenceString.lower(`fontDirs`),FfiConverterOptionalString.lower(`cacheDir`),_status)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ServersService,
|
||||
ValidatorProvider,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { registerNativePreviewHandlers } from '@affine/core/modules/code-block-preview-renderer';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
@@ -54,6 +55,7 @@ import { AIButton } from './plugins/ai-button';
|
||||
import { Auth } from './plugins/auth';
|
||||
import { HashCash } from './plugins/hashcash';
|
||||
import { NbStoreNativeDBApis } from './plugins/nbstore';
|
||||
import { Preview } from './plugins/preview';
|
||||
import { writeEndpointToken } from './proxy';
|
||||
|
||||
const storeManagerClient = createStoreManagerClient();
|
||||
@@ -85,6 +87,11 @@ framework.impl(NbstoreProvider, {
|
||||
});
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
registerNativePreviewHandlers({
|
||||
renderMermaidSvg: request => Preview.renderMermaidSvg(request),
|
||||
renderTypstSvg: request => Preview.renderTypstSvg(request),
|
||||
});
|
||||
|
||||
framework.impl(PopupWindowProvider, {
|
||||
open: (url: string) => {
|
||||
InAppBrowser.open({
|
||||
|
||||
@@ -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 }>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
import type { PreviewPlugin } from './definitions';
|
||||
|
||||
const Preview = registerPlugin<PreviewPlugin>('Preview');
|
||||
|
||||
export * from './definitions';
|
||||
export { Preview };
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dialogHandlers } from './dialog';
|
||||
import { dbEventsV1, dbHandlersV1, nbstoreHandlers } from './nbstore';
|
||||
import { previewHandlers } from './preview';
|
||||
import { provideExposed } from './provide';
|
||||
import { workspaceEvents, workspaceHandlers } from './workspace';
|
||||
|
||||
@@ -8,6 +9,7 @@ export const handlers = {
|
||||
nbstore: nbstoreHandlers,
|
||||
workspace: workspaceHandlers,
|
||||
dialog: dialogHandlers,
|
||||
preview: previewHandlers,
|
||||
};
|
||||
|
||||
export const events = {
|
||||
|
||||
69
packages/frontend/apps/electron/src/helper/preview/index.ts
Normal file
69
packages/frontend/apps/electron/src/helper/preview/index.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
type MermaidRenderRequest,
|
||||
type MermaidRenderResult,
|
||||
renderMermaidSvg,
|
||||
renderTypstSvg,
|
||||
type TypstRenderRequest,
|
||||
type TypstRenderResult,
|
||||
} from '@affine/native';
|
||||
|
||||
const TYPST_FONT_DIRS_ENV = 'AFFINE_TYPST_FONT_DIRS';
|
||||
|
||||
function parseTypstFontDirsFromEnv() {
|
||||
const value = process.env[TYPST_FONT_DIRS_ENV];
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.split(path.delimiter)
|
||||
.map(dir => dir.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getTypstFontDirCandidates() {
|
||||
const resourcesPath = process.resourcesPath ?? '';
|
||||
|
||||
return [
|
||||
...parseTypstFontDirsFromEnv(),
|
||||
path.join(resourcesPath, 'fonts'),
|
||||
path.join(resourcesPath, 'js', 'fonts'),
|
||||
path.join(resourcesPath, 'app.asar.unpacked', 'fonts'),
|
||||
path.join(resourcesPath, 'app.asar.unpacked', 'js', 'fonts'),
|
||||
];
|
||||
}
|
||||
|
||||
function resolveTypstFontDirs() {
|
||||
return Array.from(
|
||||
new Set(getTypstFontDirCandidates().map(dir => path.resolve(dir)))
|
||||
).filter(dir => fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory());
|
||||
}
|
||||
|
||||
function withTypstFontDirs(
|
||||
request: TypstRenderRequest,
|
||||
fontDirs: string[]
|
||||
): TypstRenderRequest {
|
||||
const nextOptions = request.options ? { ...request.options } : {};
|
||||
if (!nextOptions.fontDirs?.length) {
|
||||
nextOptions.fontDirs = fontDirs;
|
||||
}
|
||||
return { ...request, options: nextOptions };
|
||||
}
|
||||
|
||||
const typstFontDirs = resolveTypstFontDirs();
|
||||
|
||||
export const previewHandlers = {
|
||||
renderMermaidSvg: async (
|
||||
request: MermaidRenderRequest
|
||||
): Promise<MermaidRenderResult> => {
|
||||
return renderMermaidSvg(request);
|
||||
},
|
||||
renderTypstSvg: async (
|
||||
request: TypstRenderRequest
|
||||
): Promise<TypstRenderResult> => {
|
||||
return renderTypstSvg(withTypstFontDirs(request, typstFontDirs));
|
||||
},
|
||||
};
|
||||
85
packages/frontend/apps/electron/test/helper/preview.spec.ts
Normal file
85
packages/frontend/apps/electron/test/helper/preview.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { native } = vi.hoisted(() => ({
|
||||
native: {
|
||||
renderMermaidSvg: vi.fn(),
|
||||
renderTypstSvg: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@affine/native', () => native);
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
const typstFontDirA = path.join(tmpDir, 'fonts-a');
|
||||
const typstFontDirB = path.join(tmpDir, 'fonts-b');
|
||||
|
||||
async function loadPreviewHandlers() {
|
||||
vi.resetModules();
|
||||
const module = await import('../../src/helper/preview');
|
||||
return module.previewHandlers;
|
||||
}
|
||||
|
||||
describe('helper preview handlers', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.ensureDir(typstFontDirA);
|
||||
await fs.ensureDir(typstFontDirB);
|
||||
process.env.AFFINE_TYPST_FONT_DIRS = [
|
||||
typstFontDirA,
|
||||
typstFontDirB,
|
||||
path.join(tmpDir, 'missing'),
|
||||
].join(path.delimiter);
|
||||
native.renderMermaidSvg.mockReset();
|
||||
native.renderTypstSvg.mockReset();
|
||||
native.renderMermaidSvg.mockReturnValue({
|
||||
svg: '<svg><text>mermaid</text></svg>',
|
||||
});
|
||||
native.renderTypstSvg.mockReturnValue({
|
||||
svg: '<svg><text>typst</text></svg>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
delete process.env.AFFINE_TYPST_FONT_DIRS;
|
||||
await fs.remove(tmpDir);
|
||||
});
|
||||
|
||||
test('passes mermaid request to native renderer', async () => {
|
||||
const previewHandlers = await loadPreviewHandlers();
|
||||
const request = { code: 'flowchart TD; A-->B' };
|
||||
|
||||
await previewHandlers.renderMermaidSvg(request);
|
||||
|
||||
expect(native.renderMermaidSvg).toHaveBeenCalledWith(request);
|
||||
});
|
||||
|
||||
test('injects resolved fontDirs into typst requests', async () => {
|
||||
const previewHandlers = await loadPreviewHandlers();
|
||||
|
||||
await previewHandlers.renderTypstSvg({ code: '= hello' });
|
||||
|
||||
const [request] = native.renderTypstSvg.mock.calls[0];
|
||||
expect(request.options?.fontDirs).toEqual(
|
||||
expect.arrayContaining([
|
||||
path.resolve(typstFontDirA),
|
||||
path.resolve(typstFontDirB),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps explicit typst fontDirs', async () => {
|
||||
const previewHandlers = await loadPreviewHandlers();
|
||||
const request = {
|
||||
code: '= hello',
|
||||
options: {
|
||||
fontDirs: ['/tmp/custom-fonts'],
|
||||
},
|
||||
};
|
||||
|
||||
await previewHandlers.renderTypstSvg(request);
|
||||
|
||||
expect(native.renderTypstSvg).toHaveBeenCalledWith(request);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -47,6 +47,7 @@
|
||||
"@radix-ui/react-toolbar": "^1.1.1",
|
||||
"@sentry/react": "^10.40.0",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/mermaid-wasm": "^0.1.0",
|
||||
"@toeverything/pdf-viewer": "^0.1.1",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
@@ -57,6 +58,7 @@
|
||||
"cmdk": "^1.0.4",
|
||||
"core-js": "^3.39.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.0",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"file-type": "^21.0.0",
|
||||
"filesize": "^10.1.6",
|
||||
@@ -76,7 +78,7 @@
|
||||
"lit": "^3.2.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lottie-react": "^2.4.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"mermaid": "^11.13.0",
|
||||
"mp4-muxer": "^5.2.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderMermaidSvg } from '@affine/core/modules/code-block-preview-renderer/bridge';
|
||||
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { CodeBlockModel } from '@blocksuite/affine/model';
|
||||
@@ -7,7 +8,6 @@ import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import type { Mermaid } from 'mermaid';
|
||||
|
||||
export const CodeBlockMermaidPreview = CodeBlockPreviewExtension(
|
||||
'mermaid',
|
||||
@@ -154,7 +154,6 @@ export class MermaidPreview extends SignalWatcher(
|
||||
@query('.mermaid-preview-container')
|
||||
accessor container!: HTMLDivElement;
|
||||
|
||||
private mermaid: Mermaid | null = null;
|
||||
private retryCount = 0;
|
||||
private readonly maxRetries = 3;
|
||||
private renderTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -169,9 +168,6 @@ export class MermaidPreview extends SignalWatcher(
|
||||
private lastMouseY = 0;
|
||||
|
||||
override firstUpdated(_changedProperties: PropertyValues): void {
|
||||
this._loadMermaid().catch(error => {
|
||||
console.error('Failed to load mermaid in firstUpdated:', error);
|
||||
});
|
||||
this._scheduleRender();
|
||||
this._setupEventListeners();
|
||||
|
||||
@@ -271,7 +267,8 @@ export class MermaidPreview extends SignalWatcher(
|
||||
event.preventDefault();
|
||||
|
||||
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newScale = Math.max(0.1, Math.min(5, this.scale * delta));
|
||||
const previousScale = this.scale;
|
||||
const newScale = Math.max(0.1, Math.min(5, previousScale * delta));
|
||||
|
||||
// calculate mouse position relative to container
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
@@ -284,8 +281,8 @@ export class MermaidPreview extends SignalWatcher(
|
||||
|
||||
// update transform
|
||||
this.scale = newScale;
|
||||
this.translateX = mouseX - scaleCenterX * (newScale / this.scale);
|
||||
this.translateY = mouseY - scaleCenterY * (newScale / this.scale);
|
||||
this.translateX = mouseX - scaleCenterX * (newScale / previousScale);
|
||||
this.translateY = mouseY - scaleCenterY * (newScale / previousScale);
|
||||
|
||||
this._updateTransform();
|
||||
};
|
||||
@@ -309,44 +306,6 @@ export class MermaidPreview extends SignalWatcher(
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadMermaid() {
|
||||
try {
|
||||
// dynamic load mermaid
|
||||
const mermaidModule = await import('mermaid');
|
||||
this.mermaid = mermaidModule.default;
|
||||
|
||||
// initialize mermaid
|
||||
this.mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'strict',
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
},
|
||||
sequence: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
pie: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
journey: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
gitGraph: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load mermaid:', error);
|
||||
this.state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
// prevent duplicate rendering
|
||||
if (this.isRendering) {
|
||||
@@ -356,28 +315,25 @@ export class MermaidPreview extends SignalWatcher(
|
||||
this.isRendering = true;
|
||||
this.state = 'loading';
|
||||
|
||||
if (!this.normalizedMermaidCode) {
|
||||
const code = this.normalizedMermaidCode?.trim();
|
||||
|
||||
if (!code) {
|
||||
this.svgContent = '';
|
||||
this.state = 'fallback';
|
||||
this.isRendering = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.mermaid) {
|
||||
await this._loadMermaid();
|
||||
}
|
||||
if (!this.mermaid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate unique ID
|
||||
const diagramId = `mermaid-diagram-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// generate SVG
|
||||
const { svg } = await this.mermaid.render(
|
||||
diagramId,
|
||||
this.normalizedMermaidCode
|
||||
);
|
||||
const { svg } = await renderMermaidSvg({
|
||||
code,
|
||||
options: {
|
||||
fastText: true,
|
||||
svgOnly: true,
|
||||
theme: 'default',
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
},
|
||||
});
|
||||
|
||||
// update SVG content
|
||||
this.svgContent = svg;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderTypstSvg } from '@affine/core/modules/code-block-preview-renderer/bridge';
|
||||
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { CodeBlockModel } from '@blocksuite/affine/model';
|
||||
@@ -8,8 +9,6 @@ import { property, query, state } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ensureTypstReady, getTypst } from './typst';
|
||||
|
||||
const RENDER_DEBOUNCE_MS = 200;
|
||||
|
||||
export const CodeBlockTypstPreview = CodeBlockPreviewExtension(
|
||||
@@ -378,9 +377,7 @@ ${this.errorMessage}</pre
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureTypstReady();
|
||||
const typst = await getTypst();
|
||||
const svg = await typst.svg({ mainContent: code });
|
||||
const { svg } = await renderTypstSvg({ code });
|
||||
this.svgContent = svg;
|
||||
this.state = 'finish';
|
||||
this._resetView();
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { $typst, type BeforeBuildFn, loadFonts } from '@myriaddreamin/typst.ts';
|
||||
|
||||
const FONT_CDN_URLS = [
|
||||
'https://cdn.affine.pro/fonts/Inter-Regular.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-SemiBold.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-Italic.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff',
|
||||
'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf',
|
||||
] as const;
|
||||
|
||||
const getBeforeBuildHooks = (): BeforeBuildFn[] => [
|
||||
loadFonts([...FONT_CDN_URLS]),
|
||||
];
|
||||
|
||||
const compilerWasmUrl = new URL(
|
||||
'@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const rendererWasmUrl = new URL(
|
||||
'@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
let typstInitPromise: Promise<void> | null = null;
|
||||
|
||||
export async function ensureTypstReady() {
|
||||
if (typstInitPromise) {
|
||||
return typstInitPromise;
|
||||
}
|
||||
|
||||
typstInitPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
$typst.setCompilerInitOptions({
|
||||
beforeBuild: getBeforeBuildHooks(),
|
||||
getModule: () => compilerWasmUrl,
|
||||
});
|
||||
|
||||
$typst.setRendererInitOptions({
|
||||
beforeBuild: getBeforeBuildHooks(),
|
||||
getModule: () => rendererWasmUrl,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
typstInitPromise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return typstInitPromise;
|
||||
}
|
||||
|
||||
export async function getTypst() {
|
||||
await ensureTypstReady();
|
||||
return $typst;
|
||||
}
|
||||
|
||||
export const TYPST_FONT_URLS = FONT_CDN_URLS;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { mermaidRender, typstRender } = vi.hoisted(() => ({
|
||||
mermaidRender: vi.fn(),
|
||||
typstRender: vi.fn(),
|
||||
}));
|
||||
|
||||
const { domPurifySanitize } = vi.hoisted(() => ({
|
||||
domPurifySanitize: vi.fn((value: unknown) => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return value.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
'@affine/core/modules/code-block-preview-renderer/platform-backend',
|
||||
() => ({
|
||||
renderMermaidSvgBackend: mermaidRender,
|
||||
renderTypstSvgBackend: typstRender,
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock('dompurify', () => ({
|
||||
default: {
|
||||
sanitize: domPurifySanitize,
|
||||
},
|
||||
}));
|
||||
|
||||
import { renderMermaidSvg, renderTypstSvg } from './bridge';
|
||||
|
||||
describe('preview render bridge', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
domPurifySanitize.mockImplementation((value: unknown) => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return value.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
});
|
||||
});
|
||||
|
||||
test('uses worker renderers and only sanitizes mermaid output', async () => {
|
||||
mermaidRender.mockResolvedValue({
|
||||
svg: '<svg><script>alert(1)</script><text>mermaid</text></svg>',
|
||||
});
|
||||
typstRender.mockResolvedValue({
|
||||
svg: '<div><script>window.__xss__=1</script><svg><text>typst</text></svg></div>',
|
||||
});
|
||||
|
||||
const mermaid = await renderMermaidSvg({ code: 'flowchart TD;A-->B' });
|
||||
const typst = await renderTypstSvg({ code: '= Title' });
|
||||
|
||||
expect(mermaidRender).toHaveBeenCalledTimes(1);
|
||||
expect(typstRender).toHaveBeenCalledTimes(1);
|
||||
expect(mermaid.svg).toContain('<svg');
|
||||
expect(mermaid.svg).toContain('mermaid');
|
||||
expect(mermaid.svg).not.toContain('<script');
|
||||
expect(typst.svg).toBe(
|
||||
'<div><script>window.__xss__=1</script><svg><text>typst</text></svg></div>'
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when sanitized svg is empty', async () => {
|
||||
mermaidRender.mockResolvedValue({
|
||||
svg: '<div><text>invalid</text></div>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
renderMermaidSvg({ code: 'flowchart TD;A-->B' })
|
||||
).rejects.toThrow('Preview renderer returned invalid SVG.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
renderMermaidSvgBackend,
|
||||
renderTypstSvgBackend,
|
||||
} from '@affine/core/modules/code-block-preview-renderer/platform-backend';
|
||||
import type {
|
||||
MermaidRenderRequest,
|
||||
MermaidRenderResult,
|
||||
} from '@affine/core/modules/mermaid/renderer';
|
||||
import type {
|
||||
TypstRenderRequest,
|
||||
TypstRenderResult,
|
||||
} from '@affine/core/modules/typst/renderer';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function removeForeignObject(root: ParentNode) {
|
||||
root
|
||||
.querySelectorAll('foreignObject, foreignobject')
|
||||
.forEach(element => element.remove());
|
||||
}
|
||||
|
||||
export function sanitizeSvg(svg: string): string {
|
||||
if (
|
||||
typeof DOMParser === 'undefined' ||
|
||||
typeof XMLSerializer === 'undefined'
|
||||
) {
|
||||
const sanitized = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true } });
|
||||
if (typeof sanitized !== 'string' || !/^\s*<svg[\s>]/i.test(sanitized)) {
|
||||
return '';
|
||||
}
|
||||
return sanitized.trim();
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const parsed = parser.parseFromString(svg, 'image/svg+xml');
|
||||
const root = parsed.documentElement;
|
||||
if (!root || root.tagName.toLowerCase() !== 'svg') return '';
|
||||
|
||||
const sanitized = DOMPurify.sanitize(root, { USE_PROFILES: { svg: true } });
|
||||
if (typeof sanitized !== 'string') return '';
|
||||
|
||||
const sanitizedDoc = parser.parseFromString(sanitized, 'image/svg+xml');
|
||||
const sanitizedRoot = sanitizedDoc.documentElement;
|
||||
if (!sanitizedRoot || sanitizedRoot.tagName.toLowerCase() !== 'svg')
|
||||
return '';
|
||||
|
||||
removeForeignObject(sanitizedRoot);
|
||||
return new XMLSerializer().serializeToString(sanitizedRoot).trim();
|
||||
}
|
||||
|
||||
export async function renderMermaidSvg(
|
||||
request: MermaidRenderRequest
|
||||
): Promise<MermaidRenderResult> {
|
||||
const rendered = await renderMermaidSvgBackend(request);
|
||||
|
||||
const sanitizedSvg = sanitizeSvg(rendered.svg);
|
||||
if (!sanitizedSvg) {
|
||||
throw new Error('Preview renderer returned invalid SVG.');
|
||||
}
|
||||
return { svg: sanitizedSvg };
|
||||
}
|
||||
|
||||
export async function renderTypstSvg(
|
||||
request: TypstRenderRequest
|
||||
): Promise<TypstRenderResult> {
|
||||
const rendered = await renderTypstSvgBackend(request);
|
||||
|
||||
return { svg: rendered.svg };
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { initialize, render } = vi.hoisted(() => ({
|
||||
initialize: vi.fn(),
|
||||
render: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('mermaid', () => ({
|
||||
default: {
|
||||
initialize,
|
||||
render,
|
||||
},
|
||||
}));
|
||||
|
||||
import { renderClassicMermaidSvg } from './classic-mermaid';
|
||||
|
||||
describe('renderClassicMermaidSvg', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('serializes initialize and render across concurrent calls', async () => {
|
||||
const events: string[] = [];
|
||||
let releaseFirstRender!: () => void;
|
||||
|
||||
initialize.mockImplementation(config => {
|
||||
events.push(`init:${config.theme}`);
|
||||
});
|
||||
render
|
||||
.mockImplementationOnce(async () => {
|
||||
events.push('render:first:start');
|
||||
await new Promise<void>(resolve => {
|
||||
releaseFirstRender = resolve;
|
||||
});
|
||||
events.push('render:first:end');
|
||||
return { svg: '<svg>first</svg>' };
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
events.push('render:second:start');
|
||||
return { svg: '<svg>second</svg>' };
|
||||
});
|
||||
|
||||
const first = renderClassicMermaidSvg({
|
||||
code: 'flowchart TD;A-->B',
|
||||
options: { theme: 'default' },
|
||||
});
|
||||
const second = renderClassicMermaidSvg({
|
||||
code: 'flowchart TD;B-->C',
|
||||
options: { theme: 'modern' },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(events).toEqual(['init:default', 'render:first:start']);
|
||||
});
|
||||
|
||||
releaseFirstRender();
|
||||
|
||||
await expect(first).resolves.toEqual({ svg: '<svg>first</svg>' });
|
||||
await expect(second).resolves.toEqual({ svg: '<svg>second</svg>' });
|
||||
expect(events).toEqual([
|
||||
'init:default',
|
||||
'render:first:start',
|
||||
'render:first:end',
|
||||
'init:base',
|
||||
'render:second:start',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Mermaid } from 'mermaid';
|
||||
|
||||
import type {
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
MermaidRenderResult,
|
||||
MermaidRenderTheme,
|
||||
} from '../mermaid/renderer';
|
||||
|
||||
let mermaidPromise: Promise<Mermaid> | null = null;
|
||||
let mermaidRenderQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
function toTheme(theme: MermaidRenderTheme | undefined) {
|
||||
return theme === 'modern' ? ('base' as const) : ('default' as const);
|
||||
}
|
||||
|
||||
function createClassicMermaidConfig(options?: MermaidRenderOptions) {
|
||||
return {
|
||||
startOnLoad: false,
|
||||
theme: toTheme(options?.theme),
|
||||
securityLevel: 'strict' as const,
|
||||
fontFamily: options?.fontFamily ?? 'IBM Plex Mono',
|
||||
flowchart: { useMaxWidth: true, htmlLabels: true },
|
||||
sequence: { useMaxWidth: true },
|
||||
gantt: { useMaxWidth: true },
|
||||
pie: { useMaxWidth: true },
|
||||
journey: { useMaxWidth: true },
|
||||
gitGraph: { useMaxWidth: true },
|
||||
};
|
||||
}
|
||||
|
||||
async function loadMermaid() {
|
||||
if (!mermaidPromise) {
|
||||
mermaidPromise = import('mermaid').then(module => module.default);
|
||||
}
|
||||
return mermaidPromise;
|
||||
}
|
||||
|
||||
function createDiagramId() {
|
||||
return `mermaid-diagram-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function enqueueClassicMermaidRender<T>(task: () => Promise<T>): Promise<T> {
|
||||
const run = mermaidRenderQueue.then(task, task);
|
||||
mermaidRenderQueue = run.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return run;
|
||||
}
|
||||
|
||||
export async function renderClassicMermaidSvg(
|
||||
request: MermaidRenderRequest
|
||||
): Promise<MermaidRenderResult> {
|
||||
return enqueueClassicMermaidRender(async () => {
|
||||
const mermaid = await loadMermaid();
|
||||
mermaid.initialize(createClassicMermaidConfig(request.options));
|
||||
|
||||
const { svg } = await mermaid.render(createDiagramId(), request.code);
|
||||
return { svg };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { FeatureFlagService } from '../feature-flag';
|
||||
import { PreviewRendererFeatureSyncService } from './services/preview-renderer-feature-sync';
|
||||
|
||||
export { renderMermaidSvg, renderTypstSvg, sanitizeSvg } from './bridge';
|
||||
export {
|
||||
registerNativePreviewHandlers,
|
||||
setMermaidWasmNativeRendererEnabled,
|
||||
} from './runtime-config';
|
||||
|
||||
export function configureCodeBlockPreviewRendererModule(framework: Framework) {
|
||||
framework.service(PreviewRendererFeatureSyncService, [FeatureFlagService]);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
|
||||
import { renderClassicMermaidSvg } from './classic-mermaid';
|
||||
import { isMermaidWasmNativeRendererEnabled } from './runtime-config';
|
||||
import type { PreviewRenderRequestMap, PreviewRenderResultMap } from './types';
|
||||
|
||||
type DesktopPreviewHandlers = {
|
||||
renderMermaidSvg?: (
|
||||
request: PreviewRenderRequestMap['mermaid']
|
||||
) => Promise<PreviewRenderResultMap['mermaid']>;
|
||||
renderTypstSvg?: (
|
||||
request: PreviewRenderRequestMap['typst']
|
||||
) => Promise<PreviewRenderResultMap['typst']>;
|
||||
};
|
||||
|
||||
type DesktopPreviewApis = {
|
||||
preview?: DesktopPreviewHandlers;
|
||||
};
|
||||
|
||||
function getDesktopPreviewHandlers() {
|
||||
const previewApis = apis as unknown as DesktopPreviewApis;
|
||||
return previewApis.preview ?? null;
|
||||
}
|
||||
|
||||
function getRequiredDesktopHandler<Name extends keyof DesktopPreviewHandlers>(
|
||||
name: Name
|
||||
): NonNullable<DesktopPreviewHandlers[Name]> {
|
||||
const handlers = getDesktopPreviewHandlers();
|
||||
const handler = handlers?.[name];
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
`Electron preview handler "${String(name)}" is unavailable.`
|
||||
);
|
||||
}
|
||||
return handler as NonNullable<DesktopPreviewHandlers[Name]>;
|
||||
}
|
||||
|
||||
export async function renderMermaidSvgBackend(
|
||||
request: PreviewRenderRequestMap['mermaid']
|
||||
): Promise<PreviewRenderResultMap['mermaid']> {
|
||||
if (!isMermaidWasmNativeRendererEnabled()) {
|
||||
return renderClassicMermaidSvg(request);
|
||||
}
|
||||
|
||||
return getRequiredDesktopHandler('renderMermaidSvg')(request);
|
||||
}
|
||||
|
||||
export async function renderTypstSvgBackend(
|
||||
request: PreviewRenderRequestMap['typst']
|
||||
): Promise<PreviewRenderResultMap['typst']> {
|
||||
return getRequiredDesktopHandler('renderTypstSvg')(request);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getNativePreviewHandlers } from './runtime-config';
|
||||
import type { PreviewRenderRequestMap, PreviewRenderResultMap } from './types';
|
||||
|
||||
function getRequiredNativeHandler<
|
||||
Name extends keyof NonNullable<ReturnType<typeof getNativePreviewHandlers>>,
|
||||
>(name: Name) {
|
||||
const handler = getNativePreviewHandlers()?.[name];
|
||||
if (!handler) {
|
||||
throw new Error(`Mobile preview handler "${String(name)}" is unavailable.`);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
export async function renderMermaidSvgBackend(
|
||||
request: PreviewRenderRequestMap['mermaid']
|
||||
): Promise<PreviewRenderResultMap['mermaid']> {
|
||||
return getRequiredNativeHandler('renderMermaidSvg')(request);
|
||||
}
|
||||
|
||||
export async function renderTypstSvgBackend(
|
||||
request: PreviewRenderRequestMap['typst']
|
||||
): Promise<PreviewRenderResultMap['typst']> {
|
||||
return getRequiredNativeHandler('renderTypstSvg')(request);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getMermaidRenderer } from '@affine/core/modules/mermaid/renderer';
|
||||
import { getTypstRenderer } from '@affine/core/modules/typst/renderer';
|
||||
|
||||
import { renderClassicMermaidSvg } from './classic-mermaid';
|
||||
import { isMermaidWasmNativeRendererEnabled } from './runtime-config';
|
||||
import type { PreviewRenderRequestMap, PreviewRenderResultMap } from './types';
|
||||
|
||||
export async function renderMermaidSvgBackend(
|
||||
request: PreviewRenderRequestMap['mermaid']
|
||||
): Promise<PreviewRenderResultMap['mermaid']> {
|
||||
if (!isMermaidWasmNativeRendererEnabled()) {
|
||||
return renderClassicMermaidSvg(request);
|
||||
}
|
||||
|
||||
return getMermaidRenderer().render(request);
|
||||
}
|
||||
|
||||
export async function renderTypstSvgBackend(
|
||||
request: PreviewRenderRequestMap['typst']
|
||||
): Promise<PreviewRenderResultMap['typst']> {
|
||||
return getTypstRenderer().render(request);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type {
|
||||
MermaidRenderRequest,
|
||||
MermaidRenderResult,
|
||||
} from '@affine/core/modules/mermaid/renderer';
|
||||
import type {
|
||||
TypstRenderRequest,
|
||||
TypstRenderResult,
|
||||
} from '@affine/core/modules/typst/renderer';
|
||||
|
||||
type NativePreviewHandlers = {
|
||||
renderMermaidSvg?: (
|
||||
request: MermaidRenderRequest
|
||||
) => Promise<MermaidRenderResult>;
|
||||
renderTypstSvg?: (request: TypstRenderRequest) => Promise<TypstRenderResult>;
|
||||
};
|
||||
|
||||
let enableMermaidWasmNativeRenderer =
|
||||
BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid;
|
||||
let nativePreviewHandlers: NativePreviewHandlers | null = null;
|
||||
|
||||
export function setMermaidWasmNativeRendererEnabled(enabled: boolean) {
|
||||
enableMermaidWasmNativeRenderer = enabled;
|
||||
}
|
||||
|
||||
export function isMermaidWasmNativeRendererEnabled() {
|
||||
return enableMermaidWasmNativeRenderer;
|
||||
}
|
||||
|
||||
export function registerNativePreviewHandlers(
|
||||
handlers: NativePreviewHandlers | null
|
||||
) {
|
||||
nativePreviewHandlers = handlers;
|
||||
}
|
||||
|
||||
export function getNativePreviewHandlers() {
|
||||
return nativePreviewHandlers;
|
||||
}
|
||||
|
||||
export type { NativePreviewHandlers };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { OnEvent, Service } from '@toeverything/infra';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
|
||||
import type { FeatureFlagService } from '../../feature-flag';
|
||||
import { ApplicationStarted } from '../../lifecycle';
|
||||
import { setMermaidWasmNativeRendererEnabled } from '../runtime-config';
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.syncFlag)
|
||||
export class PreviewRendererFeatureSyncService extends Service {
|
||||
constructor(private readonly featureFlagService: FeatureFlagService) {
|
||||
super();
|
||||
}
|
||||
|
||||
syncFlag() {
|
||||
const mermaidFlag =
|
||||
this.featureFlagService.flags.enable_mermaid_wasm_native_renderer;
|
||||
|
||||
setMermaidWasmNativeRendererEnabled(!!mermaidFlag.value);
|
||||
const subscription = mermaidFlag.$.pipe(distinctUntilChanged()).subscribe(
|
||||
enabled => {
|
||||
setMermaidWasmNativeRendererEnabled(!!enabled);
|
||||
}
|
||||
);
|
||||
this.disposables.push(() => subscription.unsubscribe());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type {
|
||||
MermaidRenderRequest,
|
||||
MermaidRenderResult,
|
||||
} from '@affine/core/modules/mermaid/renderer';
|
||||
import type {
|
||||
TypstRenderRequest,
|
||||
TypstRenderResult,
|
||||
} from '@affine/core/modules/typst/renderer';
|
||||
|
||||
export type PreviewRenderRequestMap = {
|
||||
mermaid: MermaidRenderRequest;
|
||||
typst: TypstRenderRequest;
|
||||
};
|
||||
|
||||
export type PreviewRenderResultMap = {
|
||||
mermaid: MermaidRenderResult;
|
||||
typst: TypstRenderResult;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import type { FlagInfo } from './types';
|
||||
const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary';
|
||||
const isMobile = BUILD_CONFIG.isMobileEdition;
|
||||
const isIOS = BUILD_CONFIG.isIOS;
|
||||
const isAndroid = BUILD_CONFIG.isAndroid;
|
||||
|
||||
export const AFFINE_FLAGS = {
|
||||
enable_ai: {
|
||||
@@ -203,6 +204,14 @@ export const AFFINE_FLAGS = {
|
||||
configurable: isMobile && isIOS,
|
||||
defaultState: isMobile && isIOS,
|
||||
},
|
||||
enable_mermaid_wasm_native_renderer: {
|
||||
category: 'affine',
|
||||
displayName: 'Enable Native Mermaid Renderer',
|
||||
description:
|
||||
'Use the new Mermaid renderer backend. Web uses WASM, desktop uses native, and mobile always uses native. The native renderer is more than 10x faster, but its styling/aesthetic quality and the types of graphics it supports are not as good as the JS version.',
|
||||
configurable: !isIOS && !isAndroid,
|
||||
defaultState: isIOS || isAndroid,
|
||||
},
|
||||
enable_turbo_renderer: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_turbo_renderer',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { configureAppSidebarModule } from './app-sidebar';
|
||||
import { configAtMenuConfigModule } from './at-menu-config';
|
||||
import { configureBlobManagementModule } from './blob-management';
|
||||
import { configureCloudModule } from './cloud';
|
||||
import { configureCodeBlockPreviewRendererModule } from './code-block-preview-renderer';
|
||||
import { configureCollectionModule } from './collection';
|
||||
import { configureCollectionRulesModule } from './collection-rules';
|
||||
import { configureCommentModule } from './comment';
|
||||
@@ -77,6 +78,7 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureGlobalContextModule(framework);
|
||||
configureLifecycleModule(framework);
|
||||
configureFeatureFlagModule(framework);
|
||||
configureCodeBlockPreviewRendererModule(framework);
|
||||
configureCollectionModule(framework);
|
||||
configureNavigationModule(framework);
|
||||
configureTagModule(framework);
|
||||
|
||||
39
packages/frontend/core/src/modules/mermaid/renderer/index.ts
Normal file
39
packages/frontend/core/src/modules/mermaid/renderer/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
|
||||
import type {
|
||||
MermaidOps,
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
} from './types';
|
||||
|
||||
class MermaidRenderer extends WorkerOpRenderer<MermaidOps> {
|
||||
constructor() {
|
||||
super('mermaid');
|
||||
}
|
||||
|
||||
init(options?: MermaidRenderOptions) {
|
||||
return this.ensureInitialized(() => this.call('init', options));
|
||||
}
|
||||
|
||||
async render(request: MermaidRenderRequest) {
|
||||
await this.init();
|
||||
return this.call('render', request);
|
||||
}
|
||||
}
|
||||
|
||||
let sharedMermaidRenderer: MermaidRenderer | null = null;
|
||||
|
||||
export function getMermaidRenderer() {
|
||||
if (!sharedMermaidRenderer) {
|
||||
sharedMermaidRenderer = new MermaidRenderer();
|
||||
}
|
||||
return sharedMermaidRenderer;
|
||||
}
|
||||
|
||||
export type {
|
||||
MermaidOps,
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
MermaidRenderResult,
|
||||
MermaidRenderTheme,
|
||||
MermaidTextMetrics,
|
||||
} from './types';
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { MessageCommunicapable } from '@toeverything/infra/op';
|
||||
import { OpConsumer } from '@toeverything/infra/op';
|
||||
import initMmdr, { render_mermaid_svg } from '@toeverything/mermaid-wasm';
|
||||
|
||||
import type {
|
||||
MermaidOps,
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_RENDER_OPTIONS: MermaidRenderOptions = {
|
||||
fastText: true,
|
||||
svgOnly: true,
|
||||
theme: 'modern',
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
};
|
||||
|
||||
function mergeOptions(
|
||||
base: MermaidRenderOptions,
|
||||
override: MermaidRenderOptions | undefined
|
||||
): MermaidRenderOptions {
|
||||
if (!override) {
|
||||
return base;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
textMetrics: override.textMetrics ?? base.textMetrics,
|
||||
};
|
||||
}
|
||||
|
||||
class MermaidRendererBackend extends OpConsumer<MermaidOps> {
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private options: MermaidRenderOptions = DEFAULT_RENDER_OPTIONS;
|
||||
|
||||
constructor(port: MessageCommunicapable) {
|
||||
super(port);
|
||||
this.register('init', this.init.bind(this));
|
||||
this.register('render', this.render.bind(this));
|
||||
}
|
||||
|
||||
private ensureReady() {
|
||||
if (!this.initPromise) {
|
||||
this.initPromise = initMmdr().then(() => undefined);
|
||||
}
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
async init(options?: MermaidRenderOptions) {
|
||||
this.options = mergeOptions(DEFAULT_RENDER_OPTIONS, options);
|
||||
await this.ensureReady();
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
async render({ code, options }: MermaidRenderRequest) {
|
||||
await this.ensureReady();
|
||||
const mergedOptions = mergeOptions(this.options, options);
|
||||
const svg = render_mermaid_svg(code, JSON.stringify(mergedOptions));
|
||||
return { svg };
|
||||
}
|
||||
}
|
||||
|
||||
new MermaidRendererBackend(self as MessageCommunicapable);
|
||||
32
packages/frontend/core/src/modules/mermaid/renderer/types.ts
Normal file
32
packages/frontend/core/src/modules/mermaid/renderer/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type MermaidTextMetrics = {
|
||||
ascii: number;
|
||||
cjk: number;
|
||||
space: number;
|
||||
};
|
||||
|
||||
export type MermaidRenderTheme = 'modern' | 'default';
|
||||
|
||||
export type MermaidRenderOptions = {
|
||||
fastText?: boolean;
|
||||
svgOnly?: boolean;
|
||||
textMetrics?: MermaidTextMetrics;
|
||||
theme?: MermaidRenderTheme;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
};
|
||||
|
||||
export type MermaidRenderRequest = {
|
||||
code: string;
|
||||
options?: MermaidRenderOptions;
|
||||
};
|
||||
|
||||
export type MermaidRenderResult = {
|
||||
svg: string;
|
||||
};
|
||||
|
||||
export interface MermaidOps extends OpSchema {
|
||||
init: [MermaidRenderOptions | undefined, { ok: true }];
|
||||
render: [MermaidRenderRequest, MermaidRenderResult];
|
||||
}
|
||||
@@ -1,2 +1,10 @@
|
||||
export { PDFRenderer } from './renderer';
|
||||
export type { PDFMeta, RenderedPage, RenderPageOpts } from './types';
|
||||
import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
|
||||
import type { PDFOps } from './types';
|
||||
|
||||
export class PDFRenderer extends WorkerOpRenderer<PDFOps> {
|
||||
constructor() {
|
||||
super('pdf');
|
||||
}
|
||||
}
|
||||
|
||||
export type { PDFMeta, PDFOps, RenderedPage, RenderPageOpts } from './types';
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
import type { PDFMeta, RenderedPage, RenderPageOpts } from './types';
|
||||
|
||||
export interface ClientOps extends OpSchema {
|
||||
open: [{ data: ArrayBuffer }, PDFMeta];
|
||||
render: [RenderPageOpts, RenderedPage];
|
||||
}
|
||||
@@ -23,10 +23,9 @@ import {
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
|
||||
import type { ClientOps } from './ops';
|
||||
import type { PDFMeta, RenderPageOpts } from './types';
|
||||
import type { PDFMeta, PDFOps, RenderPageOpts } from './types';
|
||||
|
||||
class PDFRendererBackend extends OpConsumer<ClientOps> {
|
||||
class PDFRendererBackend extends OpConsumer<PDFOps> {
|
||||
constructor(port: MessageCommunicapable) {
|
||||
super(port);
|
||||
this.register('open', this.open.bind(this));
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { getWorkerUrl } from '@affine/env/worker';
|
||||
import { OpClient } from '@toeverything/infra/op';
|
||||
|
||||
import type { ClientOps } from './ops';
|
||||
|
||||
export class PDFRenderer extends OpClient<ClientOps> {
|
||||
private readonly worker: Worker;
|
||||
|
||||
constructor() {
|
||||
const worker = new Worker(getWorkerUrl('pdf'));
|
||||
super(worker);
|
||||
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
super.destroy();
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type PageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -21,3 +23,8 @@ export type RenderPageOpts = {
|
||||
export type RenderedPage = {
|
||||
bitmap: ImageBitmap;
|
||||
};
|
||||
|
||||
export interface PDFOps extends OpSchema {
|
||||
open: [{ data: ArrayBuffer }, PDFMeta];
|
||||
render: [RenderPageOpts, RenderedPage];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { WorkerOpRenderer } from './worker-op-renderer';
|
||||
|
||||
vi.mock('@affine/env/worker', () => ({
|
||||
getWorkerUrl: vi.fn(() => '/worker.js'),
|
||||
}));
|
||||
|
||||
class MockWorker {
|
||||
addEventListener = vi.fn();
|
||||
postMessage = vi.fn();
|
||||
removeEventListener = vi.fn();
|
||||
terminate = vi.fn();
|
||||
}
|
||||
|
||||
class TestRenderer extends WorkerOpRenderer<{
|
||||
init: [undefined, { ok: true }];
|
||||
}> {
|
||||
constructor() {
|
||||
super('test');
|
||||
}
|
||||
|
||||
init() {
|
||||
return this.ensureInitialized(async () => {
|
||||
return { ok: true } as const;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('WorkerOpRenderer', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Worker', MockWorker);
|
||||
});
|
||||
|
||||
test('rejects initialization after destroy', async () => {
|
||||
const renderer = new TestRenderer();
|
||||
|
||||
renderer.destroy();
|
||||
|
||||
await expect(renderer.init()).rejects.toThrow('renderer destroyed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getWorkerUrl } from '@affine/env/worker';
|
||||
import { OpClient, type OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
type InitTask = () => Promise<unknown>;
|
||||
|
||||
export abstract class WorkerOpRenderer<
|
||||
Ops extends OpSchema,
|
||||
> extends OpClient<Ops> {
|
||||
private readonly worker: Worker;
|
||||
private destroyed = false;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
|
||||
protected constructor(workerName: string) {
|
||||
const worker = new Worker(getWorkerUrl(workerName));
|
||||
super(worker);
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
protected ensureInitialized(task: InitTask) {
|
||||
if (this.destroyed) return Promise.reject(new Error('renderer destroyed'));
|
||||
if (!this.initPromise) {
|
||||
this.initPromise = task()
|
||||
.then(() => undefined)
|
||||
.catch(error => {
|
||||
this.initPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
protected resetInitialization() {
|
||||
this.initPromise = null;
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
super.destroy();
|
||||
this.worker.terminate();
|
||||
this.resetInitialization();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
33
packages/frontend/core/src/modules/typst/renderer/index.ts
Normal file
33
packages/frontend/core/src/modules/typst/renderer/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
|
||||
import type { TypstOps, TypstRenderOptions, TypstRenderRequest } from './types';
|
||||
|
||||
class TypstRenderer extends WorkerOpRenderer<TypstOps> {
|
||||
constructor() {
|
||||
super('typst');
|
||||
}
|
||||
|
||||
init(options?: TypstRenderOptions) {
|
||||
return this.ensureInitialized(() => this.call('init', options));
|
||||
}
|
||||
|
||||
async render(request: TypstRenderRequest) {
|
||||
await this.init();
|
||||
return this.call('render', request);
|
||||
}
|
||||
}
|
||||
|
||||
let sharedTypstRenderer: TypstRenderer | null = null;
|
||||
|
||||
export function getTypstRenderer() {
|
||||
if (!sharedTypstRenderer) {
|
||||
sharedTypstRenderer = new TypstRenderer();
|
||||
}
|
||||
return sharedTypstRenderer;
|
||||
}
|
||||
|
||||
export type {
|
||||
TypstOps,
|
||||
TypstRenderOptions,
|
||||
TypstRenderRequest,
|
||||
TypstRenderResult,
|
||||
} from './types';
|
||||
@@ -0,0 +1,84 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { loadFonts, setCompilerInitOptions, setRendererInitOptions, svg } =
|
||||
vi.hoisted(() => ({
|
||||
loadFonts: vi.fn((fontUrls: string[]) => ({ fontUrls })),
|
||||
setCompilerInitOptions: vi.fn(),
|
||||
setRendererInitOptions: vi.fn(),
|
||||
svg: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@myriaddreamin/typst.ts', () => ({
|
||||
$typst: {
|
||||
setCompilerInitOptions,
|
||||
setRendererInitOptions,
|
||||
svg,
|
||||
},
|
||||
loadFonts,
|
||||
}));
|
||||
|
||||
import { ensureTypstReady, renderTypstSvgWithOptions } from './runtime';
|
||||
|
||||
describe('typst runtime', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
svg.mockResolvedValue('<svg />');
|
||||
});
|
||||
|
||||
test('reconfigures typst when fontUrls change', async () => {
|
||||
await ensureTypstReady(['font-a']);
|
||||
await ensureTypstReady(['font-b']);
|
||||
|
||||
expect(loadFonts).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
['font-a'],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(loadFonts).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
['font-b'],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(setCompilerInitOptions).toHaveBeenCalledTimes(2);
|
||||
expect(setRendererInitOptions).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('serializes typst renders that need different configuration', async () => {
|
||||
const events: string[] = [];
|
||||
let releaseFirstRender!: () => void;
|
||||
|
||||
svg.mockImplementationOnce(async () => {
|
||||
events.push('svg:first:start');
|
||||
await new Promise<void>(resolve => {
|
||||
releaseFirstRender = resolve;
|
||||
});
|
||||
events.push('svg:first:end');
|
||||
return '<svg>first</svg>';
|
||||
});
|
||||
svg.mockImplementationOnce(async () => {
|
||||
events.push('svg:second:start');
|
||||
return '<svg>second</svg>';
|
||||
});
|
||||
|
||||
const first = renderTypstSvgWithOptions('= First', {
|
||||
fontUrls: ['font-a'],
|
||||
});
|
||||
const second = renderTypstSvgWithOptions('= Second', {
|
||||
fontUrls: ['font-b'],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(events).toEqual(['svg:first:start']);
|
||||
});
|
||||
|
||||
releaseFirstRender();
|
||||
|
||||
await expect(first).resolves.toEqual({ svg: '<svg>first</svg>' });
|
||||
await expect(second).resolves.toEqual({ svg: '<svg>second</svg>' });
|
||||
expect(events).toEqual([
|
||||
'svg:first:start',
|
||||
'svg:first:end',
|
||||
'svg:second:start',
|
||||
]);
|
||||
});
|
||||
});
|
||||
209
packages/frontend/core/src/modules/typst/renderer/runtime.ts
Normal file
209
packages/frontend/core/src/modules/typst/renderer/runtime.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { $typst, type BeforeBuildFn, loadFonts } from '@myriaddreamin/typst.ts';
|
||||
|
||||
import type { TypstRenderOptions } from './types';
|
||||
|
||||
export const DEFAULT_TYPST_FONT_URLS = [
|
||||
'https://cdn.affine.pro/fonts/Inter-Regular.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-SemiBold.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-Italic.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff',
|
||||
'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf',
|
||||
] as const;
|
||||
|
||||
export const DEFAULT_TYPST_RENDER_OPTIONS: TypstRenderOptions = {
|
||||
fontUrls: [...DEFAULT_TYPST_FONT_URLS],
|
||||
};
|
||||
|
||||
const DEFAULT_FONT_FALLBACKS: Record<string, string> = {
|
||||
'Inter-Regular.woff': 'Inter-Regular.woff2',
|
||||
'Inter-SemiBold.woff': 'Inter-SemiBold.woff2',
|
||||
'Inter-Italic.woff': 'Inter-Italic.woff2',
|
||||
'Inter-SemiBoldItalic.woff': 'Inter-SemiBoldItalic.woff2',
|
||||
'SarasaGothicCL-Regular.ttf': 'Inter-Regular.woff2',
|
||||
'Inter-Regular.woff2': 'Inter-Regular.woff2',
|
||||
'Inter-SemiBold.woff2': 'Inter-SemiBold.woff2',
|
||||
'Inter-Italic.woff2': 'Inter-Italic.woff2',
|
||||
'Inter-SemiBoldItalic.woff2': 'Inter-SemiBoldItalic.woff2',
|
||||
};
|
||||
|
||||
const compilerWasmUrl = new URL(
|
||||
'@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const rendererWasmUrl = new URL(
|
||||
'@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
type TypstWasmModuleUrls = {
|
||||
compilerWasmUrl?: string;
|
||||
rendererWasmUrl?: string;
|
||||
};
|
||||
|
||||
type TypstInitState = {
|
||||
key: string;
|
||||
promise: Promise<void>;
|
||||
};
|
||||
|
||||
let typstInitState: TypstInitState | null = null;
|
||||
let typstRenderQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
function extractInputUrl(input: RequestInfo | URL): string | null {
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if (typeof input === 'string') {
|
||||
return input;
|
||||
}
|
||||
if (typeof Request !== 'undefined' && input instanceof Request) {
|
||||
return input.url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveLocalFallbackFontUrl(sourceUrl: string): string | null {
|
||||
if (typeof location === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = new URL(sourceUrl, location.href);
|
||||
const fileName = source.pathname.split('/').at(-1);
|
||||
if (!fileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackFileName = DEFAULT_FONT_FALLBACKS[fileName];
|
||||
if (!fallbackFileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workerUrl = new URL(location.href);
|
||||
const jsPathMarker = '/js/';
|
||||
const markerIndex = workerUrl.pathname.lastIndexOf(jsPathMarker);
|
||||
const basePath =
|
||||
markerIndex >= 0 ? workerUrl.pathname.slice(0, markerIndex + 1) : '/';
|
||||
|
||||
return new URL(
|
||||
`${basePath}fonts/${fallbackFileName}`,
|
||||
workerUrl.origin
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function createTypstFontFetcher(baseFetcher: typeof fetch = fetch) {
|
||||
return async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const sourceUrl = extractInputUrl(input);
|
||||
const fallbackUrl = sourceUrl
|
||||
? resolveLocalFallbackFontUrl(sourceUrl)
|
||||
: null;
|
||||
|
||||
try {
|
||||
const response = await baseFetcher(input, init);
|
||||
if (!fallbackUrl || response.ok || fallbackUrl === sourceUrl) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const fallbackResponse = await baseFetcher(fallbackUrl, init);
|
||||
return fallbackResponse.ok ? fallbackResponse : response;
|
||||
} catch (error) {
|
||||
if (!fallbackUrl || fallbackUrl === sourceUrl) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return baseFetcher(fallbackUrl, init);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeTypstRenderOptions(
|
||||
base: TypstRenderOptions,
|
||||
override: TypstRenderOptions | undefined
|
||||
): TypstRenderOptions {
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
fontUrls: override?.fontUrls ?? base.fontUrls,
|
||||
};
|
||||
}
|
||||
|
||||
function getBeforeBuildHooks(fontUrls: string[]): BeforeBuildFn[] {
|
||||
return [
|
||||
loadFonts([...fontUrls], {
|
||||
assets: ['text'],
|
||||
fetcher: createTypstFontFetcher(),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function createTypstInitKey(
|
||||
fontUrls: string[],
|
||||
wasmModuleUrls: TypstWasmModuleUrls
|
||||
) {
|
||||
return JSON.stringify({
|
||||
fontUrls,
|
||||
compilerWasmUrl: wasmModuleUrls.compilerWasmUrl ?? compilerWasmUrl,
|
||||
rendererWasmUrl: wasmModuleUrls.rendererWasmUrl ?? rendererWasmUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function enqueueTypstRender<T>(task: () => Promise<T>): Promise<T> {
|
||||
const run = typstRenderQueue.then(task, task);
|
||||
typstRenderQueue = run.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return run;
|
||||
}
|
||||
|
||||
export async function ensureTypstReady(
|
||||
fontUrls: string[],
|
||||
wasmModuleUrls: TypstWasmModuleUrls = {}
|
||||
) {
|
||||
const key = createTypstInitKey(fontUrls, wasmModuleUrls);
|
||||
if (typstInitState?.key === key) {
|
||||
return typstInitState.promise;
|
||||
}
|
||||
|
||||
const promise = Promise.resolve()
|
||||
.then(() => {
|
||||
const compilerBeforeBuild = getBeforeBuildHooks(fontUrls);
|
||||
|
||||
$typst.setCompilerInitOptions({
|
||||
beforeBuild: compilerBeforeBuild,
|
||||
getModule: () => wasmModuleUrls.compilerWasmUrl ?? compilerWasmUrl,
|
||||
});
|
||||
$typst.setRendererInitOptions({
|
||||
getModule: () => wasmModuleUrls.rendererWasmUrl ?? rendererWasmUrl,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (typstInitState?.key === key) {
|
||||
typstInitState = null;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
typstInitState = { key, promise };
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function renderTypstSvgWithOptions(
|
||||
code: string,
|
||||
options: TypstRenderOptions | undefined,
|
||||
wasmModuleUrls?: TypstWasmModuleUrls
|
||||
) {
|
||||
const resolvedOptions = mergeTypstRenderOptions(
|
||||
DEFAULT_TYPST_RENDER_OPTIONS,
|
||||
options
|
||||
);
|
||||
return enqueueTypstRender(async () => {
|
||||
await ensureTypstReady(
|
||||
resolvedOptions.fontUrls ?? [...DEFAULT_TYPST_FONT_URLS],
|
||||
wasmModuleUrls
|
||||
);
|
||||
const svg = await $typst.svg({
|
||||
mainContent: code,
|
||||
});
|
||||
return { svg };
|
||||
});
|
||||
}
|
||||
19
packages/frontend/core/src/modules/typst/renderer/types.ts
Normal file
19
packages/frontend/core/src/modules/typst/renderer/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type TypstRenderOptions = {
|
||||
fontUrls?: string[];
|
||||
};
|
||||
|
||||
export type TypstRenderRequest = {
|
||||
code: string;
|
||||
options?: TypstRenderOptions;
|
||||
};
|
||||
|
||||
export type TypstRenderResult = {
|
||||
svg: string;
|
||||
};
|
||||
|
||||
export interface TypstOps extends OpSchema {
|
||||
init: [TypstRenderOptions | undefined, { ok: true }];
|
||||
render: [TypstRenderRequest, TypstRenderResult];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { MessageCommunicapable } from '@toeverything/infra/op';
|
||||
import { OpConsumer } from '@toeverything/infra/op';
|
||||
|
||||
import {
|
||||
DEFAULT_TYPST_RENDER_OPTIONS,
|
||||
ensureTypstReady,
|
||||
mergeTypstRenderOptions,
|
||||
renderTypstSvgWithOptions,
|
||||
} from './runtime';
|
||||
import type { TypstOps, TypstRenderOptions, TypstRenderRequest } from './types';
|
||||
|
||||
class TypstRendererBackend extends OpConsumer<TypstOps> {
|
||||
private options: TypstRenderOptions = DEFAULT_TYPST_RENDER_OPTIONS;
|
||||
|
||||
constructor(port: MessageCommunicapable) {
|
||||
super(port);
|
||||
this.register('init', this.init.bind(this));
|
||||
this.register('render', this.render.bind(this));
|
||||
}
|
||||
|
||||
async init(options?: TypstRenderOptions) {
|
||||
this.options = mergeTypstRenderOptions(
|
||||
DEFAULT_TYPST_RENDER_OPTIONS,
|
||||
options
|
||||
);
|
||||
await ensureTypstReady(
|
||||
this.options.fontUrls ?? [
|
||||
...(DEFAULT_TYPST_RENDER_OPTIONS.fontUrls ?? []),
|
||||
]
|
||||
);
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
async render({ code, options }: TypstRenderRequest) {
|
||||
const mergedOptions = mergeTypstRenderOptions(this.options, options);
|
||||
return renderTypstSvgWithOptions(code, mergedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
new TypstRendererBackend(self as MessageCommunicapable);
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "affine_mobile_native"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
@@ -40,7 +41,11 @@ objc2-foundation = { workspace = true, features = [
|
||||
homedir = { workspace = true }
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||
lru = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
mermaid-rs-renderer = { workspace = true }
|
||||
typst = { workspace = true }
|
||||
typst-as-lib = { workspace = true }
|
||||
typst-svg = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod error;
|
||||
mod ffi_types;
|
||||
mod payload_codec;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
mod preview;
|
||||
mod storage;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -14,6 +16,8 @@ pub use error::UniffiError;
|
||||
pub use ffi_types::{
|
||||
Blob, BlockInfo, CrawlResult, DocClock, DocRecord, DocUpdate, ListedBlob, MatchRange, SearchHit, SetBlob,
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub use preview::{render_mermaid_preview_svg, render_typst_preview_svg};
|
||||
pub use storage::{DocStoragePool, new_doc_storage_pool};
|
||||
|
||||
uniffi::setup_scaffolding!("affine_mobile_native");
|
||||
|
||||
155
packages/frontend/mobile-native/src/preview.rs
Normal file
155
packages/frontend/mobile-native/src/preview.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::{borrow::Cow, path::PathBuf};
|
||||
|
||||
use mermaid_rs_renderer::RenderOptions;
|
||||
use typst::{
|
||||
diag::FileResult,
|
||||
foundations::Bytes,
|
||||
layout::{Abs, PagedDocument},
|
||||
syntax::{FileId, Source},
|
||||
};
|
||||
use typst_as_lib::{
|
||||
TypstEngine,
|
||||
cached_file_resolver::{CachedFileResolver, IntoCachedFileResolver},
|
||||
file_resolver::FileResolver,
|
||||
package_resolver::{FileSystemCache, PackageResolver},
|
||||
typst_kit_options::TypstKitFontOptions,
|
||||
};
|
||||
|
||||
use crate::{Result, UniffiError};
|
||||
|
||||
const TYPST_PACKAGE_CACHE_DIR: &str = "typst-package-cache";
|
||||
|
||||
enum MobileTypstPackageResolver {
|
||||
FileSystem(CachedFileResolver<PackageResolver<FileSystemCache>>),
|
||||
InMemory(CachedFileResolver<PackageResolver<typst_as_lib::package_resolver::InMemoryCache>>),
|
||||
}
|
||||
|
||||
impl FileResolver for MobileTypstPackageResolver {
|
||||
fn resolve_binary(&self, id: FileId) -> FileResult<Cow<'_, Bytes>> {
|
||||
match self {
|
||||
Self::FileSystem(resolver) => resolver.resolve_binary(id),
|
||||
Self::InMemory(resolver) => resolver.resolve_binary(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_source(&self, id: FileId) -> FileResult<Cow<'_, Source>> {
|
||||
match self {
|
||||
Self::FileSystem(resolver) => resolver.resolve_source(id),
|
||||
Self::InMemory(resolver) => resolver.resolve_source(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_mermaid_render_options(
|
||||
theme: Option<String>,
|
||||
font_family: Option<String>,
|
||||
font_size: Option<f64>,
|
||||
) -> RenderOptions {
|
||||
let mut render_options = match theme.as_deref() {
|
||||
Some("default") => RenderOptions::mermaid_default(),
|
||||
_ => RenderOptions::modern(),
|
||||
};
|
||||
|
||||
if let Some(font_family) = font_family {
|
||||
render_options.theme.font_family = font_family;
|
||||
}
|
||||
|
||||
if let Some(font_size) = font_size {
|
||||
render_options.theme.font_size = font_size as f32;
|
||||
}
|
||||
|
||||
render_options
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn render_mermaid_preview_svg(
|
||||
code: String,
|
||||
theme: Option<String>,
|
||||
font_family: Option<String>,
|
||||
font_size: Option<f64>,
|
||||
) -> Result<String> {
|
||||
let render_options = resolve_mermaid_render_options(theme, font_family, font_size);
|
||||
|
||||
mermaid_rs_renderer::render_with_options(&code, render_options).map_err(|error| UniffiError::Err(error.to_string()))
|
||||
}
|
||||
|
||||
fn normalize_typst_svg(svg: String) -> String {
|
||||
let mut svg = svg;
|
||||
let page_background_marker = r##"<path class="typst-shape""##;
|
||||
let mut cursor = 0;
|
||||
|
||||
while let Some(relative_idx) = svg[cursor..].find(page_background_marker) {
|
||||
let idx = cursor + relative_idx;
|
||||
let rest = &svg[idx..];
|
||||
let Some(relative_end) = rest.find("/>") else {
|
||||
break;
|
||||
};
|
||||
|
||||
let end = idx + relative_end + 2;
|
||||
let path_fragment = &svg[idx..end];
|
||||
let is_page_background_path =
|
||||
path_fragment.contains(r#"d="M 0 0v "#) && path_fragment.contains(r#" h "#) && path_fragment.contains(r#" v -"#);
|
||||
|
||||
if is_page_background_path {
|
||||
svg.replace_range(idx..end, "");
|
||||
cursor = idx;
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = end;
|
||||
}
|
||||
|
||||
svg
|
||||
}
|
||||
|
||||
fn resolve_typst_font_dirs(font_dirs: Option<Vec<String>>) -> Vec<PathBuf> {
|
||||
font_dirs
|
||||
.map(|dirs| dirs.into_iter().map(PathBuf::from).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn resolve_typst_package_resolver(cache_dir: Option<String>) -> Result<MobileTypstPackageResolver> {
|
||||
let resolver = match cache_dir {
|
||||
Some(cache_dir) => {
|
||||
let cache_dir = PathBuf::from(cache_dir).join(TYPST_PACKAGE_CACHE_DIR);
|
||||
std::fs::create_dir_all(&cache_dir).map_err(|error| UniffiError::Err(error.to_string()))?;
|
||||
MobileTypstPackageResolver::FileSystem(
|
||||
PackageResolver::builder()
|
||||
.cache(FileSystemCache(cache_dir))
|
||||
.build()
|
||||
.into_cached(),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
MobileTypstPackageResolver::InMemory(PackageResolver::builder().with_in_memory_cache().build().into_cached())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn render_typst_preview_svg(
|
||||
code: String,
|
||||
font_dirs: Option<Vec<String>>,
|
||||
cache_dir: Option<String>,
|
||||
) -> Result<String> {
|
||||
let search_options = TypstKitFontOptions::new()
|
||||
.include_system_fonts(false)
|
||||
.include_embedded_fonts(true)
|
||||
.include_dirs(resolve_typst_font_dirs(font_dirs));
|
||||
let package_resolver = resolve_typst_package_resolver(cache_dir)?;
|
||||
|
||||
let engine = TypstEngine::builder()
|
||||
.main_file(code)
|
||||
.search_fonts_with(search_options)
|
||||
.add_file_resolver(package_resolver)
|
||||
.build();
|
||||
|
||||
let document = engine
|
||||
.compile::<PagedDocument>()
|
||||
.output
|
||||
.map_err(|error| UniffiError::Err(error.to_string()))?;
|
||||
|
||||
Ok(normalize_typst_svg(typst_svg::svg_merged(&document, Abs::pt(0.0))))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "affine_native"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
@@ -25,6 +26,12 @@ sqlx = { workspace = true, default-features = false, features = [
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mermaid-rs-renderer = { workspace = true }
|
||||
typst = { workspace = true }
|
||||
typst-as-lib = { workspace = true }
|
||||
typst-svg = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
|
||||
33
packages/frontend/native/index.d.ts
vendored
33
packages/frontend/native/index.d.ts
vendored
@@ -40,8 +40,41 @@ export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | u
|
||||
|
||||
/** Decode audio file into a Float32Array */
|
||||
export declare function decodeAudioSync(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null): Float32Array
|
||||
export interface MermaidRenderOptions {
|
||||
theme?: string
|
||||
fontFamily?: string
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
export interface MermaidRenderRequest {
|
||||
code: string
|
||||
options?: MermaidRenderOptions
|
||||
}
|
||||
|
||||
export interface MermaidRenderResult {
|
||||
svg: string
|
||||
}
|
||||
|
||||
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
|
||||
|
||||
export declare function renderMermaidSvg(request: MermaidRenderRequest): MermaidRenderResult
|
||||
|
||||
export declare function renderTypstSvg(request: TypstRenderRequest): TypstRenderResult
|
||||
|
||||
export interface TypstRenderOptions {
|
||||
fontUrls?: Array<string>
|
||||
fontDirs?: Array<string>
|
||||
}
|
||||
|
||||
export interface TypstRenderRequest {
|
||||
code: string
|
||||
options?: TypstRenderOptions
|
||||
}
|
||||
|
||||
export interface TypstRenderResult {
|
||||
svg: string
|
||||
}
|
||||
|
||||
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
|
||||
export declare class DocStorage {
|
||||
constructor(path: string)
|
||||
|
||||
@@ -580,6 +580,8 @@ module.exports.ShareableContent = nativeBinding.ShareableContent
|
||||
module.exports.decodeAudio = nativeBinding.decodeAudio
|
||||
module.exports.decodeAudioSync = nativeBinding.decodeAudioSync
|
||||
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
|
||||
module.exports.renderMermaidSvg = nativeBinding.renderMermaidSvg
|
||||
module.exports.renderTypstSvg = nativeBinding.renderTypstSvg
|
||||
module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse
|
||||
module.exports.DocStorage = nativeBinding.DocStorage
|
||||
module.exports.DocStoragePool = nativeBinding.DocStoragePool
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "affine_media_capture"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "affine_nbstore"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "affine_schema"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "affine_sqlite_v1"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod hashcash;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub mod preview;
|
||||
|
||||
#[cfg(not(target_arch = "arm"))]
|
||||
#[global_allocator]
|
||||
|
||||
191
packages/frontend/native/src/preview.rs
Normal file
191
packages/frontend/native/src/preview.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use mermaid_rs_renderer::RenderOptions;
|
||||
use napi::{Error, Result};
|
||||
use napi_derive::napi;
|
||||
use typst::layout::{Abs, PagedDocument};
|
||||
use typst_as_lib::{TypstEngine, typst_kit_options::TypstKitFontOptions};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MermaidRenderOptions {
|
||||
pub theme: Option<String>,
|
||||
pub font_family: Option<String>,
|
||||
pub font_size: Option<f64>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MermaidRenderRequest {
|
||||
pub code: String,
|
||||
pub options: Option<MermaidRenderOptions>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MermaidRenderResult {
|
||||
pub svg: String,
|
||||
}
|
||||
|
||||
fn resolve_mermaid_render_options(options: Option<MermaidRenderOptions>) -> RenderOptions {
|
||||
let mut render_options = match options.as_ref().and_then(|options| options.theme.as_deref()) {
|
||||
Some("default") => RenderOptions::mermaid_default(),
|
||||
_ => RenderOptions::modern(),
|
||||
};
|
||||
|
||||
if let Some(options) = options {
|
||||
if let Some(font_family) = options.font_family {
|
||||
render_options.theme.font_family = font_family;
|
||||
}
|
||||
|
||||
if let Some(font_size) = options.font_size {
|
||||
render_options.theme.font_size = font_size as f32;
|
||||
}
|
||||
}
|
||||
|
||||
render_options
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn render_mermaid_svg(request: MermaidRenderRequest) -> Result<MermaidRenderResult> {
|
||||
let render_options = resolve_mermaid_render_options(request.options);
|
||||
let svg = mermaid_rs_renderer::render_with_options(&request.code, render_options)
|
||||
.map_err(|error| Error::from_reason(error.to_string()))?;
|
||||
|
||||
Ok(MermaidRenderResult { svg })
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TypstRenderOptions {
|
||||
pub font_urls: Option<Vec<String>>,
|
||||
pub font_dirs: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TypstRenderRequest {
|
||||
pub code: String,
|
||||
pub options: Option<TypstRenderOptions>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TypstRenderResult {
|
||||
pub svg: String,
|
||||
}
|
||||
|
||||
fn resolve_local_font_dir(value: &str) -> Option<PathBuf> {
|
||||
let path = if let Some(stripped) = value.strip_prefix("file://") {
|
||||
PathBuf::from(stripped)
|
||||
} else {
|
||||
let path = PathBuf::from(value);
|
||||
if !path.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
path
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
path.parent().map(|parent| parent.to_path_buf())
|
||||
}
|
||||
|
||||
fn resolve_typst_font_dirs(options: &Option<TypstRenderOptions>) -> Vec<PathBuf> {
|
||||
let Some(options) = options.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut font_dirs = options
|
||||
.font_dirs
|
||||
.as_ref()
|
||||
.map(|dirs| dirs.iter().map(PathBuf::from).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(font_urls) = options.font_urls.as_ref() {
|
||||
font_dirs.extend(font_urls.iter().filter_map(|url| resolve_local_font_dir(url)));
|
||||
}
|
||||
|
||||
font_dirs
|
||||
}
|
||||
|
||||
fn normalize_typst_svg(svg: String) -> String {
|
||||
let mut svg = svg;
|
||||
let page_background_marker = r##"<path class="typst-shape""##;
|
||||
let mut cursor = 0;
|
||||
|
||||
while let Some(relative_idx) = svg[cursor..].find(page_background_marker) {
|
||||
let idx = cursor + relative_idx;
|
||||
let rest = &svg[idx..];
|
||||
let Some(relative_end) = rest.find("/>") else {
|
||||
break;
|
||||
};
|
||||
|
||||
let end = idx + relative_end + 2;
|
||||
let path_fragment = &svg[idx..end];
|
||||
let is_page_background_path =
|
||||
path_fragment.contains(r#"d="M 0 0v "#) && path_fragment.contains(r#" h "#) && path_fragment.contains(r#" v -"#);
|
||||
|
||||
if is_page_background_path {
|
||||
svg.replace_range(idx..end, "");
|
||||
cursor = idx;
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = end;
|
||||
}
|
||||
|
||||
svg
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn render_typst_svg(request: TypstRenderRequest) -> Result<TypstRenderResult> {
|
||||
let font_dirs = resolve_typst_font_dirs(&request.options);
|
||||
let search_options = TypstKitFontOptions::new()
|
||||
.include_system_fonts(false)
|
||||
.include_embedded_fonts(true)
|
||||
.include_dirs(font_dirs);
|
||||
|
||||
let engine = TypstEngine::builder()
|
||||
.main_file(request.code)
|
||||
.search_fonts_with(search_options)
|
||||
.with_package_file_resolver()
|
||||
.build();
|
||||
|
||||
let document = engine
|
||||
.compile::<PagedDocument>()
|
||||
.output
|
||||
.map_err(|error| Error::from_reason(error.to_string()))?;
|
||||
|
||||
let svg = normalize_typst_svg(typst_svg::svg_merged(&document, Abs::pt(0.0)));
|
||||
Ok(TypstRenderResult { svg })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_typst_svg;
|
||||
|
||||
#[test]
|
||||
fn normalize_typst_svg_removes_all_backgrounds() {
|
||||
let input = r##"<svg>
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0v 10 h 10 v -10 Z "/>
|
||||
<g></g>
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0v 10 h 10 v -10 Z "/>
|
||||
<g transform="matrix(1 0 0 1 0 10)"></g>
|
||||
</svg>"##
|
||||
.to_string();
|
||||
|
||||
let normalized = normalize_typst_svg(input);
|
||||
let retained = normalized
|
||||
.matches(r##"<path class="typst-shape" fill="#ffffff" fill-rule="nonzero""##)
|
||||
.count();
|
||||
assert_eq!(retained, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_typst_svg_keeps_non_background_paths() {
|
||||
let input = r##"<svg>
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 1 2 L 3 4 Z "/>
|
||||
</svg>"##
|
||||
.to_string();
|
||||
|
||||
let normalized = normalize_typst_svg(input);
|
||||
assert!(normalized.contains(r##"d="M 1 2 L 3 4 Z ""##));
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ import {
|
||||
|
||||
type WorkerConfig = { name: string };
|
||||
type CreateWorkerTargetConfig = (pkg: Package, entry: string) => WorkerConfig;
|
||||
type BaseWorkerOptions = {
|
||||
includeMermaidAndTypst?: boolean;
|
||||
};
|
||||
|
||||
function assertRspackSupportedPackage(pkg: Package) {
|
||||
assertRspackSupportedPackageName(pkg.name);
|
||||
@@ -49,11 +52,13 @@ async function uploadAssetsForPackage(pkg: Package, logger: Logger) {
|
||||
|
||||
function getBaseWorkerConfigs(
|
||||
pkg: Package,
|
||||
createWorkerTargetConfig: CreateWorkerTargetConfig
|
||||
createWorkerTargetConfig: CreateWorkerTargetConfig,
|
||||
options: BaseWorkerOptions = {}
|
||||
) {
|
||||
const core = new Package('@affine/core');
|
||||
const includeMermaidAndTypst = options.includeMermaidAndTypst ?? true;
|
||||
|
||||
return [
|
||||
const workerConfigs = [
|
||||
createWorkerTargetConfig(
|
||||
pkg,
|
||||
core.srcPath.join(
|
||||
@@ -71,6 +76,21 @@ function getBaseWorkerConfigs(
|
||||
).value
|
||||
),
|
||||
];
|
||||
|
||||
if (includeMermaidAndTypst) {
|
||||
workerConfigs.push(
|
||||
createWorkerTargetConfig(
|
||||
pkg,
|
||||
core.srcPath.join('modules/mermaid/renderer/mermaid.worker.ts').value
|
||||
),
|
||||
createWorkerTargetConfig(
|
||||
pkg,
|
||||
core.srcPath.join('modules/typst/renderer/typst.worker.ts').value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return workerConfigs;
|
||||
}
|
||||
|
||||
function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
|
||||
@@ -85,9 +105,7 @@ function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
case '@affine/web':
|
||||
case '@affine/mobile':
|
||||
case '@affine/ios':
|
||||
case '@affine/android': {
|
||||
case '@affine/mobile': {
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createRspackWorkerTargetConfig
|
||||
@@ -109,10 +127,35 @@ function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
|
||||
...workerConfigs,
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
case '@affine/ios':
|
||||
case '@affine/android': {
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createRspackWorkerTargetConfig,
|
||||
{ includeMermaidAndTypst: false }
|
||||
);
|
||||
workerConfigs.push(
|
||||
createRspackWorkerTargetConfig(
|
||||
pkg,
|
||||
pkg.srcPath.join('nbstore.worker.ts').value
|
||||
)
|
||||
);
|
||||
|
||||
return [
|
||||
createRspackHTMLTargetConfig(
|
||||
pkg,
|
||||
pkg.srcPath.join('index.tsx').value,
|
||||
{},
|
||||
workerConfigs.map(config => config.name)
|
||||
),
|
||||
...workerConfigs,
|
||||
] as MultiRspackOptions;
|
||||
}
|
||||
case '@affine/electron-renderer': {
|
||||
const workerConfigs = getBaseWorkerConfigs(
|
||||
pkg,
|
||||
createRspackWorkerTargetConfig
|
||||
createRspackWorkerTargetConfig,
|
||||
{ includeMermaidAndTypst: false }
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -90,6 +90,22 @@ export function createHTMLTargetConfig(
|
||||
);
|
||||
|
||||
const buildConfig = getBuildConfigFromEnv(pkg);
|
||||
const codeBlockPreviewBackendFile =
|
||||
buildConfig.distribution === 'desktop'
|
||||
? 'platform-backend.desktop.ts'
|
||||
: buildConfig.distribution === 'ios' ||
|
||||
buildConfig.distribution === 'android'
|
||||
? 'platform-backend.mobile.ts'
|
||||
: 'platform-backend.ts';
|
||||
const codeBlockPreviewBackendAlias = ProjectRoot.join(
|
||||
'packages',
|
||||
'frontend',
|
||||
'core',
|
||||
'src',
|
||||
'modules',
|
||||
'code-block-preview-renderer',
|
||||
codeBlockPreviewBackendFile
|
||||
).value;
|
||||
|
||||
console.log(
|
||||
`Building [${pkg.name}] for [${buildConfig.appBuildType}] channel in [${buildConfig.debug ? 'development' : 'production'}] mode.`
|
||||
@@ -145,6 +161,8 @@ export function createHTMLTargetConfig(
|
||||
'@preact',
|
||||
'signals-core'
|
||||
).value,
|
||||
'@affine/core/modules/code-block-preview-renderer/platform-backend':
|
||||
codeBlockPreviewBackendAlias,
|
||||
},
|
||||
},
|
||||
//#endregion
|
||||
|
||||
324
yarn.lock
324
yarn.lock
@@ -431,6 +431,7 @@ __metadata:
|
||||
"@testing-library/dom": "npm:^10.4.0"
|
||||
"@testing-library/react": "npm:^16.1.0"
|
||||
"@toeverything/infra": "workspace:*"
|
||||
"@toeverything/mermaid-wasm": "npm:^0.1.0"
|
||||
"@toeverything/pdf-viewer": "npm:^0.1.1"
|
||||
"@toeverything/theme": "npm:^1.1.23"
|
||||
"@types/animejs": "npm:^3.1.12"
|
||||
@@ -447,6 +448,7 @@ __metadata:
|
||||
cmdk: "npm:^1.0.4"
|
||||
core-js: "npm:^3.39.0"
|
||||
dayjs: "npm:^1.11.13"
|
||||
dompurify: "npm:^3.3.0"
|
||||
eventemitter2: "npm:^6.4.9"
|
||||
fake-indexeddb: "npm:^6.0.0"
|
||||
file-type: "npm:^21.0.0"
|
||||
@@ -468,7 +470,7 @@ __metadata:
|
||||
lit: "npm:^3.2.1"
|
||||
lodash-es: "npm:^4.17.23"
|
||||
lottie-react: "npm:^2.4.0"
|
||||
mermaid: "npm:^11.12.2"
|
||||
mermaid: "npm:^11.13.0"
|
||||
mp4-muxer: "npm:^5.2.2"
|
||||
nanoid: "npm:^5.1.6"
|
||||
next-themes: "npm:^0.4.4"
|
||||
@@ -1136,13 +1138,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@antfu/utils@npm:^9.2.0":
|
||||
version: 9.2.1
|
||||
resolution: "@antfu/utils@npm:9.2.1"
|
||||
checksum: 10/c629769f5301d16851de18e241cc0c7547928b080929bcfd945735eb799d13029336236bfe869935e832f3e48f8b622caf5946738158b4dd4133701f19a9b75a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@apollo/cache-control-types@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@apollo/cache-control-types@npm:1.0.3"
|
||||
@@ -1736,6 +1731,7 @@ __metadata:
|
||||
"@toeverything/theme": "npm:^1.1.23"
|
||||
"@vitest/browser-playwright": "npm:^4.0.18"
|
||||
lit: "npm:^3.2.0"
|
||||
playwright: "npm:=1.58.2"
|
||||
rxjs: "npm:^7.8.2"
|
||||
vitest: "npm:^4.0.18"
|
||||
yjs: "npm:^13.6.27"
|
||||
@@ -2800,6 +2796,7 @@ __metadata:
|
||||
lit: "npm:^3.2.0"
|
||||
lit-html: "npm:^3.2.1"
|
||||
lodash-es: "npm:^4.17.23"
|
||||
playwright: "npm:=1.58.2"
|
||||
rxjs: "npm:^7.8.2"
|
||||
vitest: "npm:^4.0.18"
|
||||
yjs: "npm:^13.6.27"
|
||||
@@ -3610,6 +3607,7 @@ __metadata:
|
||||
"@vanilla-extract/vite-plugin": "npm:^5.0.0"
|
||||
"@vitest/browser-playwright": "npm:^4.0.18"
|
||||
lit: "npm:^3.2.0"
|
||||
playwright: "npm:=1.58.2"
|
||||
rxjs: "npm:^7.8.2"
|
||||
vite: "npm:^7.2.7"
|
||||
vite-plugin-istanbul: "npm:^7.2.1"
|
||||
@@ -3680,6 +3678,7 @@ __metadata:
|
||||
lit: "npm:^3.2.0"
|
||||
lodash-es: "npm:^4.17.23"
|
||||
lz-string: "npm:^1.5.0"
|
||||
playwright: "npm:=1.58.2"
|
||||
rehype-parse: "npm:^9.0.0"
|
||||
rxjs: "npm:^7.8.2"
|
||||
unified: "npm:^11.0.5"
|
||||
@@ -3740,9 +3739,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@braintree/sanitize-url@npm:^7.1.1":
|
||||
version: 7.1.1
|
||||
resolution: "@braintree/sanitize-url@npm:7.1.1"
|
||||
checksum: 10/a8a5535c5a0a459ba593a018c554b35493dff004fd09d7147db67243df83bce3d410b89ee7dc2d95cce195b85b877c72f8ca149e1040110a945d193c67293af0
|
||||
version: 7.1.2
|
||||
resolution: "@braintree/sanitize-url@npm:7.1.2"
|
||||
checksum: 10/d9626ff8f8eb5e192cd055e6e743449c21102c76bb59e405b7028fe56230fa080bfcc80dfb1e21850a6876e75adda9f7b3c888cf0685942bb74da4d2866d6ec3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3855,45 +3854,45 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@chevrotain/cst-dts-gen@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3"
|
||||
"@chevrotain/cst-dts-gen@npm:11.1.2":
|
||||
version: 11.1.2
|
||||
resolution: "@chevrotain/cst-dts-gen@npm:11.1.2"
|
||||
dependencies:
|
||||
"@chevrotain/gast": "npm:11.0.3"
|
||||
"@chevrotain/types": "npm:11.0.3"
|
||||
lodash-es: "npm:4.17.21"
|
||||
checksum: 10/601d23fa3312bd0e32816bd3f9ca2dcba775a52192a082fd6c5e4a2e8ee068523401191babbe2c346d6d2551900a67b549f2f74d7ebb7d5b2ee1b6fa3c8857a0
|
||||
"@chevrotain/gast": "npm:11.1.2"
|
||||
"@chevrotain/types": "npm:11.1.2"
|
||||
lodash-es: "npm:4.17.23"
|
||||
checksum: 10/04a84285fd4d26129f44f8e20b99fd6ab51cb1ee4c7eb104baa4842413b9e18bb171046a665583b94ff29e2c06a2e1a65205940a75cda848aa2a5e847b99bbb6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@chevrotain/gast@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "@chevrotain/gast@npm:11.0.3"
|
||||
"@chevrotain/gast@npm:11.1.2":
|
||||
version: 11.1.2
|
||||
resolution: "@chevrotain/gast@npm:11.1.2"
|
||||
dependencies:
|
||||
"@chevrotain/types": "npm:11.0.3"
|
||||
lodash-es: "npm:4.17.21"
|
||||
checksum: 10/7169453a8fbfa994e91995523dea09eab87ab23062ad93f6e51f4a3b03f5e2958e0a8b99d5ca6fa067fccfbbbb8bcf1a4573ace2e1b5a455f6956af9eaccb35a
|
||||
"@chevrotain/types": "npm:11.1.2"
|
||||
lodash-es: "npm:4.17.23"
|
||||
checksum: 10/8733cbadfcb982f5afe0e1825446ad8e82de3044c9ae22a69771ec20292facce25e459da23c16061c894ef49ca381ad53c0796171fa0dfce08fc25305850eeca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@chevrotain/regexp-to-ast@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "@chevrotain/regexp-to-ast@npm:11.0.3"
|
||||
checksum: 10/7387a1c61c5a052de41e1172b33eaaedea166fcdb1ffe4c381b86d00051a8014855a031d28fb658768a62c833ef5f5b0689d0c40de3d7bed556f8fea24396e69
|
||||
"@chevrotain/regexp-to-ast@npm:11.1.2":
|
||||
version: 11.1.2
|
||||
resolution: "@chevrotain/regexp-to-ast@npm:11.1.2"
|
||||
checksum: 10/9ba399b2c23ae1a86f1bcb1db4b07fd3191d0f491b63303b824850ef5f8b455f8f8a39c4d7876271c23ca06f7e03b7273b07014cd9c8ccb2328bff5dc6c9df00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@chevrotain/types@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "@chevrotain/types@npm:11.0.3"
|
||||
checksum: 10/49a82b71d2de8ceb2383ff2709fa61d245f2ab2e42790b70c57102c80846edaa318d0b3645aedc904d23ea7bd9be8a58f2397b1341760a15eb5aa95a1336e2a9
|
||||
"@chevrotain/types@npm:11.1.2":
|
||||
version: 11.1.2
|
||||
resolution: "@chevrotain/types@npm:11.1.2"
|
||||
checksum: 10/ad39d7651a20b05ead9b4374f98afc39915118dda29678c2b623571aa3da3018a0f3fb799d1809605cafcae59cba637d640855ea0f97b9b01d0900d77d6781fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@chevrotain/utils@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "@chevrotain/utils@npm:11.0.3"
|
||||
checksum: 10/29b5d84373a7761ad055c53e2f540a67b5b56550d5be1c473149f6b8923eef87ff391ce021c06ac7653843b0149f6ff0cf30b5e48c3f825d295eb06a6c517bd3
|
||||
"@chevrotain/utils@npm:11.1.2":
|
||||
version: 11.1.2
|
||||
resolution: "@chevrotain/utils@npm:11.1.2"
|
||||
checksum: 10/a5ba5886547665726e6680bdc6e1c939e643c8c606d8eb3806ed178ebb2c3a8a1a581db558412374f230babfe9d8c1c37f2c3edf260c63729dbe6ff9ca4f0223
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6391,19 +6390,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@iconify/utils@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "@iconify/utils@npm:3.0.2"
|
||||
"@iconify/utils@npm:^3.0.2":
|
||||
version: 3.1.0
|
||||
resolution: "@iconify/utils@npm:3.1.0"
|
||||
dependencies:
|
||||
"@antfu/install-pkg": "npm:^1.1.0"
|
||||
"@antfu/utils": "npm:^9.2.0"
|
||||
"@iconify/types": "npm:^2.0.0"
|
||||
debug: "npm:^4.4.1"
|
||||
globals: "npm:^15.15.0"
|
||||
kolorist: "npm:^1.8.0"
|
||||
local-pkg: "npm:^1.1.1"
|
||||
mlly: "npm:^1.7.4"
|
||||
checksum: 10/b2db57b6a6b06d618b2625bf7bd056219a6c65e71a86c79c664d5e5fe03e531bc74fdd9cfa4d74e2ea469b6cd92b63012e2d588b32cade5aa7e3c31bc5124789
|
||||
mlly: "npm:^1.8.0"
|
||||
checksum: 10/28e83311ec7eca3f94a9c128c6d6f0f6aa68b7a63bcac44d08a1ea6f94d3752a7447a4354f3d02fdcdbf782ba033784ef7a65212b3afe52d9b41ef8138e96b14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7531,12 +7525,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mermaid-js/parser@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "@mermaid-js/parser@npm:0.6.3"
|
||||
"@mermaid-js/parser@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@mermaid-js/parser@npm:1.0.1"
|
||||
dependencies:
|
||||
langium: "npm:3.3.1"
|
||||
checksum: 10/ab8bbdeaf2ef556871f3267541c0b3621d70c4d108ddac36383adc7eb1c7e6bed28d068b4ad196b54314877f263f939f90f0a1a3cfe8576fab30f4514732aa2f
|
||||
langium: "npm:^4.0.0"
|
||||
checksum: 10/648c96da8464113c3694587f3f950421b1914d34fddc194b6dff7a8c28da5e37273ddc4d56a4efaeda82e93c7a168389f035dbac6c8d5b65930ee60b5063ece4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -15772,6 +15766,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@toeverything/mermaid-wasm@npm:^0.1.0":
|
||||
version: 0.1.1
|
||||
resolution: "@toeverything/mermaid-wasm@npm:0.1.1"
|
||||
checksum: 10/4b701f19aad5fbe42ddec9d63c6e3cf7641ff1ab9277142db7adcde3e9b81315e2d35cfab82acead79bb81d560eef10d1d7522ab17cca300e6dae45db0380eab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@toeverything/pdf-viewer-types@npm:0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "@toeverything/pdf-viewer-types@npm:0.1.1"
|
||||
@@ -17488,6 +17489,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@upsetjs/venn.js@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@upsetjs/venn.js@npm:2.0.0"
|
||||
dependencies:
|
||||
d3-selection: "npm:^3.0.0"
|
||||
d3-transition: "npm:^3.0.1"
|
||||
dependenciesMeta:
|
||||
d3-selection:
|
||||
optional: true
|
||||
d3-transition:
|
||||
optional: true
|
||||
checksum: 10/84505be440490666566a6f59e765e1f24b154e4ca45cfed9102f02261be45912149f706e21b3cc7a6247816a8703e1f1b667561295b587c6dcc3162a9a48a5f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vanilla-extract/babel-plugin-debug-ids@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "@vanilla-extract/babel-plugin-debug-ids@npm:1.2.2"
|
||||
@@ -18149,7 +18165,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.6.0, acorn@npm:^8.8.2":
|
||||
"acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.6.0, acorn@npm:^8.8.2":
|
||||
version: 8.16.0
|
||||
resolution: "acorn@npm:8.16.0"
|
||||
bin:
|
||||
@@ -19740,7 +19756,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chevrotain-allstar@npm:~0.3.0":
|
||||
"chevrotain-allstar@npm:~0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "chevrotain-allstar@npm:0.3.1"
|
||||
dependencies:
|
||||
@@ -19751,17 +19767,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chevrotain@npm:~11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "chevrotain@npm:11.0.3"
|
||||
"chevrotain@npm:~11.1.1":
|
||||
version: 11.1.2
|
||||
resolution: "chevrotain@npm:11.1.2"
|
||||
dependencies:
|
||||
"@chevrotain/cst-dts-gen": "npm:11.0.3"
|
||||
"@chevrotain/gast": "npm:11.0.3"
|
||||
"@chevrotain/regexp-to-ast": "npm:11.0.3"
|
||||
"@chevrotain/types": "npm:11.0.3"
|
||||
"@chevrotain/utils": "npm:11.0.3"
|
||||
lodash-es: "npm:4.17.21"
|
||||
checksum: 10/8fa6253e51320dd4c3d386315b925734943e509d7954a2cd917746c0604461191bea57b0fb8fbab1903e0508fd94bfd35ebd0f8eace77cd0f3f42a9ee4f8f676
|
||||
"@chevrotain/cst-dts-gen": "npm:11.1.2"
|
||||
"@chevrotain/gast": "npm:11.1.2"
|
||||
"@chevrotain/regexp-to-ast": "npm:11.1.2"
|
||||
"@chevrotain/types": "npm:11.1.2"
|
||||
"@chevrotain/utils": "npm:11.1.2"
|
||||
lodash-es: "npm:4.17.23"
|
||||
checksum: 10/67caa47a3d38eeb4a584960bbf5fdc83894a1419a6428f7dcf8a07f5937c1bda4c8e1acf628fe80d3be8f4c1a84426942c0a8f6bcf44a5958f1df0cf4225bc4d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21051,7 +21067,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cytoscape@npm:^3.29.3":
|
||||
"cytoscape@npm:^3.33.1":
|
||||
version: 3.33.1
|
||||
resolution: "cytoscape@npm:3.33.1"
|
||||
checksum: 10/0e8d3ea87eb624899341d6a765cfb732199af8a871beedeb94971061632ce814c2c39e8257d6628c5611ca9dadc1a723a00377d04f149e0d24f6c133a6ab8647
|
||||
@@ -21196,9 +21212,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"d3-format@npm:1 - 3, d3-format@npm:3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-format@npm:3.1.0"
|
||||
checksum: 10/a0fe23d2575f738027a3db0ce57160e5a473ccf24808c1ed46d45ef4f3211076b34a18b585547d34e365e78dcc26dd4ab15c069731fc4b1c07a26bfced09ea31
|
||||
version: 3.1.2
|
||||
resolution: "d3-format@npm:3.1.2"
|
||||
checksum: 10/811d913c2c7624cb0d2a8f0ccd7964c50945b3de3c7f7aa14c309fba7266a3ec53cbee8c05f6ad61b2b65b93e157c55a0e07db59bc3180c39dac52be8e841ab1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21295,7 +21311,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-selection@npm:2 - 3, d3-selection@npm:3":
|
||||
"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "d3-selection@npm:3.0.0"
|
||||
checksum: 10/0e5acfd305b31628b7be5009ba7303d84bb34817a88ed4dde9c8bd9c23528573fc5272f89fc04e5be03d2cbf5441a248d7274aaf55a8ef3dad46e16333d72298
|
||||
@@ -21345,7 +21361,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-transition@npm:2 - 3, d3-transition@npm:3":
|
||||
"d3-transition@npm:2 - 3, d3-transition@npm:3, d3-transition@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-transition@npm:3.0.1"
|
||||
dependencies:
|
||||
@@ -21411,13 +21427,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dagre-d3-es@npm:7.0.13":
|
||||
version: 7.0.13
|
||||
resolution: "dagre-d3-es@npm:7.0.13"
|
||||
"dagre-d3-es@npm:7.0.14":
|
||||
version: 7.0.14
|
||||
resolution: "dagre-d3-es@npm:7.0.14"
|
||||
dependencies:
|
||||
d3: "npm:^7.9.0"
|
||||
lodash-es: "npm:^4.17.21"
|
||||
checksum: 10/f6dbd373b85cc9fbcb23fba996656a0336ba48bc46f1e6d31c582418a5086caf230a4e8178b90acd7b1d14b090cbba2db50dc64484d67cf9c8856a4a2fe30cf0
|
||||
checksum: 10/f2787049ae2684de27950dfc61eb23437cbb5c3ca7ec7f58620e19f16059465b6d23324ca861961353a60bb4cdaa5c66edfa9bbe44ac2304b72dd00ab4199714
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21474,10 +21490,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dayjs@npm:^1.11.13, dayjs@npm:^1.11.18":
|
||||
version: 1.11.18
|
||||
resolution: "dayjs@npm:1.11.18"
|
||||
checksum: 10/7d29a90834cf4da2feb437c2f34b8235c3f94493a06d2f1bf9f506f1fa49eadf796f26e1d685b9fe8cb5e75ce6ee067825115e196f1af3d07b3552ff857bfc39
|
||||
"dayjs@npm:^1.11.13, dayjs@npm:^1.11.19":
|
||||
version: 1.11.20
|
||||
resolution: "dayjs@npm:1.11.20"
|
||||
checksum: 10/5347533f21a55b8bb1b1ef559be9b805514c3a8fb7e68b75fb7e73808131c59e70909c073aa44ce8a0d159195cd110cdd4081cf87ab96cb06fee3edacae791c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21945,15 +21961,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dompurify@npm:^3.2.5, dompurify@npm:^3.3.0":
|
||||
version: 3.3.2
|
||||
resolution: "dompurify@npm:3.3.2"
|
||||
"dompurify@npm:^3.3.0, dompurify@npm:^3.3.1":
|
||||
version: 3.3.3
|
||||
resolution: "dompurify@npm:3.3.3"
|
||||
dependencies:
|
||||
"@types/trusted-types": "npm:^2.0.7"
|
||||
dependenciesMeta:
|
||||
"@types/trusted-types":
|
||||
optional: true
|
||||
checksum: 10/3ca02559677ce6d9583a500f21ffbb6b9e88f1af99f69fa0d0d9442cddbac98810588c869f8b435addb5115492d6e49870024bca322169b941bafedb99c7f281
|
||||
checksum: 10/4cc9c539ed7136d46c6577613b8e20871c2b6165db01dfbd2a3c11c75f9e339c496ac6519a1c3190115def8cadae3720bef0417fc43fa28802c7407bab174da9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -24462,13 +24478,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globals@npm:^15.15.0":
|
||||
version: 15.15.0
|
||||
resolution: "globals@npm:15.15.0"
|
||||
checksum: 10/7f561c87b2fd381b27fc2db7df8a4ea7a9bb378667b8a7193e61fd2ca3a876479174e2a303a74345fbea6e1242e16db48915c1fd3bf35adcf4060a795b425e18
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globals@npm:^16.4.0":
|
||||
version: 16.5.0
|
||||
resolution: "globals@npm:16.5.0"
|
||||
@@ -26663,14 +26672,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"katex@npm:^0.16.0, katex@npm:^0.16.22, katex@npm:^0.16.27":
|
||||
version: 0.16.27
|
||||
resolution: "katex@npm:0.16.27"
|
||||
"katex@npm:^0.16.0, katex@npm:^0.16.25, katex@npm:^0.16.27":
|
||||
version: 0.16.38
|
||||
resolution: "katex@npm:0.16.38"
|
||||
dependencies:
|
||||
commander: "npm:^8.3.0"
|
||||
bin:
|
||||
katex: cli.js
|
||||
checksum: 10/7666ae11c6c1238626bffaf1a526af6ff679114d62293bf2f0e29f8a34d8e961c0edcb686c5b628158ec92a143b4bef5d83539c81b29a63c7dcf0bdb4544eec9
|
||||
checksum: 10/e9103def114d9d08ab216864e66b68e6f50a6360fdc5aa29d8edeee430e1618dd7551b9f080e9c591b3ee24c18fe6910b8fe0c89c7c4b1109abd2b63e223fbc5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -26704,13 +26713,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kolorist@npm:^1.8.0":
|
||||
version: 1.8.0
|
||||
resolution: "kolorist@npm:1.8.0"
|
||||
checksum: 10/71d5d122951cc65f2f14c3e1d7f8fd91694b374647d4f6deec3816d018cd04a44edd9578d93e00c82c2053b925e5d30a0565746c4171f4ca9fce1a13bd5f3315
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kuler@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "kuler@npm:2.0.0"
|
||||
@@ -26718,16 +26720,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"langium@npm:3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "langium@npm:3.3.1"
|
||||
"langium@npm:^4.0.0":
|
||||
version: 4.2.1
|
||||
resolution: "langium@npm:4.2.1"
|
||||
dependencies:
|
||||
chevrotain: "npm:~11.0.3"
|
||||
chevrotain-allstar: "npm:~0.3.0"
|
||||
chevrotain: "npm:~11.1.1"
|
||||
chevrotain-allstar: "npm:~0.3.1"
|
||||
vscode-languageserver: "npm:~9.0.1"
|
||||
vscode-languageserver-textdocument: "npm:~1.0.11"
|
||||
vscode-uri: "npm:~3.0.8"
|
||||
checksum: 10/6b2e5bc1ff47c6048ec24471333f3397ddb4d6419f1c2262268faff00a8f0839ac4bd4877907261273e91e82f239951249155c3aff8d395ee5e3372dfc285e04
|
||||
vscode-uri: "npm:~3.1.0"
|
||||
checksum: 10/9fd9208762ae9b551ec1deea25b6633605b06b59505a1ce6bf5b6c639779378bab35f813ffe549db57af95a5b71f9dcf61e553273a5d9d6ad2a3a99c5b7bfcc2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -27105,17 +27107,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"local-pkg@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "local-pkg@npm:1.1.2"
|
||||
dependencies:
|
||||
mlly: "npm:^1.7.4"
|
||||
pkg-types: "npm:^2.3.0"
|
||||
quansync: "npm:^0.2.11"
|
||||
checksum: 10/761d82f40849e4721fa50d86782cf75bc2befb0696f32ac99869fb6f3033b904e4018f4bb8cdfde994d710816480dc1aba8e462c67ec20fe89d4700a245d17f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"locate-path@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "locate-path@npm:2.0.0"
|
||||
@@ -27153,14 +27144,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash-es@npm:4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash-es@npm:4.17.21"
|
||||
checksum: 10/03f39878ea1e42b3199bd3f478150ab723f93cc8730ad86fec1f2804f4a07c6e30deaac73cad53a88e9c3db33348bb8ceeb274552390e7a75d7849021c02df43
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash-es@npm:^4.17.21, lodash-es@npm:^4.17.23":
|
||||
"lodash-es@npm:4.17.23, lodash-es@npm:^4.17.21, lodash-es@npm:^4.17.23":
|
||||
version: 4.17.23
|
||||
resolution: "lodash-es@npm:4.17.23"
|
||||
checksum: 10/1feae200df22eb0bd93ca86d485e77784b8a9fb1d13e91b66e9baa7a7e5e04be088c12a7e20c2250fc0bd3db1bc0ef0affc7d9e3810b6af2455a3c6bf6dde59e
|
||||
@@ -27762,12 +27746,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:^16.2.1":
|
||||
version: 16.3.0
|
||||
resolution: "marked@npm:16.3.0"
|
||||
"marked@npm:^16.3.0":
|
||||
version: 16.4.2
|
||||
resolution: "marked@npm:16.4.2"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/60497834b9acfb3b3994222509d359ecb9a197c885dfeb77e2050a287cd2f4ab19f00d5597172b47f9e0c54d9e1e13d8b2dd73322b7838599e1f16d1d6283f5b
|
||||
checksum: 10/6e40e40661dce97e271198daa2054fc31e6445892a735e416c248fba046bdfa4573cafa08dc254529f105e7178a34485eb7f82573979cfb377a4530f66e79187
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -28119,31 +28103,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mermaid@npm:^11.12.2":
|
||||
version: 11.12.2
|
||||
resolution: "mermaid@npm:11.12.2"
|
||||
"mermaid@npm:^11.13.0":
|
||||
version: 11.13.0
|
||||
resolution: "mermaid@npm:11.13.0"
|
||||
dependencies:
|
||||
"@braintree/sanitize-url": "npm:^7.1.1"
|
||||
"@iconify/utils": "npm:^3.0.1"
|
||||
"@mermaid-js/parser": "npm:^0.6.3"
|
||||
"@iconify/utils": "npm:^3.0.2"
|
||||
"@mermaid-js/parser": "npm:^1.0.1"
|
||||
"@types/d3": "npm:^7.4.3"
|
||||
cytoscape: "npm:^3.29.3"
|
||||
"@upsetjs/venn.js": "npm:^2.0.0"
|
||||
cytoscape: "npm:^3.33.1"
|
||||
cytoscape-cose-bilkent: "npm:^4.1.0"
|
||||
cytoscape-fcose: "npm:^2.2.0"
|
||||
d3: "npm:^7.9.0"
|
||||
d3-sankey: "npm:^0.12.3"
|
||||
dagre-d3-es: "npm:7.0.13"
|
||||
dayjs: "npm:^1.11.18"
|
||||
dompurify: "npm:^3.2.5"
|
||||
katex: "npm:^0.16.22"
|
||||
dagre-d3-es: "npm:7.0.14"
|
||||
dayjs: "npm:^1.11.19"
|
||||
dompurify: "npm:^3.3.1"
|
||||
katex: "npm:^0.16.25"
|
||||
khroma: "npm:^2.1.0"
|
||||
lodash-es: "npm:^4.17.21"
|
||||
marked: "npm:^16.2.1"
|
||||
lodash-es: "npm:^4.17.23"
|
||||
marked: "npm:^16.3.0"
|
||||
roughjs: "npm:^4.6.6"
|
||||
stylis: "npm:^4.3.6"
|
||||
ts-dedent: "npm:^2.2.0"
|
||||
uuid: "npm:^11.1.0"
|
||||
checksum: 10/3c07c1be97a830904c7802933664abd132d626921c3aa82db8d0fbaad35832907cbaa2250747f17e110de5d6f4bdd1fcb9f0416b42c8e59a73653e809333d3da
|
||||
checksum: 10/7bf2123b005925983eb4d528896665963002c3a617f13fe9d356e4910ff71d3ad0c3f817e717414828f9cdba147651f2ac28af15c60c085f4444f5a786691dd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -28801,15 +28786,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mlly@npm:^1.4.2, mlly@npm:^1.7.1, mlly@npm:^1.7.4":
|
||||
version: 1.7.4
|
||||
resolution: "mlly@npm:1.7.4"
|
||||
"mlly@npm:^1.4.2, mlly@npm:^1.7.1, mlly@npm:^1.7.4, mlly@npm:^1.8.0":
|
||||
version: 1.8.1
|
||||
resolution: "mlly@npm:1.8.1"
|
||||
dependencies:
|
||||
acorn: "npm:^8.14.0"
|
||||
pathe: "npm:^2.0.1"
|
||||
pkg-types: "npm:^1.3.0"
|
||||
ufo: "npm:^1.5.4"
|
||||
checksum: 10/1b36163d38c2331f8ae480e6a11da3d15927a2148d729fcd9df6d0059ca74869aa693931bd1f762f82eb534b84c921bdfbc036eb0e4da4faeb55f1349d254f35
|
||||
acorn: "npm:^8.16.0"
|
||||
pathe: "npm:^2.0.3"
|
||||
pkg-types: "npm:^1.3.1"
|
||||
ufo: "npm:^1.6.3"
|
||||
checksum: 10/8e424f0615d09adfb7d59ad8f0c8245df275cd05e483a4631a1b2c5dd7e09913a9ce8182bc1562d569941ecee25ab03f4429284265471f562da1dd308008e237
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -30088,9 +30073,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"package-manager-detector@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "package-manager-detector@npm:1.3.0"
|
||||
checksum: 10/b21155d53a8ab96d5be3bfae43cc1d397bf363782b922d1f6967d220d2a9f08234ebb76035318bf92822ce761d10451959f01019faebc08fdb4d4a8bc3103da6
|
||||
version: 1.6.0
|
||||
resolution: "package-manager-detector@npm:1.6.0"
|
||||
checksum: 10/b38a9532198cefdb98a1b7131c42cbffa55d8b997d6117811cf83f00079fd57a572db2aa5e3db5e36bcd0af84d0bec5a7d6251142427314390ed99a3d76cd0a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -30598,7 +30583,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pkg-types@npm:^1.2.0, pkg-types@npm:^1.3.0, pkg-types@npm:^1.3.1":
|
||||
"pkg-types@npm:^1.2.0, pkg-types@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "pkg-types@npm:1.3.1"
|
||||
dependencies:
|
||||
@@ -30609,7 +30594,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pkg-types@npm:^2.0.0, pkg-types@npm:^2.1.0, pkg-types@npm:^2.3.0":
|
||||
"pkg-types@npm:^2.0.0, pkg-types@npm:^2.1.0":
|
||||
version: 2.3.0
|
||||
resolution: "pkg-types@npm:2.3.0"
|
||||
dependencies:
|
||||
@@ -31416,13 +31401,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"quansync@npm:^0.2.11":
|
||||
version: 0.2.11
|
||||
resolution: "quansync@npm:0.2.11"
|
||||
checksum: 10/d4f0cc21a25052a8a6183f17752a6221829c4795b40641de67c06945b356841ff00296d3700d0332dfe8e86100fdcc02f4be7559f3f1774a753b05adb7800d01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"query-string@npm:^9.1.1":
|
||||
version: 9.2.0
|
||||
resolution: "query-string@npm:9.2.0"
|
||||
@@ -34637,9 +34615,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"tinyexec@npm:^1.0.0, tinyexec@npm:^1.0.1, tinyexec@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "tinyexec@npm:1.0.2"
|
||||
checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405
|
||||
version: 1.0.4
|
||||
resolution: "tinyexec@npm:1.0.4"
|
||||
checksum: 10/ccebe4044eef6fa5050929df7862fda70b4fb700f15d94aef8ae6109b9d194dbc3a990125d99944fd25b90fe2115e1927f055b909a604c571a81b647ede5757a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -35169,10 +35147,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ufo@npm:^1.5.4":
|
||||
version: 1.6.1
|
||||
resolution: "ufo@npm:1.6.1"
|
||||
checksum: 10/088a68133b93af183b093e5a8730a40fe7fd675d3dc0656ea7512f180af45c92300c294f14d4d46d4b2b553e3e52d3b13d4856b9885e620e7001edf85531234e
|
||||
"ufo@npm:^1.5.4, ufo@npm:^1.6.3":
|
||||
version: 1.6.3
|
||||
resolution: "ufo@npm:1.6.3"
|
||||
checksum: 10/79803984f3e414567273a666183d6a50d1bec0d852100a98f55c1e393cb705e3b88033e04029dd651714e6eec99e1b00f54fdc13f32404968251a16f8898cfe5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -36051,10 +36029,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vscode-uri@npm:~3.0.8":
|
||||
version: 3.0.8
|
||||
resolution: "vscode-uri@npm:3.0.8"
|
||||
checksum: 10/e882d6b679e0d053cbc042893c0951a135d899a192b62cd07f0a8924f11ae722067a8d6b1b5b147034becf57faf9fff9fb543b17b749fd0f17db1f54f783f07c
|
||||
"vscode-uri@npm:~3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "vscode-uri@npm:3.1.0"
|
||||
checksum: 10/80c2a2421f44b64008ef1f91dfa52a2d68105cbb4dcea197dbf5b00c65ccaccf218b615e93ec587f26fc3ba04796898f3631a9406e3b04cda970c3ca8eadf646
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user