chore: 初始化项目骨架
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
tmp/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Markdown PDF 导出器
|
||||||
|
|
||||||
|
一个面向内网部署的 Markdown 排版与 PDF 导出工具。项目目标是使用同一份 HTML 和 CSS 完成网页预览与 Chromium PDF 渲染,并提供纸张、页边距、页眉页脚、页码及主题扩展能力。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
项目处于骨架阶段,已经建立:
|
||||||
|
|
||||||
|
- React 前端;
|
||||||
|
- Fastify 后端;
|
||||||
|
- 共享导出配置模型;
|
||||||
|
- 版本化主题清单接口;
|
||||||
|
- 内置 Typora 风格主题目录;
|
||||||
|
- Docker 与测试目录预留。
|
||||||
|
|
||||||
|
Markdown 渲染和 PDF 生成将在后续阶段实现。
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
要求 Node.js 22 或更高版本。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
默认地址:
|
||||||
|
|
||||||
|
- 前端:http://localhost:5173
|
||||||
|
- 后端:http://localhost:3001
|
||||||
|
- 健康检查:http://localhost:3001/api/health
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 隐私原则
|
||||||
|
|
||||||
|
文档和资源文件仅用于当前转换请求。正式实现将为每次请求创建隔离的临时目录,并在成功或失败后统一清理,不接入数据库或文档历史存储。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@md-to-pdf/server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@md-to-pdf/core": "0.1.0",
|
||||||
|
"fastify": "^5.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"tsx": "^4.21.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Fastify from "fastify";
|
||||||
|
import {
|
||||||
|
EXPORT_CONFIG_VERSION,
|
||||||
|
defaultExportConfig,
|
||||||
|
supportedPaperFormats
|
||||||
|
} from "@md-to-pdf/core";
|
||||||
|
|
||||||
|
const port = Number.parseInt(process.env.PORT ?? "3001", 10);
|
||||||
|
const host = process.env.HOST ?? "0.0.0.0";
|
||||||
|
|
||||||
|
const app = Fastify({
|
||||||
|
logger: true
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/health", async () => ({
|
||||||
|
status: "ok",
|
||||||
|
service: "md-to-pdf",
|
||||||
|
configVersion: EXPORT_CONFIG_VERSION
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.get("/api/capabilities", async () => ({
|
||||||
|
defaultExportConfig,
|
||||||
|
supportedPaperFormats,
|
||||||
|
implemented: ["project-skeleton", "export-config", "theme-manifest"],
|
||||||
|
planned: ["markdown-render", "html-preview", "pdf-export"]
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
try {
|
||||||
|
await app.listen({ port, host });
|
||||||
|
} catch (error) {
|
||||||
|
app.log.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start();
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="可配置、可主题化的 Markdown PDF 导出工具"
|
||||||
|
/>
|
||||||
|
<title>Markdown PDF 导出器</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@md-to-pdf/web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"typecheck": "tsc -b --pretty false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@md-to-pdf/core": "0.1.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { defaultExportConfig, paperFormatLabels } from "@md-to-pdf/core";
|
||||||
|
|
||||||
|
const capabilities = [
|
||||||
|
"Markdown 本地预览",
|
||||||
|
"Chromium PDF 下载",
|
||||||
|
"纸张与页边距",
|
||||||
|
"页眉、页脚与页码",
|
||||||
|
"可扩展 CSS 主题"
|
||||||
|
];
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<main className="shell">
|
||||||
|
<section className="intro" aria-labelledby="page-title">
|
||||||
|
<p className="eyebrow">内网文档工具</p>
|
||||||
|
<h1 id="page-title">Markdown PDF 导出器</h1>
|
||||||
|
<p className="summary">
|
||||||
|
使用同一份 HTML 与主题 CSS 完成网页预览和 Chromium PDF 渲染。
|
||||||
|
</p>
|
||||||
|
<div className="status">
|
||||||
|
<span className="status-dot" aria-hidden="true" />
|
||||||
|
项目骨架已就绪
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel" aria-labelledby="config-title">
|
||||||
|
<div>
|
||||||
|
<p className="panel-label">默认导出预设</p>
|
||||||
|
<h2 id="config-title">{defaultExportConfig.name}</h2>
|
||||||
|
</div>
|
||||||
|
<dl className="facts">
|
||||||
|
<div>
|
||||||
|
<dt>纸张</dt>
|
||||||
|
<dd>{paperFormatLabels[defaultExportConfig.paper.format]}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>方向</dt>
|
||||||
|
<dd>
|
||||||
|
{defaultExportConfig.paper.orientation === "portrait"
|
||||||
|
? "纵向"
|
||||||
|
: "横向"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>主题</dt>
|
||||||
|
<dd>{defaultExportConfig.themeId}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="capabilities" aria-label="规划能力">
|
||||||
|
{capabilities.map((item, index) => (
|
||||||
|
<article key={item}>
|
||||||
|
<span>{String(index + 1).padStart(2, "0")}</span>
|
||||||
|
<p>{item}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("root");
|
||||||
|
|
||||||
|
if (!rootElement) {
|
||||||
|
throw new Error("未找到应用挂载节点");
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
:root {
|
||||||
|
color: #1d2522;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 10%, rgb(197 225 210 / 45%), transparent 30rem),
|
||||||
|
#f2f0e9;
|
||||||
|
font-family:
|
||||||
|
Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(1120px, calc(100% - 40px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 88px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow,
|
||||||
|
.panel-label {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #34705a;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
max-width: 700px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
font-family: Georgia, "Songti SC", serif;
|
||||||
|
font-size: clamp(3rem, 8vw, 6.4rem);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
line-height: 0.94;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
max-width: 610px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
color: #56615d;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #315a4b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3da073;
|
||||||
|
box-shadow: 0 0 0 5px rgb(61 160 115 / 13%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(420px, 1.25fr);
|
||||||
|
gap: 48px;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 80px;
|
||||||
|
padding: 34px;
|
||||||
|
border: 1px solid rgb(41 64 55 / 14%);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgb(255 255 255 / 58%);
|
||||||
|
box-shadow: 0 24px 60px rgb(49 67 59 / 8%);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-family: Georgia, "Songti SC", serif;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts div {
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 13px;
|
||||||
|
background: #e6ebe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts dt {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
color: #6b756f;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts dd {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
margin-top: 20px;
|
||||||
|
border-top: 1px solid rgb(41 64 55 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities article {
|
||||||
|
min-height: 132px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-right: 1px solid rgb(41 64 55 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities article:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities span {
|
||||||
|
color: #7e8983;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities p {
|
||||||
|
margin: 42px 0 0;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.shell {
|
||||||
|
width: min(100% - 28px, 1120px);
|
||||||
|
padding: 48px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin-top: 56px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities article {
|
||||||
|
min-height: auto;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid rgb(41 64 55 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities p {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"allowJs": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://localhost:3001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
本目录将在 PDF 渲染阶段加入基于固定版本 Chromium 的容器配置。
|
||||||
|
|
||||||
|
计划包括:
|
||||||
|
|
||||||
|
- 非 root 运行用户;
|
||||||
|
- Chromium 运行依赖;
|
||||||
|
- 临时目录容量限制;
|
||||||
|
- 健康检查;
|
||||||
|
- Docker Compose 内网部署;
|
||||||
|
- 请求结束后的临时资源清理。
|
||||||
Generated
+2856
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "md-to-pdf",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "可配置、可主题化的 Markdown PDF 导出工具",
|
||||||
|
"workspaces": [
|
||||||
|
"apps/*",
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/web && npm run build -w @md-to-pdf/server",
|
||||||
|
"dev": "npm run build -w @md-to-pdf/core && concurrently -k -n core,server,web \"npm:dev:core\" \"npm:dev:server\" \"npm:dev:web\"",
|
||||||
|
"dev:core": "npm run dev -w @md-to-pdf/core",
|
||||||
|
"dev:server": "npm run dev -w @md-to-pdf/server",
|
||||||
|
"dev:web": "npm run dev -w @md-to-pdf/web",
|
||||||
|
"typecheck": "npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@md-to-pdf/core",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.1.13"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const EXPORT_CONFIG_VERSION = 1;
|
||||||
|
|
||||||
|
export const supportedPaperFormats = [
|
||||||
|
"A3",
|
||||||
|
"A4",
|
||||||
|
"A5",
|
||||||
|
"Letter",
|
||||||
|
"Legal",
|
||||||
|
"Tabloid",
|
||||||
|
"Custom"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const paperFormatLabels: Record<
|
||||||
|
(typeof supportedPaperFormats)[number],
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
A3: "A3",
|
||||||
|
A4: "A4",
|
||||||
|
A5: "A5",
|
||||||
|
Letter: "Letter",
|
||||||
|
Legal: "Legal",
|
||||||
|
Tabloid: "Tabloid",
|
||||||
|
Custom: "自定义"
|
||||||
|
};
|
||||||
|
|
||||||
|
const lengthSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d+(?:\.\d+)?(?:mm|cm|in)$/, "长度必须包含 mm、cm 或 in 单位");
|
||||||
|
|
||||||
|
const marginSchema = z.object({
|
||||||
|
top: lengthSchema,
|
||||||
|
right: lengthSchema,
|
||||||
|
bottom: lengthSchema,
|
||||||
|
left: lengthSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerFooterSlotSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
content: z.string().max(500)
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerFooterSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
height: lengthSchema,
|
||||||
|
showDivider: z.boolean(),
|
||||||
|
fontSize: lengthSchema,
|
||||||
|
color: z.string(),
|
||||||
|
left: headerFooterSlotSchema,
|
||||||
|
center: headerFooterSlotSchema,
|
||||||
|
right: headerFooterSlotSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const paperSchema = z
|
||||||
|
.object({
|
||||||
|
format: z.enum(supportedPaperFormats),
|
||||||
|
orientation: z.enum(["portrait", "landscape"]),
|
||||||
|
width: lengthSchema.optional(),
|
||||||
|
height: lengthSchema.optional(),
|
||||||
|
margins: marginSchema
|
||||||
|
})
|
||||||
|
.superRefine((paper, context) => {
|
||||||
|
if (paper.format === "Custom" && (!paper.width || !paper.height)) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "自定义纸张必须同时指定宽度和高度",
|
||||||
|
path: ["width"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const pageNumberFormatSchema = z.enum([
|
||||||
|
"page",
|
||||||
|
"page-total",
|
||||||
|
"chinese-page-total",
|
||||||
|
"dash-page",
|
||||||
|
"custom"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const exportConfigSchema = z.object({
|
||||||
|
version: z.literal(EXPORT_CONFIG_VERSION),
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
themeId: z.string().min(1),
|
||||||
|
paper: paperSchema,
|
||||||
|
header: headerFooterSchema,
|
||||||
|
footer: headerFooterSchema,
|
||||||
|
pageNumber: z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
position: z.enum(["header", "footer"]),
|
||||||
|
alignment: z.enum(["left", "center", "right"]),
|
||||||
|
format: pageNumberFormatSchema,
|
||||||
|
template: z.string().max(500).optional(),
|
||||||
|
startFrom: z.number().int().positive()
|
||||||
|
}),
|
||||||
|
metadata: z.object({
|
||||||
|
title: z.string().max(300),
|
||||||
|
author: z.string().max(300),
|
||||||
|
subject: z.string().max(500),
|
||||||
|
keywords: z.array(z.string().max(100)).max(30),
|
||||||
|
language: z.string().max(30)
|
||||||
|
}),
|
||||||
|
print: z.object({
|
||||||
|
background: z.boolean(),
|
||||||
|
scale: z.number().min(0.1).max(2),
|
||||||
|
pageBreakBeforeH1: z.boolean()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExportConfig = z.infer<typeof exportConfigSchema>;
|
||||||
|
export type PaperFormat = ExportConfig["paper"]["format"];
|
||||||
|
export type HeaderFooterConfig = ExportConfig["header"];
|
||||||
|
|
||||||
|
const disabledSlot = {
|
||||||
|
enabled: false,
|
||||||
|
content: ""
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const defaultExportConfig: ExportConfig = {
|
||||||
|
version: EXPORT_CONFIG_VERSION,
|
||||||
|
name: "默认 A4 技术文档",
|
||||||
|
themeId: "typora-like",
|
||||||
|
paper: {
|
||||||
|
format: "A4",
|
||||||
|
orientation: "portrait",
|
||||||
|
margins: {
|
||||||
|
top: "20mm",
|
||||||
|
right: "18mm",
|
||||||
|
bottom: "20mm",
|
||||||
|
left: "18mm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
enabled: false,
|
||||||
|
height: "8mm",
|
||||||
|
showDivider: false,
|
||||||
|
fontSize: "3mm",
|
||||||
|
color: "#6b7280",
|
||||||
|
left: disabledSlot,
|
||||||
|
center: disabledSlot,
|
||||||
|
right: disabledSlot
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
enabled: true,
|
||||||
|
height: "8mm",
|
||||||
|
showDivider: false,
|
||||||
|
fontSize: "3mm",
|
||||||
|
color: "#6b7280",
|
||||||
|
left: disabledSlot,
|
||||||
|
center: {
|
||||||
|
enabled: true,
|
||||||
|
content: "${page} / ${pages}"
|
||||||
|
},
|
||||||
|
right: disabledSlot
|
||||||
|
},
|
||||||
|
pageNumber: {
|
||||||
|
enabled: true,
|
||||||
|
position: "footer",
|
||||||
|
alignment: "center",
|
||||||
|
format: "page-total",
|
||||||
|
startFrom: 1
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
title: "",
|
||||||
|
author: "",
|
||||||
|
subject: "",
|
||||||
|
keywords: [],
|
||||||
|
language: "zh-CN"
|
||||||
|
},
|
||||||
|
print: {
|
||||||
|
background: true,
|
||||||
|
scale: 1,
|
||||||
|
pageBreakBeforeH1: false
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./export-config.js";
|
||||||
|
export * from "./theme.js";
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const THEME_MANIFEST_VERSION = 1;
|
||||||
|
|
||||||
|
export const themeFeatureSchema = z.enum([
|
||||||
|
"code",
|
||||||
|
"table",
|
||||||
|
"task-list",
|
||||||
|
"footnote",
|
||||||
|
"katex",
|
||||||
|
"mermaid"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const themeManifestSchema = z.object({
|
||||||
|
manifestVersion: z.literal(THEME_MANIFEST_VERSION),
|
||||||
|
id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
version: z.string().min(1).max(30),
|
||||||
|
description: z.string().max(500),
|
||||||
|
author: z.string().max(100),
|
||||||
|
license: z.string().max(100),
|
||||||
|
entry: z.string().endsWith(".css"),
|
||||||
|
print: z.string().endsWith(".css").optional(),
|
||||||
|
domPreset: z.enum(["typora", "github", "generic"]),
|
||||||
|
defaultFontSize: z.string(),
|
||||||
|
supportedFeatures: z.array(themeFeatureSchema),
|
||||||
|
bundled: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ThemeManifest = z.infer<typeof themeManifestSchema>;
|
||||||
|
export type ThemeFeature = z.infer<typeof themeFeatureSchema>;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 测试
|
||||||
|
|
||||||
|
后续测试分为:
|
||||||
|
|
||||||
|
- 导出配置与主题清单单元测试;
|
||||||
|
- Markdown 渲染快照测试;
|
||||||
|
- HTML 预览接口测试;
|
||||||
|
- PDF 生成端到端测试;
|
||||||
|
- 临时文件和路径安全测试。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@media print {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write {
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write h1,
|
||||||
|
#write h2,
|
||||||
|
#write h3,
|
||||||
|
#write h4 {
|
||||||
|
break-after: avoid-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write pre,
|
||||||
|
#write blockquote,
|
||||||
|
#write table,
|
||||||
|
#write figure,
|
||||||
|
#write img,
|
||||||
|
#write svg {
|
||||||
|
break-inside: avoid-page;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
:root {
|
||||||
|
--md-text: #2f363d;
|
||||||
|
--md-muted: #68737d;
|
||||||
|
--md-border: #d8dee4;
|
||||||
|
--md-code-background: #f6f8fa;
|
||||||
|
--md-accent: #2563a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write {
|
||||||
|
width: min(100%, 860px);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--md-text);
|
||||||
|
font-family:
|
||||||
|
"Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.75;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write h1,
|
||||||
|
#write h2,
|
||||||
|
#write h3,
|
||||||
|
#write h4 {
|
||||||
|
color: #20262c;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write h1 {
|
||||||
|
margin: 2.2em 0 1em;
|
||||||
|
padding-bottom: 0.35em;
|
||||||
|
border-bottom: 1px solid var(--md-border);
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write h2 {
|
||||||
|
margin: 2em 0 0.8em;
|
||||||
|
font-size: 1.55em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write h3 {
|
||||||
|
margin: 1.7em 0 0.7em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write p {
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write a {
|
||||||
|
color: var(--md-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write blockquote {
|
||||||
|
margin: 1.25em 0;
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border-left: 4px solid #b7c3cd;
|
||||||
|
color: var(--md-muted);
|
||||||
|
background: #f8fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write code {
|
||||||
|
padding: 0.12em 0.35em;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--md-code-background);
|
||||||
|
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write pre,
|
||||||
|
#write .md-fences {
|
||||||
|
margin: 1.25em 0;
|
||||||
|
padding: 1em 1.15em;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e4e8ec;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--md-code-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#write pre code {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write th,
|
||||||
|
#write td {
|
||||||
|
padding: 0.55em 0.75em;
|
||||||
|
border: 1px solid var(--md-border);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write th {
|
||||||
|
background: #f4f6f8;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write img,
|
||||||
|
#write svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"manifestVersion": 1,
|
||||||
|
"id": "typora-like",
|
||||||
|
"name": "Typora 风格",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "项目自有的简洁技术文档主题,不复制 Typora 官方主题代码。",
|
||||||
|
"author": "md-to-pdf contributors",
|
||||||
|
"license": "项目自有",
|
||||||
|
"entry": "theme.css",
|
||||||
|
"print": "print.css",
|
||||||
|
"domPreset": "typora",
|
||||||
|
"defaultFontSize": "16px",
|
||||||
|
"supportedFeatures": [
|
||||||
|
"code",
|
||||||
|
"table",
|
||||||
|
"task-list",
|
||||||
|
"footnote",
|
||||||
|
"katex",
|
||||||
|
"mermaid"
|
||||||
|
],
|
||||||
|
"bundled": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user