mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-22 04:21:40 +08:00
feat(editor): migrate typst mermaid to native (#14499)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Native/WASM Mermaid and Typst SVG preview rendering on desktop and mobile, plus cross-platform Preview plugin integrations. * **Improvements** * Centralized, sanitized rendering bridge with automatic Typst font-directory handling and configurable native renderer selection. * More consistent and robust error serialization and worker-backed preview flows for improved stability and performance. * **Tests** * Extensive unit and integration tests for preview rendering, font discovery, sanitization, and error serialization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user