@wenyan-md/cli 1.0.10 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.docker.md +31 -0
- package/README.md +70 -159
- package/dist/cli.js +129 -1
- package/dist/commands/client.js +208 -0
- package/dist/commands/publish.js +22 -18
- package/dist/commands/render.js +6 -46
- package/dist/commands/serve.js +220 -0
- package/dist/commands/theme.js +25 -36
- package/dist/index.js +6 -43
- package/dist/types/commands/client.d.ts +2 -0
- package/dist/types/commands/publish.d.ts +1 -1
- package/dist/types/commands/render.d.ts +6 -5
- package/dist/types/commands/serve.d.ts +6 -0
- package/dist/types/commands/theme.d.ts +16 -2
- package/dist/types/index.d.ts +6 -2
- package/dist/types/types.d.ts +8 -6
- package/dist/types/utils.d.ts +5 -0
- package/dist/types.js +8 -1
- package/dist/utils.js +23 -2
- package/package.json +17 -4
package/dist/commands/publish.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { publishToWechatDraft } from "@wenyan-md/core/publish";
|
|
2
|
+
import { AppError } from "../types.js";
|
|
3
|
+
import { prepareRenderContext } from "./render.js";
|
|
3
4
|
export async function publishCommand(inputContent, options) {
|
|
4
|
-
await
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.error(`上传失败,\n${data}`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
5
|
+
const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options);
|
|
6
|
+
if (!gzhContent.title)
|
|
7
|
+
throw new AppError("未能找到文章标题");
|
|
8
|
+
if (!gzhContent.cover)
|
|
9
|
+
throw new AppError("未能找到文章封面");
|
|
10
|
+
const data = await publishToWechatDraft({
|
|
11
|
+
title: gzhContent.title,
|
|
12
|
+
content: gzhContent.content,
|
|
13
|
+
cover: gzhContent.cover,
|
|
14
|
+
author: gzhContent.author,
|
|
15
|
+
source_url: gzhContent.source_url,
|
|
16
|
+
}, {
|
|
17
|
+
relativePath: absoluteDirPath,
|
|
20
18
|
});
|
|
19
|
+
if (data.media_id) {
|
|
20
|
+
return data.media_id;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
throw new AppError(`上传失败,\n${data}`);
|
|
24
|
+
}
|
|
21
25
|
}
|
package/dist/commands/render.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
import { configStore, renderStyledContent } from "@wenyan-md/core/wrapper";
|
|
2
|
-
import {
|
|
2
|
+
import { getInputContent, getNormalizeFilePath } from "../utils.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import
|
|
4
|
+
import { AppError } from "../types.js";
|
|
5
5
|
// --- 处理输入源、文件路径和主题 ---
|
|
6
6
|
export async function prepareRenderContext(inputContent, options) {
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
// 1. 尝试从 Stdin 读取
|
|
10
|
-
if (!inputContent && !process.stdin.isTTY) {
|
|
11
|
-
inputContent = await readStdin();
|
|
12
|
-
}
|
|
13
|
-
// 2. 尝试从文件读取
|
|
14
|
-
if (!inputContent && file) {
|
|
15
|
-
const normalizePath = getNormalizeFilePath(file);
|
|
16
|
-
inputContent = await fs.readFile(normalizePath, "utf-8");
|
|
17
|
-
absoluteDirPath = path.dirname(normalizePath);
|
|
18
|
-
}
|
|
19
|
-
// 3. 校验输入
|
|
20
|
-
if (!inputContent) {
|
|
21
|
-
throw new Error("Error: missing input-content (no argument, no stdin, and no file).");
|
|
22
|
-
}
|
|
7
|
+
const { content, absoluteDirPath } = await getInputContent(inputContent, options);
|
|
8
|
+
const { theme, customTheme, highlight, macStyle, footnote } = options;
|
|
23
9
|
let handledCustomTheme = customTheme;
|
|
24
10
|
// 4. 当用户传入自定义主题路径时,优先级最高
|
|
25
11
|
if (customTheme) {
|
|
@@ -31,10 +17,10 @@ export async function prepareRenderContext(inputContent, options) {
|
|
|
31
17
|
handledCustomTheme = configStore.getThemeById(theme);
|
|
32
18
|
}
|
|
33
19
|
if (!handledCustomTheme && !theme) {
|
|
34
|
-
throw new
|
|
20
|
+
throw new AppError(`theme "${theme}" not found.`);
|
|
35
21
|
}
|
|
36
22
|
// 5. 执行核心渲染
|
|
37
|
-
const gzhContent = await renderStyledContent(
|
|
23
|
+
const gzhContent = await renderStyledContent(content, {
|
|
38
24
|
themeId: theme,
|
|
39
25
|
hlThemeId: highlight,
|
|
40
26
|
isMacStyle: macStyle,
|
|
@@ -43,29 +29,3 @@ export async function prepareRenderContext(inputContent, options) {
|
|
|
43
29
|
});
|
|
44
30
|
return { gzhContent, absoluteDirPath };
|
|
45
31
|
}
|
|
46
|
-
// --- 统一的错误处理包装器 ---
|
|
47
|
-
export async function runCommandWrapper(action) {
|
|
48
|
-
try {
|
|
49
|
-
await action();
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
if (error instanceof Error) {
|
|
53
|
-
if (error.message.startsWith("Error:")) {
|
|
54
|
-
console.error(error.message);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.error("An unexpected error occurred:", error.message);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
console.error("An unexpected error occurred:", error);
|
|
62
|
-
}
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
export async function renderCommand(inputContent, options) {
|
|
67
|
-
await runCommandWrapper(async () => {
|
|
68
|
-
const { gzhContent } = await prepareRenderContext(inputContent, options);
|
|
69
|
-
console.log(gzhContent.content);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { AppError } from "../types.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import fsPromises from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import { configDir } from "@wenyan-md/core/wrapper";
|
|
8
|
+
import multer from "multer";
|
|
9
|
+
import { getNormalizeFilePath } from "../utils.js";
|
|
10
|
+
import { publishToWechatDraft } from "@wenyan-md/core/publish";
|
|
11
|
+
const UPLOAD_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
12
|
+
const UPLOAD_DIR = path.join(configDir, "uploads");
|
|
13
|
+
export async function serveCommand(options) {
|
|
14
|
+
// 确保临时目录存在
|
|
15
|
+
if (!fs.existsSync(UPLOAD_DIR)) {
|
|
16
|
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
// 服务启动时立即执行一次后台清理
|
|
19
|
+
cleanupOldUploads();
|
|
20
|
+
// 定期清理过期的上传文件
|
|
21
|
+
setInterval(cleanupOldUploads, UPLOAD_TTL_MS).unref();
|
|
22
|
+
const app = express();
|
|
23
|
+
const port = options.port || 3000;
|
|
24
|
+
const auth = createAuthHandler(options);
|
|
25
|
+
app.use(express.json({ limit: "10mb" }));
|
|
26
|
+
const storage = multer.diskStorage({
|
|
27
|
+
destination: (req, file, cb) => {
|
|
28
|
+
cb(null, UPLOAD_DIR);
|
|
29
|
+
},
|
|
30
|
+
filename: (req, file, cb) => {
|
|
31
|
+
const fileId = crypto.randomUUID();
|
|
32
|
+
const ext = file.originalname.split(".").pop() || "";
|
|
33
|
+
cb(null, ext ? `${fileId}.${ext}` : fileId);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const upload = multer({
|
|
37
|
+
storage,
|
|
38
|
+
limits: {
|
|
39
|
+
fileSize: 10 * 1024 * 1024, // 10MB
|
|
40
|
+
},
|
|
41
|
+
fileFilter: (req, file, cb) => {
|
|
42
|
+
const ext = file.originalname.split(".").pop()?.toLowerCase();
|
|
43
|
+
// 1. 定义允许的图片类型
|
|
44
|
+
const allowedImageTypes = ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"];
|
|
45
|
+
const allowedImageExts = ["jpg", "jpeg", "png", "gif", "webp", "svg"];
|
|
46
|
+
// 2. 分别判断文件大类
|
|
47
|
+
const isImage = allowedImageTypes.includes(file.mimetype) || (ext && allowedImageExts.includes(ext));
|
|
48
|
+
const isMarkdown = ext === "md" || file.mimetype === "text/markdown" || file.mimetype === "text/plain";
|
|
49
|
+
const isCss = ext === "css" || file.mimetype === "text/css";
|
|
50
|
+
const isJson = ext === "json" || file.mimetype === "application/json";
|
|
51
|
+
// 3. 综合放行逻辑
|
|
52
|
+
if (isImage || isMarkdown || isCss || isJson) {
|
|
53
|
+
cb(null, true);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
cb(new AppError("不支持的文件类型,仅支持图片、Markdown、CSS 和 JSON 文件"));
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
// 健康检查
|
|
61
|
+
app.get("/health", (_req, res) => {
|
|
62
|
+
res.json({ status: "ok", service: "wenyan-cli", version: options.version || "unknown" });
|
|
63
|
+
});
|
|
64
|
+
// 鉴权探针
|
|
65
|
+
app.get("/verify", auth, (_req, res) => {
|
|
66
|
+
res.json({ success: true, message: "Authorized" });
|
|
67
|
+
});
|
|
68
|
+
// 发布接口 - 读取 json 文件内容并发布
|
|
69
|
+
app.post("/publish", auth, async (req, res) => {
|
|
70
|
+
const body = req.body;
|
|
71
|
+
validateRequest(body);
|
|
72
|
+
// 根据 fileId 去找刚上传的 json 文件并读取内容
|
|
73
|
+
const files = await fsPromises.readdir(UPLOAD_DIR);
|
|
74
|
+
const matchedFile = files.find((f) => f === body.fileId);
|
|
75
|
+
if (!matchedFile) {
|
|
76
|
+
throw new AppError(`文件不存在或已过期,请重新上传 (ID: ${body.fileId})`);
|
|
77
|
+
}
|
|
78
|
+
// 简单的防呆校验,防止直接提交纯图片的 fileId 到发布接口
|
|
79
|
+
const ext = path.extname(matchedFile).toLowerCase();
|
|
80
|
+
if (ext !== ".json") {
|
|
81
|
+
throw new AppError("请提供 JSON 文件的 fileId,不能直接发布图片文件");
|
|
82
|
+
}
|
|
83
|
+
// 找到上传文件并提取文本内容
|
|
84
|
+
const filePath = path.join(UPLOAD_DIR, matchedFile);
|
|
85
|
+
const fileContent = await fsPromises.readFile(filePath, "utf-8");
|
|
86
|
+
const gzhContent = JSON.parse(fileContent);
|
|
87
|
+
// 公共的 asset:// 替换逻辑
|
|
88
|
+
const resolveAssetPath = (assetUrl) => {
|
|
89
|
+
const assetFileId = assetUrl.replace("asset://", "");
|
|
90
|
+
const matchedAsset = files.find((f) => f === assetFileId || path.parse(f).name === assetFileId);
|
|
91
|
+
return matchedAsset ? getNormalizeFilePath(path.join(UPLOAD_DIR, matchedAsset)) : assetUrl;
|
|
92
|
+
};
|
|
93
|
+
// 替换 HTML 内容里的 asset://
|
|
94
|
+
gzhContent.content = gzhContent.content.replace(/(<img\b[^>]*?\bsrc\s*=\s*["'])(asset:\/\/[^"']+)(["'])/gi, (_match, prefix, assetUrl, suffix) => prefix + resolveAssetPath(assetUrl) + suffix);
|
|
95
|
+
// 替换封面里的 asset://
|
|
96
|
+
if (gzhContent.cover && gzhContent.cover.startsWith("asset://")) {
|
|
97
|
+
gzhContent.cover = resolveAssetPath(gzhContent.cover);
|
|
98
|
+
}
|
|
99
|
+
const data = await publishToWechatDraft({
|
|
100
|
+
title: gzhContent.title,
|
|
101
|
+
content: gzhContent.content,
|
|
102
|
+
cover: gzhContent.cover,
|
|
103
|
+
author: gzhContent.author,
|
|
104
|
+
source_url: gzhContent.source_url,
|
|
105
|
+
});
|
|
106
|
+
if (data.media_id) {
|
|
107
|
+
res.json({
|
|
108
|
+
media_id: data.media_id,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw new AppError(`上传失败,\n${data}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// 上传接口
|
|
116
|
+
app.post("/upload", auth, upload.single("file"), async (req, res) => {
|
|
117
|
+
if (!req.file) {
|
|
118
|
+
throw new AppError("未找到上传的文件");
|
|
119
|
+
}
|
|
120
|
+
const newFilename = req.file.filename;
|
|
121
|
+
res.json({
|
|
122
|
+
success: true,
|
|
123
|
+
data: {
|
|
124
|
+
fileId: newFilename,
|
|
125
|
+
originalFilename: req.file.originalname,
|
|
126
|
+
mimetype: req.file.mimetype,
|
|
127
|
+
size: req.file.size,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
app.use(errorHandler);
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const server = app.listen(port, () => {
|
|
134
|
+
console.log(`文颜 Server 已启动,监听端口 ${port}`);
|
|
135
|
+
console.log(`健康检查:http://localhost:${port}/health`);
|
|
136
|
+
console.log(`鉴权探针:http://localhost:${port}/verify`);
|
|
137
|
+
console.log(`发布接口:POST http://localhost:${port}/publish`);
|
|
138
|
+
console.log(`上传接口:POST http://localhost:${port}/upload`);
|
|
139
|
+
});
|
|
140
|
+
server.on("error", (err) => {
|
|
141
|
+
if (err.code === "EADDRINUSE") {
|
|
142
|
+
console.error(`端口 ${port} 已被占用`);
|
|
143
|
+
reject(new Error(`端口 ${port} 已被占用`));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
reject(err);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
process.on("SIGINT", () => {
|
|
150
|
+
console.log("\n正在关闭服务器...");
|
|
151
|
+
server.close(() => {
|
|
152
|
+
console.log("服务器已关闭");
|
|
153
|
+
resolve();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
process.on("SIGTERM", () => {
|
|
157
|
+
server.close(() => resolve());
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function errorHandler(error, _req, res, next) {
|
|
162
|
+
if (res.headersSent) {
|
|
163
|
+
return next(error);
|
|
164
|
+
}
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
// 修复:multer 抛出的文件限制错误(如超出大小),应判断为客户端 400 错误
|
|
167
|
+
const isAppError = error instanceof AppError;
|
|
168
|
+
const isMulterError = error.name === "MulterError";
|
|
169
|
+
const statusCode = isAppError || isMulterError ? 400 : 500;
|
|
170
|
+
if (statusCode === 500) {
|
|
171
|
+
console.error("[Server Error]:", error);
|
|
172
|
+
}
|
|
173
|
+
res.status(statusCode).json({
|
|
174
|
+
code: -1,
|
|
175
|
+
desc: message,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function createAuthHandler(config) {
|
|
179
|
+
return (req, res, next) => {
|
|
180
|
+
if (!config.apiKey) {
|
|
181
|
+
return next();
|
|
182
|
+
}
|
|
183
|
+
const clientApiKey = req.headers["x-api-key"];
|
|
184
|
+
if (clientApiKey === config.apiKey) {
|
|
185
|
+
next();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
res.status(401).json({
|
|
189
|
+
code: -1,
|
|
190
|
+
desc: "Unauthorized: Invalid API Key",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function validateRequest(req) {
|
|
196
|
+
if (!req.fileId) {
|
|
197
|
+
throw new AppError("缺少必要参数:fileId");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function cleanupOldUploads() {
|
|
201
|
+
try {
|
|
202
|
+
const files = await fsPromises.readdir(UPLOAD_DIR);
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
const filePath = path.join(UPLOAD_DIR, file);
|
|
206
|
+
try {
|
|
207
|
+
const stats = await fsPromises.stat(filePath);
|
|
208
|
+
if (now - stats.mtimeMs > UPLOAD_TTL_MS) {
|
|
209
|
+
await fsPromises.unlink(filePath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
// 忽略单个文件处理错误
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
console.error("Cleanup task error:", e);
|
|
219
|
+
}
|
|
220
|
+
}
|
package/dist/commands/theme.js
CHANGED
|
@@ -2,49 +2,42 @@ import { getAllGzhThemes } from "@wenyan-md/core";
|
|
|
2
2
|
import { getNormalizeFilePath } from "../utils.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { configStore } from "@wenyan-md/core/wrapper";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (list) {
|
|
8
|
-
listThemes();
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
if (add) {
|
|
12
|
-
await addTheme(name, path);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
if (rm) {
|
|
16
|
-
await removeTheme(rm);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function listThemes() {
|
|
5
|
+
import { AppError } from "../types.js";
|
|
6
|
+
export function listThemes() {
|
|
21
7
|
const themes = getAllGzhThemes();
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
const themeList = themes.map((theme) => {
|
|
9
|
+
return {
|
|
10
|
+
id: theme.meta.id,
|
|
11
|
+
name: theme.meta.name,
|
|
12
|
+
description: theme.meta.description,
|
|
13
|
+
isBuiltin: true,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
24
16
|
const customThemes = configStore.getThemes();
|
|
25
17
|
if (customThemes.length > 0) {
|
|
26
|
-
console.log("\n自定义主题:");
|
|
27
18
|
customThemes.forEach((theme) => {
|
|
28
|
-
|
|
19
|
+
themeList.push({
|
|
20
|
+
id: theme.id,
|
|
21
|
+
name: theme.id,
|
|
22
|
+
description: theme.description,
|
|
23
|
+
isBuiltin: false,
|
|
24
|
+
});
|
|
29
25
|
});
|
|
30
26
|
}
|
|
31
|
-
|
|
27
|
+
return themeList;
|
|
32
28
|
}
|
|
33
|
-
async function addTheme(name, path) {
|
|
29
|
+
export async function addTheme(name, path) {
|
|
34
30
|
if (!name || !path) {
|
|
35
|
-
|
|
36
|
-
return;
|
|
31
|
+
throw new AppError("添加主题时必须提供名称(name)和路径(path)");
|
|
37
32
|
}
|
|
38
33
|
if (checkThemeExists(name) || checkCustomThemeExists(name)) {
|
|
39
|
-
|
|
40
|
-
return;
|
|
34
|
+
throw new AppError(`主题 "${name}" 已存在`);
|
|
41
35
|
}
|
|
42
36
|
if (path.startsWith("http")) {
|
|
43
|
-
console.log(
|
|
37
|
+
console.log(`正在从远程获取主题: ${path} ...`);
|
|
44
38
|
const response = await fetch(path);
|
|
45
39
|
if (!response.ok) {
|
|
46
|
-
|
|
47
|
-
return;
|
|
40
|
+
throw new AppError(`无法从远程获取主题: ${response.statusText}`);
|
|
48
41
|
}
|
|
49
42
|
const content = await response.text();
|
|
50
43
|
configStore.addThemeToConfig(name, content);
|
|
@@ -54,19 +47,15 @@ async function addTheme(name, path) {
|
|
|
54
47
|
const content = await fs.readFile(normalizePath, "utf-8");
|
|
55
48
|
configStore.addThemeToConfig(name, content);
|
|
56
49
|
}
|
|
57
|
-
console.log(`✅ 主题 "${name}" 已添加\n`);
|
|
58
50
|
}
|
|
59
|
-
async function removeTheme(name) {
|
|
51
|
+
export async function removeTheme(name) {
|
|
60
52
|
if (checkThemeExists(name)) {
|
|
61
|
-
|
|
62
|
-
return;
|
|
53
|
+
throw new AppError(`默认主题 "${name}" 不能删除`);
|
|
63
54
|
}
|
|
64
55
|
if (!checkCustomThemeExists(name)) {
|
|
65
|
-
|
|
66
|
-
return;
|
|
56
|
+
throw new AppError(`自定义主题 "${name}" 不存在`);
|
|
67
57
|
}
|
|
68
58
|
configStore.deleteThemeFromConfig(name);
|
|
69
|
-
console.log(`✅ 主题 "${name}" 已删除\n`);
|
|
70
59
|
}
|
|
71
60
|
function checkThemeExists(themeId) {
|
|
72
61
|
const themes = getAllGzhThemes();
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
const program = new Command();
|
|
8
|
-
program
|
|
9
|
-
.name("wenyan")
|
|
10
|
-
.description("A CLI for WenYan Markdown Render.")
|
|
11
|
-
.version(version, "-v, --version", "output the current version")
|
|
12
|
-
.action(() => {
|
|
13
|
-
program.outputHelp();
|
|
14
|
-
});
|
|
15
|
-
const addCommonOptions = (cmd) => {
|
|
16
|
-
return cmd
|
|
17
|
-
.argument("[input-content]", "markdown content (string input)")
|
|
18
|
-
.option("-f, --file <path>", "read markdown content from local file")
|
|
19
|
-
.option("-t, --theme <theme-id>", "ID of the theme to use", "default")
|
|
20
|
-
.option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
|
|
21
|
-
.option("-c, --custom-theme <path>", "path to custom theme CSS file")
|
|
22
|
-
.option("--mac-style", "display codeblock with mac style", true)
|
|
23
|
-
.option("--no-mac-style", "disable mac style")
|
|
24
|
-
.option("--footnote", "convert link to footnote", true)
|
|
25
|
-
.option("--no-footnote", "disable footnote");
|
|
26
|
-
};
|
|
27
|
-
const pubCmd = program
|
|
28
|
-
.command("publish")
|
|
29
|
-
.description("Render a markdown file to styled HTML and publish to wechat GZH");
|
|
30
|
-
addCommonOptions(pubCmd).action(publishCommand);
|
|
31
|
-
const renderCmd = program.command("render").description("Render a markdown file to styled HTML");
|
|
32
|
-
addCommonOptions(renderCmd).action(renderCommand);
|
|
33
|
-
program
|
|
34
|
-
.command("theme")
|
|
35
|
-
.description("Manage themes")
|
|
36
|
-
.option("-l, --list", "List all available themes")
|
|
37
|
-
.option("--add", "Add a new custom theme")
|
|
38
|
-
.option("--name <name>", "Name of the new custom theme")
|
|
39
|
-
.option("--path <path>", "Path to the new custom theme CSS file")
|
|
40
|
-
.option("--rm <name>", "Name of the custom theme to remove")
|
|
41
|
-
.action(themeCommand);
|
|
42
|
-
return program;
|
|
43
|
-
}
|
|
1
|
+
export * from "./utils.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./commands/client.js";
|
|
4
|
+
export * from "./commands/publish.js";
|
|
5
|
+
export * from "./commands/render.js";
|
|
6
|
+
export * from "./commands/theme.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { RenderOptions } from "../types.js";
|
|
2
|
-
export declare function publishCommand(inputContent: string | undefined, options: RenderOptions): Promise<
|
|
2
|
+
export declare function publishCommand(inputContent: string | undefined, options: RenderOptions): Promise<string>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { StyledContent } from "@wenyan-md/core/wrapper";
|
|
1
2
|
import { RenderOptions } from "../types.js";
|
|
2
|
-
|
|
3
|
-
gzhContent:
|
|
3
|
+
interface RenderContext {
|
|
4
|
+
gzhContent: StyledContent;
|
|
4
5
|
absoluteDirPath: string | undefined;
|
|
5
|
-
}
|
|
6
|
-
export declare function
|
|
7
|
-
export
|
|
6
|
+
}
|
|
7
|
+
export declare function prepareRenderContext(inputContent: string | undefined, options: RenderOptions): Promise<RenderContext>;
|
|
8
|
+
export {};
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export interface ThemeOptions {
|
|
2
|
+
list?: boolean;
|
|
3
|
+
add?: boolean;
|
|
4
|
+
name?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
rm?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ThemeInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
isBuiltin: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function listThemes(): ThemeInfo[];
|
|
15
|
+
export declare function addTheme(name?: string, path?: string): Promise<void>;
|
|
16
|
+
export declare function removeTheme(name: string): Promise<void>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
export * from "./utils.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./commands/client.js";
|
|
4
|
+
export * from "./commands/publish.js";
|
|
5
|
+
export * from "./commands/render.js";
|
|
6
|
+
export * from "./commands/theme.js";
|
package/dist/types/types.d.ts
CHANGED
|
@@ -6,10 +6,12 @@ export interface RenderOptions {
|
|
|
6
6
|
macStyle: boolean;
|
|
7
7
|
footnote: boolean;
|
|
8
8
|
}
|
|
9
|
-
export interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
export interface PublishOptions extends RenderOptions {
|
|
10
|
+
server?: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
clientVersion?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class AppError extends Error {
|
|
15
|
+
message: string;
|
|
16
|
+
constructor(message: string);
|
|
15
17
|
}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
import { RenderOptions } from "./types.js";
|
|
1
2
|
export declare function readStdin(): Promise<string>;
|
|
2
3
|
export declare function getNormalizeFilePath(inputPath: string): string;
|
|
4
|
+
export declare function getInputContent(inputContent: string | undefined, options: RenderOptions): Promise<{
|
|
5
|
+
content: string;
|
|
6
|
+
absoluteDirPath: string | undefined;
|
|
7
|
+
}>;
|
package/dist/types.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { AppError } from "./types.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
2
4
|
export async function readStdin() {
|
|
3
5
|
return new Promise((resolve, reject) => {
|
|
4
6
|
let data = "";
|
|
@@ -18,8 +20,8 @@ function normalizePath(p) {
|
|
|
18
20
|
}
|
|
19
21
|
export function getNormalizeFilePath(inputPath) {
|
|
20
22
|
const isContainer = !!process.env.CONTAINERIZED;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const hostFilePath = normalizePath(process.env.HOST_FILE_PATH || "");
|
|
24
|
+
if (isContainer && hostFilePath) {
|
|
23
25
|
const containerFilePath = normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads");
|
|
24
26
|
let relativePart = normalizePath(inputPath);
|
|
25
27
|
if (relativePart.startsWith(hostFilePath)) {
|
|
@@ -34,3 +36,22 @@ export function getNormalizeFilePath(inputPath) {
|
|
|
34
36
|
return path.resolve(inputPath);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
39
|
+
export async function getInputContent(inputContent, options) {
|
|
40
|
+
const { file } = options;
|
|
41
|
+
let absoluteDirPath = undefined;
|
|
42
|
+
// 1. 尝试从 Stdin 读取
|
|
43
|
+
if (!inputContent && !process.stdin.isTTY) {
|
|
44
|
+
inputContent = await readStdin();
|
|
45
|
+
}
|
|
46
|
+
// 2. 尝试从文件读取
|
|
47
|
+
if (!inputContent && file) {
|
|
48
|
+
const normalizePath = getNormalizeFilePath(file);
|
|
49
|
+
inputContent = await fs.readFile(normalizePath, "utf-8");
|
|
50
|
+
absoluteDirPath = path.dirname(normalizePath);
|
|
51
|
+
}
|
|
52
|
+
// 3. 校验输入
|
|
53
|
+
if (!inputContent) {
|
|
54
|
+
throw new AppError("missing input-content (no argument, no stdin, and no file).");
|
|
55
|
+
}
|
|
56
|
+
return { content: inputContent, absoluteDirPath };
|
|
57
|
+
}
|