import { MarkdownDocumentParseError, renderMarkdown } from "@md-to-pdf/renderer"; import { createThemeRegistry, type ThemeRegistryOptions } from "./theme-registry.js"; import { createImageResourceResolver, type ImageResolutionContext, type ImageResourceResolverOptions } from "./image-resources.js"; export const MAXIMUM_MARKDOWN_LENGTH = 1_500_000; export interface MarkdownRenderRequest { markdown?: unknown; language?: unknown; resources?: unknown; } export class ApplicationRequestError extends Error { constructor( readonly statusCode: 400 | 404 | 413, readonly code: string, message: string ) { super(message); this.name = "ApplicationRequestError"; } } export interface ApplicationServiceOptions extends ThemeRegistryOptions, ImageResourceResolverOptions {} export function createApplicationService( options: ApplicationServiceOptions ) { const themes = createThemeRegistry(options); const resolveImages = createImageResourceResolver(options); async function render( request: MarkdownRenderRequest, context: ImageResolutionContext = {} ) { const { markdown, language, resources } = request; if (typeof markdown !== "string") { throw new ApplicationRequestError( 400, "INVALID_MARKDOWN", "markdown 必须是字符串" ); } if (markdown.length > MAXIMUM_MARKDOWN_LENGTH) { throw new ApplicationRequestError( 413, "MARKDOWN_TOO_LARGE", "Markdown 内容不能超过 1.5 MB" ); } if (language !== undefined && typeof language !== "string") { throw new ApplicationRequestError( 400, "INVALID_LANGUAGE", "language 必须是字符串" ); } try { const images = await resolveImages(markdown, resources, context); return renderMarkdown(markdown, { ...(language ? { language } : {}), imageSourceMap: images.sources, warnings: images.warnings }); } catch (error) { if (error instanceof MarkdownDocumentParseError) { throw new ApplicationRequestError( 400, error.code, error.message ); } if (error instanceof Error) { const tooLarge = /超过|大小/u.test(error.message); throw new ApplicationRequestError( tooLarge ? 413 : 400, tooLarge ? "IMAGE_RESOURCES_TOO_LARGE" : "INVALID_IMAGE_RESOURCES", error.message ); } throw error; } } async function listThemes() { return { themes: (await themes.list()).map(({ manifest, source }) => ({ id: manifest.id, name: manifest.name, version: manifest.version, description: manifest.description, bundled: manifest.bundled, source })) }; } return { getThemeAsset: themes.getAsset, getThemeCss: themes.getCss, invalidateThemes: themes.invalidate, listThemes, render }; } export type ApplicationService = ReturnType< typeof createApplicationService >;