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:
DarkSky
2026-03-20 04:04:40 +08:00
committed by GitHub
parent 16a8f17717
commit 7ac8b14b65
85 changed files with 4926 additions and 835 deletions

2703
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ resolver = "3"
criterion2 = { version = "3", default-features = false } criterion2 = { version = "3", default-features = false }
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
dispatch2 = "0.3" 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" dotenvy = "0.15"
file-format = { version = "0.28", features = ["reader"] } file-format = { version = "0.28", features = ["reader"] }
homedir = "0.3" homedir = "0.3"
@@ -59,6 +59,7 @@ resolver = "3"
lru = "0.16" lru = "0.16"
matroska = "0.30" matroska = "0.30"
memory-indexer = "0.3.0" memory-indexer = "0.3.0"
mermaid-rs-renderer = { git = "https://github.com/toeverything/mermaid-rs-renderer", rev = "fba9097", default-features = false }
mimalloc = "0.1" mimalloc = "0.1"
mp4parse = "0.17" mp4parse = "0.17"
nanoid = "0.4" nanoid = "0.4"
@@ -122,6 +123,14 @@ resolver = "3"
tree-sitter-rust = { version = "0.24" } tree-sitter-rust = { version = "0.24" }
tree-sitter-scala = { version = "0.24" } tree-sitter-scala = { version = "0.24" }
tree-sitter-typescript = { version = "0.23" } 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" uniffi = "0.29"
url = { version = "2.5" } url = { version = "2.5" }
uuid = "1.8" uuid = "1.8"

View File

@@ -32,6 +32,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitest/browser-playwright": "^4.0.18", "@vitest/browser-playwright": "^4.0.18",
"playwright": "=1.58.2",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
"exports": { "exports": {

View File

@@ -35,6 +35,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitest/browser-playwright": "^4.0.18", "@vitest/browser-playwright": "^4.0.18",
"playwright": "=1.58.2",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
"exports": { "exports": {

View File

@@ -34,6 +34,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitest/browser-playwright": "^4.0.18", "@vitest/browser-playwright": "^4.0.18",
"playwright": "=1.58.2",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
"exports": { "exports": {

View File

@@ -42,6 +42,7 @@
"devDependencies": { "devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0", "@vanilla-extract/vite-plugin": "^5.0.0",
"@vitest/browser-playwright": "^4.0.18", "@vitest/browser-playwright": "^4.0.18",
"playwright": "=1.58.2",
"vite": "^7.2.7", "vite": "^7.2.7",
"vite-plugin-istanbul": "^7.2.1", "vite-plugin-istanbul": "^7.2.1",
"vite-plugin-wasm": "^3.5.0", "vite-plugin-wasm": "^3.5.0",

48
deny.toml Normal file
View 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

View File

@@ -2,6 +2,7 @@
edition = "2024" edition = "2024"
license-file = "LICENSE" license-file = "LICENSE"
name = "affine_server_native" name = "affine_server_native"
publish = false
version = "1.0.0" version = "1.0.0"
[lib] [lib]

View File

@@ -10,6 +10,7 @@ interface TestOps extends OpSchema {
add: [{ a: number; b: number }, number]; add: [{ a: number; b: number }, number];
bin: [Uint8Array, Uint8Array]; bin: [Uint8Array, Uint8Array];
sub: [Uint8Array, number]; sub: [Uint8Array, number];
init: [{ fastText?: boolean } | undefined, { ok: true }];
} }
declare module 'vitest' { declare module 'vitest' {
@@ -84,6 +85,55 @@ describe('op client', () => {
expect(data.byteLength).toBe(0); 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 => { it('should cancel call', async ctx => {
const promise = ctx.producer.call('add', { a: 1, b: 2 }); const promise = ctx.producer.call('add', { a: 1, b: 2 });

View File

@@ -40,18 +40,14 @@ describe('op consumer', () => {
it('should throw if no handler registered', async ctx => { it('should throw if no handler registered', async ctx => {
ctx.handlers.call({ type: 'call', id: 'add:1', name: 'add', payload: {} }); ctx.handlers.call({ type: 'call', id: 'add:1', name: 'add', payload: {} });
await vi.advanceTimersToNextTimerAsync(); await vi.advanceTimersToNextTimerAsync();
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(` expect(ctx.postMessage.mock.lastCall?.[0]).toMatchObject({
[ type: 'return',
{ id: 'add:1',
"error": { error: {
"message": "Handler for operation [add] is not registered.", message: 'Handler for operation [add] is not registered.',
"name": "Error", name: 'Error',
}, },
"id": "add:1", });
"type": "return",
},
]
`);
}); });
it('should handle call message', async ctx => { 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 => { it('should handle cancel message', async ctx => {
ctx.consumer.register('add', ({ a, b }, { signal }) => { ctx.consumer.register('add', ({ a, b }, { signal }) => {
const { reject, resolve, promise } = Promise.withResolvers<number>(); const { reject, resolve, promise } = Promise.withResolvers<number>();

View File

@@ -16,6 +16,96 @@ import {
} from './message'; } from './message';
import type { OpInput, OpNames, OpOutput, OpSchema } from './types'; 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 { interface OpCallContext {
signal: AbortSignal; signal: AbortSignal;
} }
@@ -71,15 +161,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
this.port.postMessage({ this.port.postMessage({
type: 'return', type: 'return',
id: msg.id, id: msg.id,
error: pick(error, [ error: serializeError(error),
'name',
'message',
'code',
'type',
'status',
'data',
'stacktrace',
]),
} satisfies ReturnMessage); } satisfies ReturnMessage);
}, },
complete: () => { complete: () => {
@@ -109,15 +191,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
this.port.postMessage({ this.port.postMessage({
type: 'error', type: 'error',
id: msg.id, id: msg.id,
error: pick(error, [ error: serializeError(error),
'name',
'message',
'code',
'type',
'status',
'data',
'stacktrace',
]),
} satisfies SubscriptionErrorMessage); } satisfies SubscriptionErrorMessage);
}, },
complete: () => { complete: () => {

View File

@@ -12,7 +12,16 @@ export interface OpSchema {
[key: string]: [any, any?]; [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 OpNames<T extends OpSchema> = ValuesOf<KeyToKey<T>>;
export type OpInput< export type OpInput<

View File

@@ -2,6 +2,7 @@
edition = "2024" edition = "2024"
license-file = "LICENSE" license-file = "LICENSE"
name = "affine_common" name = "affine_common"
publish = false
version = "0.1.0" version = "0.1.0"
[features] [features]

View File

@@ -19,6 +19,7 @@ import app.affine.pro.plugin.AFFiNEThemePlugin
import app.affine.pro.plugin.AuthPlugin import app.affine.pro.plugin.AuthPlugin
import app.affine.pro.plugin.HashCashPlugin import app.affine.pro.plugin.HashCashPlugin
import app.affine.pro.plugin.NbStorePlugin import app.affine.pro.plugin.NbStorePlugin
import app.affine.pro.plugin.PreviewPlugin
import app.affine.pro.service.GraphQLService import app.affine.pro.service.GraphQLService
import app.affine.pro.service.SSEService import app.affine.pro.service.SSEService
import app.affine.pro.service.WebService import app.affine.pro.service.WebService
@@ -52,6 +53,7 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AFFiNEThemePlugi
AuthPlugin::class.java, AuthPlugin::class.java,
HashCashPlugin::class.java, HashCashPlugin::class.java,
NbStorePlugin::class.java, NbStorePlugin::class.java,
PreviewPlugin::class.java,
) )
) )
} }

View File

@@ -1,8 +1,6 @@
package app.affine.pro.ai.chat 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.CopilotChatHistory
import com.affine.pro.graphql.fragment.CopilotChatMessage
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@@ -53,7 +51,7 @@ data class ChatMessage(
createAt = Clock.System.now(), createAt = Clock.System.now(),
) )
fun from(message: CopilotChatMessage) = ChatMessage( fun from(message: CopilotChatHistory.Message) = ChatMessage(
id = message.id, id = message.id,
role = Role.fromValue(message.role), role = Role.fromValue(message.role),
content = message.content, content = message.content,

View File

@@ -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)
}
}
}
}

View File

@@ -72,7 +72,7 @@ class GraphQLService @Inject constructor() {
).mapCatching { data -> ).mapCatching { data ->
data.currentUser?.copilot?.chats?.paginatedCopilotChats?.edges?.map { item -> item.node.copilotChatHistory }?.firstOrNull { history -> data.currentUser?.copilot?.chats?.paginatedCopilotChats?.edges?.map { item -> item.node.copilotChatHistory }?.firstOrNull { history ->
history.sessionId == sessionId history.sessionId == sessionId
}?.messages?.map { msg -> msg.copilotChatMessage } ?: emptyList() }?.messages ?: emptyList()
} }
suspend fun getCopilotHistoryIds( suspend fun getCopilotHistoryIds(

View File

@@ -792,6 +792,10 @@ internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback {
@@ -816,6 +820,10 @@ internal interface IntegrityCheckingUniffiLib : Library {
): Short ): Short
fun uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool( fun uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool(
): Short ): 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( fun uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks(
): Short ): Short
fun uniffi_affine_mobile_native_checksum_method_docstoragepool_connect( 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 ): RustBuffer.ByValue
fun uniffi_affine_mobile_native_fn_func_new_doc_storage_pool(uniffi_out_err: UniffiRustCallStatus, fun uniffi_affine_mobile_native_fn_func_new_doc_storage_pool(uniffi_out_err: UniffiRustCallStatus,
): Pointer ): 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, fun ffi_affine_mobile_native_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus,
): RustBuffer.ByValue ): RustBuffer.ByValue
fun ffi_affine_mobile_native_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, 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()) { 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") 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()) { 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") 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 * @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)
}
)
}

View File

@@ -15,6 +15,7 @@ import {
ServersService, ServersService,
ValidatorProvider, ValidatorProvider,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
import { registerNativePreviewHandlers } from '@affine/core/modules/code-block-preview-renderer';
import { DocsService } from '@affine/core/modules/doc'; import { DocsService } from '@affine/core/modules/doc';
import { GlobalContextService } from '@affine/core/modules/global-context'; import { GlobalContextService } from '@affine/core/modules/global-context';
import { I18nProvider } from '@affine/core/modules/i18n'; import { I18nProvider } from '@affine/core/modules/i18n';
@@ -54,6 +55,7 @@ import { AIButton } from './plugins/ai-button';
import { Auth } from './plugins/auth'; import { Auth } from './plugins/auth';
import { HashCash } from './plugins/hashcash'; import { HashCash } from './plugins/hashcash';
import { NbStoreNativeDBApis } from './plugins/nbstore'; import { NbStoreNativeDBApis } from './plugins/nbstore';
import { Preview } from './plugins/preview';
import { writeEndpointToken } from './proxy'; import { writeEndpointToken } from './proxy';
const storeManagerClient = createStoreManagerClient(); const storeManagerClient = createStoreManagerClient();
@@ -85,6 +87,11 @@ framework.impl(NbstoreProvider, {
}); });
const frameworkProvider = framework.provider(); const frameworkProvider = framework.provider();
registerNativePreviewHandlers({
renderMermaidSvg: request => Preview.renderMermaidSvg(request),
renderTypstSvg: request => Preview.renderTypstSvg(request),
});
framework.impl(PopupWindowProvider, { framework.impl(PopupWindowProvider, {
open: (url: string) => { open: (url: string) => {
InAppBrowser.open({ InAppBrowser.open({

View File

@@ -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 }>;
}

View 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 };

View File

@@ -1,5 +1,6 @@
import { dialogHandlers } from './dialog'; import { dialogHandlers } from './dialog';
import { dbEventsV1, dbHandlersV1, nbstoreHandlers } from './nbstore'; import { dbEventsV1, dbHandlersV1, nbstoreHandlers } from './nbstore';
import { previewHandlers } from './preview';
import { provideExposed } from './provide'; import { provideExposed } from './provide';
import { workspaceEvents, workspaceHandlers } from './workspace'; import { workspaceEvents, workspaceHandlers } from './workspace';
@@ -8,6 +9,7 @@ export const handlers = {
nbstore: nbstoreHandlers, nbstore: nbstoreHandlers,
workspace: workspaceHandlers, workspace: workspaceHandlers,
dialog: dialogHandlers, dialog: dialogHandlers,
preview: previewHandlers,
}; };
export const events = { export const events = {

View 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));
},
};

View 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);
});
});

View File

@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/Recouse/EventSource", "location" : "https://github.com/Recouse/EventSource",
"state" : { "state" : {
"revision" : "7b2f4f585d3927876bd76eaede9fdff779eff102", "revision" : "713f8c0a0270a80a968c007ddc0d6067e80a5393",
"version" : "0.1.5" "version" : "0.1.7"
} }
}, },
{ {
@@ -41,8 +41,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/Litext", "location" : "https://github.com/Lakr233/Litext",
"state" : { "state" : {
"revision" : "c7e83f2f580ce34a102ca9ba9d2bb24e507dccd9", "revision" : "a2ed9b63ae623a20591effc72f9db7d04e41a64c",
"version" : "0.5.6" "version" : "1.2.1"
} }
}, },
{ {
@@ -77,8 +77,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/RevenueCat/purchases-ios-spm.git", "location" : "https://github.com/RevenueCat/purchases-ios-spm.git",
"state" : { "state" : {
"revision" : "8f5df97653eb361a2097119479332afccf0aa816", "revision" : "2913a336eb37dc06795cdbaa5b5de330b6707669",
"version" : "5.58.0" "version" : "5.65.0"
} }
}, },
{ {

View File

@@ -34,6 +34,7 @@ class AFFiNEViewController: CAPBridgeViewController {
NavigationGesturePlugin(), NavigationGesturePlugin(),
NbStorePlugin(), NbStorePlugin(),
PayWallPlugin(associatedController: self), PayWallPlugin(associatedController: self),
PreviewPlugin(),
] ]
plugins.forEach { bridge?.registerPluginInstance($0) } plugins.forEach { bridge?.registerPluginInstance($0) }
} }

View File

@@ -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)
}
}
}
}

View File

@@ -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) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #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 { private enum InitializationResult {
case ok case ok
@@ -2666,6 +2709,12 @@ private let initializationResult: InitializationResult = {
if (uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool() != 32882) { if (uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool() != 32882) {
return InitializationResult.apiChecksumMismatch 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) { if (uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks() != 51151) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }

View File

@@ -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 #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 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 #endif
#ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUSTBUFFER_ALLOC #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 #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 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 #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS

View File

@@ -8,4 +8,18 @@
import ApolloAPI 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). /// 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
}
}

View File

@@ -8,4 +8,22 @@
import ApolloAPI 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). /// 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
}
}

View File

@@ -26,6 +26,7 @@ private extension InputBoxData {
} }
public extension ChatManager { public extension ChatManager {
@MainActor
func startUserRequest(editorData: InputBoxData, sessionId: String) { func startUserRequest(editorData: InputBoxData, sessionId: String) {
append(sessionId: sessionId, UserMessageCellViewModel( append(sessionId: sessionId, UserMessageCellViewModel(
id: .init(), id: .init(),
@@ -163,7 +164,7 @@ private extension ChatManager {
assert(!Thread.isMainThread) assert(!Thread.isMainThread)
print("[+] starting copilot response for session: \(sessionId)") 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 // packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx
"docs": editorData.documentAttachments.map(\.documentID), // affine doc "docs": editorData.documentAttachments.map(\.documentID), // affine doc
"files": [String](), // attachment in context, keep nil for now "files": [String](), // attachment in context, keep nil for now
@@ -193,18 +194,14 @@ private extension ChatManager {
}, },
].flatMap(\.self) ].flatMap(\.self)
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) }) assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
guard let input = try? CreateChatMessageInput( let input = CreateChatMessageInput(
attachments: [], attachments: [],
blob: attachmentCount == 1 ? "" : .none, blob: attachmentCount == 1 ? "" : .none,
blobs: attachmentCount > 1 && attachmentCount != 0 ? .some([]) : .none, blobs: attachmentCount > 1 && attachmentCount != 0 ? .some([]) : .none,
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"), content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)), params: .some(messageParameters),
sessionId: sessionId sessionId: sessionId
) else { )
report(sessionId, ChatError.unknownError)
assertionFailure() // very unlikely to happen
return
}
let mutation = CreateCopilotMessageMutation(options: input) let mutation = CreateCopilotMessageMutation(options: input)
QLService.shared.client.upload(operation: mutation, files: uploadableAttachments) { result in QLService.shared.client.upload(operation: mutation, files: uploadableAttachments) { result in
print("[*] createCopilotMessage result: \(result)") print("[*] createCopilotMessage result: \(result)")
@@ -277,7 +274,7 @@ private extension ChatManager {
let eventSource = EventSource() let eventSource = EventSource()
let dataTask = eventSource.dataTask(for: request) let dataTask = eventSource.dataTask(for: request)
var document = "" 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() { for await event in dataTask.events() {
switch event { switch event {
case .open: case .open:
@@ -287,7 +284,7 @@ private extension ChatManager {
case let .event(event): case let .event(event):
guard let data = event.data else { continue } guard let data = event.data else { continue }
document += data document += data
self.writeMarkdownContent( await self.writeMarkdownContent(
document + loadingIndicator, document + loadingIndicator,
sessionId: sessionId, sessionId: sessionId,
vmId: vmId vmId: vmId
@@ -297,13 +294,13 @@ private extension ChatManager {
print("[*] connection closed") print("[*] connection closed")
} }
} }
self.writeMarkdownContent(document, sessionId: sessionId, vmId: vmId) await self.writeMarkdownContent(document, sessionId: sessionId, vmId: vmId)
self.closeAll() self.closeAll()
})) }))
self.closable.append(closable) self.closable.append(closable)
} }
private func writeMarkdownContent( @MainActor private func writeMarkdownContent(
_ document: String, _ document: String,
sessionId: SessionID, sessionId: SessionID,
vmId: UUID vmId: UUID

View File

@@ -9,7 +9,7 @@ import Foundation
import MarkdownView import MarkdownView
extension IntelligentContext { extension IntelligentContext {
func prepareMarkdownViewThemes() { @MainActor func prepareMarkdownViewThemes() {
MarkdownTheme.default.colors.body = .affineTextPrimary MarkdownTheme.default.colors.body = .affineTextPrimary
MarkdownTheme.default.colors.highlight = .affineTextLink MarkdownTheme.default.colors.highlight = .affineTextLink
} }

View File

@@ -40,7 +40,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
var preprocessedContent: MarkdownTextView.PreprocessedContent var preprocessedContent: MarkdownTextView.PreprocessedContent
init( @MainActor init(
id: UUID, id: UUID,
content: String, content: String,
timestamp: Date, timestamp: Date,

View File

@@ -45,13 +45,13 @@ EXTERNAL SOURCES:
:path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency" :path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Capacitor: 12914e6f1b7835e161a74ebd19cb361efa37a7dd Capacitor: a5bf59e09f9dd82694fdcca4d107b4d215ac470f
CapacitorApp: 63b237168fc869e758481dba283315a85743ee78 CapacitorApp: 3ddbd30ac18c321531c3da5e707b60873d89dd60
CapacitorBrowser: b98aa3db018a2ce4c68242d27e596c344f3b81b3 CapacitorBrowser: 66aa8ff09cdca2a327ce464b113b470e6f667753
CapacitorCordova: 31bbe4466000c6b86d9b7f1181ee286cff0205aa CapacitorCordova: 31bbe4466000c6b86d9b7f1181ee286cff0205aa
CapacitorHaptics: ce15be8f287fa2c61c7d2d9e958885b90cf0bebc CapacitorHaptics: d17da7dd984cae34111b3f097ccd3e21f9feec62
CapacitorKeyboard: 5660c760113bfa48962817a785879373cf5339c3 CapacitorKeyboard: 45cae3956a6f4fb1753f9a4df3e884aeaed8fe82
CapacitorPluginAppTrackingTransparency: 92ae9c1cfb5cf477753db9269689332a686f675a CapacitorPluginAppTrackingTransparency: 2a2792623a5a72795f2e8f9ab3f1147573732fd8
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082 PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082

View File

@@ -17,6 +17,7 @@ import {
SubscriptionService, SubscriptionService,
ValidatorProvider, ValidatorProvider,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
import { registerNativePreviewHandlers } from '@affine/core/modules/code-block-preview-renderer';
import { DocsService } from '@affine/core/modules/doc'; import { DocsService } from '@affine/core/modules/doc';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { GlobalContextService } from '@affine/core/modules/global-context'; import { GlobalContextService } from '@affine/core/modules/global-context';
@@ -71,6 +72,7 @@ import { Auth } from './plugins/auth';
import { Hashcash } from './plugins/hashcash'; import { Hashcash } from './plugins/hashcash';
import { NbStoreNativeDBApis } from './plugins/nbstore'; import { NbStoreNativeDBApis } from './plugins/nbstore';
import { PayWall } from './plugins/paywall'; import { PayWall } from './plugins/paywall';
import { Preview } from './plugins/preview';
import { writeEndpointToken } from './proxy'; import { writeEndpointToken } from './proxy';
import { enableNavigationGesture$ } from './web-navigation-control'; import { enableNavigationGesture$ } from './web-navigation-control';
@@ -215,6 +217,11 @@ framework.impl(NativePaywallProvider, {
const frameworkProvider = framework.provider(); const frameworkProvider = framework.provider();
registerNativePreviewHandlers({
renderMermaidSvg: request => Preview.renderMermaidSvg(request),
renderTypstSvg: request => Preview.renderTypstSvg(request),
});
// ------ some apis for native ------ // ------ some apis for native ------
(window as any).getCurrentServerBaseUrl = () => { (window as any).getCurrentServerBaseUrl = () => {
const globalContextService = frameworkProvider.get(GlobalContextService); const globalContextService = frameworkProvider.get(GlobalContextService);

View File

@@ -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 }>;
}

View 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 };

View File

@@ -47,6 +47,7 @@
"@radix-ui/react-toolbar": "^1.1.1", "@radix-ui/react-toolbar": "^1.1.1",
"@sentry/react": "^10.40.0", "@sentry/react": "^10.40.0",
"@toeverything/infra": "workspace:*", "@toeverything/infra": "workspace:*",
"@toeverything/mermaid-wasm": "^0.1.0",
"@toeverything/pdf-viewer": "^0.1.1", "@toeverything/pdf-viewer": "^0.1.1",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.23",
"@vanilla-extract/dynamic": "^2.1.2", "@vanilla-extract/dynamic": "^2.1.2",
@@ -57,6 +58,7 @@
"cmdk": "^1.0.4", "cmdk": "^1.0.4",
"core-js": "^3.39.0", "core-js": "^3.39.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.3.0",
"eventemitter2": "^6.4.9", "eventemitter2": "^6.4.9",
"file-type": "^21.0.0", "file-type": "^21.0.0",
"filesize": "^10.1.6", "filesize": "^10.1.6",
@@ -76,7 +78,7 @@
"lit": "^3.2.1", "lit": "^3.2.1",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.23",
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
"mermaid": "^11.12.2", "mermaid": "^11.13.0",
"mp4-muxer": "^5.2.2", "mp4-muxer": "^5.2.2",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",

View File

@@ -1,3 +1,4 @@
import { renderMermaidSvg } from '@affine/core/modules/code-block-preview-renderer/bridge';
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code'; import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { CodeBlockModel } from '@blocksuite/affine/model'; 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 { property, query, state } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js'; import { choose } from 'lit/directives/choose.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import type { Mermaid } from 'mermaid';
export const CodeBlockMermaidPreview = CodeBlockPreviewExtension( export const CodeBlockMermaidPreview = CodeBlockPreviewExtension(
'mermaid', 'mermaid',
@@ -154,7 +154,6 @@ export class MermaidPreview extends SignalWatcher(
@query('.mermaid-preview-container') @query('.mermaid-preview-container')
accessor container!: HTMLDivElement; accessor container!: HTMLDivElement;
private mermaid: Mermaid | null = null;
private retryCount = 0; private retryCount = 0;
private readonly maxRetries = 3; private readonly maxRetries = 3;
private renderTimeout: ReturnType<typeof setTimeout> | null = null; private renderTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -169,9 +168,6 @@ export class MermaidPreview extends SignalWatcher(
private lastMouseY = 0; private lastMouseY = 0;
override firstUpdated(_changedProperties: PropertyValues): void { override firstUpdated(_changedProperties: PropertyValues): void {
this._loadMermaid().catch(error => {
console.error('Failed to load mermaid in firstUpdated:', error);
});
this._scheduleRender(); this._scheduleRender();
this._setupEventListeners(); this._setupEventListeners();
@@ -271,7 +267,8 @@ export class MermaidPreview extends SignalWatcher(
event.preventDefault(); event.preventDefault();
const delta = event.deltaY > 0 ? 0.9 : 1.1; 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 // calculate mouse position relative to container
const rect = this.container.getBoundingClientRect(); const rect = this.container.getBoundingClientRect();
@@ -284,8 +281,8 @@ export class MermaidPreview extends SignalWatcher(
// update transform // update transform
this.scale = newScale; this.scale = newScale;
this.translateX = mouseX - scaleCenterX * (newScale / this.scale); this.translateX = mouseX - scaleCenterX * (newScale / previousScale);
this.translateY = mouseY - scaleCenterY * (newScale / this.scale); this.translateY = mouseY - scaleCenterY * (newScale / previousScale);
this._updateTransform(); 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() { private async _render() {
// prevent duplicate rendering // prevent duplicate rendering
if (this.isRendering) { if (this.isRendering) {
@@ -356,28 +315,25 @@ export class MermaidPreview extends SignalWatcher(
this.isRendering = true; this.isRendering = true;
this.state = 'loading'; this.state = 'loading';
if (!this.normalizedMermaidCode) { const code = this.normalizedMermaidCode?.trim();
if (!code) {
this.svgContent = '';
this.state = 'fallback'; this.state = 'fallback';
this.isRendering = false; this.isRendering = false;
return; return;
} }
if (!this.mermaid) {
await this._loadMermaid();
}
if (!this.mermaid) {
return;
}
try { try {
// generate unique ID const { svg } = await renderMermaidSvg({
const diagramId = `mermaid-diagram-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; code,
options: {
// generate SVG fastText: true,
const { svg } = await this.mermaid.render( svgOnly: true,
diagramId, theme: 'default',
this.normalizedMermaidCode fontFamily: 'IBM Plex Mono',
); },
});
// update SVG content // update SVG content
this.svgContent = svg; this.svgContent = svg;

View File

@@ -1,3 +1,4 @@
import { renderTypstSvg } from '@affine/core/modules/code-block-preview-renderer/bridge';
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code'; import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { CodeBlockModel } from '@blocksuite/affine/model'; 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 { choose } from 'lit/directives/choose.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { ensureTypstReady, getTypst } from './typst';
const RENDER_DEBOUNCE_MS = 200; const RENDER_DEBOUNCE_MS = 200;
export const CodeBlockTypstPreview = CodeBlockPreviewExtension( export const CodeBlockTypstPreview = CodeBlockPreviewExtension(
@@ -378,9 +377,7 @@ ${this.errorMessage}</pre
} }
try { try {
await ensureTypstReady(); const { svg } = await renderTypstSvg({ code });
const typst = await getTypst();
const svg = await typst.svg({ mainContent: code });
this.svgContent = svg; this.svgContent = svg;
this.state = 'finish'; this.state = 'finish';
this._resetView(); this._resetView();

View File

@@ -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;

View File

@@ -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.');
});
});

View File

@@ -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 };
}

View File

@@ -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',
]);
});
});

View File

@@ -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 };
});
}

View File

@@ -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]);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 };

View File

@@ -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());
}
}

View File

@@ -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;
};

View File

@@ -4,6 +4,7 @@ import type { FlagInfo } from './types';
const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary'; const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary';
const isMobile = BUILD_CONFIG.isMobileEdition; const isMobile = BUILD_CONFIG.isMobileEdition;
const isIOS = BUILD_CONFIG.isIOS; const isIOS = BUILD_CONFIG.isIOS;
const isAndroid = BUILD_CONFIG.isAndroid;
export const AFFINE_FLAGS = { export const AFFINE_FLAGS = {
enable_ai: { enable_ai: {
@@ -203,6 +204,14 @@ export const AFFINE_FLAGS = {
configurable: isMobile && isIOS, configurable: isMobile && isIOS,
defaultState: 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: { enable_turbo_renderer: {
category: 'blocksuite', category: 'blocksuite',
bsFlag: 'enable_turbo_renderer', bsFlag: 'enable_turbo_renderer',

View File

@@ -13,6 +13,7 @@ import { configureAppSidebarModule } from './app-sidebar';
import { configAtMenuConfigModule } from './at-menu-config'; import { configAtMenuConfigModule } from './at-menu-config';
import { configureBlobManagementModule } from './blob-management'; import { configureBlobManagementModule } from './blob-management';
import { configureCloudModule } from './cloud'; import { configureCloudModule } from './cloud';
import { configureCodeBlockPreviewRendererModule } from './code-block-preview-renderer';
import { configureCollectionModule } from './collection'; import { configureCollectionModule } from './collection';
import { configureCollectionRulesModule } from './collection-rules'; import { configureCollectionRulesModule } from './collection-rules';
import { configureCommentModule } from './comment'; import { configureCommentModule } from './comment';
@@ -77,6 +78,7 @@ export function configureCommonModules(framework: Framework) {
configureGlobalContextModule(framework); configureGlobalContextModule(framework);
configureLifecycleModule(framework); configureLifecycleModule(framework);
configureFeatureFlagModule(framework); configureFeatureFlagModule(framework);
configureCodeBlockPreviewRendererModule(framework);
configureCollectionModule(framework); configureCollectionModule(framework);
configureNavigationModule(framework); configureNavigationModule(framework);
configureTagModule(framework); configureTagModule(framework);

View 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';

View File

@@ -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);

View 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];
}

View File

@@ -1,2 +1,10 @@
export { PDFRenderer } from './renderer'; import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
export type { PDFMeta, RenderedPage, RenderPageOpts } from './types'; import type { PDFOps } from './types';
export class PDFRenderer extends WorkerOpRenderer<PDFOps> {
constructor() {
super('pdf');
}
}
export type { PDFMeta, PDFOps, RenderedPage, RenderPageOpts } from './types';

View File

@@ -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];
}

View File

@@ -23,10 +23,9 @@ import {
switchMap, switchMap,
} from 'rxjs'; } from 'rxjs';
import type { ClientOps } from './ops'; import type { PDFMeta, PDFOps, RenderPageOpts } from './types';
import type { PDFMeta, RenderPageOpts } from './types';
class PDFRendererBackend extends OpConsumer<ClientOps> { class PDFRendererBackend extends OpConsumer<PDFOps> {
constructor(port: MessageCommunicapable) { constructor(port: MessageCommunicapable) {
super(port); super(port);
this.register('open', this.open.bind(this)); this.register('open', this.open.bind(this));

View File

@@ -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();
}
}

View File

@@ -1,3 +1,5 @@
import type { OpSchema } from '@toeverything/infra/op';
export type PageSize = { export type PageSize = {
width: number; width: number;
height: number; height: number;
@@ -21,3 +23,8 @@ export type RenderPageOpts = {
export type RenderedPage = { export type RenderedPage = {
bitmap: ImageBitmap; bitmap: ImageBitmap;
}; };
export interface PDFOps extends OpSchema {
open: [{ data: ArrayBuffer }, PDFMeta];
render: [RenderPageOpts, RenderedPage];
}

View File

@@ -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');
});
});

View File

@@ -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();
}
}

View 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';

View File

@@ -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',
]);
});
});

View 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 };
});
}

View 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];
}

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
[package] [package]
edition = "2024" edition = "2024"
name = "affine_mobile_native" name = "affine_mobile_native"
publish = false
version = "0.0.0" version = "0.0.0"
[lib] [lib]
@@ -40,7 +41,11 @@ objc2-foundation = { workspace = true, features = [
homedir = { workspace = true } homedir = { workspace = true }
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] [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] [build-dependencies]
uniffi = { workspace = true, features = ["build"] } uniffi = { workspace = true, features = ["build"] }

View File

@@ -1,6 +1,8 @@
mod error; mod error;
mod ffi_types; mod ffi_types;
mod payload_codec; mod payload_codec;
#[cfg(any(target_os = "android", target_os = "ios"))]
mod preview;
mod storage; mod storage;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@@ -14,6 +16,8 @@ pub use error::UniffiError;
pub use ffi_types::{ pub use ffi_types::{
Blob, BlockInfo, CrawlResult, DocClock, DocRecord, DocUpdate, ListedBlob, MatchRange, SearchHit, SetBlob, 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}; pub use storage::{DocStoragePool, new_doc_storage_pool};
uniffi::setup_scaffolding!("affine_mobile_native"); uniffi::setup_scaffolding!("affine_mobile_native");

View 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))))
}

View File

@@ -1,6 +1,7 @@
[package] [package]
edition = "2024" edition = "2024"
name = "affine_native" name = "affine_native"
publish = false
version = "0.0.0" version = "0.0.0"
[lib] [lib]
@@ -25,6 +26,12 @@ sqlx = { workspace = true, default-features = false, features = [
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] } 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] [target.'cfg(not(target_os = "linux"))'.dependencies]
mimalloc = { workspace = true } mimalloc = { workspace = true }

View File

@@ -40,8 +40,41 @@ export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | u
/** Decode audio file into a Float32Array */ /** Decode audio file into a Float32Array */
export declare function decodeAudioSync(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null): 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 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 function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
export declare class DocStorage { export declare class DocStorage {
constructor(path: string) constructor(path: string)

View File

@@ -580,6 +580,8 @@ module.exports.ShareableContent = nativeBinding.ShareableContent
module.exports.decodeAudio = nativeBinding.decodeAudio module.exports.decodeAudio = nativeBinding.decodeAudio
module.exports.decodeAudioSync = nativeBinding.decodeAudioSync module.exports.decodeAudioSync = nativeBinding.decodeAudioSync
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
module.exports.renderMermaidSvg = nativeBinding.renderMermaidSvg
module.exports.renderTypstSvg = nativeBinding.renderTypstSvg
module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse
module.exports.DocStorage = nativeBinding.DocStorage module.exports.DocStorage = nativeBinding.DocStorage
module.exports.DocStoragePool = nativeBinding.DocStoragePool module.exports.DocStoragePool = nativeBinding.DocStoragePool

View File

@@ -1,6 +1,7 @@
[package] [package]
edition = "2024" edition = "2024"
name = "affine_media_capture" name = "affine_media_capture"
publish = false
version = "0.0.0" version = "0.0.0"
[lib] [lib]

View File

@@ -1,6 +1,7 @@
[package] [package]
edition = "2024" edition = "2024"
name = "affine_nbstore" name = "affine_nbstore"
publish = false
version = "0.0.0" version = "0.0.0"
[lib] [lib]

View File

@@ -1,6 +1,7 @@
[package] [package]
edition = "2024" edition = "2024"
name = "affine_schema" name = "affine_schema"
publish = false
version = "0.0.0" version = "0.0.0"
[dependencies] [dependencies]

View File

@@ -1,6 +1,7 @@
[package] [package]
edition = "2024" edition = "2024"
name = "affine_sqlite_v1" name = "affine_sqlite_v1"
publish = false
version = "0.0.0" version = "0.0.0"
[lib] [lib]

View File

@@ -1,4 +1,6 @@
pub mod hashcash; pub mod hashcash;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub mod preview;
#[cfg(not(target_arch = "arm"))] #[cfg(not(target_arch = "arm"))]
#[global_allocator] #[global_allocator]

View 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 ""##));
}
}

View File

@@ -27,6 +27,9 @@ import {
type WorkerConfig = { name: string }; type WorkerConfig = { name: string };
type CreateWorkerTargetConfig = (pkg: Package, entry: string) => WorkerConfig; type CreateWorkerTargetConfig = (pkg: Package, entry: string) => WorkerConfig;
type BaseWorkerOptions = {
includeMermaidAndTypst?: boolean;
};
function assertRspackSupportedPackage(pkg: Package) { function assertRspackSupportedPackage(pkg: Package) {
assertRspackSupportedPackageName(pkg.name); assertRspackSupportedPackageName(pkg.name);
@@ -49,11 +52,13 @@ async function uploadAssetsForPackage(pkg: Package, logger: Logger) {
function getBaseWorkerConfigs( function getBaseWorkerConfigs(
pkg: Package, pkg: Package,
createWorkerTargetConfig: CreateWorkerTargetConfig createWorkerTargetConfig: CreateWorkerTargetConfig,
options: BaseWorkerOptions = {}
) { ) {
const core = new Package('@affine/core'); const core = new Package('@affine/core');
const includeMermaidAndTypst = options.includeMermaidAndTypst ?? true;
return [ const workerConfigs = [
createWorkerTargetConfig( createWorkerTargetConfig(
pkg, pkg,
core.srcPath.join( core.srcPath.join(
@@ -71,6 +76,21 @@ function getBaseWorkerConfigs(
).value ).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 { function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
@@ -85,9 +105,7 @@ function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
] as MultiRspackOptions; ] as MultiRspackOptions;
} }
case '@affine/web': case '@affine/web':
case '@affine/mobile': case '@affine/mobile': {
case '@affine/ios':
case '@affine/android': {
const workerConfigs = getBaseWorkerConfigs( const workerConfigs = getBaseWorkerConfigs(
pkg, pkg,
createRspackWorkerTargetConfig createRspackWorkerTargetConfig
@@ -109,10 +127,35 @@ function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
...workerConfigs, ...workerConfigs,
] as MultiRspackOptions; ] 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': { case '@affine/electron-renderer': {
const workerConfigs = getBaseWorkerConfigs( const workerConfigs = getBaseWorkerConfigs(
pkg, pkg,
createRspackWorkerTargetConfig createRspackWorkerTargetConfig,
{ includeMermaidAndTypst: false }
); );
return [ return [

View File

@@ -90,6 +90,22 @@ export function createHTMLTargetConfig(
); );
const buildConfig = getBuildConfigFromEnv(pkg); 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( console.log(
`Building [${pkg.name}] for [${buildConfig.appBuildType}] channel in [${buildConfig.debug ? 'development' : 'production'}] mode.` `Building [${pkg.name}] for [${buildConfig.appBuildType}] channel in [${buildConfig.debug ? 'development' : 'production'}] mode.`
@@ -145,6 +161,8 @@ export function createHTMLTargetConfig(
'@preact', '@preact',
'signals-core' 'signals-core'
).value, ).value,
'@affine/core/modules/code-block-preview-renderer/platform-backend':
codeBlockPreviewBackendAlias,
}, },
}, },
//#endregion //#endregion

324
yarn.lock
View File

@@ -431,6 +431,7 @@ __metadata:
"@testing-library/dom": "npm:^10.4.0" "@testing-library/dom": "npm:^10.4.0"
"@testing-library/react": "npm:^16.1.0" "@testing-library/react": "npm:^16.1.0"
"@toeverything/infra": "workspace:*" "@toeverything/infra": "workspace:*"
"@toeverything/mermaid-wasm": "npm:^0.1.0"
"@toeverything/pdf-viewer": "npm:^0.1.1" "@toeverything/pdf-viewer": "npm:^0.1.1"
"@toeverything/theme": "npm:^1.1.23" "@toeverything/theme": "npm:^1.1.23"
"@types/animejs": "npm:^3.1.12" "@types/animejs": "npm:^3.1.12"
@@ -447,6 +448,7 @@ __metadata:
cmdk: "npm:^1.0.4" cmdk: "npm:^1.0.4"
core-js: "npm:^3.39.0" core-js: "npm:^3.39.0"
dayjs: "npm:^1.11.13" dayjs: "npm:^1.11.13"
dompurify: "npm:^3.3.0"
eventemitter2: "npm:^6.4.9" eventemitter2: "npm:^6.4.9"
fake-indexeddb: "npm:^6.0.0" fake-indexeddb: "npm:^6.0.0"
file-type: "npm:^21.0.0" file-type: "npm:^21.0.0"
@@ -468,7 +470,7 @@ __metadata:
lit: "npm:^3.2.1" lit: "npm:^3.2.1"
lodash-es: "npm:^4.17.23" lodash-es: "npm:^4.17.23"
lottie-react: "npm:^2.4.0" lottie-react: "npm:^2.4.0"
mermaid: "npm:^11.12.2" mermaid: "npm:^11.13.0"
mp4-muxer: "npm:^5.2.2" mp4-muxer: "npm:^5.2.2"
nanoid: "npm:^5.1.6" nanoid: "npm:^5.1.6"
next-themes: "npm:^0.4.4" next-themes: "npm:^0.4.4"
@@ -1136,13 +1138,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@apollo/cache-control-types@npm:^1.0.3":
version: 1.0.3 version: 1.0.3
resolution: "@apollo/cache-control-types@npm:1.0.3" resolution: "@apollo/cache-control-types@npm:1.0.3"
@@ -1736,6 +1731,7 @@ __metadata:
"@toeverything/theme": "npm:^1.1.23" "@toeverything/theme": "npm:^1.1.23"
"@vitest/browser-playwright": "npm:^4.0.18" "@vitest/browser-playwright": "npm:^4.0.18"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
playwright: "npm:=1.58.2"
rxjs: "npm:^7.8.2" rxjs: "npm:^7.8.2"
vitest: "npm:^4.0.18" vitest: "npm:^4.0.18"
yjs: "npm:^13.6.27" yjs: "npm:^13.6.27"
@@ -2800,6 +2796,7 @@ __metadata:
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
lit-html: "npm:^3.2.1" lit-html: "npm:^3.2.1"
lodash-es: "npm:^4.17.23" lodash-es: "npm:^4.17.23"
playwright: "npm:=1.58.2"
rxjs: "npm:^7.8.2" rxjs: "npm:^7.8.2"
vitest: "npm:^4.0.18" vitest: "npm:^4.0.18"
yjs: "npm:^13.6.27" yjs: "npm:^13.6.27"
@@ -3610,6 +3607,7 @@ __metadata:
"@vanilla-extract/vite-plugin": "npm:^5.0.0" "@vanilla-extract/vite-plugin": "npm:^5.0.0"
"@vitest/browser-playwright": "npm:^4.0.18" "@vitest/browser-playwright": "npm:^4.0.18"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
playwright: "npm:=1.58.2"
rxjs: "npm:^7.8.2" rxjs: "npm:^7.8.2"
vite: "npm:^7.2.7" vite: "npm:^7.2.7"
vite-plugin-istanbul: "npm:^7.2.1" vite-plugin-istanbul: "npm:^7.2.1"
@@ -3680,6 +3678,7 @@ __metadata:
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.23" lodash-es: "npm:^4.17.23"
lz-string: "npm:^1.5.0" lz-string: "npm:^1.5.0"
playwright: "npm:=1.58.2"
rehype-parse: "npm:^9.0.0" rehype-parse: "npm:^9.0.0"
rxjs: "npm:^7.8.2" rxjs: "npm:^7.8.2"
unified: "npm:^11.0.5" unified: "npm:^11.0.5"
@@ -3740,9 +3739,9 @@ __metadata:
linkType: hard linkType: hard
"@braintree/sanitize-url@npm:^7.1.1": "@braintree/sanitize-url@npm:^7.1.1":
version: 7.1.1 version: 7.1.2
resolution: "@braintree/sanitize-url@npm:7.1.1" resolution: "@braintree/sanitize-url@npm:7.1.2"
checksum: 10/a8a5535c5a0a459ba593a018c554b35493dff004fd09d7147db67243df83bce3d410b89ee7dc2d95cce195b85b877c72f8ca149e1040110a945d193c67293af0 checksum: 10/d9626ff8f8eb5e192cd055e6e743449c21102c76bb59e405b7028fe56230fa080bfcc80dfb1e21850a6876e75adda9f7b3c888cf0685942bb74da4d2866d6ec3
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3855,45 +3854,45 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@chevrotain/cst-dts-gen@npm:11.0.3": "@chevrotain/cst-dts-gen@npm:11.1.2":
version: 11.0.3 version: 11.1.2
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3" resolution: "@chevrotain/cst-dts-gen@npm:11.1.2"
dependencies: dependencies:
"@chevrotain/gast": "npm:11.0.3" "@chevrotain/gast": "npm:11.1.2"
"@chevrotain/types": "npm:11.0.3" "@chevrotain/types": "npm:11.1.2"
lodash-es: "npm:4.17.21" lodash-es: "npm:4.17.23"
checksum: 10/601d23fa3312bd0e32816bd3f9ca2dcba775a52192a082fd6c5e4a2e8ee068523401191babbe2c346d6d2551900a67b549f2f74d7ebb7d5b2ee1b6fa3c8857a0 checksum: 10/04a84285fd4d26129f44f8e20b99fd6ab51cb1ee4c7eb104baa4842413b9e18bb171046a665583b94ff29e2c06a2e1a65205940a75cda848aa2a5e847b99bbb6
languageName: node languageName: node
linkType: hard linkType: hard
"@chevrotain/gast@npm:11.0.3": "@chevrotain/gast@npm:11.1.2":
version: 11.0.3 version: 11.1.2
resolution: "@chevrotain/gast@npm:11.0.3" resolution: "@chevrotain/gast@npm:11.1.2"
dependencies: dependencies:
"@chevrotain/types": "npm:11.0.3" "@chevrotain/types": "npm:11.1.2"
lodash-es: "npm:4.17.21" lodash-es: "npm:4.17.23"
checksum: 10/7169453a8fbfa994e91995523dea09eab87ab23062ad93f6e51f4a3b03f5e2958e0a8b99d5ca6fa067fccfbbbb8bcf1a4573ace2e1b5a455f6956af9eaccb35a checksum: 10/8733cbadfcb982f5afe0e1825446ad8e82de3044c9ae22a69771ec20292facce25e459da23c16061c894ef49ca381ad53c0796171fa0dfce08fc25305850eeca
languageName: node languageName: node
linkType: hard linkType: hard
"@chevrotain/regexp-to-ast@npm:11.0.3": "@chevrotain/regexp-to-ast@npm:11.1.2":
version: 11.0.3 version: 11.1.2
resolution: "@chevrotain/regexp-to-ast@npm:11.0.3" resolution: "@chevrotain/regexp-to-ast@npm:11.1.2"
checksum: 10/7387a1c61c5a052de41e1172b33eaaedea166fcdb1ffe4c381b86d00051a8014855a031d28fb658768a62c833ef5f5b0689d0c40de3d7bed556f8fea24396e69 checksum: 10/9ba399b2c23ae1a86f1bcb1db4b07fd3191d0f491b63303b824850ef5f8b455f8f8a39c4d7876271c23ca06f7e03b7273b07014cd9c8ccb2328bff5dc6c9df00
languageName: node languageName: node
linkType: hard linkType: hard
"@chevrotain/types@npm:11.0.3": "@chevrotain/types@npm:11.1.2":
version: 11.0.3 version: 11.1.2
resolution: "@chevrotain/types@npm:11.0.3" resolution: "@chevrotain/types@npm:11.1.2"
checksum: 10/49a82b71d2de8ceb2383ff2709fa61d245f2ab2e42790b70c57102c80846edaa318d0b3645aedc904d23ea7bd9be8a58f2397b1341760a15eb5aa95a1336e2a9 checksum: 10/ad39d7651a20b05ead9b4374f98afc39915118dda29678c2b623571aa3da3018a0f3fb799d1809605cafcae59cba637d640855ea0f97b9b01d0900d77d6781fe
languageName: node languageName: node
linkType: hard linkType: hard
"@chevrotain/utils@npm:11.0.3": "@chevrotain/utils@npm:11.1.2":
version: 11.0.3 version: 11.1.2
resolution: "@chevrotain/utils@npm:11.0.3" resolution: "@chevrotain/utils@npm:11.1.2"
checksum: 10/29b5d84373a7761ad055c53e2f540a67b5b56550d5be1c473149f6b8923eef87ff391ce021c06ac7653843b0149f6ff0cf30b5e48c3f825d295eb06a6c517bd3 checksum: 10/a5ba5886547665726e6680bdc6e1c939e643c8c606d8eb3806ed178ebb2c3a8a1a581db558412374f230babfe9d8c1c37f2c3edf260c63729dbe6ff9ca4f0223
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6391,19 +6390,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@iconify/utils@npm:^3.0.1": "@iconify/utils@npm:^3.0.2":
version: 3.0.2 version: 3.1.0
resolution: "@iconify/utils@npm:3.0.2" resolution: "@iconify/utils@npm:3.1.0"
dependencies: dependencies:
"@antfu/install-pkg": "npm:^1.1.0" "@antfu/install-pkg": "npm:^1.1.0"
"@antfu/utils": "npm:^9.2.0"
"@iconify/types": "npm:^2.0.0" "@iconify/types": "npm:^2.0.0"
debug: "npm:^4.4.1" mlly: "npm:^1.8.0"
globals: "npm:^15.15.0" checksum: 10/28e83311ec7eca3f94a9c128c6d6f0f6aa68b7a63bcac44d08a1ea6f94d3752a7447a4354f3d02fdcdbf782ba033784ef7a65212b3afe52d9b41ef8138e96b14
kolorist: "npm:^1.8.0"
local-pkg: "npm:^1.1.1"
mlly: "npm:^1.7.4"
checksum: 10/b2db57b6a6b06d618b2625bf7bd056219a6c65e71a86c79c664d5e5fe03e531bc74fdd9cfa4d74e2ea469b6cd92b63012e2d588b32cade5aa7e3c31bc5124789
languageName: node languageName: node
linkType: hard linkType: hard
@@ -7531,12 +7525,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mermaid-js/parser@npm:^0.6.3": "@mermaid-js/parser@npm:^1.0.1":
version: 0.6.3 version: 1.0.1
resolution: "@mermaid-js/parser@npm:0.6.3" resolution: "@mermaid-js/parser@npm:1.0.1"
dependencies: dependencies:
langium: "npm:3.3.1" langium: "npm:^4.0.0"
checksum: 10/ab8bbdeaf2ef556871f3267541c0b3621d70c4d108ddac36383adc7eb1c7e6bed28d068b4ad196b54314877f263f939f90f0a1a3cfe8576fab30f4514732aa2f checksum: 10/648c96da8464113c3694587f3f950421b1914d34fddc194b6dff7a8c28da5e37273ddc4d56a4efaeda82e93c7a168389f035dbac6c8d5b65930ee60b5063ece4
languageName: node languageName: node
linkType: hard linkType: hard
@@ -15772,6 +15766,13 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "@toeverything/pdf-viewer-types@npm:0.1.1":
version: 0.1.1 version: 0.1.1
resolution: "@toeverything/pdf-viewer-types@npm:0.1.1" resolution: "@toeverything/pdf-viewer-types@npm:0.1.1"
@@ -17488,6 +17489,21 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@vanilla-extract/babel-plugin-debug-ids@npm:^1.2.2":
version: 1.2.2 version: 1.2.2
resolution: "@vanilla-extract/babel-plugin-debug-ids@npm:1.2.2" resolution: "@vanilla-extract/babel-plugin-debug-ids@npm:1.2.2"
@@ -18149,7 +18165,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 8.16.0
resolution: "acorn@npm:8.16.0" resolution: "acorn@npm:8.16.0"
bin: bin:
@@ -19740,7 +19756,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chevrotain-allstar@npm:~0.3.0": "chevrotain-allstar@npm:~0.3.1":
version: 0.3.1 version: 0.3.1
resolution: "chevrotain-allstar@npm:0.3.1" resolution: "chevrotain-allstar@npm:0.3.1"
dependencies: dependencies:
@@ -19751,17 +19767,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chevrotain@npm:~11.0.3": "chevrotain@npm:~11.1.1":
version: 11.0.3 version: 11.1.2
resolution: "chevrotain@npm:11.0.3" resolution: "chevrotain@npm:11.1.2"
dependencies: dependencies:
"@chevrotain/cst-dts-gen": "npm:11.0.3" "@chevrotain/cst-dts-gen": "npm:11.1.2"
"@chevrotain/gast": "npm:11.0.3" "@chevrotain/gast": "npm:11.1.2"
"@chevrotain/regexp-to-ast": "npm:11.0.3" "@chevrotain/regexp-to-ast": "npm:11.1.2"
"@chevrotain/types": "npm:11.0.3" "@chevrotain/types": "npm:11.1.2"
"@chevrotain/utils": "npm:11.0.3" "@chevrotain/utils": "npm:11.1.2"
lodash-es: "npm:4.17.21" lodash-es: "npm:4.17.23"
checksum: 10/8fa6253e51320dd4c3d386315b925734943e509d7954a2cd917746c0604461191bea57b0fb8fbab1903e0508fd94bfd35ebd0f8eace77cd0f3f42a9ee4f8f676 checksum: 10/67caa47a3d38eeb4a584960bbf5fdc83894a1419a6428f7dcf8a07f5937c1bda4c8e1acf628fe80d3be8f4c1a84426942c0a8f6bcf44a5958f1df0cf4225bc4d
languageName: node languageName: node
linkType: hard linkType: hard
@@ -21051,7 +21067,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cytoscape@npm:^3.29.3": "cytoscape@npm:^3.33.1":
version: 3.33.1 version: 3.33.1
resolution: "cytoscape@npm:3.33.1" resolution: "cytoscape@npm:3.33.1"
checksum: 10/0e8d3ea87eb624899341d6a765cfb732199af8a871beedeb94971061632ce814c2c39e8257d6628c5611ca9dadc1a723a00377d04f149e0d24f6c133a6ab8647 checksum: 10/0e8d3ea87eb624899341d6a765cfb732199af8a871beedeb94971061632ce814c2c39e8257d6628c5611ca9dadc1a723a00377d04f149e0d24f6c133a6ab8647
@@ -21196,9 +21212,9 @@ __metadata:
linkType: hard linkType: hard
"d3-format@npm:1 - 3, d3-format@npm:3": "d3-format@npm:1 - 3, d3-format@npm:3":
version: 3.1.0 version: 3.1.2
resolution: "d3-format@npm:3.1.0" resolution: "d3-format@npm:3.1.2"
checksum: 10/a0fe23d2575f738027a3db0ce57160e5a473ccf24808c1ed46d45ef4f3211076b34a18b585547d34e365e78dcc26dd4ab15c069731fc4b1c07a26bfced09ea31 checksum: 10/811d913c2c7624cb0d2a8f0ccd7964c50945b3de3c7f7aa14c309fba7266a3ec53cbee8c05f6ad61b2b65b93e157c55a0e07db59bc3180c39dac52be8e841ab1
languageName: node languageName: node
linkType: hard linkType: hard
@@ -21295,7 +21311,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 3.0.0
resolution: "d3-selection@npm:3.0.0" resolution: "d3-selection@npm:3.0.0"
checksum: 10/0e5acfd305b31628b7be5009ba7303d84bb34817a88ed4dde9c8bd9c23528573fc5272f89fc04e5be03d2cbf5441a248d7274aaf55a8ef3dad46e16333d72298 checksum: 10/0e5acfd305b31628b7be5009ba7303d84bb34817a88ed4dde9c8bd9c23528573fc5272f89fc04e5be03d2cbf5441a248d7274aaf55a8ef3dad46e16333d72298
@@ -21345,7 +21361,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 3.0.1
resolution: "d3-transition@npm:3.0.1" resolution: "d3-transition@npm:3.0.1"
dependencies: dependencies:
@@ -21411,13 +21427,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dagre-d3-es@npm:7.0.13": "dagre-d3-es@npm:7.0.14":
version: 7.0.13 version: 7.0.14
resolution: "dagre-d3-es@npm:7.0.13" resolution: "dagre-d3-es@npm:7.0.14"
dependencies: dependencies:
d3: "npm:^7.9.0" d3: "npm:^7.9.0"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.21"
checksum: 10/f6dbd373b85cc9fbcb23fba996656a0336ba48bc46f1e6d31c582418a5086caf230a4e8178b90acd7b1d14b090cbba2db50dc64484d67cf9c8856a4a2fe30cf0 checksum: 10/f2787049ae2684de27950dfc61eb23437cbb5c3ca7ec7f58620e19f16059465b6d23324ca861961353a60bb4cdaa5c66edfa9bbe44ac2304b72dd00ab4199714
languageName: node languageName: node
linkType: hard linkType: hard
@@ -21474,10 +21490,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dayjs@npm:^1.11.13, dayjs@npm:^1.11.18": "dayjs@npm:^1.11.13, dayjs@npm:^1.11.19":
version: 1.11.18 version: 1.11.20
resolution: "dayjs@npm:1.11.18" resolution: "dayjs@npm:1.11.20"
checksum: 10/7d29a90834cf4da2feb437c2f34b8235c3f94493a06d2f1bf9f506f1fa49eadf796f26e1d685b9fe8cb5e75ce6ee067825115e196f1af3d07b3552ff857bfc39 checksum: 10/5347533f21a55b8bb1b1ef559be9b805514c3a8fb7e68b75fb7e73808131c59e70909c073aa44ce8a0d159195cd110cdd4081cf87ab96cb06fee3edacae791c6
languageName: node languageName: node
linkType: hard linkType: hard
@@ -21945,15 +21961,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dompurify@npm:^3.2.5, dompurify@npm:^3.3.0": "dompurify@npm:^3.3.0, dompurify@npm:^3.3.1":
version: 3.3.2 version: 3.3.3
resolution: "dompurify@npm:3.3.2" resolution: "dompurify@npm:3.3.3"
dependencies: dependencies:
"@types/trusted-types": "npm:^2.0.7" "@types/trusted-types": "npm:^2.0.7"
dependenciesMeta: dependenciesMeta:
"@types/trusted-types": "@types/trusted-types":
optional: true optional: true
checksum: 10/3ca02559677ce6d9583a500f21ffbb6b9e88f1af99f69fa0d0d9442cddbac98810588c869f8b435addb5115492d6e49870024bca322169b941bafedb99c7f281 checksum: 10/4cc9c539ed7136d46c6577613b8e20871c2b6165db01dfbd2a3c11c75f9e339c496ac6519a1c3190115def8cadae3720bef0417fc43fa28802c7407bab174da9
languageName: node languageName: node
linkType: hard linkType: hard
@@ -24462,13 +24478,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "globals@npm:^16.4.0":
version: 16.5.0 version: 16.5.0
resolution: "globals@npm:16.5.0" resolution: "globals@npm:16.5.0"
@@ -26663,14 +26672,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"katex@npm:^0.16.0, katex@npm:^0.16.22, katex@npm:^0.16.27": "katex@npm:^0.16.0, katex@npm:^0.16.25, katex@npm:^0.16.27":
version: 0.16.27 version: 0.16.38
resolution: "katex@npm:0.16.27" resolution: "katex@npm:0.16.38"
dependencies: dependencies:
commander: "npm:^8.3.0" commander: "npm:^8.3.0"
bin: bin:
katex: cli.js katex: cli.js
checksum: 10/7666ae11c6c1238626bffaf1a526af6ff679114d62293bf2f0e29f8a34d8e961c0edcb686c5b628158ec92a143b4bef5d83539c81b29a63c7dcf0bdb4544eec9 checksum: 10/e9103def114d9d08ab216864e66b68e6f50a6360fdc5aa29d8edeee430e1618dd7551b9f080e9c591b3ee24c18fe6910b8fe0c89c7c4b1109abd2b63e223fbc5
languageName: node languageName: node
linkType: hard linkType: hard
@@ -26704,13 +26713,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "kuler@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "kuler@npm:2.0.0" resolution: "kuler@npm:2.0.0"
@@ -26718,16 +26720,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"langium@npm:3.3.1": "langium@npm:^4.0.0":
version: 3.3.1 version: 4.2.1
resolution: "langium@npm:3.3.1" resolution: "langium@npm:4.2.1"
dependencies: dependencies:
chevrotain: "npm:~11.0.3" chevrotain: "npm:~11.1.1"
chevrotain-allstar: "npm:~0.3.0" chevrotain-allstar: "npm:~0.3.1"
vscode-languageserver: "npm:~9.0.1" vscode-languageserver: "npm:~9.0.1"
vscode-languageserver-textdocument: "npm:~1.0.11" vscode-languageserver-textdocument: "npm:~1.0.11"
vscode-uri: "npm:~3.0.8" vscode-uri: "npm:~3.1.0"
checksum: 10/6b2e5bc1ff47c6048ec24471333f3397ddb4d6419f1c2262268faff00a8f0839ac4bd4877907261273e91e82f239951249155c3aff8d395ee5e3372dfc285e04 checksum: 10/9fd9208762ae9b551ec1deea25b6633605b06b59505a1ce6bf5b6c639779378bab35f813ffe549db57af95a5b71f9dcf61e553273a5d9d6ad2a3a99c5b7bfcc2
languageName: node languageName: node
linkType: hard linkType: hard
@@ -27105,17 +27107,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "locate-path@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "locate-path@npm:2.0.0" resolution: "locate-path@npm:2.0.0"
@@ -27153,14 +27144,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lodash-es@npm:4.17.21": "lodash-es@npm:4.17.23, lodash-es@npm:^4.17.21, lodash-es@npm:^4.17.23":
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":
version: 4.17.23 version: 4.17.23
resolution: "lodash-es@npm:4.17.23" resolution: "lodash-es@npm:4.17.23"
checksum: 10/1feae200df22eb0bd93ca86d485e77784b8a9fb1d13e91b66e9baa7a7e5e04be088c12a7e20c2250fc0bd3db1bc0ef0affc7d9e3810b6af2455a3c6bf6dde59e checksum: 10/1feae200df22eb0bd93ca86d485e77784b8a9fb1d13e91b66e9baa7a7e5e04be088c12a7e20c2250fc0bd3db1bc0ef0affc7d9e3810b6af2455a3c6bf6dde59e
@@ -27762,12 +27746,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"marked@npm:^16.2.1": "marked@npm:^16.3.0":
version: 16.3.0 version: 16.4.2
resolution: "marked@npm:16.3.0" resolution: "marked@npm:16.4.2"
bin: bin:
marked: bin/marked.js marked: bin/marked.js
checksum: 10/60497834b9acfb3b3994222509d359ecb9a197c885dfeb77e2050a287cd2f4ab19f00d5597172b47f9e0c54d9e1e13d8b2dd73322b7838599e1f16d1d6283f5b checksum: 10/6e40e40661dce97e271198daa2054fc31e6445892a735e416c248fba046bdfa4573cafa08dc254529f105e7178a34485eb7f82573979cfb377a4530f66e79187
languageName: node languageName: node
linkType: hard linkType: hard
@@ -28119,31 +28103,32 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mermaid@npm:^11.12.2": "mermaid@npm:^11.13.0":
version: 11.12.2 version: 11.13.0
resolution: "mermaid@npm:11.12.2" resolution: "mermaid@npm:11.13.0"
dependencies: dependencies:
"@braintree/sanitize-url": "npm:^7.1.1" "@braintree/sanitize-url": "npm:^7.1.1"
"@iconify/utils": "npm:^3.0.1" "@iconify/utils": "npm:^3.0.2"
"@mermaid-js/parser": "npm:^0.6.3" "@mermaid-js/parser": "npm:^1.0.1"
"@types/d3": "npm:^7.4.3" "@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-cose-bilkent: "npm:^4.1.0"
cytoscape-fcose: "npm:^2.2.0" cytoscape-fcose: "npm:^2.2.0"
d3: "npm:^7.9.0" d3: "npm:^7.9.0"
d3-sankey: "npm:^0.12.3" d3-sankey: "npm:^0.12.3"
dagre-d3-es: "npm:7.0.13" dagre-d3-es: "npm:7.0.14"
dayjs: "npm:^1.11.18" dayjs: "npm:^1.11.19"
dompurify: "npm:^3.2.5" dompurify: "npm:^3.3.1"
katex: "npm:^0.16.22" katex: "npm:^0.16.25"
khroma: "npm:^2.1.0" khroma: "npm:^2.1.0"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.23"
marked: "npm:^16.2.1" marked: "npm:^16.3.0"
roughjs: "npm:^4.6.6" roughjs: "npm:^4.6.6"
stylis: "npm:^4.3.6" stylis: "npm:^4.3.6"
ts-dedent: "npm:^2.2.0" ts-dedent: "npm:^2.2.0"
uuid: "npm:^11.1.0" uuid: "npm:^11.1.0"
checksum: 10/3c07c1be97a830904c7802933664abd132d626921c3aa82db8d0fbaad35832907cbaa2250747f17e110de5d6f4bdd1fcb9f0416b42c8e59a73653e809333d3da checksum: 10/7bf2123b005925983eb4d528896665963002c3a617f13fe9d356e4910ff71d3ad0c3f817e717414828f9cdba147651f2ac28af15c60c085f4444f5a786691dd1
languageName: node languageName: node
linkType: hard linkType: hard
@@ -28801,15 +28786,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mlly@npm:^1.4.2, mlly@npm:^1.7.1, 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.7.4 version: 1.8.1
resolution: "mlly@npm:1.7.4" resolution: "mlly@npm:1.8.1"
dependencies: dependencies:
acorn: "npm:^8.14.0" acorn: "npm:^8.16.0"
pathe: "npm:^2.0.1" pathe: "npm:^2.0.3"
pkg-types: "npm:^1.3.0" pkg-types: "npm:^1.3.1"
ufo: "npm:^1.5.4" ufo: "npm:^1.6.3"
checksum: 10/1b36163d38c2331f8ae480e6a11da3d15927a2148d729fcd9df6d0059ca74869aa693931bd1f762f82eb534b84c921bdfbc036eb0e4da4faeb55f1349d254f35 checksum: 10/8e424f0615d09adfb7d59ad8f0c8245df275cd05e483a4631a1b2c5dd7e09913a9ce8182bc1562d569941ecee25ab03f4429284265471f562da1dd308008e237
languageName: node languageName: node
linkType: hard linkType: hard
@@ -30088,9 +30073,9 @@ __metadata:
linkType: hard linkType: hard
"package-manager-detector@npm:^1.3.0": "package-manager-detector@npm:^1.3.0":
version: 1.3.0 version: 1.6.0
resolution: "package-manager-detector@npm:1.3.0" resolution: "package-manager-detector@npm:1.6.0"
checksum: 10/b21155d53a8ab96d5be3bfae43cc1d397bf363782b922d1f6967d220d2a9f08234ebb76035318bf92822ce761d10451959f01019faebc08fdb4d4a8bc3103da6 checksum: 10/b38a9532198cefdb98a1b7131c42cbffa55d8b997d6117811cf83f00079fd57a572db2aa5e3db5e36bcd0af84d0bec5a7d6251142427314390ed99a3d76cd0a0
languageName: node languageName: node
linkType: hard linkType: hard
@@ -30598,7 +30583,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 1.3.1
resolution: "pkg-types@npm:1.3.1" resolution: "pkg-types@npm:1.3.1"
dependencies: dependencies:
@@ -30609,7 +30594,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.3.0
resolution: "pkg-types@npm:2.3.0" resolution: "pkg-types@npm:2.3.0"
dependencies: dependencies:
@@ -31416,13 +31401,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "query-string@npm:^9.1.1":
version: 9.2.0 version: 9.2.0
resolution: "query-string@npm:9.2.0" resolution: "query-string@npm:9.2.0"
@@ -34637,9 +34615,9 @@ __metadata:
linkType: hard linkType: hard
"tinyexec@npm:^1.0.0, tinyexec@npm:^1.0.1, tinyexec@npm:^1.0.2": "tinyexec@npm:^1.0.0, tinyexec@npm:^1.0.1, tinyexec@npm:^1.0.2":
version: 1.0.2 version: 1.0.4
resolution: "tinyexec@npm:1.0.2" resolution: "tinyexec@npm:1.0.4"
checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 checksum: 10/ccebe4044eef6fa5050929df7862fda70b4fb700f15d94aef8ae6109b9d194dbc3a990125d99944fd25b90fe2115e1927f055b909a604c571a81b647ede5757a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -35169,10 +35147,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ufo@npm:^1.5.4": "ufo@npm:^1.5.4, ufo@npm:^1.6.3":
version: 1.6.1 version: 1.6.3
resolution: "ufo@npm:1.6.1" resolution: "ufo@npm:1.6.3"
checksum: 10/088a68133b93af183b093e5a8730a40fe7fd675d3dc0656ea7512f180af45c92300c294f14d4d46d4b2b553e3e52d3b13d4856b9885e620e7001edf85531234e checksum: 10/79803984f3e414567273a666183d6a50d1bec0d852100a98f55c1e393cb705e3b88033e04029dd651714e6eec99e1b00f54fdc13f32404968251a16f8898cfe5
languageName: node languageName: node
linkType: hard linkType: hard
@@ -36051,10 +36029,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vscode-uri@npm:~3.0.8": "vscode-uri@npm:~3.1.0":
version: 3.0.8 version: 3.1.0
resolution: "vscode-uri@npm:3.0.8" resolution: "vscode-uri@npm:3.1.0"
checksum: 10/e882d6b679e0d053cbc042893c0951a135d899a192b62cd07f0a8924f11ae722067a8d6b1b5b147034becf57faf9fff9fb543b17b749fd0f17db1f54f783f07c checksum: 10/80c2a2421f44b64008ef1f91dfa52a2d68105cbb4dcea197dbf5b00c65ccaccf218b615e93ec587f26fc3ba04796898f3631a9406e3b04cda970c3ca8eadf646
languageName: node languageName: node
linkType: hard linkType: hard