multi-publisher 1.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/LICENSE +21 -0
- package/README.md +370 -0
- package/dist/adapters/baijiahao.d.ts +14 -0
- package/dist/adapters/baijiahao.js +77 -0
- package/dist/adapters/base-adapter.d.ts +41 -0
- package/dist/adapters/base-adapter.js +84 -0
- package/dist/adapters/bilibili.d.ts +14 -0
- package/dist/adapters/bilibili.js +86 -0
- package/dist/adapters/cnblogs.d.ts +14 -0
- package/dist/adapters/cnblogs.js +90 -0
- package/dist/adapters/csdn.d.ts +29 -0
- package/dist/adapters/csdn.js +174 -0
- package/dist/adapters/cto51.d.ts +14 -0
- package/dist/adapters/cto51.js +72 -0
- package/dist/adapters/douban.d.ts +14 -0
- package/dist/adapters/douban.js +49 -0
- package/dist/adapters/eastmoney.d.ts +14 -0
- package/dist/adapters/eastmoney.js +47 -0
- package/dist/adapters/imooc.d.ts +14 -0
- package/dist/adapters/imooc.js +78 -0
- package/dist/adapters/index.d.ts +26 -0
- package/dist/adapters/index.js +22 -0
- package/dist/adapters/interface.d.ts +44 -0
- package/dist/adapters/interface.js +1 -0
- package/dist/adapters/juejin.d.ts +23 -0
- package/dist/adapters/juejin.js +166 -0
- package/dist/adapters/oschina.d.ts +14 -0
- package/dist/adapters/oschina.js +77 -0
- package/dist/adapters/registry.d.ts +61 -0
- package/dist/adapters/registry.js +146 -0
- package/dist/adapters/segmentfault.d.ts +14 -0
- package/dist/adapters/segmentfault.js +73 -0
- package/dist/adapters/sohu.d.ts +14 -0
- package/dist/adapters/sohu.js +72 -0
- package/dist/adapters/toutiao.d.ts +9 -0
- package/dist/adapters/toutiao.js +199 -0
- package/dist/adapters/wechat-publisher.d.ts +55 -0
- package/dist/adapters/wechat-publisher.js +210 -0
- package/dist/adapters/weibo.d.ts +14 -0
- package/dist/adapters/weibo.js +99 -0
- package/dist/adapters/weixin.d.ts +14 -0
- package/dist/adapters/weixin.js +63 -0
- package/dist/adapters/woshipm.d.ts +14 -0
- package/dist/adapters/woshipm.js +72 -0
- package/dist/adapters/xiaohongshu.d.ts +9 -0
- package/dist/adapters/xiaohongshu.js +199 -0
- package/dist/adapters/xueqiu.d.ts +14 -0
- package/dist/adapters/xueqiu.js +74 -0
- package/dist/adapters/yuque.d.ts +13 -0
- package/dist/adapters/yuque.js +25 -0
- package/dist/adapters/zhihu.d.ts +23 -0
- package/dist/adapters/zhihu.js +142 -0
- package/dist/cli/cookie.d.ts +5 -0
- package/dist/cli/cookie.js +80 -0
- package/dist/cli/credential.d.ts +6 -0
- package/dist/cli/credential.js +32 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +72 -0
- package/dist/cli/login.d.ts +3 -0
- package/dist/cli/login.js +101 -0
- package/dist/cli/platforms.d.ts +1 -0
- package/dist/cli/platforms.js +35 -0
- package/dist/cli/publish-all.d.ts +6 -0
- package/dist/cli/publish-all.js +101 -0
- package/dist/cli/publish.d.ts +7 -0
- package/dist/cli/publish.js +78 -0
- package/dist/cli/render.d.ts +9 -0
- package/dist/cli/render.js +40 -0
- package/dist/commands/login.d.ts +43 -0
- package/dist/commands/login.js +248 -0
- package/dist/commands/publish.d.ts +19 -0
- package/dist/commands/publish.js +275 -0
- package/dist/config.d.ts +177 -0
- package/dist/config.js +333 -0
- package/dist/core/mathjax.d.ts +4 -0
- package/dist/core/mathjax.js +33 -0
- package/dist/core/parser.d.ts +17 -0
- package/dist/core/parser.js +42 -0
- package/dist/core/renderer.d.ts +18 -0
- package/dist/core/renderer.js +30 -0
- package/dist/core/styler.d.ts +8 -0
- package/dist/core/styler.js +11 -0
- package/dist/core/theme.d.ts +9 -0
- package/dist/core/theme.js +420 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/runtime/browser-runtime.d.ts +79 -0
- package/dist/runtime/browser-runtime.js +501 -0
- package/dist/runtime/index.d.ts +12 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/node-runtime.d.ts +5 -0
- package/dist/runtime/node-runtime.js +25 -0
- package/dist/tools/xiaohongshu-sniffer.d.ts +1 -0
- package/dist/tools/xiaohongshu-sniffer.js +54 -0
- package/package.json +62 -0
- package/themes/all-themes-preview.html +522 -0
- package/themes/previews/cyberpunk.png +0 -0
- package/themes/previews/minimal.png +0 -0
- package/themes/previews/nord.png +0 -0
- package/themes/previews/wechat.png +0 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一配置管理 - 所有平台配置存在单一 config.json
|
|
3
|
+
*
|
|
4
|
+
* 文件路径: ~/.config/multi-publisher/config.json
|
|
5
|
+
*
|
|
6
|
+
* 迁移: 首次启动时自动从旧文件(credential.json/token.json/cookies/)合并数据
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
function getConfigDir() {
|
|
12
|
+
// 优先使用用户目录下的 .config 路径,Windows 上为 C:\Users\<user>\.config
|
|
13
|
+
return path.join(os.homedir(), '.config', 'multi-publisher');
|
|
14
|
+
}
|
|
15
|
+
function getConfigPath() {
|
|
16
|
+
return path.join(getConfigDir(), 'config.json');
|
|
17
|
+
}
|
|
18
|
+
let configCache = null;
|
|
19
|
+
async function ensureDir() {
|
|
20
|
+
try {
|
|
21
|
+
await fs.mkdir(getConfigDir(), { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
/** 读取配置(带缓存) */
|
|
26
|
+
export async function loadConfig() {
|
|
27
|
+
if (configCache)
|
|
28
|
+
return configCache;
|
|
29
|
+
const filePath = getConfigPath();
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
32
|
+
configCache = JSON.parse(content);
|
|
33
|
+
return configCache;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
configCache = { version: 1 };
|
|
37
|
+
return configCache;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** 写配置(立即刷盘,清缓存) */
|
|
41
|
+
async function saveConfig(config) {
|
|
42
|
+
await ensureDir();
|
|
43
|
+
configCache = config;
|
|
44
|
+
const filePath = getConfigPath();
|
|
45
|
+
const tmp = filePath + '.tmp';
|
|
46
|
+
await fs.writeFile(tmp, JSON.stringify(config, null, 2), 'utf-8');
|
|
47
|
+
await fs.rename(tmp, filePath);
|
|
48
|
+
}
|
|
49
|
+
/** 原子更新配置(读取-修改-写入) */
|
|
50
|
+
export async function updateConfig(updater) {
|
|
51
|
+
const config = await loadConfig();
|
|
52
|
+
updater(config);
|
|
53
|
+
await saveConfig(config);
|
|
54
|
+
}
|
|
55
|
+
/** 迁移旧版分散文件到 config.json */
|
|
56
|
+
export async function migrateLegacyConfig() {
|
|
57
|
+
const dir = getConfigDir();
|
|
58
|
+
const newConfigPath = getConfigPath();
|
|
59
|
+
// 检查是否已有新配置文件
|
|
60
|
+
try {
|
|
61
|
+
await fs.access(newConfigPath);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
const oldFiles = [
|
|
66
|
+
{ path: path.join(dir, 'credential.json'), key: 'weixin', parser: (raw) => {
|
|
67
|
+
const r = raw;
|
|
68
|
+
return { appId: r.appId || r.app_id, appSecret: r.appSecret || r.app_secret };
|
|
69
|
+
} },
|
|
70
|
+
{ path: path.join(dir, 'token.json'), key: 'weixin', parser: (raw) => raw },
|
|
71
|
+
];
|
|
72
|
+
let hasLegacy = false;
|
|
73
|
+
const merged = { version: 1 };
|
|
74
|
+
for (const { path: filePath, key, parser } of oldFiles) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
77
|
+
const parsed = parser(raw);
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
const m = merged;
|
|
80
|
+
const existing = m[key] || {};
|
|
81
|
+
m[key] = Object.assign({}, existing, parsed);
|
|
82
|
+
hasLegacy = true;
|
|
83
|
+
}
|
|
84
|
+
catch { /* no legacy file, skip */ }
|
|
85
|
+
}
|
|
86
|
+
// 迁移 cookies/*.json
|
|
87
|
+
const cookiesDir = path.join(dir, 'cookies');
|
|
88
|
+
try {
|
|
89
|
+
const files = await fs.readdir(cookiesDir);
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
if (!file.endsWith('.json'))
|
|
92
|
+
continue;
|
|
93
|
+
const platform = file.replace('.json', '');
|
|
94
|
+
const raw = JSON.parse(await fs.readFile(path.join(cookiesDir, file), 'utf-8'));
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
const m = merged;
|
|
97
|
+
m[platform] = { cookies: raw.cookies || raw };
|
|
98
|
+
hasLegacy = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch { /* no cookies dir, skip */ }
|
|
102
|
+
if (!hasLegacy)
|
|
103
|
+
return false;
|
|
104
|
+
await saveConfig(merged);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/** 统一配置管理器(各平台 publisher 内部使用) */
|
|
108
|
+
export class ConfigStore {
|
|
109
|
+
/** 获取微信配置 */
|
|
110
|
+
static async getWeixin() {
|
|
111
|
+
const config = await loadConfig();
|
|
112
|
+
return config.weixin || {};
|
|
113
|
+
}
|
|
114
|
+
/** 更新微信配置 */
|
|
115
|
+
static async setWeixin(update) {
|
|
116
|
+
await updateConfig((c) => {
|
|
117
|
+
c.weixin = { ...c.weixin, ...update };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** 获取知乎 Cookie */
|
|
121
|
+
static async getZhihuCookies() {
|
|
122
|
+
const config = await loadConfig();
|
|
123
|
+
return config.zhihu?.cookies || null;
|
|
124
|
+
}
|
|
125
|
+
/** 设置知乎 Cookie */
|
|
126
|
+
static async setZhihuCookies(cookies) {
|
|
127
|
+
await updateConfig((c) => {
|
|
128
|
+
c.zhihu = { ...c.zhihu, cookies };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** 获取掘金 Cookie */
|
|
132
|
+
static async getJuejinCookies() {
|
|
133
|
+
const config = await loadConfig();
|
|
134
|
+
return config.juejin?.cookies || null;
|
|
135
|
+
}
|
|
136
|
+
/** 设置掘金 Cookie */
|
|
137
|
+
static async setJuejinCookies(cookies) {
|
|
138
|
+
await updateConfig((c) => {
|
|
139
|
+
c.juejin = { ...c.juejin, cookies };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** 获取 CSDN Cookie */
|
|
143
|
+
static async getCSDNCookies() {
|
|
144
|
+
const config = await loadConfig();
|
|
145
|
+
return config.csdn?.cookies || null;
|
|
146
|
+
}
|
|
147
|
+
/** 设置 CSDN Cookie */
|
|
148
|
+
static async setCSDNCookies(cookies) {
|
|
149
|
+
await updateConfig((c) => {
|
|
150
|
+
c.csdn = { ...c.csdn, cookies };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** 获取头条号 Cookie */
|
|
154
|
+
static async getToutiaoCookies() {
|
|
155
|
+
const config = await loadConfig();
|
|
156
|
+
return config.toutiao?.cookies || null;
|
|
157
|
+
}
|
|
158
|
+
/** 设置头条号 Cookie */
|
|
159
|
+
static async setToutiaoCookies(cookies) {
|
|
160
|
+
await updateConfig((c) => {
|
|
161
|
+
c.toutiao = { ...c.toutiao, cookies };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** 获取简书 Cookie */
|
|
165
|
+
static async getJianshuCookies() {
|
|
166
|
+
const config = await loadConfig();
|
|
167
|
+
return config.jianshu?.cookies || null;
|
|
168
|
+
}
|
|
169
|
+
/** 设置简书 Cookie */
|
|
170
|
+
static async setJianshuCookies(cookies) {
|
|
171
|
+
await updateConfig((c) => {
|
|
172
|
+
c.jianshu = { ...c.jianshu, cookies };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** 获取微博 Cookie */
|
|
176
|
+
static async getWeiboCookies() {
|
|
177
|
+
const config = await loadConfig();
|
|
178
|
+
return config.weibo?.cookies || null;
|
|
179
|
+
}
|
|
180
|
+
/** 设置微博 Cookie */
|
|
181
|
+
static async setWeiboCookies(cookies) {
|
|
182
|
+
await updateConfig((c) => {
|
|
183
|
+
c.weibo = { ...c.weibo, cookies };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** 获取小红书 Cookie */
|
|
187
|
+
static async getXiaohongshuCookies() {
|
|
188
|
+
const config = await loadConfig();
|
|
189
|
+
return config.xiaohongshu?.cookies || null;
|
|
190
|
+
}
|
|
191
|
+
/** 设置小红书 Cookie */
|
|
192
|
+
static async setXiaohongshuCookies(cookies) {
|
|
193
|
+
await updateConfig((c) => {
|
|
194
|
+
c.xiaohongshu = { ...c.xiaohongshu, cookies };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** 获取百家号 Cookie */
|
|
198
|
+
static async getBaijiahaoCookies() {
|
|
199
|
+
const config = await loadConfig();
|
|
200
|
+
return config.baijiahao?.cookies || null;
|
|
201
|
+
}
|
|
202
|
+
/** 设置百家号 Cookie */
|
|
203
|
+
static async setBaijiahaoCookies(cookies) {
|
|
204
|
+
await updateConfig((c) => {
|
|
205
|
+
c.baijiahao = { ...c.baijiahao, cookies };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** 获取B站 Cookie */
|
|
209
|
+
static async getBilibiliCookies() {
|
|
210
|
+
const config = await loadConfig();
|
|
211
|
+
return config.bilibili?.cookies || null;
|
|
212
|
+
}
|
|
213
|
+
/** 设置B站 Cookie */
|
|
214
|
+
static async setBilibiliCookies(cookies) {
|
|
215
|
+
await updateConfig((c) => {
|
|
216
|
+
c.bilibili = { ...c.bilibili, cookies };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** 获取思否 Cookie */
|
|
220
|
+
static async getSegmentfaultCookies() {
|
|
221
|
+
const config = await loadConfig();
|
|
222
|
+
return config.segmentfault?.cookies || null;
|
|
223
|
+
}
|
|
224
|
+
/** 设置思否 Cookie */
|
|
225
|
+
static async setSegmentfaultCookies(cookies) {
|
|
226
|
+
await updateConfig((c) => {
|
|
227
|
+
c.segmentfault = { ...c.segmentfault, cookies };
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** 获取博客园 Cookie */
|
|
231
|
+
static async getCnblogsCookies() {
|
|
232
|
+
const config = await loadConfig();
|
|
233
|
+
return config.cnblogs?.cookies || null;
|
|
234
|
+
}
|
|
235
|
+
/** 设置博客园 Cookie */
|
|
236
|
+
static async setCnblogsCookies(cookies) {
|
|
237
|
+
await updateConfig((c) => {
|
|
238
|
+
c.cnblogs = { ...c.cnblogs, cookies };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** 获取开源中国 Cookie */
|
|
242
|
+
static async getOschinaCookies() {
|
|
243
|
+
const config = await loadConfig();
|
|
244
|
+
return config.oschina?.cookies || null;
|
|
245
|
+
}
|
|
246
|
+
/** 设置开源中国 Cookie */
|
|
247
|
+
static async setOschinaCookies(cookies) {
|
|
248
|
+
await updateConfig((c) => {
|
|
249
|
+
c.oschina = { ...c.oschina, cookies };
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/** 获取慕课网 Cookie */
|
|
253
|
+
static async getImoocCookies() {
|
|
254
|
+
const config = await loadConfig();
|
|
255
|
+
return config.imooc?.cookies || null;
|
|
256
|
+
}
|
|
257
|
+
/** 设置慕课网 Cookie */
|
|
258
|
+
static async setImoocCookies(cookies) {
|
|
259
|
+
await updateConfig((c) => {
|
|
260
|
+
c.imooc = { ...c.imooc, cookies };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** 获取 51CTO Cookie */
|
|
264
|
+
static async getCto51Cookies() {
|
|
265
|
+
const config = await loadConfig();
|
|
266
|
+
return config.cto51?.cookies || null;
|
|
267
|
+
}
|
|
268
|
+
/** 设置 51CTO Cookie */
|
|
269
|
+
static async setCto51Cookies(cookies) {
|
|
270
|
+
await updateConfig((c) => {
|
|
271
|
+
c.cto51 = { ...c.cto51, cookies };
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** 获取雪球 Cookie */
|
|
275
|
+
static async getYueqiuCookies() {
|
|
276
|
+
const config = await loadConfig();
|
|
277
|
+
return config.yueqiu?.cookies || null;
|
|
278
|
+
}
|
|
279
|
+
/** 设置雪球 Cookie */
|
|
280
|
+
static async setYueqiuCookies(cookies) {
|
|
281
|
+
await updateConfig((c) => {
|
|
282
|
+
c.yueqiu = { ...c.yueqiu, cookies };
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/** 获取人人都是产品经理 Cookie */
|
|
286
|
+
static async getWoshipmCookies() {
|
|
287
|
+
const config = await loadConfig();
|
|
288
|
+
return config.woshipm?.cookies || null;
|
|
289
|
+
}
|
|
290
|
+
/** 设置人人都是产品经理 Cookie */
|
|
291
|
+
static async setWoshipmCookies(cookies) {
|
|
292
|
+
await updateConfig((c) => {
|
|
293
|
+
c.woshipm = { ...c.woshipm, cookies };
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/** 获取豆瓣 Cookie */
|
|
297
|
+
static async getDoubanCookies() {
|
|
298
|
+
const config = await loadConfig();
|
|
299
|
+
return config.douban?.cookies || null;
|
|
300
|
+
}
|
|
301
|
+
/** 设置豆瓣 Cookie */
|
|
302
|
+
static async setDoubanCookies(cookies) {
|
|
303
|
+
await updateConfig((c) => {
|
|
304
|
+
c.douban = { ...c.douban, cookies };
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** 获取搜狐 Cookie */
|
|
308
|
+
static async getSohuCookies() {
|
|
309
|
+
const config = await loadConfig();
|
|
310
|
+
return config.sohu?.cookies || null;
|
|
311
|
+
}
|
|
312
|
+
/** 设置搜狐 Cookie */
|
|
313
|
+
static async setSohuCookies(cookies) {
|
|
314
|
+
await updateConfig((c) => {
|
|
315
|
+
c.sohu = { ...c.sohu, cookies };
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/** 获取东方财富 Cookie */
|
|
319
|
+
static async getEastmoneyCookies() {
|
|
320
|
+
const config = await loadConfig();
|
|
321
|
+
return config.eastmoney?.cookies || null;
|
|
322
|
+
}
|
|
323
|
+
/** 设置东方财富 Cookie */
|
|
324
|
+
static async setEastmoneyCookies(cookies) {
|
|
325
|
+
await updateConfig((c) => {
|
|
326
|
+
c.eastmoney = { ...c.eastmoney, cookies };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/** 获取配置目录路径 */
|
|
330
|
+
static getDir() {
|
|
331
|
+
return getConfigDir();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaTeX 公式处理
|
|
3
|
+
* 使用 codecogs 外部服务将 LaTeX 转为图片
|
|
4
|
+
*/
|
|
5
|
+
function isLatexFormula(text) {
|
|
6
|
+
if (/[\\^_{}]/.test(text))
|
|
7
|
+
return true;
|
|
8
|
+
if (/[α-ωΑ-Ω]/.test(text))
|
|
9
|
+
return true;
|
|
10
|
+
if (/[∑∏∫∂∇∞≠≤≥±×÷√]/.test(text))
|
|
11
|
+
return true;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 将 HTML 中的 LaTeX 公式替换为图片
|
|
16
|
+
*/
|
|
17
|
+
export function processMath(htmlContent) {
|
|
18
|
+
// Process $$...$$ (display math)
|
|
19
|
+
let result = htmlContent.replace(/\$\$([^$]+)\$\$/g, (match, latex) => {
|
|
20
|
+
if (!isLatexFormula(latex))
|
|
21
|
+
return match;
|
|
22
|
+
const encoded = encodeURIComponent(latex.trim());
|
|
23
|
+
return `<p style="text-align:center"><img src="https://latex.codecogs.com/png.latex?\\dpi{150}%20${encoded}" alt="formula" style="vertical-align:middle;max-width:100%"></p>`;
|
|
24
|
+
});
|
|
25
|
+
// Process $...$ (inline math)
|
|
26
|
+
result = result.replace(/\$([^$\n]+)\$/g, (match, latex) => {
|
|
27
|
+
if (!isLatexFormula(latex))
|
|
28
|
+
return match;
|
|
29
|
+
const encoded = encodeURIComponent(latex.trim());
|
|
30
|
+
return `<img src="https://latex.codecogs.com/png.latex?\\dpi{120}%20${encoded}" alt="formula" style="vertical-align:middle">`;
|
|
31
|
+
});
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ArticleMeta {
|
|
2
|
+
title: string;
|
|
3
|
+
author?: string;
|
|
4
|
+
cover?: string;
|
|
5
|
+
source_url?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ParsedArticle {
|
|
10
|
+
meta: ArticleMeta;
|
|
11
|
+
body: string;
|
|
12
|
+
html: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 解析 Markdown 内容,返回 front-matter 元数据 + HTML
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseMarkdown(content: string): ParsedArticle;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 解析 + front-matter 提取
|
|
3
|
+
*/
|
|
4
|
+
import fm from 'front-matter';
|
|
5
|
+
import { marked } from 'marked';
|
|
6
|
+
import { markedHighlight } from 'marked-highlight';
|
|
7
|
+
import hljs from 'highlight.js';
|
|
8
|
+
/**
|
|
9
|
+
* 创建 marked 实例(集成 highlight.js 代码高亮)
|
|
10
|
+
*/
|
|
11
|
+
function createMarked() {
|
|
12
|
+
return marked.use(markedHighlight({
|
|
13
|
+
langPrefix: 'hljs language-',
|
|
14
|
+
highlight(code, lang) {
|
|
15
|
+
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
16
|
+
return hljs.highlight(code, { language }).value;
|
|
17
|
+
},
|
|
18
|
+
}), {
|
|
19
|
+
gfm: true,
|
|
20
|
+
breaks: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const markedInstance = createMarked();
|
|
24
|
+
/**
|
|
25
|
+
* 解析 Markdown 内容,返回 front-matter 元数据 + HTML
|
|
26
|
+
*/
|
|
27
|
+
export function parseMarkdown(content) {
|
|
28
|
+
const parsed = fm(content);
|
|
29
|
+
const html = markedInstance.parse(parsed.body);
|
|
30
|
+
return {
|
|
31
|
+
meta: {
|
|
32
|
+
title: parsed.attributes.title || '无标题',
|
|
33
|
+
author: parsed.attributes.author,
|
|
34
|
+
cover: parsed.attributes.cover,
|
|
35
|
+
source_url: parsed.attributes.source_url,
|
|
36
|
+
description: parsed.attributes.description,
|
|
37
|
+
tags: parsed.attributes.tags,
|
|
38
|
+
},
|
|
39
|
+
body: parsed.body,
|
|
40
|
+
html: typeof html === 'string' ? html : '',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RenderOptions {
|
|
2
|
+
theme?: string;
|
|
3
|
+
customCss?: string;
|
|
4
|
+
highlight?: string;
|
|
5
|
+
macStyle?: boolean;
|
|
6
|
+
footnote?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface RenderResult {
|
|
9
|
+
title: string;
|
|
10
|
+
html: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
cover?: string;
|
|
13
|
+
source_url?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 渲染 Markdown 文件为平台 HTML
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderMarkdown(content: string, options?: RenderOptions): Promise<RenderResult>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 渲染管道 - 将 Markdown 渲染为平台可用的 HTML
|
|
3
|
+
*/
|
|
4
|
+
import { parseMarkdown } from './parser.js';
|
|
5
|
+
import { processMath } from './mathjax.js';
|
|
6
|
+
import { inlineStyles } from './styler.js';
|
|
7
|
+
import { loadThemeCss, DEFAULT_CSS } from './theme.js';
|
|
8
|
+
/**
|
|
9
|
+
* 渲染 Markdown 文件为平台 HTML
|
|
10
|
+
*/
|
|
11
|
+
export async function renderMarkdown(content, options = {}) {
|
|
12
|
+
const { theme = 'default', customCss, macStyle = true, } = options;
|
|
13
|
+
// 1. 解析 front-matter + Markdown → HTML
|
|
14
|
+
const parsed = parseMarkdown(content);
|
|
15
|
+
// 2. 处理 LaTeX 公式($...$ 和 $$...$$)
|
|
16
|
+
let html = processMath(parsed.html);
|
|
17
|
+
// 3. 加载主题 CSS
|
|
18
|
+
const themeCss = customCss ?? (await loadThemeCss(theme)) ?? DEFAULT_CSS;
|
|
19
|
+
// 4. CSS 内联
|
|
20
|
+
html = inlineStyles(html, themeCss);
|
|
21
|
+
// 5. 包裹 section 标签(微信公众号要求)
|
|
22
|
+
html = `<section style="margin-left:6px;margin-right:6px;line-height:1.75em">${html}</section>`;
|
|
23
|
+
return {
|
|
24
|
+
title: parsed.meta.title,
|
|
25
|
+
html,
|
|
26
|
+
author: parsed.meta.author,
|
|
27
|
+
cover: parsed.meta.cover,
|
|
28
|
+
source_url: parsed.meta.source_url,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ThemeInfo {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
isBuiltin: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_CSS = "\np {\n color: rgb(51, 51, 51);\n font-size: 15px;\n line-height: 1.75em;\n margin: 0 0 1em 0;\n word-wrap: break-word;\n}\nh1, h2, h3, h4, h5, h6 {\n font-weight: bold;\n margin: 1em 0 0.5em 0;\n}\nh1 { font-size: 1.25em; line-height: 1.4em; }\nh2 { font-size: 1.125em; }\nh3 { font-size: 1.05em; }\nh4, h5, h6 { font-size: 1em; }\nli p { margin: 0; }\nul, ol { margin: 1em 0; padding-left: 2em; }\nli { margin-bottom: 0.4em; }\npre {\n background-color: #f6f8fa;\n border-radius: 6px;\n padding: 16px;\n overflow-x: auto;\n font-size: 14px;\n line-height: 1.6;\n margin: 1em 0;\n}\ncode {\n background-color: rgba(175, 184, 193, 0.2);\n border-radius: 3px;\n padding: 0.2em 0.4em;\n font-size: 0.9em;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n}\npre code { background: none; padding: 0; font-size: 14px; }\nblockquote { border-left: 4px solid #ddd; padding-left: 1em; margin: 1em 0; color: #666; }\nhr { border: none; border-top: 1px solid #ddd; margin: 1.5em 0; }\ni, cite, em, var, address { font-style: italic; }\nb, strong { font-weight: bolder; }\nimg { max-width: 100%; height: auto; display: block; margin: 1em auto; }\ntable { border-collapse: collapse; width: 100%; margin: 1em 0; }\ntable th, table td { border: 1px solid #ddd; padding: 8px 12px; }\ntable th { background-color: #f6f8fa; font-weight: bold; }\na { color: #0579b7; text-decoration: none; }\n";
|
|
8
|
+
export declare function listThemes(): Promise<ThemeInfo[]>;
|
|
9
|
+
export declare function loadThemeCss(themeId: string): Promise<string | null>;
|