feat: 实现网页实时预览
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import Fastify from "fastify";
|
||||
import {
|
||||
EXPORT_CONFIG_VERSION,
|
||||
defaultExportConfig,
|
||||
supportedPaperFormats
|
||||
} from "@md-to-pdf/core";
|
||||
import { RENDERER_VERSION, renderMarkdown } from "@md-to-pdf/renderer";
|
||||
import { createThemeRegistry } from "./theme-registry.js";
|
||||
|
||||
interface RenderRequestBody {
|
||||
markdown?: unknown;
|
||||
language?: unknown;
|
||||
}
|
||||
|
||||
const projectRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
|
||||
export interface BuildAppOptions {
|
||||
localThemeRoot?: string;
|
||||
logger?: boolean;
|
||||
}
|
||||
|
||||
export function buildApp(options: BuildAppOptions = {}) {
|
||||
const app = Fastify({
|
||||
logger: options.logger ?? true,
|
||||
bodyLimit: 2 * 1024 * 1024
|
||||
});
|
||||
const themes = createThemeRegistry({
|
||||
bundledRoot: resolve(projectRoot, "themes"),
|
||||
localRoot:
|
||||
options.localThemeRoot ?? resolve(projectRoot, ".local", "themes")
|
||||
});
|
||||
|
||||
app.get("/api/health", async () => ({
|
||||
status: "ok",
|
||||
service: "md-to-pdf",
|
||||
configVersion: EXPORT_CONFIG_VERSION,
|
||||
rendererVersion: RENDERER_VERSION
|
||||
}));
|
||||
|
||||
app.get("/api/capabilities", async () => ({
|
||||
defaultExportConfig,
|
||||
supportedPaperFormats,
|
||||
implemented: [
|
||||
"project-skeleton",
|
||||
"export-config",
|
||||
"theme-manifest",
|
||||
"markdown-render",
|
||||
"html-preview"
|
||||
],
|
||||
planned: ["pdf-export"]
|
||||
}));
|
||||
|
||||
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<{ Params: { themeId: string } }>(
|
||||
"/api/themes/:themeId/css",
|
||||
async (request, reply) => {
|
||||
const { themeId } = request.params;
|
||||
|
||||
const css = await themes.getCss(themeId);
|
||||
if (css === undefined) {
|
||||
return reply.code(404).send({
|
||||
error: "THEME_NOT_FOUND",
|
||||
message: "未找到指定主题"
|
||||
});
|
||||
}
|
||||
|
||||
return reply
|
||||
.header("content-type", "text/css; charset=utf-8")
|
||||
.header("cache-control", "public, max-age=300")
|
||||
.send(css);
|
||||
}
|
||||
);
|
||||
|
||||
app.get<{ Params: { themeId: string; "*": string } }>(
|
||||
"/api/themes/:themeId/assets/*",
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const asset = await themes.getAsset(
|
||||
request.params.themeId,
|
||||
request.params["*"]
|
||||
);
|
||||
if (!asset) {
|
||||
return reply.code(404).send({
|
||||
error: "THEME_NOT_FOUND",
|
||||
message: "未找到指定主题"
|
||||
});
|
||||
}
|
||||
|
||||
return reply
|
||||
.header("content-type", asset.contentType)
|
||||
.header("cache-control", "public, max-age=300")
|
||||
.header("content-security-policy", "default-src 'none'; sandbox")
|
||||
.header("x-content-type-options", "nosniff")
|
||||
.send(asset.content);
|
||||
} catch {
|
||||
return reply.code(404).send({
|
||||
error: "THEME_ASSET_NOT_FOUND",
|
||||
message: "未找到指定主题资源"
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Body: RenderRequestBody }>(
|
||||
"/api/render",
|
||||
async (request, reply) => {
|
||||
const { markdown, language } = request.body ?? {};
|
||||
|
||||
if (typeof markdown !== "string") {
|
||||
return reply.code(400).send({
|
||||
error: "INVALID_MARKDOWN",
|
||||
message: "markdown 必须是字符串"
|
||||
});
|
||||
}
|
||||
|
||||
if (markdown.length > 1_500_000) {
|
||||
return reply.code(413).send({
|
||||
error: "MARKDOWN_TOO_LARGE",
|
||||
message: "Markdown 内容不能超过 1.5 MB"
|
||||
});
|
||||
}
|
||||
|
||||
if (language !== undefined && typeof language !== "string") {
|
||||
return reply.code(400).send({
|
||||
error: "INVALID_LANGUAGE",
|
||||
message: "language 必须是字符串"
|
||||
});
|
||||
}
|
||||
|
||||
return renderMarkdown(markdown, {
|
||||
...(typeof language === "string" ? { language } : {})
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return app;
|
||||
}
|
||||
Reference in New Issue
Block a user