@lyhue1991/wxgzh 0.1.4 → 0.2.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.md
CHANGED
|
@@ -415,6 +415,7 @@ enableComment: true
|
|
|
415
415
|
|
|
416
416
|
```bash
|
|
417
417
|
wxgzh --help
|
|
418
|
+
wxgzh --version
|
|
418
419
|
wxgzh article.md
|
|
419
420
|
wxgzh config --list
|
|
420
421
|
wxgzh config --list-themes
|
|
@@ -424,4 +425,3 @@ wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg
|
|
|
424
425
|
wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg
|
|
425
426
|
```
|
|
426
427
|
|
|
427
|
-
|
|
Binary file
|
package/dist/cli/index.js
CHANGED
|
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_fs_1 = require("node:fs");
|
|
39
40
|
const promises_1 = require("node:fs/promises");
|
|
40
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
41
42
|
const commander_1 = require("commander");
|
|
@@ -65,6 +66,14 @@ const EXAMPLES_TEXT = [
|
|
|
65
66
|
' $ wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg',
|
|
66
67
|
' $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg'
|
|
67
68
|
].join('\n');
|
|
69
|
+
function getCliVersion() {
|
|
70
|
+
const packageJsonPath = node_path_1.default.resolve(__dirname, '../../package.json');
|
|
71
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf8'));
|
|
72
|
+
if (typeof packageJson.version !== 'string' || packageJson.version.trim() === '') {
|
|
73
|
+
throw new Error('读取版本号失败: package.json 中缺少有效的 version 字段');
|
|
74
|
+
}
|
|
75
|
+
return packageJson.version;
|
|
76
|
+
}
|
|
68
77
|
function resolveCoverValue(value) {
|
|
69
78
|
return typeof value === 'string' ? value : undefined;
|
|
70
79
|
}
|
|
@@ -123,6 +132,7 @@ async function main() {
|
|
|
123
132
|
.description('把 Markdown 文章处理成微信公众号草稿,支持主题、图片修复、封面生成与一键发布')
|
|
124
133
|
.usage('[article.md] [options]')
|
|
125
134
|
.helpOption('-h, --help', '查看帮助')
|
|
135
|
+
.version(getCliVersion(), '-V, --version', '查看版本')
|
|
126
136
|
.showHelpAfterError('(使用 --help 查看完整帮助)')
|
|
127
137
|
.argument('[article]', 'Markdown 文件路径;传入后会自动执行 md2html -> fix -> cover -> publish')
|
|
128
138
|
.option('--theme <theme>', `指定主题名,可用值:${(0, themes_1.listAvailableThemes)().join(', ')}`)
|
package/dist/core/cover.js
CHANGED
|
@@ -7,12 +7,17 @@ exports.listCoverPresets = listCoverPresets;
|
|
|
7
7
|
exports.createCover = createCover;
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const opentype_js_1 = require("opentype.js");
|
|
10
11
|
const sharp_1 = __importDefault(require("sharp"));
|
|
11
12
|
const BUILTIN_BACKGROUND_DIR = node_path_1.default.resolve(__dirname, '../../assets/backgrounds');
|
|
13
|
+
const COVER_TITLE_FONT_PATH = node_path_1.default.resolve(__dirname, '../../assets/fonts/仓耳小丸子.ttf');
|
|
12
14
|
const FALLBACK_GRADIENT = {
|
|
13
15
|
start: '#e8f3ef',
|
|
14
16
|
end: '#c9ded6'
|
|
15
17
|
};
|
|
18
|
+
const DEFAULT_TITLE_LINE_LENGTH = 15;
|
|
19
|
+
const COVER_TEXT_COLOR = '#000000b8';
|
|
20
|
+
let cachedCoverFont;
|
|
16
21
|
function escapeXml(value) {
|
|
17
22
|
return value
|
|
18
23
|
.replace(/&/g, '&')
|
|
@@ -21,8 +26,16 @@ function escapeXml(value) {
|
|
|
21
26
|
.replace(/"/g, '"')
|
|
22
27
|
.replace(/'/g, ''');
|
|
23
28
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
29
|
+
function getCoverFont() {
|
|
30
|
+
if (!(0, node_fs_1.existsSync)(COVER_TITLE_FONT_PATH)) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!cachedCoverFont) {
|
|
34
|
+
const fontBuffer = (0, node_fs_1.readFileSync)(COVER_TITLE_FONT_PATH);
|
|
35
|
+
const arrayBuffer = fontBuffer.buffer.slice(fontBuffer.byteOffset, fontBuffer.byteOffset + fontBuffer.byteLength);
|
|
36
|
+
cachedCoverFont = (0, opentype_js_1.parse)(arrayBuffer);
|
|
37
|
+
}
|
|
38
|
+
return cachedCoverFont;
|
|
26
39
|
}
|
|
27
40
|
function readFiles(dirPath, pattern) {
|
|
28
41
|
try {
|
|
@@ -79,29 +92,66 @@ function buildGradientBackground(width, height) {
|
|
|
79
92
|
</svg>`;
|
|
80
93
|
return Buffer.from(svg);
|
|
81
94
|
}
|
|
95
|
+
function getFontLineMetrics(font, fontSize) {
|
|
96
|
+
const unitsPerEm = font.unitsPerEm || 1000;
|
|
97
|
+
const ascender = Math.round(font.ascender / unitsPerEm * fontSize);
|
|
98
|
+
const descender = Math.abs(Math.round(font.descender / unitsPerEm * fontSize));
|
|
99
|
+
return {
|
|
100
|
+
ascender,
|
|
101
|
+
lineHeight: Math.max(Math.round((ascender + descender) * 1.08), fontSize)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function buildFontPath(font, text, fontSize) {
|
|
105
|
+
const pathObject = font.getPath(text, 0, 0, fontSize);
|
|
106
|
+
const box = pathObject.getBoundingBox();
|
|
107
|
+
return {
|
|
108
|
+
pathData: pathObject.toPathData(3),
|
|
109
|
+
width: Math.max(box.x2 - box.x1, 0),
|
|
110
|
+
x1: box.x1,
|
|
111
|
+
x2: box.x2
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function buildPathElement(pathData, x, y) {
|
|
115
|
+
return `<path d="${pathData}" transform="translate(${x.toFixed(3)} ${y.toFixed(3)})" fill="${COVER_TEXT_COLOR}" />`;
|
|
116
|
+
}
|
|
117
|
+
function buildTitlePath(font, line, index, canvasWidth, titleTop, lineHeight, ascender, fontSize) {
|
|
118
|
+
const glyphPath = buildFontPath(font, line, fontSize);
|
|
119
|
+
const baselineY = titleTop + index * lineHeight + ascender;
|
|
120
|
+
const startX = (canvasWidth - glyphPath.width) / 2 - glyphPath.x1;
|
|
121
|
+
return {
|
|
122
|
+
right: startX + glyphPath.x2,
|
|
123
|
+
path: buildPathElement(glyphPath.pathData, startX, baselineY)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function buildAuthorPath(font, text, fontSize, right, baselineY) {
|
|
127
|
+
const glyphPath = buildFontPath(font, text, fontSize);
|
|
128
|
+
const startX = right - glyphPath.x2;
|
|
129
|
+
return buildPathElement(glyphPath.pathData, startX, baselineY);
|
|
130
|
+
}
|
|
82
131
|
function buildTextSvg(params) {
|
|
83
|
-
const
|
|
132
|
+
const font = getCoverFont();
|
|
133
|
+
if (!font) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const lines = buildTitleLines(params.title, DEFAULT_TITLE_LINE_LENGTH);
|
|
84
137
|
const sizes = estimateFontSize(params.width, lines);
|
|
85
|
-
const
|
|
138
|
+
const titleMetrics = getFontLineMetrics(font, sizes.title);
|
|
139
|
+
const authorMetrics = getFontLineMetrics(font, sizes.author);
|
|
140
|
+
const lineHeight = titleMetrics.lineHeight;
|
|
86
141
|
const titleBlockHeight = lineHeight * lines.length;
|
|
87
142
|
const titleTop = Math.round((params.height - titleBlockHeight) / 2);
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return `<text x="50%" y="${y}" text-anchor="middle" font-size="${sizes.title}" font-family="${escapeAttribute(fontFamily)}" fill="rgba(0,0,0,0.72)">${escapeXml(line)}</text>`;
|
|
96
|
-
})
|
|
97
|
-
.join('');
|
|
98
|
-
const authorText = params.author.trim()
|
|
99
|
-
? `<text x="${titleRight}" y="${authorY}" text-anchor="end" font-size="${sizes.author}" font-family="${escapeAttribute(fontFamily)}" fill="rgba(0,0,0,0.72)">${escapeXml(params.author.trim())}</text>`
|
|
143
|
+
const titlePaths = lines.map((line, index) => buildTitlePath(font, line, index, params.width, titleTop, lineHeight, titleMetrics.ascender, sizes.title));
|
|
144
|
+
const titleRight = titlePaths.length > 0
|
|
145
|
+
? Math.max(...titlePaths.map((item) => item.right))
|
|
146
|
+
: params.width / 2;
|
|
147
|
+
const authorText = params.author.trim();
|
|
148
|
+
const authorPath = authorText
|
|
149
|
+
? buildAuthorPath(font, authorText, sizes.author, titleRight, titleTop + titleBlockHeight + Math.round(sizes.author * 0.4) + authorMetrics.ascender)
|
|
100
150
|
: '';
|
|
101
151
|
const svg = `
|
|
102
152
|
<svg width="${params.width}" height="${params.height}" viewBox="0 0 ${params.width} ${params.height}" xmlns="http://www.w3.org/2000/svg">
|
|
103
|
-
${
|
|
104
|
-
${
|
|
153
|
+
${titlePaths.map((item) => item.path).join('')}
|
|
154
|
+
${authorPath}
|
|
105
155
|
</svg>`;
|
|
106
156
|
return Buffer.from(svg);
|
|
107
157
|
}
|
|
@@ -137,8 +187,13 @@ async function createCover(options) {
|
|
|
137
187
|
const background = backgroundPath
|
|
138
188
|
? (0, sharp_1.default)(backgroundPath).resize(width, height, { fit: 'cover' })
|
|
139
189
|
: (0, sharp_1.default)(buildGradientBackground(width, height));
|
|
140
|
-
|
|
141
|
-
.
|
|
190
|
+
const textSvg = buildTextSvg({
|
|
191
|
+
title: options.title,
|
|
192
|
+
author,
|
|
193
|
+
width,
|
|
194
|
+
height
|
|
195
|
+
});
|
|
196
|
+
const overlays = [
|
|
142
197
|
{
|
|
143
198
|
input: {
|
|
144
199
|
create: {
|
|
@@ -148,16 +203,13 @@ async function createCover(options) {
|
|
|
148
203
|
background: { r: 255, g: 255, b: 255, alpha: 0.58 }
|
|
149
204
|
}
|
|
150
205
|
}
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
input: buildTextSvg({
|
|
154
|
-
title: options.title,
|
|
155
|
-
author,
|
|
156
|
-
width,
|
|
157
|
-
height
|
|
158
|
-
})
|
|
159
206
|
}
|
|
160
|
-
]
|
|
207
|
+
];
|
|
208
|
+
if (textSvg) {
|
|
209
|
+
overlays.push({ input: textSvg });
|
|
210
|
+
}
|
|
211
|
+
await background
|
|
212
|
+
.composite(overlays)
|
|
161
213
|
.jpeg({ quality: 92 })
|
|
162
214
|
.toFile(outputPath);
|
|
163
215
|
return outputPath;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyhue1991/wxgzh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Markdown to WeChat Official Account draft CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/cli/index.js",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"markdown-it": "^14.1.0",
|
|
38
38
|
"markdown-it-mathjax3": "^5.2.0",
|
|
39
39
|
"mathjax": "^4.1.1",
|
|
40
|
+
"opentype.js": "^1.3.4",
|
|
40
41
|
"sharp": "^0.33.5"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
package/spec.md
CHANGED
|
@@ -482,6 +482,7 @@ POST https://api.weixin.qq.com/cgi-bin/draft/add
|
|
|
482
482
|
npm run typecheck
|
|
483
483
|
npm run build
|
|
484
484
|
node dist/cli/index.js --help
|
|
485
|
+
node dist/cli/index.js --version
|
|
485
486
|
node dist/cli/index.js config --help
|
|
486
487
|
node dist/cli/index.js md2html --help
|
|
487
488
|
node dist/cli/index.js fix --help
|