@wenyan-md/core 1.0.15 → 1.0.17
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.md +59 -66
- package/dist/publish.js +44 -22
- package/dist/types/publish.d.ts +6 -1
- package/dist/types/runtimeEnv.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,37 +13,37 @@
|
|
|
13
13
|
|
|
14
14
|
**文颜(Wenyan)** 是一款多平台 Markdown 排版与发布工具,支持将 Markdown 一键转换并发布至:
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
16
|
+
- 微信公众号
|
|
17
|
+
- 知乎
|
|
18
|
+
- 今日头条
|
|
19
|
+
- 以及其它内容平台(持续扩展中)
|
|
20
20
|
|
|
21
21
|
文颜的目标是:**让写作者与开发者专注内容,而不是排版和平台适配**。
|
|
22
22
|
|
|
23
23
|
本仓库是 **文颜的核心库(CORE)**,适合以下场景:
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
25
|
+
- 嵌入 Node.js / Web 项目
|
|
26
|
+
- 构建自定义写作或发布系统
|
|
27
|
+
- 与 CLI / 桌面端 / MCP / AI 系统集成
|
|
28
|
+
- 二次开发排版或发布能力
|
|
29
29
|
|
|
30
30
|
## 文颜的不同版本
|
|
31
31
|
|
|
32
32
|
文颜目前提供多种形态,覆盖不同使用场景:
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
- [macOS App Store 版](https://github.com/caol64/wenyan) - MAC 桌面应用
|
|
35
|
+
- [跨平台桌面版](https://github.com/caol64/wenyan-pc) - Windows / Linux
|
|
36
|
+
- [CLI 版本](https://github.com/caol64/wenyan-cli) - 命令行 / CI 自动化发布
|
|
37
|
+
- [MCP 版本](https://github.com/caol64/wenyan-mcp) - AI 自动发文
|
|
38
|
+
- 👉 **CORE 版本**(本项目)- 文颜核心能力库
|
|
39
39
|
|
|
40
40
|
## 功能特性
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
- 使用内置主题对 Markdown 内容排版
|
|
43
|
+
- 自动处理并上传图片(本地 / 网络)
|
|
44
|
+
- 支持数学公式(MathJax)
|
|
45
|
+
- 一键发布文章到微信公众号草稿箱
|
|
46
|
+
- 可在 Node / 浏览器 环境中运行
|
|
47
47
|
|
|
48
48
|
## 主题效果预览
|
|
49
49
|
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
|
|
52
52
|
文颜内置并适配了多个优秀的 Typora 主题,在此感谢原作者:
|
|
53
53
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
54
|
+
- [Orange Heart](https://github.com/evgo2017/typora-theme-orange-heart)
|
|
55
|
+
- [Rainbow](https://github.com/thezbm/typora-theme-rainbow)
|
|
56
|
+
- [Lapis](https://github.com/YiNNx/typora-theme-lapis)
|
|
57
|
+
- [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
|
|
58
|
+
- [Maize](https://github.com/BEATREE/typora-maize-theme)
|
|
59
|
+
- [Purple](https://github.com/hliu202/typora-purple-theme)
|
|
60
|
+
- [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
|
|
61
61
|
|
|
62
62
|
## 安装方式
|
|
63
63
|
|
|
@@ -81,41 +81,35 @@ const theme = "lapis";
|
|
|
81
81
|
const highlightTheme = "solarized-light";
|
|
82
82
|
const isMacStyle = true;
|
|
83
83
|
|
|
84
|
-
const { title, cover, content, description } = await getGzhContent(
|
|
85
|
-
inputContent,
|
|
86
|
-
theme,
|
|
87
|
-
highlightTheme,
|
|
88
|
-
isMacStyle,
|
|
89
|
-
isAddFootnote,
|
|
90
|
-
);
|
|
84
|
+
const { title, cover, content, description } = await getGzhContent(inputContent, theme, highlightTheme, isMacStyle, isAddFootnote);
|
|
91
85
|
```
|
|
92
86
|
|
|
93
87
|
#### 参数说明
|
|
94
88
|
|
|
95
|
-
| 参数名
|
|
96
|
-
|
|
|
97
|
-
| `inputContent`
|
|
98
|
-
| `theme`
|
|
99
|
-
| `highlightTheme` | `string`
|
|
100
|
-
| `isMacStyle`
|
|
101
|
-
| `isAddFootnote`
|
|
89
|
+
| 参数名 | 类型 | 说明 |
|
|
90
|
+
| ---------------- | --------- | --------------------------------- |
|
|
91
|
+
| `inputContent` | `string` | 输入的 Markdown 文本,必填 |
|
|
92
|
+
| `theme` | `string` | 排版主题 ID,必填 |
|
|
93
|
+
| `highlightTheme` | `string` | 代码高亮主题,必填 |
|
|
94
|
+
| `isMacStyle` | `boolean` | 是否启用代码块 Mac 风格,默认开启 |
|
|
95
|
+
| `isAddFootnote` | `boolean` | 是否将链接转脚注,默认开启 |
|
|
102
96
|
|
|
103
97
|
排版主题:
|
|
104
98
|
|
|
105
|
-
-
|
|
99
|
+
- default / orangeheart / rainbow / lapis / pie / maize / purple / phycat
|
|
106
100
|
|
|
107
101
|
高亮主题:
|
|
108
102
|
|
|
109
|
-
-
|
|
103
|
+
- atom-one-dark / atom-one-light / dracula / github-dark / github / monokai / solarized-dark / solarized-light / xcode
|
|
110
104
|
|
|
111
105
|
#### 返回值
|
|
112
106
|
|
|
113
|
-
| 字段
|
|
114
|
-
|
|
|
115
|
-
| `title`
|
|
116
|
-
| `cover`
|
|
117
|
-
| `content`
|
|
118
|
-
| `description` | `string` | frontmatter 中的文章简介
|
|
107
|
+
| 字段 | 类型 | 说明 |
|
|
108
|
+
| ------------- | -------- | ------------------------------- |
|
|
109
|
+
| `title` | `string` | 从 frontmatter 中获取的文章标题 |
|
|
110
|
+
| `cover` | `string` | 封面图 |
|
|
111
|
+
| `content` | `string` | 转换后的 HTML 内容 |
|
|
112
|
+
| `description` | `string` | frontmatter 中的文章简介 |
|
|
119
113
|
|
|
120
114
|
---
|
|
121
115
|
|
|
@@ -124,26 +118,26 @@ const { title, cover, content, description } = await getGzhContent(
|
|
|
124
118
|
```ts
|
|
125
119
|
import { publishToDraft } from "@wenyan-md/core/publish";
|
|
126
120
|
|
|
127
|
-
const data = await publishToDraft(title, content, cover, wechatAppId, wechatAppSecret);
|
|
121
|
+
const data = await publishToDraft(title, content, cover, { wechatAppId, wechatAppSecret });
|
|
128
122
|
|
|
129
123
|
if (data.media_id) {
|
|
130
|
-
|
|
124
|
+
console.log("上传成功:", data.media_id);
|
|
131
125
|
}
|
|
132
126
|
```
|
|
133
127
|
|
|
134
128
|
#### 参数说明
|
|
135
129
|
|
|
136
|
-
| 参数名
|
|
137
|
-
|
|
|
138
|
-
| `title`
|
|
139
|
-
| `content`
|
|
140
|
-
| `cover`
|
|
141
|
-
| `wechatAppId`
|
|
142
|
-
| `wechatAppSecret` | `string` | 微信公众号 APP_SECRET |
|
|
130
|
+
| 参数名 | 类型 | 说明 |
|
|
131
|
+
| ----------------- | -------- | --------------------------------------------------- |
|
|
132
|
+
| `title` | `string` | `getGzhContent`接口返回的`title` |
|
|
133
|
+
| `content` | `string` | `getGzhContent`接口返回的`content` |
|
|
134
|
+
| `cover` | `string` | `getGzhContent`接口返回的`cover` |
|
|
135
|
+
| `wechatAppId` | `string` | 微信公众号 APPID(如果通过环境变量注入可省略) |
|
|
136
|
+
| `wechatAppSecret` | `string` | 微信公众号 APP_SECRET(如果通过环境变量注入可省略) |
|
|
143
137
|
|
|
144
|
-
#### 环境变量注入APPID和APP_SECRET
|
|
138
|
+
#### 环境变量注入 APPID 和 APP_SECRET
|
|
145
139
|
|
|
146
|
-
也可以通过环境变量注入APPID和APP_SECRET:
|
|
140
|
+
也可以通过环境变量注入 APPID 和 APP_SECRET:
|
|
147
141
|
|
|
148
142
|
```sh
|
|
149
143
|
export WECHAT_APP_ID=xxx
|
|
@@ -156,13 +150,13 @@ import { publishToDraft } from "@wenyan-md/core/publish";
|
|
|
156
150
|
const data = await publishToDraft(title, content, cover);
|
|
157
151
|
|
|
158
152
|
if (data.media_id) {
|
|
159
|
-
|
|
153
|
+
console.log("上传成功:", data.media_id);
|
|
160
154
|
}
|
|
161
155
|
```
|
|
162
156
|
|
|
163
157
|
## 浏览器直接使用
|
|
164
158
|
|
|
165
|
-
文颜 CORE 提供浏览器可直接引入的 IIFE
|
|
159
|
+
文颜 CORE 提供浏览器可直接引入的 IIFE 构建版本,适合前端或纯静态页面使用。目前浏览器版本仅支持“Markdown 排版美化”,不支持“发布到微信公众号草稿箱”功能。
|
|
166
160
|
|
|
167
161
|
```html
|
|
168
162
|
<script src="https://cdn.jsdelivr.net/npm/css-tree/dist/csstree.js"></script>
|
|
@@ -170,10 +164,9 @@ if (data.media_id) {
|
|
|
170
164
|
<script src="https://cdn.jsdelivr.net/npm/@wenyan-md/core/dist/browser/wenyan-core.js"></script>
|
|
171
165
|
|
|
172
166
|
<script>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
document.body.innerHTML = html;
|
|
167
|
+
const { configureMarked, renderMarkdown, themes } = WenyanCore;
|
|
168
|
+
const html = await renderMarkdown('# Hello from Browser');
|
|
169
|
+
document.body.innerHTML = html;
|
|
177
170
|
</script>
|
|
178
171
|
```
|
|
179
172
|
|
|
@@ -189,8 +182,8 @@ description: 文章简介
|
|
|
189
182
|
---
|
|
190
183
|
```
|
|
191
184
|
|
|
192
|
-
|
|
193
|
-
|
|
185
|
+
- `title`:必填
|
|
186
|
+
- `cover`:本地路径或网络图片(正文有图可省略)
|
|
194
187
|
|
|
195
188
|
## 微信公众号 IP 白名单
|
|
196
189
|
|
package/dist/publish.js
CHANGED
|
@@ -64,22 +64,44 @@ async function publishArticle(title, content, thumbMediaId, accessToken) {
|
|
|
64
64
|
return await response.json();
|
|
65
65
|
}
|
|
66
66
|
function normalizePath(p) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
68
|
+
}
|
|
69
|
+
function isAbsolutePath(path2) {
|
|
70
|
+
if (!path2) return false;
|
|
71
|
+
const winAbsPattern = /^[a-zA-Z]:\//;
|
|
72
|
+
const linuxAbsPattern = /^\//;
|
|
73
|
+
return winAbsPattern.test(path2) || linuxAbsPattern.test(path2);
|
|
73
74
|
}
|
|
74
75
|
const RuntimeEnv = {
|
|
75
76
|
isContainer: !!process.env.CONTAINERIZED,
|
|
76
77
|
hostFilePath: normalizePath(process.env.HOST_FILE_PATH || ""),
|
|
77
78
|
containerFilePath: normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads"),
|
|
78
|
-
resolveLocalPath(inputPath) {
|
|
79
|
-
if (!this.isContainer
|
|
80
|
-
|
|
79
|
+
resolveLocalPath(inputPath, relativeBase) {
|
|
80
|
+
if (!this.isContainer) {
|
|
81
|
+
if (relativeBase) {
|
|
82
|
+
return path.resolve(relativeBase, inputPath);
|
|
83
|
+
} else {
|
|
84
|
+
if (!path.isAbsolute(inputPath)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return path.normalize(inputPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let normalizedInput = normalizePath(inputPath);
|
|
93
|
+
relativeBase = normalizePath(relativeBase || "");
|
|
94
|
+
if (relativeBase) {
|
|
95
|
+
if (!isAbsolutePath(normalizedInput)) {
|
|
96
|
+
normalizedInput = relativeBase + (normalizedInput.startsWith("/") ? "" : "/") + normalizedInput;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (!isAbsolutePath(normalizedInput)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
81
104
|
}
|
|
82
|
-
const normalizedInput = normalizePath(inputPath);
|
|
83
105
|
if (normalizedInput.startsWith(this.hostFilePath)) {
|
|
84
106
|
let relativePart = normalizedInput.slice(this.hostFilePath.length);
|
|
85
107
|
if (relativePart && !relativePart.startsWith("/")) {
|
|
@@ -93,7 +115,7 @@ const RuntimeEnv = {
|
|
|
93
115
|
return normalizedInput;
|
|
94
116
|
}
|
|
95
117
|
};
|
|
96
|
-
async function uploadImage(imageUrl, accessToken, fileName) {
|
|
118
|
+
async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
|
|
97
119
|
let fileData;
|
|
98
120
|
let finalName;
|
|
99
121
|
if (imageUrl.startsWith("http")) {
|
|
@@ -111,16 +133,15 @@ async function uploadImage(imageUrl, accessToken, fileName) {
|
|
|
111
133
|
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
112
134
|
fileData = new Blob([buffer], { type: contentType });
|
|
113
135
|
} else {
|
|
114
|
-
const resolvedPath = RuntimeEnv.resolveLocalPath(imageUrl);
|
|
115
|
-
const
|
|
116
|
-
const stats = await stat(safePath);
|
|
136
|
+
const resolvedPath = RuntimeEnv.resolveLocalPath(imageUrl, relativePath);
|
|
137
|
+
const stats = await stat(resolvedPath);
|
|
117
138
|
if (stats.size === 0) {
|
|
118
|
-
throw new Error(`本地图片大小为0,无法上传: ${
|
|
139
|
+
throw new Error(`本地图片大小为0,无法上传: ${resolvedPath}`);
|
|
119
140
|
}
|
|
120
141
|
const fileNameFromLocal = path$1.basename(resolvedPath);
|
|
121
142
|
const ext = path$1.extname(fileNameFromLocal);
|
|
122
143
|
finalName = fileName ?? (ext === "" ? `${fileNameFromLocal}.jpg` : fileNameFromLocal);
|
|
123
|
-
fileData = await fileFromPath(
|
|
144
|
+
fileData = await fileFromPath(resolvedPath);
|
|
124
145
|
}
|
|
125
146
|
const data = await uploadMaterial("image", fileData, finalName, accessToken);
|
|
126
147
|
if (data.errcode) {
|
|
@@ -128,7 +149,7 @@ async function uploadImage(imageUrl, accessToken, fileName) {
|
|
|
128
149
|
}
|
|
129
150
|
return data;
|
|
130
151
|
}
|
|
131
|
-
async function uploadImages(content, accessToken) {
|
|
152
|
+
async function uploadImages(content, accessToken, relativePath) {
|
|
132
153
|
if (!content.includes("<img")) {
|
|
133
154
|
return { html: content, firstImageId: "" };
|
|
134
155
|
}
|
|
@@ -139,7 +160,7 @@ async function uploadImages(content, accessToken) {
|
|
|
139
160
|
const dataSrc = element.getAttribute("src");
|
|
140
161
|
if (dataSrc) {
|
|
141
162
|
if (!dataSrc.startsWith("https://mmbiz.qpic.cn")) {
|
|
142
|
-
const resp = await uploadImage(dataSrc, accessToken);
|
|
163
|
+
const resp = await uploadImage(dataSrc, accessToken, void 0, relativePath);
|
|
143
164
|
element.setAttribute("src", resp.url);
|
|
144
165
|
return resp.media_id;
|
|
145
166
|
} else {
|
|
@@ -153,7 +174,8 @@ async function uploadImages(content, accessToken) {
|
|
|
153
174
|
const updatedHtml = dom.serialize();
|
|
154
175
|
return { html: updatedHtml, firstImageId };
|
|
155
176
|
}
|
|
156
|
-
async function publishToDraft(title, content, cover
|
|
177
|
+
async function publishToDraft(title, content, cover = "", options = {}) {
|
|
178
|
+
const { appId, appSecret, relativePath } = options;
|
|
157
179
|
const accessToken = await fetchAccessToken(appId, appSecret);
|
|
158
180
|
if (!accessToken.access_token) {
|
|
159
181
|
if (accessToken.errcode) {
|
|
@@ -162,14 +184,14 @@ async function publishToDraft(title, content, cover, appId, appSecret) {
|
|
|
162
184
|
throw new Error(`获取 Access Token 失败: ${accessToken}`);
|
|
163
185
|
}
|
|
164
186
|
}
|
|
165
|
-
const { html, firstImageId } = await uploadImages(content, accessToken.access_token);
|
|
187
|
+
const { html, firstImageId } = await uploadImages(content, accessToken.access_token, relativePath);
|
|
166
188
|
let thumbMediaId = "";
|
|
167
189
|
if (cover) {
|
|
168
|
-
const resp = await uploadImage(cover, accessToken.access_token, "cover.jpg");
|
|
190
|
+
const resp = await uploadImage(cover, accessToken.access_token, "cover.jpg", relativePath);
|
|
169
191
|
thumbMediaId = resp.media_id;
|
|
170
192
|
} else {
|
|
171
193
|
if (firstImageId.startsWith("https://mmbiz.qpic.cn")) {
|
|
172
|
-
const resp = await uploadImage(firstImageId, accessToken.access_token, "cover.jpg");
|
|
194
|
+
const resp = await uploadImage(firstImageId, accessToken.access_token, "cover.jpg", relativePath);
|
|
173
195
|
thumbMediaId = resp.media_id;
|
|
174
196
|
} else {
|
|
175
197
|
thumbMediaId = firstImageId;
|
package/dist/types/publish.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface PublishOptions {
|
|
2
|
+
appId?: string;
|
|
3
|
+
appSecret?: string;
|
|
4
|
+
relativePath?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function publishToDraft(title: string, content: string, cover?: string, options?: PublishOptions): Promise<any>;
|
package/package.json
CHANGED