mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 18:40:00 +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:
+18
-62
@@ -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;
|
||||
|
||||
+2
-5
@@ -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;
|
||||
Reference in New Issue
Block a user