feat: 发布 v0.4.0 桌面端
This commit is contained in:
+44
-121
@@ -10,10 +10,11 @@ import {
|
||||
supportedPaperFormats
|
||||
} from "@md-to-pdf/core";
|
||||
import {
|
||||
MarkdownDocumentParseError,
|
||||
RENDERER_VERSION,
|
||||
renderMarkdown
|
||||
} from "@md-to-pdf/renderer";
|
||||
ApplicationRequestError,
|
||||
createApplicationService,
|
||||
type ApplicationService
|
||||
} from "@md-to-pdf/application";
|
||||
import { RENDERER_VERSION } from "@md-to-pdf/renderer";
|
||||
import {
|
||||
createPdfGenerator,
|
||||
PdfEngineClosedError,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
PdfRenderTimeoutError,
|
||||
type PdfGenerator
|
||||
} from "./pdf-engine.js";
|
||||
import { createThemeRegistry } from "./theme-registry.js";
|
||||
|
||||
interface RenderRequestBody {
|
||||
markdown?: unknown;
|
||||
@@ -34,66 +34,6 @@ interface PdfRequestBody extends RenderRequestBody {
|
||||
}
|
||||
|
||||
const projectRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const maximumMarkdownLength = 1_500_000;
|
||||
|
||||
function validateMarkdownRequest(
|
||||
body: RenderRequestBody | undefined
|
||||
):
|
||||
| { markdown: string; language?: string }
|
||||
| { statusCode: 400 | 413; error: string; message: string } {
|
||||
const { markdown, language } = body ?? {};
|
||||
|
||||
if (typeof markdown !== "string") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
error: "INVALID_MARKDOWN",
|
||||
message: "markdown 必须是字符串"
|
||||
};
|
||||
}
|
||||
if (markdown.length > maximumMarkdownLength) {
|
||||
return {
|
||||
statusCode: 413,
|
||||
error: "MARKDOWN_TOO_LARGE",
|
||||
message: "Markdown 内容不能超过 1.5 MB"
|
||||
};
|
||||
}
|
||||
if (language !== undefined && typeof language !== "string") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
error: "INVALID_LANGUAGE",
|
||||
message: "language 必须是字符串"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
markdown,
|
||||
...(typeof language === "string" ? { language } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function renderMarkdownRequest(
|
||||
markdown: string,
|
||||
language: string | undefined
|
||||
) {
|
||||
try {
|
||||
return {
|
||||
success: true as const,
|
||||
document: renderMarkdown(markdown, {
|
||||
...(language ? { language } : {})
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof MarkdownDocumentParseError) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: error.code,
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function encodeRfc5987(value: string) {
|
||||
return encodeURIComponent(value).replace(
|
||||
/[!'()*]/g,
|
||||
@@ -127,6 +67,7 @@ export interface BuildAppOptions {
|
||||
logger?: boolean;
|
||||
pdfGenerator?: PdfGenerator;
|
||||
prewarmPdfBrowser?: boolean;
|
||||
applicationService?: ApplicationService;
|
||||
}
|
||||
|
||||
function milliseconds(value: number) {
|
||||
@@ -169,12 +110,14 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
logger: options.logger ?? true,
|
||||
bodyLimit: 2 * 1024 * 1024
|
||||
});
|
||||
const themes = createThemeRegistry({
|
||||
bundledRoot: resolve(projectRoot, "themes"),
|
||||
localRoot:
|
||||
options.localThemeRoot ?? resolve(projectRoot, ".local", "themes"),
|
||||
onWarning: (message) => app.log.warn(message)
|
||||
});
|
||||
const applicationService =
|
||||
options.applicationService ??
|
||||
createApplicationService({
|
||||
bundledRoot: resolve(projectRoot, "themes"),
|
||||
localRoot:
|
||||
options.localThemeRoot ?? resolve(projectRoot, ".local", "themes"),
|
||||
onWarning: (message) => app.log.warn(message)
|
||||
});
|
||||
const pdfGenerator =
|
||||
options.pdfGenerator ?? createPdfGenerator();
|
||||
|
||||
@@ -213,23 +156,16 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
planned: []
|
||||
}));
|
||||
|
||||
app.get("/api/themes", async () => ({
|
||||
themes: (await themes.list()).map(({ manifest, source }) => ({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
description: manifest.description,
|
||||
bundled: manifest.bundled,
|
||||
source
|
||||
}))
|
||||
}));
|
||||
app.get("/api/themes", async () =>
|
||||
applicationService.listThemes()
|
||||
);
|
||||
|
||||
app.get<{ Params: { themeId: string } }>(
|
||||
"/api/themes/:themeId/css",
|
||||
async (request, reply) => {
|
||||
const { themeId } = request.params;
|
||||
|
||||
const css = await themes.getCss(themeId);
|
||||
const css = await applicationService.getThemeCss(themeId);
|
||||
if (css === undefined) {
|
||||
return reply.code(404).send({
|
||||
error: "THEME_NOT_FOUND",
|
||||
@@ -248,7 +184,7 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
"/api/themes/:themeId/assets/*",
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const asset = await themes.getAsset(
|
||||
const asset = await applicationService.getThemeAsset(
|
||||
request.params.themeId,
|
||||
request.params["*"]
|
||||
);
|
||||
@@ -277,25 +213,17 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
app.post<{ Body: RenderRequestBody }>(
|
||||
"/api/render",
|
||||
async (request, reply) => {
|
||||
const validated = validateMarkdownRequest(request.body);
|
||||
if ("statusCode" in validated) {
|
||||
return reply.code(validated.statusCode).send({
|
||||
error: validated.error,
|
||||
message: validated.message
|
||||
});
|
||||
try {
|
||||
return applicationService.render(request.body ?? {});
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationRequestError) {
|
||||
return reply.code(error.statusCode).send({
|
||||
error: error.code,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const rendered = renderMarkdownRequest(
|
||||
validated.markdown,
|
||||
validated.language
|
||||
);
|
||||
if (!rendered.success) {
|
||||
return reply.code(400).send({
|
||||
error: rendered.error,
|
||||
message: rendered.message
|
||||
});
|
||||
}
|
||||
return rendered.document;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -303,14 +231,6 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
"/api/pdf",
|
||||
async (request, reply) => {
|
||||
const requestStartedAt = performance.now();
|
||||
const validated = validateMarkdownRequest(request.body);
|
||||
if ("statusCode" in validated) {
|
||||
return reply.code(validated.statusCode).send({
|
||||
error: validated.error,
|
||||
message: validated.message
|
||||
});
|
||||
}
|
||||
|
||||
const parsedConfig = exportConfigSchema.safeParse(
|
||||
request.body?.exportConfig
|
||||
);
|
||||
@@ -333,7 +253,9 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
}
|
||||
|
||||
const themeStartedAt = performance.now();
|
||||
const themeCss = await themes.getCss(parsedConfig.data.themeId);
|
||||
const themeCss = await applicationService.getThemeCss(
|
||||
parsedConfig.data.themeId
|
||||
);
|
||||
const themeMs = performance.now() - themeStartedAt;
|
||||
if (themeCss === undefined) {
|
||||
return reply.code(404).send({
|
||||
@@ -343,18 +265,19 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
}
|
||||
|
||||
const markdownStartedAt = performance.now();
|
||||
const renderedResult = renderMarkdownRequest(
|
||||
validated.markdown,
|
||||
validated.language
|
||||
);
|
||||
const markdownMs = performance.now() - markdownStartedAt;
|
||||
if (!renderedResult.success) {
|
||||
return reply.code(400).send({
|
||||
error: renderedResult.error,
|
||||
message: renderedResult.message
|
||||
});
|
||||
let rendered;
|
||||
try {
|
||||
rendered = applicationService.render(request.body ?? {});
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationRequestError) {
|
||||
return reply.code(error.statusCode).send({
|
||||
error: error.code,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const rendered = renderedResult.document;
|
||||
const markdownMs = performance.now() - markdownStartedAt;
|
||||
|
||||
try {
|
||||
const generated = await pdfGenerator.generate(
|
||||
|
||||
Reference in New Issue
Block a user