mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-23 01:07:12 +08:00
Compare commits
2 Commits
v0.26.3-be
...
darksky/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d385514fca | ||
|
|
3d01766f55 |
2030
Cargo.lock
generated
2030
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,9 @@ export default defineConfig({
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
name: 'chromium',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
provider: 'playwright',
|
||||
isolate: false,
|
||||
providerOptions: {},
|
||||
},
|
||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||
testTimeout: 500,
|
||||
|
||||
@@ -8,10 +8,9 @@ export default defineConfig({
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
name: 'chromium',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
provider: 'playwright',
|
||||
isolate: false,
|
||||
providerOptions: {},
|
||||
},
|
||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||
testTimeout: 500,
|
||||
|
||||
@@ -8,10 +8,9 @@ export default defineConfig({
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
name: 'chromium',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
provider: 'playwright',
|
||||
isolate: false,
|
||||
providerOptions: {},
|
||||
},
|
||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
||||
testTimeout: 500,
|
||||
|
||||
@@ -19,11 +19,7 @@ export default defineConfig(_configEnv =>
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: process.env.CI === 'true',
|
||||
instances: [
|
||||
{ browser: 'chromium' },
|
||||
{ browser: 'firefox' },
|
||||
{ browser: 'webkit' },
|
||||
],
|
||||
instances: [{ browser: 'chromium' }],
|
||||
provider: 'playwright',
|
||||
isolate: false,
|
||||
viewport: {
|
||||
|
||||
@@ -276,22 +276,16 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.models.history.create(
|
||||
{
|
||||
spaceId: snapshot.spaceId,
|
||||
docId: snapshot.docId,
|
||||
timestamp: snapshot.timestamp,
|
||||
blob: Buffer.from(snapshot.bin),
|
||||
editorId: snapshot.editor,
|
||||
},
|
||||
historyMaxAge
|
||||
);
|
||||
} catch (e) {
|
||||
// safe to ignore
|
||||
// only happens when duplicated history record created in multi processes
|
||||
this.logger.error('Failed to create history record', e);
|
||||
}
|
||||
await this.models.history.create(
|
||||
{
|
||||
spaceId: snapshot.spaceId,
|
||||
docId: snapshot.docId,
|
||||
timestamp: snapshot.timestamp,
|
||||
blob: Buffer.from(snapshot.bin),
|
||||
editorId: snapshot.editor,
|
||||
},
|
||||
historyMaxAge
|
||||
);
|
||||
|
||||
metrics.doc
|
||||
.counter('history_created_counter', {
|
||||
|
||||
@@ -74,6 +74,27 @@ test('should create a history record', async t => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should not fail on duplicated history record', async t => {
|
||||
const snapshot = {
|
||||
spaceId: workspace.id,
|
||||
docId: randomUUID(),
|
||||
blob: Uint8Array.from([1, 2, 3]),
|
||||
timestamp: Date.now(),
|
||||
editorId: user.id,
|
||||
};
|
||||
|
||||
const created1 = await t.context.history.create(snapshot, 1000);
|
||||
const created2 = await t.context.history.create(snapshot, 1000);
|
||||
t.deepEqual(created1.timestamp, snapshot.timestamp);
|
||||
t.deepEqual(created2.timestamp, snapshot.timestamp);
|
||||
|
||||
const histories = await t.context.history.findMany(
|
||||
snapshot.spaceId,
|
||||
snapshot.docId
|
||||
);
|
||||
t.is(histories.length, 1);
|
||||
});
|
||||
|
||||
test('should return null when history timestamp not match', async t => {
|
||||
const snapshot = {
|
||||
spaceId: workspace.id,
|
||||
|
||||
@@ -33,22 +33,33 @@ export class HistoryModel extends BaseModel {
|
||||
* Create a doc history with a max age.
|
||||
*/
|
||||
async create(snapshot: Doc, maxAge: number): Promise<DocHistorySimple> {
|
||||
const row = await this.db.snapshotHistory.create({
|
||||
select: {
|
||||
timestamp: true,
|
||||
createdByUser: { select: publicUserSelect },
|
||||
const timestamp = new Date(snapshot.timestamp);
|
||||
const expiredAt = new Date(Date.now() + maxAge);
|
||||
|
||||
// This method may be called concurrently by multiple processes for the same
|
||||
// (workspaceId, docId, timestamp). Using upsert avoids duplicate key errors
|
||||
// that would otherwise abort the surrounding transaction.
|
||||
const row = await this.db.snapshotHistory.upsert({
|
||||
where: {
|
||||
workspaceId_id_timestamp: {
|
||||
workspaceId: snapshot.spaceId,
|
||||
id: snapshot.docId,
|
||||
timestamp,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
select: { timestamp: true, createdByUser: { select: publicUserSelect } },
|
||||
create: {
|
||||
workspaceId: snapshot.spaceId,
|
||||
id: snapshot.docId,
|
||||
timestamp: new Date(snapshot.timestamp),
|
||||
timestamp,
|
||||
blob: snapshot.blob,
|
||||
createdBy: snapshot.editorId,
|
||||
expiredAt: new Date(Date.now() + maxAge),
|
||||
expiredAt,
|
||||
},
|
||||
update: { expiredAt },
|
||||
});
|
||||
this.logger.debug(
|
||||
`Created history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
|
||||
`Upserted history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
|
||||
);
|
||||
return {
|
||||
timestamp: row.timestamp.getTime(),
|
||||
|
||||
@@ -10,6 +10,7 @@ interface TestOps extends OpSchema {
|
||||
add: [{ a: number; b: number }, number];
|
||||
bin: [Uint8Array, Uint8Array];
|
||||
sub: [Uint8Array, number];
|
||||
init: [{ fastText?: boolean } | undefined, { ok: true }];
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
@@ -84,6 +85,55 @@ describe('op client', () => {
|
||||
expect(data.byteLength).toBe(0);
|
||||
});
|
||||
|
||||
it('should send optional payload call with abort signal', async ctx => {
|
||||
const abortController = new AbortController();
|
||||
const result = ctx.producer.call(
|
||||
'init',
|
||||
{ fastText: true },
|
||||
abortController.signal
|
||||
);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "init:1",
|
||||
"name": "init",
|
||||
"payload": {
|
||||
"fastText": true,
|
||||
},
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'init:1',
|
||||
data: { ok: true },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('should send undefined payload for optional input call', async ctx => {
|
||||
const result = ctx.producer.call('init', undefined);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "init:1",
|
||||
"name": "init",
|
||||
"payload": undefined,
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'init:1',
|
||||
data: { ok: true },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('should cancel call', async ctx => {
|
||||
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
||||
|
||||
|
||||
@@ -40,18 +40,14 @@ describe('op consumer', () => {
|
||||
it('should throw if no handler registered', async ctx => {
|
||||
ctx.handlers.call({ type: 'call', id: 'add:1', name: 'add', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"message": "Handler for operation [add] is not registered.",
|
||||
"name": "Error",
|
||||
},
|
||||
"id": "add:1",
|
||||
"type": "return",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(ctx.postMessage.mock.lastCall?.[0]).toMatchObject({
|
||||
type: 'return',
|
||||
id: 'add:1',
|
||||
error: {
|
||||
message: 'Handler for operation [add] is not registered.',
|
||||
name: 'Error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle call message', async ctx => {
|
||||
@@ -73,6 +69,38 @@ describe('op consumer', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should serialize string errors with message', async ctx => {
|
||||
ctx.consumer.register('any', () => {
|
||||
throw 'worker panic';
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchObject({
|
||||
type: 'return',
|
||||
id: 'any:1',
|
||||
error: {
|
||||
name: 'Error',
|
||||
message: 'worker panic',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize plain object errors with fallback message', async ctx => {
|
||||
ctx.consumer.register('any', () => {
|
||||
throw { reason: 'panic', code: 'E_PANIC' };
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
|
||||
const message = ctx.postMessage.mock.calls[0][0]?.error?.message;
|
||||
expect(typeof message).toBe('string');
|
||||
expect(message).toContain('"reason":"panic"');
|
||||
expect(message).toContain('"code":"E_PANIC"');
|
||||
});
|
||||
|
||||
it('should handle cancel message', async ctx => {
|
||||
ctx.consumer.register('add', ({ a, b }, { signal }) => {
|
||||
const { reject, resolve, promise } = Promise.withResolvers<number>();
|
||||
|
||||
@@ -16,6 +16,96 @@ import {
|
||||
} from './message';
|
||||
import type { OpInput, OpNames, OpOutput, OpSchema } from './types';
|
||||
|
||||
const SERIALIZABLE_ERROR_FIELDS = [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
] as const;
|
||||
|
||||
type SerializableErrorShape = Partial<
|
||||
Record<(typeof SERIALIZABLE_ERROR_FIELDS)[number], unknown>
|
||||
> & {
|
||||
name?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function getFallbackErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof error === 'number' ||
|
||||
typeof error === 'boolean' ||
|
||||
typeof error === 'bigint' ||
|
||||
typeof error === 'symbol'
|
||||
) {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
if (error === null || error === undefined) {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonMessage = JSON.stringify(error);
|
||||
if (jsonMessage && jsonMessage !== '{}') {
|
||||
return jsonMessage;
|
||||
}
|
||||
} catch {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
function serializeError(error: unknown): Error {
|
||||
const valueToPick =
|
||||
error && typeof error === 'object'
|
||||
? error
|
||||
: ({} as Record<string, unknown>);
|
||||
const serialized = pick(
|
||||
valueToPick,
|
||||
SERIALIZABLE_ERROR_FIELDS
|
||||
) as SerializableErrorShape;
|
||||
|
||||
if (!serialized.message || typeof serialized.message !== 'string') {
|
||||
serialized.message = getFallbackErrorMessage(error);
|
||||
}
|
||||
|
||||
if (!serialized.name || typeof serialized.name !== 'string') {
|
||||
if (error instanceof Error && error.name) {
|
||||
serialized.name = error.name;
|
||||
} else if (error && typeof error === 'object') {
|
||||
const constructorName = error.constructor?.name;
|
||||
serialized.name =
|
||||
typeof constructorName === 'string' && constructorName.length > 0
|
||||
? constructorName
|
||||
: 'Error';
|
||||
} else {
|
||||
serialized.name = 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!serialized.stacktrace &&
|
||||
error instanceof Error &&
|
||||
typeof error.stack === 'string'
|
||||
) {
|
||||
serialized.stacktrace = error.stack;
|
||||
}
|
||||
|
||||
return serialized as Error;
|
||||
}
|
||||
|
||||
interface OpCallContext {
|
||||
signal: AbortSignal;
|
||||
}
|
||||
@@ -71,15 +161,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
this.port.postMessage({
|
||||
type: 'return',
|
||||
id: msg.id,
|
||||
error: pick(error, [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
]),
|
||||
error: serializeError(error),
|
||||
} satisfies ReturnMessage);
|
||||
},
|
||||
complete: () => {
|
||||
@@ -109,15 +191,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
this.port.postMessage({
|
||||
type: 'error',
|
||||
id: msg.id,
|
||||
error: pick(error, [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
]),
|
||||
error: serializeError(error),
|
||||
} satisfies SubscriptionErrorMessage);
|
||||
},
|
||||
complete: () => {
|
||||
|
||||
@@ -12,7 +12,16 @@ export interface OpSchema {
|
||||
[key: string]: [any, any?];
|
||||
}
|
||||
|
||||
type RequiredInput<In> = In extends void ? [] : In extends never ? [] : [In];
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false;
|
||||
|
||||
type RequiredInput<In> =
|
||||
IsAny<In> extends true
|
||||
? [In]
|
||||
: [In] extends [never]
|
||||
? []
|
||||
: [In] extends [void]
|
||||
? []
|
||||
: [In];
|
||||
|
||||
export type OpNames<T extends OpSchema> = ValuesOf<KeyToKey<T>>;
|
||||
export type OpInput<
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dialogHandlers } from './dialog';
|
||||
import { dbEventsV1, dbHandlersV1, nbstoreHandlers } from './nbstore';
|
||||
import { previewHandlers } from './preview';
|
||||
import { provideExposed } from './provide';
|
||||
import { workspaceEvents, workspaceHandlers } from './workspace';
|
||||
|
||||
@@ -8,6 +9,7 @@ export const handlers = {
|
||||
nbstore: nbstoreHandlers,
|
||||
workspace: workspaceHandlers,
|
||||
dialog: dialogHandlers,
|
||||
preview: previewHandlers,
|
||||
};
|
||||
|
||||
export const events = {
|
||||
|
||||
69
packages/frontend/apps/electron/src/helper/preview/index.ts
Normal file
69
packages/frontend/apps/electron/src/helper/preview/index.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
type MermaidRenderRequest,
|
||||
type MermaidRenderResult,
|
||||
renderMermaidSvg,
|
||||
renderTypstSvg,
|
||||
type TypstRenderRequest,
|
||||
type TypstRenderResult,
|
||||
} from '@affine/native';
|
||||
|
||||
const TYPST_FONT_DIRS_ENV = 'AFFINE_TYPST_FONT_DIRS';
|
||||
|
||||
function parseTypstFontDirsFromEnv() {
|
||||
const value = process.env[TYPST_FONT_DIRS_ENV];
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.split(path.delimiter)
|
||||
.map(dir => dir.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getTypstFontDirCandidates() {
|
||||
const resourcesPath = process.resourcesPath ?? '';
|
||||
|
||||
return [
|
||||
...parseTypstFontDirsFromEnv(),
|
||||
path.join(resourcesPath, 'fonts'),
|
||||
path.join(resourcesPath, 'js', 'fonts'),
|
||||
path.join(resourcesPath, 'app.asar.unpacked', 'fonts'),
|
||||
path.join(resourcesPath, 'app.asar.unpacked', 'js', 'fonts'),
|
||||
];
|
||||
}
|
||||
|
||||
function resolveTypstFontDirs() {
|
||||
return Array.from(
|
||||
new Set(getTypstFontDirCandidates().map(dir => path.resolve(dir)))
|
||||
).filter(dir => fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory());
|
||||
}
|
||||
|
||||
function withTypstFontDirs(
|
||||
request: TypstRenderRequest,
|
||||
fontDirs: string[]
|
||||
): TypstRenderRequest {
|
||||
const nextOptions = request.options ? { ...request.options } : {};
|
||||
if (!nextOptions.fontDirs?.length) {
|
||||
nextOptions.fontDirs = fontDirs;
|
||||
}
|
||||
return { ...request, options: nextOptions };
|
||||
}
|
||||
|
||||
const typstFontDirs = resolveTypstFontDirs();
|
||||
|
||||
export const previewHandlers = {
|
||||
renderMermaidSvg: async (
|
||||
request: MermaidRenderRequest
|
||||
): Promise<MermaidRenderResult> => {
|
||||
return renderMermaidSvg(request);
|
||||
},
|
||||
renderTypstSvg: async (
|
||||
request: TypstRenderRequest
|
||||
): Promise<TypstRenderResult> => {
|
||||
return renderTypstSvg(withTypstFontDirs(request, typstFontDirs));
|
||||
},
|
||||
};
|
||||
85
packages/frontend/apps/electron/test/helper/preview.spec.ts
Normal file
85
packages/frontend/apps/electron/test/helper/preview.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { native } = vi.hoisted(() => ({
|
||||
native: {
|
||||
renderMermaidSvg: vi.fn(),
|
||||
renderTypstSvg: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@affine/native', () => native);
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
const typstFontDirA = path.join(tmpDir, 'fonts-a');
|
||||
const typstFontDirB = path.join(tmpDir, 'fonts-b');
|
||||
|
||||
async function loadPreviewHandlers() {
|
||||
vi.resetModules();
|
||||
const module = await import('../../src/helper/preview');
|
||||
return module.previewHandlers;
|
||||
}
|
||||
|
||||
describe('helper preview handlers', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.ensureDir(typstFontDirA);
|
||||
await fs.ensureDir(typstFontDirB);
|
||||
process.env.AFFINE_TYPST_FONT_DIRS = [
|
||||
typstFontDirA,
|
||||
typstFontDirB,
|
||||
path.join(tmpDir, 'missing'),
|
||||
].join(path.delimiter);
|
||||
native.renderMermaidSvg.mockReset();
|
||||
native.renderTypstSvg.mockReset();
|
||||
native.renderMermaidSvg.mockReturnValue({
|
||||
svg: '<svg><text>mermaid</text></svg>',
|
||||
});
|
||||
native.renderTypstSvg.mockReturnValue({
|
||||
svg: '<svg><text>typst</text></svg>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
delete process.env.AFFINE_TYPST_FONT_DIRS;
|
||||
await fs.remove(tmpDir);
|
||||
});
|
||||
|
||||
test('passes mermaid request to native renderer', async () => {
|
||||
const previewHandlers = await loadPreviewHandlers();
|
||||
const request = { code: 'flowchart TD; A-->B' };
|
||||
|
||||
await previewHandlers.renderMermaidSvg(request);
|
||||
|
||||
expect(native.renderMermaidSvg).toHaveBeenCalledWith(request);
|
||||
});
|
||||
|
||||
test('injects resolved fontDirs into typst requests', async () => {
|
||||
const previewHandlers = await loadPreviewHandlers();
|
||||
|
||||
await previewHandlers.renderTypstSvg({ code: '= hello' });
|
||||
|
||||
const [request] = native.renderTypstSvg.mock.calls[0];
|
||||
expect(request.options?.fontDirs).toEqual(
|
||||
expect.arrayContaining([
|
||||
path.resolve(typstFontDirA),
|
||||
path.resolve(typstFontDirB),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps explicit typst fontDirs', async () => {
|
||||
const previewHandlers = await loadPreviewHandlers();
|
||||
const request = {
|
||||
code: '= hello',
|
||||
options: {
|
||||
fontDirs: ['/tmp/custom-fonts'],
|
||||
},
|
||||
};
|
||||
|
||||
await previewHandlers.renderTypstSvg(request);
|
||||
|
||||
expect(native.renderTypstSvg).toHaveBeenCalledWith(request);
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@
|
||||
"@radix-ui/react-toolbar": "^1.1.1",
|
||||
"@sentry/react": "^9.47.1",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/mermaid-wasm": "^0.1.0",
|
||||
"@toeverything/pdf-viewer": "^0.1.1",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
@@ -57,6 +58,7 @@
|
||||
"cmdk": "^1.0.4",
|
||||
"core-js": "^3.39.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.0",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"file-type": "^21.0.0",
|
||||
"filesize": "^10.1.6",
|
||||
@@ -76,7 +78,6 @@
|
||||
"lit": "^3.2.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lottie-react": "^2.4.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"mp4-muxer": "^5.2.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderMermaidSvg } from '@affine/core/modules/code-block-preview-renderer/bridge';
|
||||
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { CodeBlockModel } from '@blocksuite/affine/model';
|
||||
@@ -7,7 +8,6 @@ import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import type { Mermaid } from 'mermaid';
|
||||
|
||||
export const CodeBlockMermaidPreview = CodeBlockPreviewExtension(
|
||||
'mermaid',
|
||||
@@ -154,7 +154,6 @@ export class MermaidPreview extends SignalWatcher(
|
||||
@query('.mermaid-preview-container')
|
||||
accessor container!: HTMLDivElement;
|
||||
|
||||
private mermaid: Mermaid | null = null;
|
||||
private retryCount = 0;
|
||||
private readonly maxRetries = 3;
|
||||
private renderTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -169,9 +168,6 @@ export class MermaidPreview extends SignalWatcher(
|
||||
private lastMouseY = 0;
|
||||
|
||||
override firstUpdated(_changedProperties: PropertyValues): void {
|
||||
this._loadMermaid().catch(error => {
|
||||
console.error('Failed to load mermaid in firstUpdated:', error);
|
||||
});
|
||||
this._scheduleRender();
|
||||
this._setupEventListeners();
|
||||
|
||||
@@ -271,7 +267,8 @@ export class MermaidPreview extends SignalWatcher(
|
||||
event.preventDefault();
|
||||
|
||||
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newScale = Math.max(0.1, Math.min(5, this.scale * delta));
|
||||
const previousScale = this.scale;
|
||||
const newScale = Math.max(0.1, Math.min(5, previousScale * delta));
|
||||
|
||||
// calculate mouse position relative to container
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
@@ -284,8 +281,8 @@ export class MermaidPreview extends SignalWatcher(
|
||||
|
||||
// update transform
|
||||
this.scale = newScale;
|
||||
this.translateX = mouseX - scaleCenterX * (newScale / this.scale);
|
||||
this.translateY = mouseY - scaleCenterY * (newScale / this.scale);
|
||||
this.translateX = mouseX - scaleCenterX * (newScale / previousScale);
|
||||
this.translateY = mouseY - scaleCenterY * (newScale / previousScale);
|
||||
|
||||
this._updateTransform();
|
||||
};
|
||||
@@ -309,44 +306,6 @@ export class MermaidPreview extends SignalWatcher(
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadMermaid() {
|
||||
try {
|
||||
// dynamic load mermaid
|
||||
const mermaidModule = await import('mermaid');
|
||||
this.mermaid = mermaidModule.default;
|
||||
|
||||
// initialize mermaid
|
||||
this.mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'strict',
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
},
|
||||
sequence: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
pie: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
journey: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
gitGraph: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load mermaid:', error);
|
||||
this.state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
// prevent duplicate rendering
|
||||
if (this.isRendering) {
|
||||
@@ -356,28 +315,25 @@ export class MermaidPreview extends SignalWatcher(
|
||||
this.isRendering = true;
|
||||
this.state = 'loading';
|
||||
|
||||
if (!this.normalizedMermaidCode) {
|
||||
const code = this.normalizedMermaidCode?.trim();
|
||||
|
||||
if (!code) {
|
||||
this.svgContent = '';
|
||||
this.state = 'fallback';
|
||||
this.isRendering = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.mermaid) {
|
||||
await this._loadMermaid();
|
||||
}
|
||||
if (!this.mermaid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate unique ID
|
||||
const diagramId = `mermaid-diagram-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// generate SVG
|
||||
const { svg } = await this.mermaid.render(
|
||||
diagramId,
|
||||
this.normalizedMermaidCode
|
||||
);
|
||||
const { svg } = await renderMermaidSvg({
|
||||
code,
|
||||
options: {
|
||||
fastText: true,
|
||||
svgOnly: true,
|
||||
theme: 'default',
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
},
|
||||
});
|
||||
|
||||
// update SVG content
|
||||
this.svgContent = svg;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderTypstSvg } from '@affine/core/modules/code-block-preview-renderer/bridge';
|
||||
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { CodeBlockModel } from '@blocksuite/affine/model';
|
||||
@@ -8,8 +9,6 @@ import { property, query, state } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ensureTypstReady, getTypst } from './typst';
|
||||
|
||||
const RENDER_DEBOUNCE_MS = 200;
|
||||
|
||||
export const CodeBlockTypstPreview = CodeBlockPreviewExtension(
|
||||
@@ -378,9 +377,7 @@ ${this.errorMessage}</pre
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureTypstReady();
|
||||
const typst = await getTypst();
|
||||
const svg = await typst.svg({ mainContent: code });
|
||||
const { svg } = await renderTypstSvg({ code });
|
||||
this.svgContent = svg;
|
||||
this.state = 'finish';
|
||||
this._resetView();
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { $typst, type BeforeBuildFn, loadFonts } from '@myriaddreamin/typst.ts';
|
||||
|
||||
const FONT_CDN_URLS = [
|
||||
'https://cdn.affine.pro/fonts/Inter-Regular.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-SemiBold.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-Italic.woff',
|
||||
'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff',
|
||||
'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf',
|
||||
] as const;
|
||||
|
||||
const getBeforeBuildHooks = (): BeforeBuildFn[] => [
|
||||
loadFonts([...FONT_CDN_URLS]),
|
||||
];
|
||||
|
||||
const compilerWasmUrl = new URL(
|
||||
'@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const rendererWasmUrl = new URL(
|
||||
'@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
let typstInitPromise: Promise<void> | null = null;
|
||||
|
||||
export async function ensureTypstReady() {
|
||||
if (typstInitPromise) {
|
||||
return typstInitPromise;
|
||||
}
|
||||
|
||||
typstInitPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
$typst.setCompilerInitOptions({
|
||||
beforeBuild: getBeforeBuildHooks(),
|
||||
getModule: () => compilerWasmUrl,
|
||||
});
|
||||
|
||||
$typst.setRendererInitOptions({
|
||||
beforeBuild: getBeforeBuildHooks(),
|
||||
getModule: () => rendererWasmUrl,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
typstInitPromise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return typstInitPromise;
|
||||
}
|
||||
|
||||
export async function getTypst() {
|
||||
await ensureTypstReady();
|
||||
return $typst;
|
||||
}
|
||||
|
||||
export const TYPST_FONT_URLS = FONT_CDN_URLS;
|
||||
@@ -0,0 +1,134 @@
|
||||
import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { desktopPreviewApis, mermaidRender, typstRender } = vi.hoisted(() => {
|
||||
return {
|
||||
mermaidRender: vi.fn(),
|
||||
typstRender: vi.fn(),
|
||||
desktopPreviewApis: {} as {
|
||||
preview?: {
|
||||
renderMermaidSvg?: (request: {
|
||||
code: string;
|
||||
}) => Promise<{ svg: string }>;
|
||||
renderTypstSvg?: (request: {
|
||||
code: string;
|
||||
}) => Promise<{ svg: string }>;
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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/mermaid/renderer', () => ({
|
||||
getMermaidRenderer: () => ({
|
||||
render: mermaidRender,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@affine/core/modules/typst/renderer', () => ({
|
||||
getTypstRenderer: () => ({
|
||||
render: typstRender,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@affine/electron-api', () => ({
|
||||
apis: desktopPreviewApis,
|
||||
}));
|
||||
|
||||
vi.mock('dompurify', () => ({
|
||||
default: {
|
||||
sanitize: domPurifySanitize,
|
||||
},
|
||||
}));
|
||||
|
||||
import { renderMermaidSvg, renderTypstSvg } from './bridge';
|
||||
|
||||
const initialBuildConfig = globalThis.BUILD_CONFIG;
|
||||
|
||||
describe('preview render bridge', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
domPurifySanitize.mockImplementation((value: unknown) => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return value.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
});
|
||||
globalThis.BUILD_CONFIG = {
|
||||
...initialBuildConfig,
|
||||
isElectron: false,
|
||||
};
|
||||
desktopPreviewApis.preview = undefined;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
globalThis.BUILD_CONFIG = initialBuildConfig;
|
||||
});
|
||||
|
||||
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('prefers desktop preview handlers on electron', async () => {
|
||||
const renderMermaidFromDesktop = vi.fn().mockResolvedValue({
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg"><text>desktop</text></svg>`,
|
||||
});
|
||||
const renderTypstFromDesktop = vi.fn().mockResolvedValue({
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg"><text>desktop</text></svg>`,
|
||||
});
|
||||
desktopPreviewApis.preview = {
|
||||
renderMermaidSvg: renderMermaidFromDesktop,
|
||||
renderTypstSvg: renderTypstFromDesktop,
|
||||
};
|
||||
globalThis.BUILD_CONFIG = {
|
||||
...initialBuildConfig,
|
||||
isElectron: true,
|
||||
};
|
||||
|
||||
const mermaid = await renderMermaidSvg({ code: 'flowchart TD;A-->B' });
|
||||
const typst = await renderTypstSvg({ code: '= Title' });
|
||||
|
||||
expect(renderMermaidFromDesktop).toHaveBeenCalledTimes(1);
|
||||
expect(renderTypstFromDesktop).toHaveBeenCalledTimes(1);
|
||||
expect(mermaidRender).not.toHaveBeenCalled();
|
||||
expect(typstRender).not.toHaveBeenCalled();
|
||||
expect(mermaid.svg).toContain('<svg');
|
||||
expect(typst.svg).toBe(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg"><text>desktop</text></svg>`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when sanitized svg is empty', async () => {
|
||||
mermaidRender.mockResolvedValue({
|
||||
svg: '<div><text>invalid</text></div>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
renderMermaidSvg({ code: 'flowchart TD;A-->B' })
|
||||
).rejects.toThrow('Preview renderer returned invalid SVG.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
getMermaidRenderer,
|
||||
type MermaidRenderRequest,
|
||||
type MermaidRenderResult,
|
||||
} from '@affine/core/modules/mermaid/renderer';
|
||||
import {
|
||||
getTypstRenderer,
|
||||
type TypstRenderRequest,
|
||||
type TypstRenderResult,
|
||||
} from '@affine/core/modules/typst/renderer';
|
||||
import { apis } from '@affine/electron-api';
|
||||
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();
|
||||
}
|
||||
|
||||
type DesktopPreviewHandlers = {
|
||||
renderMermaidSvg?: (
|
||||
request: MermaidRenderRequest
|
||||
) => Promise<MermaidRenderResult>;
|
||||
renderTypstSvg?: (request: TypstRenderRequest) => Promise<TypstRenderResult>;
|
||||
};
|
||||
|
||||
type DesktopPreviewApis = {
|
||||
preview?: DesktopPreviewHandlers;
|
||||
};
|
||||
|
||||
function getDesktopPreviewHandlers() {
|
||||
if (!BUILD_CONFIG.isElectron || !apis) return null;
|
||||
|
||||
const previewApis = apis as unknown as DesktopPreviewApis;
|
||||
return previewApis.preview ?? null;
|
||||
}
|
||||
|
||||
export async function renderMermaidSvg(
|
||||
request: MermaidRenderRequest
|
||||
): Promise<MermaidRenderResult> {
|
||||
const desktopPreviewHandlers = getDesktopPreviewHandlers();
|
||||
const rendered = desktopPreviewHandlers?.renderMermaidSvg
|
||||
? await desktopPreviewHandlers.renderMermaidSvg(request)
|
||||
: await getMermaidRenderer().render(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 desktopPreviewHandlers = getDesktopPreviewHandlers();
|
||||
const rendered = desktopPreviewHandlers?.renderTypstSvg
|
||||
? await desktopPreviewHandlers.renderTypstSvg(request)
|
||||
: await getTypstRenderer().render(request);
|
||||
|
||||
return { svg: rendered.svg };
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
39
packages/frontend/core/src/modules/mermaid/renderer/index.ts
Normal file
39
packages/frontend/core/src/modules/mermaid/renderer/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
|
||||
import type {
|
||||
MermaidOps,
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
} from './types';
|
||||
|
||||
class MermaidRenderer extends WorkerOpRenderer<MermaidOps> {
|
||||
constructor() {
|
||||
super('mermaid');
|
||||
}
|
||||
|
||||
init(options?: MermaidRenderOptions) {
|
||||
return this.ensureInitialized(() => this.call('init', options));
|
||||
}
|
||||
|
||||
async render(request: MermaidRenderRequest) {
|
||||
await this.init();
|
||||
return this.call('render', request);
|
||||
}
|
||||
}
|
||||
|
||||
let sharedMermaidRenderer: MermaidRenderer | null = null;
|
||||
|
||||
export function getMermaidRenderer() {
|
||||
if (!sharedMermaidRenderer) {
|
||||
sharedMermaidRenderer = new MermaidRenderer();
|
||||
}
|
||||
return sharedMermaidRenderer;
|
||||
}
|
||||
|
||||
export type {
|
||||
MermaidOps,
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
MermaidRenderResult,
|
||||
MermaidRenderTheme,
|
||||
MermaidTextMetrics,
|
||||
} from './types';
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { MessageCommunicapable } from '@toeverything/infra/op';
|
||||
import { OpConsumer } from '@toeverything/infra/op';
|
||||
import initMmdr, { render_mermaid_svg } from '@toeverything/mermaid-wasm';
|
||||
|
||||
import type {
|
||||
MermaidOps,
|
||||
MermaidRenderOptions,
|
||||
MermaidRenderRequest,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_RENDER_OPTIONS: MermaidRenderOptions = {
|
||||
fastText: true,
|
||||
svgOnly: true,
|
||||
theme: 'modern',
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
};
|
||||
|
||||
function mergeOptions(
|
||||
base: MermaidRenderOptions,
|
||||
override: MermaidRenderOptions | undefined
|
||||
): MermaidRenderOptions {
|
||||
if (!override) {
|
||||
return base;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
textMetrics: override.textMetrics ?? base.textMetrics,
|
||||
};
|
||||
}
|
||||
|
||||
class MermaidRendererBackend extends OpConsumer<MermaidOps> {
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private options: MermaidRenderOptions = DEFAULT_RENDER_OPTIONS;
|
||||
|
||||
constructor(port: MessageCommunicapable) {
|
||||
super(port);
|
||||
this.register('init', this.init.bind(this));
|
||||
this.register('render', this.render.bind(this));
|
||||
}
|
||||
|
||||
private ensureReady() {
|
||||
if (!this.initPromise) {
|
||||
this.initPromise = initMmdr().then(() => undefined);
|
||||
}
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
async init(options?: MermaidRenderOptions) {
|
||||
this.options = mergeOptions(DEFAULT_RENDER_OPTIONS, options);
|
||||
await this.ensureReady();
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
async render({ code, options }: MermaidRenderRequest) {
|
||||
await this.ensureReady();
|
||||
const mergedOptions = mergeOptions(this.options, options);
|
||||
const svg = render_mermaid_svg(code, JSON.stringify(mergedOptions));
|
||||
return { svg };
|
||||
}
|
||||
}
|
||||
|
||||
new MermaidRendererBackend(self as MessageCommunicapable);
|
||||
32
packages/frontend/core/src/modules/mermaid/renderer/types.ts
Normal file
32
packages/frontend/core/src/modules/mermaid/renderer/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type MermaidTextMetrics = {
|
||||
ascii: number;
|
||||
cjk: number;
|
||||
space: number;
|
||||
};
|
||||
|
||||
export type MermaidRenderTheme = 'modern' | 'default';
|
||||
|
||||
export type MermaidRenderOptions = {
|
||||
fastText?: boolean;
|
||||
svgOnly?: boolean;
|
||||
textMetrics?: MermaidTextMetrics;
|
||||
theme?: MermaidRenderTheme;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
};
|
||||
|
||||
export type MermaidRenderRequest = {
|
||||
code: string;
|
||||
options?: MermaidRenderOptions;
|
||||
};
|
||||
|
||||
export type MermaidRenderResult = {
|
||||
svg: string;
|
||||
};
|
||||
|
||||
export interface MermaidOps extends OpSchema {
|
||||
init: [MermaidRenderOptions | undefined, { ok: true }];
|
||||
render: [MermaidRenderRequest, MermaidRenderResult];
|
||||
}
|
||||
@@ -1,2 +1,10 @@
|
||||
export { PDFRenderer } from './renderer';
|
||||
export type { PDFMeta, RenderedPage, RenderPageOpts } from './types';
|
||||
import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
|
||||
import type { PDFOps } from './types';
|
||||
|
||||
export class PDFRenderer extends WorkerOpRenderer<PDFOps> {
|
||||
constructor() {
|
||||
super('pdf');
|
||||
}
|
||||
}
|
||||
|
||||
export type { PDFMeta, PDFOps, RenderedPage, RenderPageOpts } from './types';
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
import type { PDFMeta, RenderedPage, RenderPageOpts } from './types';
|
||||
|
||||
export interface ClientOps extends OpSchema {
|
||||
open: [{ data: ArrayBuffer }, PDFMeta];
|
||||
render: [RenderPageOpts, RenderedPage];
|
||||
}
|
||||
@@ -23,10 +23,9 @@ import {
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
|
||||
import type { ClientOps } from './ops';
|
||||
import type { PDFMeta, RenderPageOpts } from './types';
|
||||
import type { PDFMeta, PDFOps, RenderPageOpts } from './types';
|
||||
|
||||
class PDFRendererBackend extends OpConsumer<ClientOps> {
|
||||
class PDFRendererBackend extends OpConsumer<PDFOps> {
|
||||
constructor(port: MessageCommunicapable) {
|
||||
super(port);
|
||||
this.register('open', this.open.bind(this));
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { getWorkerUrl } from '@affine/env/worker';
|
||||
import { OpClient } from '@toeverything/infra/op';
|
||||
|
||||
import type { ClientOps } from './ops';
|
||||
|
||||
export class PDFRenderer extends OpClient<ClientOps> {
|
||||
private readonly worker: Worker;
|
||||
|
||||
constructor() {
|
||||
const worker = new Worker(getWorkerUrl('pdf'));
|
||||
super(worker);
|
||||
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
super.destroy();
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type PageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -21,3 +23,8 @@ export type RenderPageOpts = {
|
||||
export type RenderedPage = {
|
||||
bitmap: ImageBitmap;
|
||||
};
|
||||
|
||||
export interface PDFOps extends OpSchema {
|
||||
open: [{ data: ArrayBuffer }, PDFMeta];
|
||||
render: [RenderPageOpts, RenderedPage];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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 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.initPromise) {
|
||||
this.initPromise = task()
|
||||
.then(() => undefined)
|
||||
.catch(error => {
|
||||
this.initPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
protected resetInitialization() {
|
||||
this.initPromise = null;
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
super.destroy();
|
||||
this.worker.terminate();
|
||||
this.resetInitialization();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
33
packages/frontend/core/src/modules/typst/renderer/index.ts
Normal file
33
packages/frontend/core/src/modules/typst/renderer/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { WorkerOpRenderer } from '../../shared/worker-op-renderer';
|
||||
import type { TypstOps, TypstRenderOptions, TypstRenderRequest } from './types';
|
||||
|
||||
class TypstRenderer extends WorkerOpRenderer<TypstOps> {
|
||||
constructor() {
|
||||
super('typst');
|
||||
}
|
||||
|
||||
init(options?: TypstRenderOptions) {
|
||||
return this.ensureInitialized(() => this.call('init', options));
|
||||
}
|
||||
|
||||
async render(request: TypstRenderRequest) {
|
||||
await this.init();
|
||||
return this.call('render', request);
|
||||
}
|
||||
}
|
||||
|
||||
let sharedTypstRenderer: TypstRenderer | null = null;
|
||||
|
||||
export function getTypstRenderer() {
|
||||
if (!sharedTypstRenderer) {
|
||||
sharedTypstRenderer = new TypstRenderer();
|
||||
}
|
||||
return sharedTypstRenderer;
|
||||
}
|
||||
|
||||
export type {
|
||||
TypstOps,
|
||||
TypstRenderOptions,
|
||||
TypstRenderRequest,
|
||||
TypstRenderResult,
|
||||
} from './types';
|
||||
177
packages/frontend/core/src/modules/typst/renderer/runtime.ts
Normal file
177
packages/frontend/core/src/modules/typst/renderer/runtime.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
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;
|
||||
};
|
||||
|
||||
let typstInitPromise: Promise<void> | null = null;
|
||||
|
||||
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(),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export async function ensureTypstReady(
|
||||
fontUrls: string[],
|
||||
wasmModuleUrls: TypstWasmModuleUrls = {}
|
||||
) {
|
||||
if (typstInitPromise) {
|
||||
return typstInitPromise;
|
||||
}
|
||||
|
||||
typstInitPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
const compilerBeforeBuild = getBeforeBuildHooks(fontUrls);
|
||||
|
||||
$typst.setCompilerInitOptions({
|
||||
beforeBuild: compilerBeforeBuild,
|
||||
getModule: () => wasmModuleUrls.compilerWasmUrl ?? compilerWasmUrl,
|
||||
});
|
||||
$typst.setRendererInitOptions({
|
||||
getModule: () => wasmModuleUrls.rendererWasmUrl ?? rendererWasmUrl,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
typstInitPromise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return typstInitPromise;
|
||||
}
|
||||
|
||||
export async function renderTypstSvgWithOptions(
|
||||
code: string,
|
||||
options: TypstRenderOptions | undefined,
|
||||
wasmModuleUrls?: TypstWasmModuleUrls
|
||||
) {
|
||||
const resolvedOptions = mergeTypstRenderOptions(
|
||||
DEFAULT_TYPST_RENDER_OPTIONS,
|
||||
options
|
||||
);
|
||||
await ensureTypstReady(
|
||||
resolvedOptions.fontUrls ?? [...DEFAULT_TYPST_FONT_URLS],
|
||||
wasmModuleUrls
|
||||
);
|
||||
const svg = await $typst.svg({
|
||||
mainContent: code,
|
||||
});
|
||||
return { svg };
|
||||
}
|
||||
20
packages/frontend/core/src/modules/typst/renderer/types.ts
Normal file
20
packages/frontend/core/src/modules/typst/renderer/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type TypstRenderOptions = {
|
||||
fontUrls?: string[];
|
||||
theme?: 'light' | 'dark';
|
||||
};
|
||||
|
||||
export type TypstRenderRequest = {
|
||||
code: string;
|
||||
options?: TypstRenderOptions;
|
||||
};
|
||||
|
||||
export type TypstRenderResult = {
|
||||
svg: string;
|
||||
};
|
||||
|
||||
export interface TypstOps extends OpSchema {
|
||||
init: [TypstRenderOptions | undefined, { ok: true }];
|
||||
render: [TypstRenderRequest, TypstRenderResult];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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 ?? []);
|
||||
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);
|
||||
@@ -11,6 +11,7 @@ affine_common = { workspace = true, features = ["hashcash"] }
|
||||
affine_media_capture = { path = "./media_capture" }
|
||||
affine_nbstore = { workspace = true, features = ["napi"] }
|
||||
affine_sqlite_v1 = { path = "./sqlite_v1" }
|
||||
mermaid-rs-renderer = { git = "https://github.com/toeverything/mermaid-rs-renderer", rev = "d294dbf", default-features = false }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
@@ -24,6 +25,14 @@ sqlx = { workspace = true, default-features = false, features = [
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
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"
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
43
packages/frontend/native/index.d.ts
vendored
43
packages/frontend/native/index.d.ts
vendored
@@ -40,8 +40,51 @@ export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | u
|
||||
|
||||
/** Decode audio file into a Float32Array */
|
||||
export declare function decodeAudioSync(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null): Float32Array
|
||||
export interface MermaidRenderOptions {
|
||||
fastText?: boolean
|
||||
svgOnly?: boolean
|
||||
textMetrics?: MermaidTextMetrics
|
||||
theme?: string
|
||||
fontFamily?: string
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
export interface MermaidRenderRequest {
|
||||
code: string
|
||||
options?: MermaidRenderOptions
|
||||
}
|
||||
|
||||
export interface MermaidRenderResult {
|
||||
svg: string
|
||||
}
|
||||
|
||||
export interface MermaidTextMetrics {
|
||||
ascii: number
|
||||
cjk: number
|
||||
space: number
|
||||
}
|
||||
|
||||
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>
|
||||
theme?: string
|
||||
fontDirs?: Array<string>
|
||||
}
|
||||
|
||||
export interface TypstRenderRequest {
|
||||
code: string
|
||||
options?: TypstRenderOptions
|
||||
}
|
||||
|
||||
export interface TypstRenderResult {
|
||||
svg: string
|
||||
}
|
||||
|
||||
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
|
||||
export declare class DocStorage {
|
||||
constructor(path: string)
|
||||
|
||||
@@ -77,8 +77,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-android-arm64')
|
||||
const bindingPackageVersion = require('@affine/native-android-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -93,8 +93,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-android-arm-eabi')
|
||||
const bindingPackageVersion = require('@affine/native-android-arm-eabi/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -114,8 +114,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-win32-x64-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-win32-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -130,8 +130,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-win32-x64-msvc')
|
||||
const bindingPackageVersion = require('@affine/native-win32-x64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -147,8 +147,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-win32-ia32-msvc')
|
||||
const bindingPackageVersion = require('@affine/native-win32-ia32-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -163,8 +163,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-win32-arm64-msvc')
|
||||
const bindingPackageVersion = require('@affine/native-win32-arm64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -182,8 +182,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-darwin-universal')
|
||||
const bindingPackageVersion = require('@affine/native-darwin-universal/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -198,8 +198,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-darwin-x64')
|
||||
const bindingPackageVersion = require('@affine/native-darwin-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -214,8 +214,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-darwin-arm64')
|
||||
const bindingPackageVersion = require('@affine/native-darwin-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -234,8 +234,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-freebsd-x64')
|
||||
const bindingPackageVersion = require('@affine/native-freebsd-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -250,8 +250,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-freebsd-arm64')
|
||||
const bindingPackageVersion = require('@affine/native-freebsd-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -271,8 +271,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-x64-musl')
|
||||
const bindingPackageVersion = require('@affine/native-linux-x64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -287,8 +287,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-x64-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-linux-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -305,8 +305,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-arm64-musl')
|
||||
const bindingPackageVersion = require('@affine/native-linux-arm64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -321,8 +321,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-arm64-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-linux-arm64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -339,8 +339,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-arm-musleabihf')
|
||||
const bindingPackageVersion = require('@affine/native-linux-arm-musleabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -355,8 +355,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-arm-gnueabihf')
|
||||
const bindingPackageVersion = require('@affine/native-linux-arm-gnueabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -373,8 +373,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-loong64-musl')
|
||||
const bindingPackageVersion = require('@affine/native-linux-loong64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -389,8 +389,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-loong64-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-linux-loong64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -407,8 +407,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-riscv64-musl')
|
||||
const bindingPackageVersion = require('@affine/native-linux-riscv64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -423,8 +423,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-riscv64-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-linux-riscv64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -440,8 +440,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-ppc64-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-linux-ppc64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -456,8 +456,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-linux-s390x-gnu')
|
||||
const bindingPackageVersion = require('@affine/native-linux-s390x-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -476,8 +476,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-openharmony-arm64')
|
||||
const bindingPackageVersion = require('@affine/native-openharmony-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -492,8 +492,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-openharmony-x64')
|
||||
const bindingPackageVersion = require('@affine/native-openharmony-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -508,8 +508,8 @@ function requireNative() {
|
||||
try {
|
||||
const binding = require('@affine/native-openharmony-arm')
|
||||
const bindingPackageVersion = require('@affine/native-openharmony-arm/package.json').version
|
||||
if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
if (bindingPackageVersion !== '0.26.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.26.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
@@ -580,6 +580,8 @@ module.exports.ShareableContent = nativeBinding.ShareableContent
|
||||
module.exports.decodeAudio = nativeBinding.decodeAudio
|
||||
module.exports.decodeAudioSync = nativeBinding.decodeAudioSync
|
||||
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
|
||||
module.exports.renderMermaidSvg = nativeBinding.renderMermaidSvg
|
||||
module.exports.renderTypstSvg = nativeBinding.renderTypstSvg
|
||||
module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse
|
||||
module.exports.DocStorage = nativeBinding.DocStorage
|
||||
module.exports.DocStoragePool = nativeBinding.DocStoragePool
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod hashcash;
|
||||
pub mod preview;
|
||||
|
||||
#[cfg(not(target_arch = "arm"))]
|
||||
#[global_allocator]
|
||||
|
||||
174
packages/frontend/native/src/preview.rs
Normal file
174
packages/frontend/native/src/preview.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
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 MermaidTextMetrics {
|
||||
pub ascii: f64,
|
||||
pub cjk: f64,
|
||||
pub space: f64,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MermaidRenderOptions {
|
||||
pub fast_text: Option<bool>,
|
||||
pub svg_only: Option<bool>,
|
||||
pub text_metrics: Option<MermaidTextMetrics>,
|
||||
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 theme: Option<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_typst_font_dirs(options: &Option<TypstRenderOptions>) -> Vec<PathBuf> {
|
||||
options
|
||||
.as_ref()
|
||||
.and_then(|options| options.font_dirs.as_ref())
|
||||
.map(|dirs| dirs.iter().map(PathBuf::from).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
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 ""##));
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,14 @@ function getBaseWorkerConfigs(
|
||||
pkg,
|
||||
core.srcPath.join('modules/pdf/renderer/pdf.worker.ts').value
|
||||
),
|
||||
createWorkerTargetConfig(
|
||||
pkg,
|
||||
core.srcPath.join('modules/mermaid/renderer/mermaid.worker.ts').value
|
||||
),
|
||||
createWorkerTargetConfig(
|
||||
pkg,
|
||||
core.srcPath.join('modules/typst/renderer/typst.worker.ts').value
|
||||
),
|
||||
createWorkerTargetConfig(
|
||||
pkg,
|
||||
core.srcPath.join(
|
||||
|
||||
@@ -46,6 +46,11 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
test: {
|
||||
workspace: [
|
||||
'.',
|
||||
'./packages/frontend/apps/electron',
|
||||
'./blocksuite/**/*/vitest.config.ts',
|
||||
],
|
||||
setupFiles: [
|
||||
resolve(rootDir, './scripts/setup/polyfill.ts'),
|
||||
resolve(rootDir, './scripts/setup/lit.ts'),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineWorkspace } from 'vitest/config';
|
||||
|
||||
export default defineWorkspace([
|
||||
'.',
|
||||
'./packages/frontend/apps/electron',
|
||||
'./blocksuite/**/*/vitest.config.ts',
|
||||
]);
|
||||
Reference in New Issue
Block a user