feat: 发布 v0.4.0 桌面端

This commit is contained in:
SkyJourney
2026-07-27 20:56:39 +08:00
parent 92a5bfd016
commit 4d60741f5a
90 changed files with 10431 additions and 223 deletions
+44 -121
View File
@@ -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(