@lyhue1991/wxgzh 0.1.3 → 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# wxgzh
|
|
1
|
+
# wxgzh,这个SKILL打通了AI写公众号文章的最后一公里
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
这是一个面向微信公众号的 SKILL,基于 NodeJS CLI.
|
|
4
4
|
|
|
5
5
|
把 Markdown 文章处理成适合公众号发布的 HTML,上传图片,生成封面,并提交到公众号草稿箱。
|
|
6
6
|
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
npx skills add lyhue1991/wxgzh
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
效果演示
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
## 一、功能概述
|
|
15
21
|
|
|
@@ -409,6 +415,7 @@ enableComment: true
|
|
|
409
415
|
|
|
410
416
|
```bash
|
|
411
417
|
wxgzh --help
|
|
418
|
+
wxgzh --version
|
|
412
419
|
wxgzh article.md
|
|
413
420
|
wxgzh config --list
|
|
414
421
|
wxgzh config --list-themes
|
|
@@ -418,4 +425,3 @@ wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg
|
|
|
418
425
|
wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg
|
|
419
426
|
```
|
|
420
427
|
|
|
421
|
-
|
|
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/converter.js
CHANGED
|
@@ -260,6 +260,32 @@ section.tbl-wrapper a {
|
|
|
260
260
|
text-decoration: none !important;
|
|
261
261
|
}
|
|
262
262
|
`;
|
|
263
|
+
const WECHAT_CODE_BLOCK_INLINE_CSS = `
|
|
264
|
+
pre {
|
|
265
|
+
overflow-x: auto !important;
|
|
266
|
+
overflow-y: hidden !important;
|
|
267
|
+
-webkit-overflow-scrolling: touch;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pre code.hljs,
|
|
271
|
+
pre code[class*="language-"] {
|
|
272
|
+
display: block !important;
|
|
273
|
+
width: max-content !important;
|
|
274
|
+
min-width: 100% !important;
|
|
275
|
+
box-sizing: border-box !important;
|
|
276
|
+
white-space: nowrap !important;
|
|
277
|
+
word-break: normal !important;
|
|
278
|
+
overflow-wrap: normal !important;
|
|
279
|
+
overflow: visible !important;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
pre code.hljs *,
|
|
283
|
+
pre code[class*="language-"] * {
|
|
284
|
+
white-space: inherit !important;
|
|
285
|
+
word-break: inherit !important;
|
|
286
|
+
overflow-wrap: inherit !important;
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
263
289
|
const WECHAT_MATH_INLINE_CSS = `
|
|
264
290
|
span.math-inline {
|
|
265
291
|
display: inline-block !important;
|
|
@@ -587,51 +613,59 @@ function createDocumentHtml(content, metadata) {
|
|
|
587
613
|
].join('\n');
|
|
588
614
|
}
|
|
589
615
|
function setInlineStyleProperty(existingStyle, property, value) {
|
|
590
|
-
const entries =
|
|
591
|
-
if (existingStyle) {
|
|
592
|
-
for (const declaration of existingStyle.split(';')) {
|
|
593
|
-
const trimmed = declaration.trim();
|
|
594
|
-
if (!trimmed) {
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
const separatorIndex = trimmed.indexOf(':');
|
|
598
|
-
if (separatorIndex === -1) {
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
const name = trimmed.slice(0, separatorIndex).trim();
|
|
602
|
-
const declarationValue = trimmed.slice(separatorIndex + 1).trim();
|
|
603
|
-
if (!name || !declarationValue) {
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
entries.set(name, declarationValue);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
616
|
+
const entries = parseInlineStyle(existingStyle);
|
|
609
617
|
entries.set(property, value);
|
|
610
|
-
return
|
|
611
|
-
.map(([name, declarationValue]) => `${name}: ${declarationValue}`)
|
|
612
|
-
.join('; ');
|
|
618
|
+
return stringifyInlineStyle(entries) ?? '';
|
|
613
619
|
}
|
|
614
620
|
function removeInlineStyleProperties(existingStyle, properties) {
|
|
621
|
+
const entries = parseInlineStyle(existingStyle);
|
|
622
|
+
const propertiesToRemove = new Set(properties.map((property) => property.trim().toLowerCase()).filter(Boolean));
|
|
623
|
+
for (const name of Array.from(entries.keys())) {
|
|
624
|
+
if (propertiesToRemove.has(name.toLowerCase())) {
|
|
625
|
+
entries.delete(name);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return stringifyInlineStyle(entries);
|
|
629
|
+
}
|
|
630
|
+
function parseInlineStyle(existingStyle) {
|
|
631
|
+
const entries = new Map();
|
|
615
632
|
if (!existingStyle) {
|
|
616
|
-
return
|
|
633
|
+
return entries;
|
|
617
634
|
}
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
.
|
|
624
|
-
const separatorIndex = declaration.indexOf(':');
|
|
635
|
+
for (const declaration of existingStyle.split(';')) {
|
|
636
|
+
const trimmed = declaration.trim();
|
|
637
|
+
if (!trimmed) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const separatorIndex = trimmed.indexOf(':');
|
|
625
641
|
if (separatorIndex === -1) {
|
|
626
|
-
|
|
642
|
+
continue;
|
|
627
643
|
}
|
|
628
|
-
const name =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
644
|
+
const name = trimmed.slice(0, separatorIndex).trim();
|
|
645
|
+
const declarationValue = trimmed.slice(separatorIndex + 1).trim();
|
|
646
|
+
if (!name || !declarationValue) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
entries.set(name, declarationValue);
|
|
650
|
+
}
|
|
651
|
+
return entries;
|
|
652
|
+
}
|
|
653
|
+
function stringifyInlineStyle(entries) {
|
|
654
|
+
if (entries.size === 0) {
|
|
632
655
|
return undefined;
|
|
633
656
|
}
|
|
634
|
-
return
|
|
657
|
+
return Array.from(entries.entries())
|
|
658
|
+
.map(([name, declarationValue]) => `${name}: ${declarationValue}`)
|
|
659
|
+
.join('; ');
|
|
660
|
+
}
|
|
661
|
+
function getInlineStyleProperty(existingStyle, property) {
|
|
662
|
+
const entries = parseInlineStyle(existingStyle);
|
|
663
|
+
for (const [name, declarationValue] of entries.entries()) {
|
|
664
|
+
if (name.trim().toLowerCase() === property.trim().toLowerCase()) {
|
|
665
|
+
return declarationValue;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return undefined;
|
|
635
669
|
}
|
|
636
670
|
function normalizeInlinedListStyles($) {
|
|
637
671
|
$('ol').each((_, element) => {
|
|
@@ -677,6 +711,95 @@ function removeWechatUnsafeListWhitespace($) {
|
|
|
677
711
|
});
|
|
678
712
|
});
|
|
679
713
|
}
|
|
714
|
+
function normalizeWechatCodeBlocks($) {
|
|
715
|
+
const backgroundStyleProperties = [
|
|
716
|
+
'background',
|
|
717
|
+
'background-color',
|
|
718
|
+
'background-image',
|
|
719
|
+
'background-repeat',
|
|
720
|
+
'background-position',
|
|
721
|
+
'background-size'
|
|
722
|
+
];
|
|
723
|
+
const codeStyleProperties = [
|
|
724
|
+
...backgroundStyleProperties,
|
|
725
|
+
'border',
|
|
726
|
+
'border-top',
|
|
727
|
+
'border-right',
|
|
728
|
+
'border-bottom',
|
|
729
|
+
'border-left',
|
|
730
|
+
'border-color',
|
|
731
|
+
'border-style',
|
|
732
|
+
'border-width',
|
|
733
|
+
'border-radius',
|
|
734
|
+
'box-shadow',
|
|
735
|
+
'padding',
|
|
736
|
+
'padding-top',
|
|
737
|
+
'padding-right',
|
|
738
|
+
'padding-bottom',
|
|
739
|
+
'padding-left'
|
|
740
|
+
];
|
|
741
|
+
const wrapperStyleProperties = ['margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left'];
|
|
742
|
+
$('pre').each((_, element) => {
|
|
743
|
+
const $pre = $(element);
|
|
744
|
+
const $code = $pre.children('code').first();
|
|
745
|
+
if ($code.length === 0) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (!$pre.parent().is('section.code-block-wrapper')) {
|
|
749
|
+
$pre.wrap('<section class="code-block-wrapper"></section>');
|
|
750
|
+
}
|
|
751
|
+
const $wrapper = $pre.parent();
|
|
752
|
+
const codeStyle = $code.attr('style');
|
|
753
|
+
const preStyle = $pre.attr('style');
|
|
754
|
+
for (const property of wrapperStyleProperties) {
|
|
755
|
+
const value = getInlineStyleProperty(codeStyle, property) ?? getInlineStyleProperty(preStyle, property);
|
|
756
|
+
if (!value) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
$wrapper.attr('style', setInlineStyleProperty($wrapper.attr('style'), property, value));
|
|
760
|
+
}
|
|
761
|
+
$wrapper.attr('style', setInlineStyleProperty($wrapper.attr('style'), 'display', 'block !important'));
|
|
762
|
+
$wrapper.attr('style', setInlineStyleProperty($wrapper.attr('style'), 'overflow-x', 'auto !important'));
|
|
763
|
+
$wrapper.attr('style', setInlineStyleProperty($wrapper.attr('style'), 'overflow-y', 'hidden !important'));
|
|
764
|
+
$wrapper.attr('style', setInlineStyleProperty($wrapper.attr('style'), '-webkit-overflow-scrolling', 'touch'));
|
|
765
|
+
for (const property of codeStyleProperties) {
|
|
766
|
+
const value = getInlineStyleProperty(codeStyle, property);
|
|
767
|
+
if (!value) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), property, value));
|
|
771
|
+
}
|
|
772
|
+
$pre.attr('style', removeInlineStyleProperties($pre.attr('style'), wrapperStyleProperties));
|
|
773
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'display', 'inline-block !important'));
|
|
774
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'width', 'max-content !important'));
|
|
775
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'min-width', '100% !important'));
|
|
776
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'max-width', 'none !important'));
|
|
777
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'box-sizing', 'border-box !important'));
|
|
778
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'white-space', 'normal !important'));
|
|
779
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'word-break', 'normal !important'));
|
|
780
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'overflow', 'visible !important'));
|
|
781
|
+
$pre.attr('style', setInlineStyleProperty($pre.attr('style'), 'vertical-align', 'top !important'));
|
|
782
|
+
$code.attr('style', removeInlineStyleProperties($code.attr('style'), [...codeStyleProperties, ...wrapperStyleProperties]));
|
|
783
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'display', 'inline-block !important'));
|
|
784
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'width', 'max-content !important'));
|
|
785
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'min-width', '100% !important'));
|
|
786
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'max-width', 'none !important'));
|
|
787
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'white-space', 'nowrap !important'));
|
|
788
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'word-break', 'normal !important'));
|
|
789
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'overflow-wrap', 'normal !important'));
|
|
790
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'border', '0 !important'));
|
|
791
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'border-radius', '0 !important'));
|
|
792
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'padding', '0 !important'));
|
|
793
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), 'margin', '0 !important'));
|
|
794
|
+
for (const property of backgroundStyleProperties) {
|
|
795
|
+
const value = getInlineStyleProperty(codeStyle, property) ?? getInlineStyleProperty(preStyle, property);
|
|
796
|
+
if (!value) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
$code.attr('style', setInlineStyleProperty($code.attr('style'), property, value));
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
680
803
|
function cleanInlinedHtml(html) {
|
|
681
804
|
const $ = cheerio.load(html);
|
|
682
805
|
$('pre > code').each((_, element) => {
|
|
@@ -685,6 +808,7 @@ function cleanInlinedHtml(html) {
|
|
|
685
808
|
});
|
|
686
809
|
normalizeInlinedListStyles($);
|
|
687
810
|
removeWechatUnsafeListWhitespace($);
|
|
811
|
+
normalizeWechatCodeBlocks($);
|
|
688
812
|
const $article = $('body > #write > article').first();
|
|
689
813
|
const $firstChild = $article.children().first();
|
|
690
814
|
if ($firstChild.length > 0) {
|
|
@@ -739,7 +863,7 @@ async function renderMarkdownToHtml(body, metadata) {
|
|
|
739
863
|
const articleHtml = $('article').html() ?? '';
|
|
740
864
|
const baseHtml = createDocumentHtml(articleHtml, metadata);
|
|
741
865
|
const inlinedHtml = (0, css_inline_1.inline)(baseHtml, {
|
|
742
|
-
extraCss: `${themeCss}\n${HIGHLIGHT_INLINE_CSS}\n${WECHAT_BLOCKQUOTE_INLINE_CSS}\n${WECHAT_TABLE_INLINE_CSS}\n${WECHAT_MATH_INLINE_CSS}\n${WECHAT_LAYOUT_INLINE_CSS}\n${customCss}`,
|
|
866
|
+
extraCss: `${themeCss}\n${HIGHLIGHT_INLINE_CSS}\n${WECHAT_BLOCKQUOTE_INLINE_CSS}\n${WECHAT_TABLE_INLINE_CSS}\n${WECHAT_CODE_BLOCK_INLINE_CSS}\n${WECHAT_MATH_INLINE_CSS}\n${WECHAT_LAYOUT_INLINE_CSS}\n${customCss}`,
|
|
743
867
|
keepAtRules: true,
|
|
744
868
|
applyWidthAttributes: false,
|
|
745
869
|
applyHeightAttributes: false
|
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
|