@lyhue1991/wxgzh 0.1.1 → 0.1.3

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,15 @@
1
1
  # wxgzh
2
2
 
3
- 一个面向微信公众号的 Node.js CLI 工具:把 Markdown 文章处理成适合公众号发布的 HTML,修复图片,并提交到公众号草稿箱。
3
+ 一个面向微信公众号的 SKILL,基于 NodeJS CLI.
4
+
5
+ 把 Markdown 文章处理成适合公众号发布的 HTML,上传图片,生成封面,并提交到公众号草稿箱。
6
+
7
+ <mark>安装此SKILL</mark>
8
+
9
+ ```
10
+ npx skills add lyhue1991/wxgzh
11
+ ```
12
+
4
13
 
5
14
  ## 一、功能概述
6
15
 
@@ -12,31 +21,40 @@
12
21
  - 支持主题样式、自定义 CSS、自动摘要、封面图生成。
13
22
  - 也支持分步执行,便于你单独检查 HTML、封面或发布结果。
14
23
 
15
- 默认一键流程:
24
+ **手动操作方法**
25
+
26
+ * step1: 一键安装(要求:Node.js >= 18)
16
27
 
17
28
  ```bash
18
- wxgzh article.md
29
+ npm install -g @lyhue1991/wxgzh
30
+ ```
31
+ 安装完成后可直接使用 `wxgzh` 命令。
32
+
33
+
34
+ * step2: 一键配置 (提前准备好微信公众号AppID和 AppSecret , 并配置好IP白名单)
35
+
36
+ ```bash
37
+ wxgzh config --appid 你的AppID --appsecret 你的AppSecret
38
+
39
+ ```
40
+
41
+ * step3: 一键投稿 (将本地markdown文章直接投递到公众号草稿箱, 人工确认后即可发布)
42
+
43
+ ```bash
44
+ wxgzh article.md --author 文章作者姓名
19
45
  ```
20
46
 
21
47
  这条命令会自动执行:
22
48
 
23
49
  1. 读取 `article.md`
24
50
  2. 转换成公众号可用 HTML
25
- 3. 上传正文中的本地图片到微信
51
+ 3. 上传正文中的本地图片和非微信图床图片到微信公众号图床
26
52
  4. 自动生成封面图(如果你没有提供)
27
53
  5. 提交到微信公众号草稿箱
28
54
 
29
- ## 安装
30
55
 
31
- 要求:Node.js >= 18
32
56
 
33
- ```bash
34
- npm install -g @lyhue1991/wxgzh
35
- ```
36
-
37
- 安装完成后可直接使用 `wxgzh` 命令。
38
-
39
- ## 二、基本配置
57
+ ## 二、配置说明
40
58
 
41
59
  ### 1. 如何获取公众号 `appid` 和 `appsecret`
42
60
 
@@ -99,8 +117,6 @@ wxgzh config --list-themes
99
117
  当前内置主题来自 `styles/` 目录,例如:`default`、`blue`、`green`、`red`、`yellow`、`brown`、`black`、`orange`。
100
118
 
101
119
 
102
-
103
-
104
120
  ## 三、快速流程
105
121
 
106
122
  最常见用法:
@@ -374,7 +390,10 @@ enableComment: true
374
390
 
375
391
  命令行参数优先级高于 Front Matter,Front Matter 又高于默认配置。
376
392
 
377
- ## 支持的 Markdown 内容
393
+
394
+ ## 六,备忘清单
395
+
396
+ ### 1. 支持的 Markdown 内容
378
397
 
379
398
  本项目当前重点支持这些常见内容:
380
399
 
@@ -386,9 +405,10 @@ enableComment: true
386
405
  - 表格
387
406
  - 数学公式(行内与块级)
388
407
 
389
- ## 常用命令速查
408
+ ### 2. 常用命令速查
390
409
 
391
410
  ```bash
411
+ wxgzh --help
392
412
  wxgzh article.md
393
413
  wxgzh config --list
394
414
  wxgzh config --list-themes
@@ -398,34 +418,4 @@ wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg
398
418
  wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg
399
419
  ```
400
420
 
401
- ## 开发与构建
402
-
403
- 开发环境安装依赖:
404
-
405
- ```bash
406
- npm install
407
- ```
408
-
409
- 类型检查:
410
-
411
- ```bash
412
- npm run typecheck
413
- ```
414
-
415
- 构建:
416
-
417
- ```bash
418
- npm run build
419
- ```
420
-
421
- 查看帮助:
422
-
423
- ```bash
424
- node dist/cli/index.js --help
425
- ```
426
-
427
- ## 说明
428
421
 
429
- - 配置文件保存在用户目录下,不会写进项目源码目录。
430
- - `dist/` 是编译输出目录,日常修改应以 `src/` 为准。
431
- - 如果一键发布失败,建议改用“分步流程”逐步检查 `HTML`、图片上传和封面图。
@@ -178,6 +178,35 @@ q table:last-child {
178
178
  margin-bottom: 0 !important;
179
179
  }
180
180
  `;
181
+ const WECHAT_LAYOUT_INLINE_CSS = `
182
+ p,
183
+ h1,
184
+ h2,
185
+ h3,
186
+ h4,
187
+ h5,
188
+ h6,
189
+ ul,
190
+ ol,
191
+ dl,
192
+ blockquote,
193
+ q,
194
+ pre,
195
+ hr,
196
+ section.img-wrapper,
197
+ section.tbl-wrapper,
198
+ section.math-block {
199
+ margin-left: 0 !important;
200
+ margin-right: 0 !important;
201
+ }
202
+
203
+ section.img-wrapper,
204
+ section.tbl-wrapper,
205
+ section.math-block {
206
+ width: 100% !important;
207
+ box-sizing: border-box !important;
208
+ }
209
+ `;
181
210
  const WECHAT_TABLE_INLINE_CSS = `
182
211
  section.tbl-wrapper {
183
212
  margin: 1.4em 3% !important;
@@ -342,6 +371,9 @@ function escapeHtmlAttribute(value) {
342
371
  function isTextNodeEmpty(node) {
343
372
  return node.type === 'text' && !node.data.trim();
344
373
  }
374
+ function isTagNode(node, tagName) {
375
+ return node.type === 'tag' && node.tagName.toLowerCase() === tagName;
376
+ }
345
377
  function isStandaloneImageParagraph($paragraph) {
346
378
  const nodes = $paragraph.contents().toArray().filter((node) => !isTextNodeEmpty(node));
347
379
  return nodes.every((node) => node.type === 'tag' && ['img', 'br'].includes(node.tagName.toLowerCase()));
@@ -381,10 +413,33 @@ async function loadCustomCss() {
381
413
  function normalizeListItems($) {
382
414
  $('li').each((_, element) => {
383
415
  const $item = $(element);
384
- const childElements = $item
385
- .contents()
386
- .toArray()
387
- .filter((node) => !isTextNodeEmpty(node));
416
+ const initialChildren = $item.contents().toArray();
417
+ const hasParagraphChild = initialChildren.some((node) => isTagNode(node, 'p'));
418
+ if (hasParagraphChild) {
419
+ const fragments = [];
420
+ let previousWasParagraph = false;
421
+ for (const node of initialChildren) {
422
+ if (isTextNodeEmpty(node)) {
423
+ continue;
424
+ }
425
+ if (isTagNode(node, 'p')) {
426
+ const paragraphHtml = renderSoftBreakFriendlyHtml(node.children ?? []);
427
+ if (!paragraphHtml.trim()) {
428
+ continue;
429
+ }
430
+ if (previousWasParagraph) {
431
+ fragments.push('<br>');
432
+ }
433
+ fragments.push(paragraphHtml);
434
+ previousWasParagraph = true;
435
+ continue;
436
+ }
437
+ fragments.push(renderSoftBreakFriendlyNode(node, true));
438
+ previousWasParagraph = false;
439
+ }
440
+ $item.html(fragments.join(''));
441
+ }
442
+ const childElements = $item.contents().toArray().filter((node) => !isTextNodeEmpty(node));
388
443
  if (childElements.length === 0) {
389
444
  return;
390
445
  }
@@ -396,7 +451,7 @@ function normalizeListItems($) {
396
451
  return ['p', 'ul', 'ol', 'section', 'blockquote', 'pre', 'table'].includes(tagName);
397
452
  });
398
453
  const firstChild = childElements[0];
399
- if (!hasBlockChild && firstChild?.type === 'tag' && firstChild.tagName.toLowerCase() === 'span' && childElements.length === 1) {
454
+ if (!hasBlockChild && isTagNode(firstChild, 'span') && childElements.length === 1) {
400
455
  return;
401
456
  }
402
457
  if (hasBlockChild) {
@@ -556,12 +611,80 @@ function setInlineStyleProperty(existingStyle, property, value) {
556
611
  .map(([name, declarationValue]) => `${name}: ${declarationValue}`)
557
612
  .join('; ');
558
613
  }
614
+ function removeInlineStyleProperties(existingStyle, properties) {
615
+ if (!existingStyle) {
616
+ return undefined;
617
+ }
618
+ const propertiesToRemove = new Set(properties.map((property) => property.trim().toLowerCase()).filter(Boolean));
619
+ const declarations = existingStyle
620
+ .split(';')
621
+ .map((declaration) => declaration.trim())
622
+ .filter(Boolean)
623
+ .filter((declaration) => {
624
+ const separatorIndex = declaration.indexOf(':');
625
+ if (separatorIndex === -1) {
626
+ return true;
627
+ }
628
+ const name = declaration.slice(0, separatorIndex).trim().toLowerCase();
629
+ return !propertiesToRemove.has(name);
630
+ });
631
+ if (declarations.length === 0) {
632
+ return undefined;
633
+ }
634
+ return declarations.join('; ');
635
+ }
636
+ function normalizeInlinedListStyles($) {
637
+ $('ol').each((_, element) => {
638
+ const $list = $(element);
639
+ const depth = $list.parents('ol').length;
640
+ const cleanedStyle = removeInlineStyleProperties($list.attr('style'), ['list-style', 'list-style-type', 'list-style-position']);
641
+ const listType = depth === 0 ? '1' : depth === 1 ? 'a' : 'i';
642
+ if (cleanedStyle) {
643
+ $list.attr('style', cleanedStyle);
644
+ }
645
+ else {
646
+ $list.removeAttr('style');
647
+ }
648
+ $list.attr('type', listType);
649
+ });
650
+ $('ul').each((_, element) => {
651
+ const $list = $(element);
652
+ const cleanedStyle = removeInlineStyleProperties($list.attr('style'), ['list-style', 'list-style-type', 'list-style-position']);
653
+ if (cleanedStyle) {
654
+ $list.attr('style', cleanedStyle);
655
+ return;
656
+ }
657
+ $list.removeAttr('style');
658
+ });
659
+ $('ol > li, ul > li').each((_, element) => {
660
+ const $item = $(element);
661
+ const cleanedStyle = removeInlineStyleProperties($item.attr('style'), ['list-style', 'list-style-type', 'list-style-position']);
662
+ if (cleanedStyle) {
663
+ $item.attr('style', cleanedStyle);
664
+ return;
665
+ }
666
+ $item.removeAttr('style');
667
+ });
668
+ }
669
+ function removeWechatUnsafeListWhitespace($) {
670
+ $('ol, ul').each((_, element) => {
671
+ $(element)
672
+ .contents()
673
+ .toArray()
674
+ .filter((node) => isTextNodeEmpty(node))
675
+ .forEach((node) => {
676
+ $(node).remove();
677
+ });
678
+ });
679
+ }
559
680
  function cleanInlinedHtml(html) {
560
681
  const $ = cheerio.load(html);
561
682
  $('pre > code').each((_, element) => {
562
683
  const $element = $(element);
563
684
  $element.html(renderWechatFriendlyCodeHtml($element.contents().toArray()));
564
685
  });
686
+ normalizeInlinedListStyles($);
687
+ removeWechatUnsafeListWhitespace($);
565
688
  const $article = $('body > #write > article').first();
566
689
  const $firstChild = $article.children().first();
567
690
  if ($firstChild.length > 0) {
@@ -616,7 +739,7 @@ async function renderMarkdownToHtml(body, metadata) {
616
739
  const articleHtml = $('article').html() ?? '';
617
740
  const baseHtml = createDocumentHtml(articleHtml, metadata);
618
741
  const inlinedHtml = (0, css_inline_1.inline)(baseHtml, {
619
- extraCss: `${themeCss}\n${HIGHLIGHT_INLINE_CSS}\n${WECHAT_BLOCKQUOTE_INLINE_CSS}\n${WECHAT_TABLE_INLINE_CSS}\n${WECHAT_MATH_INLINE_CSS}\n${customCss}`,
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}`,
620
743
  keepAtRules: true,
621
744
  applyWidthAttributes: false,
622
745
  applyHeightAttributes: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyhue1991/wxgzh",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Markdown to WeChat Official Account draft CLI",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli/index.js",