Compare commits

..

2 Commits

Author SHA1 Message Date
DarkSky
d385514fca feat: init native mermaid & typst integrate 2026-02-22 20:41:17 +08:00
DarkSky
3d01766f55 fix: history may duplicate on concurrency (#14487)
#### PR Dependency Tree


* **PR #14487** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Enhanced history record creation to prevent duplicate entries in
concurrent scenarios.

* **Tests**
  * Added validation for idempotent history record creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-22 02:13:51 +08:00
44 changed files with 3455 additions and 1278 deletions

2030
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -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: {

View File

@@ -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', {

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

@@ -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: () => {

View File

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

View File

@@ -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 = {

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

@@ -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",

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

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 { 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();

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

View File

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

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

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

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

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 = {
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];
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
pub mod hashcash;
pub mod preview;
#[cfg(not(target_arch = "arm"))]
#[global_allocator]

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([
'.',
'./packages/frontend/apps/electron',
'./blocksuite/**/*/vitest.config.ts',
]);

993
yarn.lock

File diff suppressed because it is too large Load Diff