mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-23 07:40:46 +08:00
fix: lint
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package app.affine.pro.plugin
|
||||
|
||||
import android.net.Uri
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
@@ -9,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
import uniffi.affine_mobile_native.renderMermaidPreviewSvg
|
||||
import uniffi.affine_mobile_native.renderTypstPreviewSvg
|
||||
import java.io.File
|
||||
|
||||
private fun JSObject.getOptionalString(key: String): String? {
|
||||
return if (has(key) && !isNull(key)) getString(key) else null
|
||||
@@ -18,6 +20,39 @@ private fun JSObject.getOptionalDouble(key: String): Double? {
|
||||
return if (has(key) && !isNull(key)) getDouble(key) else null
|
||||
}
|
||||
|
||||
private fun resolveLocalFontDir(fontUrl: String): String? {
|
||||
val uri = Uri.parse(fontUrl)
|
||||
val path = when {
|
||||
uri.scheme == null -> {
|
||||
val file = File(fontUrl)
|
||||
if (!file.isAbsolute) {
|
||||
return null
|
||||
}
|
||||
file.path
|
||||
}
|
||||
uri.scheme == "file" -> uri.path
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
||||
val file = File(path)
|
||||
val directory = if (file.isDirectory) file else file.parentFile ?: return null
|
||||
return directory.absolutePath
|
||||
}
|
||||
|
||||
private fun JSObject.resolveTypstFontDirs(): List<String>? {
|
||||
val fontUrls = optJSONArray("fontUrls") ?: return null
|
||||
val fontDirs = buildList(fontUrls.length()) {
|
||||
repeat(fontUrls.length()) { index ->
|
||||
val fontUrl = fontUrls.optString(index, null)
|
||||
?: throw IllegalArgumentException("Typst preview fontUrls must be strings.")
|
||||
val fontDir = resolveLocalFontDir(fontUrl)
|
||||
?: throw IllegalArgumentException("Typst preview on mobile only supports local font file URLs or absolute font directories.")
|
||||
add(fontDir)
|
||||
}
|
||||
}
|
||||
return fontDirs.distinct()
|
||||
}
|
||||
|
||||
@CapacitorPlugin(name = "Preview")
|
||||
class PreviewPlugin : Plugin() {
|
||||
|
||||
@@ -48,9 +83,10 @@ class PreviewPlugin : Plugin() {
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
val code = call.getStringEnsure("code")
|
||||
val options = call.getObject("options")
|
||||
val svg = renderTypstPreviewSvg(
|
||||
code = code,
|
||||
fontDirs = null,
|
||||
fontDirs = options?.resolveTypstFontDirs(),
|
||||
cacheDir = context.cacheDir.absolutePath,
|
||||
)
|
||||
call.resolve(JSObject().apply {
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface PreviewPlugin {
|
||||
code: string;
|
||||
options?: {
|
||||
fontUrls?: string[];
|
||||
theme?: 'light' | 'dark';
|
||||
};
|
||||
}): Promise<{ svg: string }>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
private func resolveLocalFontDir(from fontURL: String) -> String? {
|
||||
let path: String
|
||||
if fontURL.hasPrefix("file://") {
|
||||
guard let url = URL(string: fontURL), url.isFileURL else {
|
||||
return nil
|
||||
}
|
||||
path = url.path
|
||||
} else {
|
||||
let candidate = (fontURL as NSString).standardizingPath
|
||||
guard candidate.hasPrefix("/") else {
|
||||
return nil
|
||||
}
|
||||
path = candidate
|
||||
}
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory),
|
||||
isDirectory.boolValue
|
||||
{
|
||||
return path
|
||||
}
|
||||
|
||||
let directory = (path as NSString).deletingLastPathComponent
|
||||
return directory.isEmpty ? nil : directory
|
||||
}
|
||||
|
||||
private func resolveTypstFontDirs(from options: [AnyHashable: Any]?) throws -> [String]? {
|
||||
guard let fontUrls = options?["fontUrls"] as? [String] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Array(Set(try fontUrls.map { fontURL in
|
||||
guard let fontDir = resolveLocalFontDir(from: fontURL) else {
|
||||
throw NSError(
|
||||
domain: "PreviewPlugin",
|
||||
code: 1,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Typst preview on mobile only supports local font file URLs or absolute font directories."
|
||||
]
|
||||
)
|
||||
}
|
||||
return fontDir
|
||||
}))
|
||||
}
|
||||
|
||||
@objc(PreviewPlugin)
|
||||
public class PreviewPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "PreviewPlugin"
|
||||
@@ -32,8 +77,10 @@ public class PreviewPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
let code = try call.getStringEnsure("code")
|
||||
let options = call.getObject("options")
|
||||
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path
|
||||
let svg = try renderTypstPreviewSvg(code: code, fontDirs: nil, cacheDir: cacheDir)
|
||||
let fontDirs = try resolveTypstFontDirs(from: options)
|
||||
let svg = try renderTypstPreviewSvg(code: code, fontDirs: fontDirs, cacheDir: cacheDir)
|
||||
call.resolve(["svg": svg])
|
||||
} catch {
|
||||
call.reject("Failed to render Typst preview, \(error)", nil, error)
|
||||
|
||||
@@ -9,21 +9,17 @@ import ApolloAPI
|
||||
|
||||
/// The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
public struct JSON: CustomScalarType, Hashable, ExpressibleByDictionaryLiteral {
|
||||
public let object: ApolloAPI.JSONObject
|
||||
public let value: JSONValue
|
||||
|
||||
public init(_jsonValue value: JSONValue) throws {
|
||||
object = try ApolloAPI.JSONObject(_jsonValue: value)
|
||||
}
|
||||
|
||||
public init(_ object: ApolloAPI.JSONObject) {
|
||||
self.object = object
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public init(dictionaryLiteral elements: (String, JSONValue)...) {
|
||||
object = .init(uniqueKeysWithValues: elements)
|
||||
value = ApolloAPI.JSONObject(uniqueKeysWithValues: elements) as JSONValue
|
||||
}
|
||||
|
||||
public var _jsonValue: JSONValue {
|
||||
object
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,22 @@
|
||||
import ApolloAPI
|
||||
|
||||
/// The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
public typealias JSONObject = JSON
|
||||
public struct JSONObject: CustomScalarType, Hashable, ExpressibleByDictionaryLiteral {
|
||||
public let object: ApolloAPI.JSONObject
|
||||
|
||||
public init(_jsonValue value: JSONValue) throws {
|
||||
object = try ApolloAPI.JSONObject(_jsonValue: value)
|
||||
}
|
||||
|
||||
public init(_ object: ApolloAPI.JSONObject) {
|
||||
self.object = object
|
||||
}
|
||||
|
||||
public init(dictionaryLiteral elements: (String, JSONValue)...) {
|
||||
object = ApolloAPI.JSONObject(uniqueKeysWithValues: elements)
|
||||
}
|
||||
|
||||
public var _jsonValue: JSONValue {
|
||||
object
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface PreviewPlugin {
|
||||
code: string;
|
||||
options?: {
|
||||
fontUrls?: string[];
|
||||
theme?: 'light' | 'dark';
|
||||
};
|
||||
}): Promise<{ svg: string }>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { initialize, render } = vi.hoisted(() => ({
|
||||
initialize: vi.fn(),
|
||||
render: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('mermaid', () => ({
|
||||
default: {
|
||||
initialize,
|
||||
render,
|
||||
},
|
||||
}));
|
||||
|
||||
import { renderClassicMermaidSvg } from './classic-mermaid';
|
||||
|
||||
describe('renderClassicMermaidSvg', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('serializes initialize and render across concurrent calls', async () => {
|
||||
const events: string[] = [];
|
||||
let releaseFirstRender!: () => void;
|
||||
|
||||
initialize.mockImplementation(config => {
|
||||
events.push(`init:${config.theme}`);
|
||||
});
|
||||
render
|
||||
.mockImplementationOnce(async () => {
|
||||
events.push('render:first:start');
|
||||
await new Promise<void>(resolve => {
|
||||
releaseFirstRender = resolve;
|
||||
});
|
||||
events.push('render:first:end');
|
||||
return { svg: '<svg>first</svg>' };
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
events.push('render:second:start');
|
||||
return { svg: '<svg>second</svg>' };
|
||||
});
|
||||
|
||||
const first = renderClassicMermaidSvg({
|
||||
code: 'flowchart TD;A-->B',
|
||||
options: { theme: 'default' },
|
||||
});
|
||||
const second = renderClassicMermaidSvg({
|
||||
code: 'flowchart TD;B-->C',
|
||||
options: { theme: 'modern' },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(events).toEqual(['init:default', 'render:first:start']);
|
||||
});
|
||||
|
||||
releaseFirstRender();
|
||||
|
||||
await expect(first).resolves.toEqual({ svg: '<svg>first</svg>' });
|
||||
await expect(second).resolves.toEqual({ svg: '<svg>second</svg>' });
|
||||
expect(events).toEqual([
|
||||
'init:default',
|
||||
'render:first:start',
|
||||
'render:first:end',
|
||||
'init:base',
|
||||
'render:second:start',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '../mermaid/renderer';
|
||||
|
||||
let mermaidPromise: Promise<Mermaid> | null = null;
|
||||
let mermaidRenderQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
function toTheme(theme: MermaidRenderTheme | undefined) {
|
||||
return theme === 'modern' ? ('base' as const) : ('default' as const);
|
||||
@@ -39,12 +40,23 @@ function createDiagramId() {
|
||||
return `mermaid-diagram-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function enqueueClassicMermaidRender<T>(task: () => Promise<T>): Promise<T> {
|
||||
const run = mermaidRenderQueue.then(task, task);
|
||||
mermaidRenderQueue = run.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return run;
|
||||
}
|
||||
|
||||
export async function renderClassicMermaidSvg(
|
||||
request: MermaidRenderRequest
|
||||
): Promise<MermaidRenderResult> {
|
||||
const mermaid = await loadMermaid();
|
||||
mermaid.initialize(createClassicMermaidConfig(request.options));
|
||||
return enqueueClassicMermaidRender(async () => {
|
||||
const mermaid = await loadMermaid();
|
||||
mermaid.initialize(createClassicMermaidConfig(request.options));
|
||||
|
||||
const { svg } = await mermaid.render(createDiagramId(), request.code);
|
||||
return { svg };
|
||||
const { svg } = await mermaid.render(createDiagramId(), request.code);
|
||||
return { svg };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { WorkerOpRenderer } from './worker-op-renderer';
|
||||
|
||||
vi.mock('@affine/env/worker', () => ({
|
||||
getWorkerUrl: vi.fn(() => '/worker.js'),
|
||||
}));
|
||||
|
||||
class MockWorker {
|
||||
addEventListener = vi.fn();
|
||||
postMessage = vi.fn();
|
||||
removeEventListener = vi.fn();
|
||||
terminate = vi.fn();
|
||||
}
|
||||
|
||||
class TestRenderer extends WorkerOpRenderer<{
|
||||
init: [undefined, { ok: true }];
|
||||
}> {
|
||||
constructor() {
|
||||
super('test');
|
||||
}
|
||||
|
||||
init() {
|
||||
return this.ensureInitialized(async () => {
|
||||
return { ok: true } as const;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('WorkerOpRenderer', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Worker', MockWorker);
|
||||
});
|
||||
|
||||
test('rejects initialization after destroy', async () => {
|
||||
const renderer = new TestRenderer();
|
||||
|
||||
renderer.destroy();
|
||||
|
||||
await expect(renderer.init()).rejects.toThrow('renderer destroyed');
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ export abstract class WorkerOpRenderer<
|
||||
Ops extends OpSchema,
|
||||
> extends OpClient<Ops> {
|
||||
private readonly worker: Worker;
|
||||
private destroyed = false;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
|
||||
protected constructor(workerName: string) {
|
||||
@@ -16,6 +17,7 @@ export abstract class WorkerOpRenderer<
|
||||
}
|
||||
|
||||
protected ensureInitialized(task: InitTask) {
|
||||
if (this.destroyed) return Promise.reject(new Error('renderer destroyed'));
|
||||
if (!this.initPromise) {
|
||||
this.initPromise = task()
|
||||
.then(() => undefined)
|
||||
@@ -32,6 +34,8 @@ export abstract class WorkerOpRenderer<
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
super.destroy();
|
||||
this.worker.terminate();
|
||||
this.resetInitialization();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const { loadFonts, setCompilerInitOptions, setRendererInitOptions, svg } =
|
||||
vi.hoisted(() => ({
|
||||
loadFonts: vi.fn((fontUrls: string[]) => ({ fontUrls })),
|
||||
setCompilerInitOptions: vi.fn(),
|
||||
setRendererInitOptions: vi.fn(),
|
||||
svg: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@myriaddreamin/typst.ts', () => ({
|
||||
$typst: {
|
||||
setCompilerInitOptions,
|
||||
setRendererInitOptions,
|
||||
svg,
|
||||
},
|
||||
loadFonts,
|
||||
}));
|
||||
|
||||
import { ensureTypstReady, renderTypstSvgWithOptions } from './runtime';
|
||||
|
||||
describe('typst runtime', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
svg.mockResolvedValue('<svg />');
|
||||
});
|
||||
|
||||
test('reconfigures typst when fontUrls change', async () => {
|
||||
await ensureTypstReady(['font-a']);
|
||||
await ensureTypstReady(['font-b']);
|
||||
|
||||
expect(loadFonts).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
['font-a'],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(loadFonts).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
['font-b'],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(setCompilerInitOptions).toHaveBeenCalledTimes(2);
|
||||
expect(setRendererInitOptions).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('serializes typst renders that need different configuration', async () => {
|
||||
const events: string[] = [];
|
||||
let releaseFirstRender!: () => void;
|
||||
|
||||
svg.mockImplementationOnce(async () => {
|
||||
events.push('svg:first:start');
|
||||
await new Promise<void>(resolve => {
|
||||
releaseFirstRender = resolve;
|
||||
});
|
||||
events.push('svg:first:end');
|
||||
return '<svg>first</svg>';
|
||||
});
|
||||
svg.mockImplementationOnce(async () => {
|
||||
events.push('svg:second:start');
|
||||
return '<svg>second</svg>';
|
||||
});
|
||||
|
||||
const first = renderTypstSvgWithOptions('= First', {
|
||||
fontUrls: ['font-a'],
|
||||
});
|
||||
const second = renderTypstSvgWithOptions('= Second', {
|
||||
fontUrls: ['font-b'],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(events).toEqual(['svg:first:start']);
|
||||
});
|
||||
|
||||
releaseFirstRender();
|
||||
|
||||
await expect(first).resolves.toEqual({ svg: '<svg>first</svg>' });
|
||||
await expect(second).resolves.toEqual({ svg: '<svg>second</svg>' });
|
||||
expect(events).toEqual([
|
||||
'svg:first:start',
|
||||
'svg:first:end',
|
||||
'svg:second:start',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,13 @@ type TypstWasmModuleUrls = {
|
||||
rendererWasmUrl?: string;
|
||||
};
|
||||
|
||||
let typstInitPromise: Promise<void> | null = null;
|
||||
type TypstInitState = {
|
||||
key: string;
|
||||
promise: Promise<void>;
|
||||
};
|
||||
|
||||
let typstInitState: TypstInitState | null = null;
|
||||
let typstRenderQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
function extractInputUrl(input: RequestInfo | URL): string | null {
|
||||
if (input instanceof URL) {
|
||||
@@ -129,15 +135,36 @@ function getBeforeBuildHooks(fontUrls: string[]): BeforeBuildFn[] {
|
||||
];
|
||||
}
|
||||
|
||||
function createTypstInitKey(
|
||||
fontUrls: string[],
|
||||
wasmModuleUrls: TypstWasmModuleUrls
|
||||
) {
|
||||
return JSON.stringify({
|
||||
fontUrls,
|
||||
compilerWasmUrl: wasmModuleUrls.compilerWasmUrl ?? compilerWasmUrl,
|
||||
rendererWasmUrl: wasmModuleUrls.rendererWasmUrl ?? rendererWasmUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function enqueueTypstRender<T>(task: () => Promise<T>): Promise<T> {
|
||||
const run = typstRenderQueue.then(task, task);
|
||||
typstRenderQueue = run.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return run;
|
||||
}
|
||||
|
||||
export async function ensureTypstReady(
|
||||
fontUrls: string[],
|
||||
wasmModuleUrls: TypstWasmModuleUrls = {}
|
||||
) {
|
||||
if (typstInitPromise) {
|
||||
return typstInitPromise;
|
||||
const key = createTypstInitKey(fontUrls, wasmModuleUrls);
|
||||
if (typstInitState?.key === key) {
|
||||
return typstInitState.promise;
|
||||
}
|
||||
|
||||
typstInitPromise = Promise.resolve()
|
||||
const promise = Promise.resolve()
|
||||
.then(() => {
|
||||
const compilerBeforeBuild = getBeforeBuildHooks(fontUrls);
|
||||
|
||||
@@ -150,11 +177,14 @@ export async function ensureTypstReady(
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
typstInitPromise = null;
|
||||
if (typstInitState?.key === key) {
|
||||
typstInitState = null;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
return typstInitPromise;
|
||||
typstInitState = { key, promise };
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function renderTypstSvgWithOptions(
|
||||
@@ -166,12 +196,14 @@ export async function renderTypstSvgWithOptions(
|
||||
DEFAULT_TYPST_RENDER_OPTIONS,
|
||||
options
|
||||
);
|
||||
await ensureTypstReady(
|
||||
resolvedOptions.fontUrls ?? [...DEFAULT_TYPST_FONT_URLS],
|
||||
wasmModuleUrls
|
||||
);
|
||||
const svg = await $typst.svg({
|
||||
mainContent: code,
|
||||
return enqueueTypstRender(async () => {
|
||||
await ensureTypstReady(
|
||||
resolvedOptions.fontUrls ?? [...DEFAULT_TYPST_FONT_URLS],
|
||||
wasmModuleUrls
|
||||
);
|
||||
const svg = await $typst.svg({
|
||||
mainContent: code,
|
||||
});
|
||||
return { svg };
|
||||
});
|
||||
return { svg };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { OpSchema } from '@toeverything/infra/op';
|
||||
|
||||
export type TypstRenderOptions = {
|
||||
fontUrls?: string[];
|
||||
theme?: 'light' | 'dark';
|
||||
};
|
||||
|
||||
export type TypstRenderRequest = {
|
||||
|
||||
@@ -23,7 +23,11 @@ class TypstRendererBackend extends OpConsumer<TypstOps> {
|
||||
DEFAULT_TYPST_RENDER_OPTIONS,
|
||||
options
|
||||
);
|
||||
await ensureTypstReady(this.options.fontUrls ?? []);
|
||||
await ensureTypstReady(
|
||||
this.options.fontUrls ?? [
|
||||
...(DEFAULT_TYPST_RENDER_OPTIONS.fontUrls ?? []),
|
||||
]
|
||||
);
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
|
||||
10
packages/frontend/native/index.d.ts
vendored
10
packages/frontend/native/index.d.ts
vendored
@@ -41,9 +41,6 @@ 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
|
||||
@@ -58,12 +55,6 @@ 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
|
||||
@@ -72,7 +63,6 @@ export declare function renderTypstSvg(request: TypstRenderRequest): TypstRender
|
||||
|
||||
export interface TypstRenderOptions {
|
||||
fontUrls?: Array<string>
|
||||
theme?: string
|
||||
fontDirs?: Array<string>
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,8 @@ 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>,
|
||||
@@ -65,7 +55,6 @@ pub fn render_mermaid_svg(request: MermaidRenderRequest) -> Result<MermaidRender
|
||||
#[napi(object)]
|
||||
pub struct TypstRenderOptions {
|
||||
pub font_urls: Option<Vec<String>>,
|
||||
pub theme: Option<String>,
|
||||
pub font_dirs: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -80,12 +69,40 @@ pub struct TypstRenderResult {
|
||||
pub svg: String,
|
||||
}
|
||||
|
||||
fn resolve_local_font_dir(value: &str) -> Option<PathBuf> {
|
||||
let path = if let Some(stripped) = value.strip_prefix("file://") {
|
||||
PathBuf::from(stripped)
|
||||
} else {
|
||||
let path = PathBuf::from(value);
|
||||
if !path.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
path
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
path.parent().map(|parent| parent.to_path_buf())
|
||||
}
|
||||
|
||||
fn resolve_typst_font_dirs(options: &Option<TypstRenderOptions>) -> Vec<PathBuf> {
|
||||
options
|
||||
let Some(options) = options.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut font_dirs = options
|
||||
.font_dirs
|
||||
.as_ref()
|
||||
.and_then(|options| options.font_dirs.as_ref())
|
||||
.map(|dirs| dirs.iter().map(PathBuf::from).collect())
|
||||
.unwrap_or_default()
|
||||
.map(|dirs| dirs.iter().map(PathBuf::from).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(font_urls) = options.font_urls.as_ref() {
|
||||
font_dirs.extend(font_urls.iter().filter_map(|url| resolve_local_font_dir(url)));
|
||||
}
|
||||
|
||||
font_dirs
|
||||
}
|
||||
|
||||
fn normalize_typst_svg(svg: String) -> String {
|
||||
|
||||
Reference in New Issue
Block a user