@vdfor/confluence-sync 0.0.1 → 0.1.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 ADDED
@@ -0,0 +1,124 @@
1
+ # @vdfor/confluence-sync
2
+
3
+ Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # pnpm
9
+ pnpm add -g @vdfor/confluence-sync
10
+
11
+ # npm
12
+ npm install -g @vdfor/confluence-sync
13
+
14
+ # yarn
15
+ yarn global add @vdfor/confluence-sync
16
+ ```
17
+
18
+ ## 配置
19
+
20
+ ### 方式1:环境变量
21
+
22
+ ```bash
23
+ # 必填:Confluence 服务地址
24
+ export CONFLUENCE_BASE_URL="https://your-confluence-instance.com"
25
+
26
+ # 认证方式(二选一)
27
+ # 方式1(推荐):Personal Access Token
28
+ export CONFLUENCE_PAT="your-personal-access-token"
29
+
30
+ # 方式2:Basic Auth
31
+ export CONFLUENCE_USERNAME="your-username"
32
+ export CONFLUENCE_API_TOKEN="your-api-token"
33
+ ```
34
+
35
+ ### 方式2:CLI 参数(优先于环境变量)
36
+
37
+ ```bash
38
+ confluence-sync upload --file test.md \
39
+ --base-url https://your-confluence-instance.com \
40
+ --pat your-token
41
+ ```
42
+
43
+ ## 命令
44
+
45
+ ### upload - 上传 Markdown 为新页面
46
+
47
+ ```bash
48
+ confluence-sync upload --file <path> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
49
+ ```
50
+
51
+ ### update - 更新已有页面
52
+
53
+ ```bash
54
+ confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
55
+ ```
56
+
57
+ ### upload-file - 上传附件
58
+
59
+ ```bash
60
+ confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
61
+ ```
62
+
63
+ ### list-pages - 列出空间页面
64
+
65
+ ```bash
66
+ confluence-sync list-pages [--space <key>]
67
+ ```
68
+
69
+ ### list-files - 列出页面附件
70
+
71
+ ```bash
72
+ confluence-sync list-files --page-id <ID>
73
+ ```
74
+
75
+ ### get-content - 获取页面内容
76
+
77
+ ```bash
78
+ confluence-sync get-content --page-id <ID> [--markdown]
79
+ ```
80
+
81
+ 使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
82
+
83
+ ## 配置参数
84
+
85
+ | 参数 | 说明 |
86
+ |------|------|
87
+ | `--base-url` | Confluence 服务地址 |
88
+ | `--pat` | Personal Access Token |
89
+ | `--username` | Confluence 登录名 |
90
+ | `--api-token` | API Token 或密码 |
91
+ | `--space` | Space Key |
92
+ | `--file` | 本地文件路径 |
93
+ | `--title` | 页面标题 |
94
+ | `--page-id` | 页面 ID |
95
+ | `--parent` | 父页面 ID 或标题 |
96
+ | `--dry-run` | 仅打印转换结果,不实际上传 |
97
+ | `--minor-edit` | 标记为小修改(不通知关注者) |
98
+ | `--markdown` | get-content 时输出为 Markdown 格式 |
99
+
100
+ ## 示例
101
+
102
+ ```bash
103
+ # 上传 Markdown 文件
104
+ confluence-sync upload --file README.md --title "项目文档" --space MYSPACE
105
+
106
+ # 更新已有页面
107
+ confluence-sync update --file README.md --page-id 123456
108
+
109
+ # 上传附件
110
+ confluence-sync upload-file --page-id 123456 --file image.png
111
+
112
+ # 列出空间所有页面
113
+ confluence-sync list-pages --space MYSPACE
114
+
115
+ # 获取页面内容
116
+ confluence-sync get-content --page-id 123456
117
+
118
+ # 获取页面内容并转换为 Markdown
119
+ confluence-sync get-content --page-id 123456 --markdown
120
+ ```
121
+
122
+ ## License
123
+
124
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vdfor/confluence-sync",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "author": "vdfor",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -37,9 +37,9 @@
37
37
  "upload": "node upload.mjs upload",
38
38
  "update": "node upload.mjs update",
39
39
  "help": "node upload.mjs help",
40
- "release:beta": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v && pnpm publish --registry https://registry.npmjs.org/ --access public --tag beta --no-git-checks",
41
- "release": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v && pnpm publish --registry https://registry.npmjs.org/ --access public --no-git-checks",
42
- "v": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v",
40
+ "release:beta": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && NPM_CONFIG_GIT_CHECKS=false pnpm version $v --no-git-tag-version --no-git-checks && pnpm publish --tag beta --access public --no-git-checks",
41
+ "release": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && NPM_CONFIG_GIT_CHECKS=false pnpm version $v --no-git-tag-version --no-git-checks && pnpm publish --access public --no-git-checks",
42
+ "v": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v --no-git-tag-version --no-git-checks",
43
43
  "clean": "rimraf dist node_modules"
44
44
  }
45
45
  }
@@ -271,6 +271,104 @@ export function mdToConfluence(md) {
271
271
  return renderTokens(tokens);
272
272
  }
273
273
 
274
+ // ── Confluence Storage Format → Markdown ─────────────────────────────────────
275
+
276
+ /**
277
+ * 将 Confluence storage format(XHTML)转换为 Markdown
278
+ * @param {string} html - Confluence storage format HTML
279
+ * @returns {string} Markdown 文本
280
+ */
281
+ export function confluenceToMd(html) {
282
+ if (!html) return '';
283
+
284
+ let md = html;
285
+
286
+ // 代码块:<ac:structured-macro ac:name="code">
287
+ md = md.replace(/<ac:structured-macro\s+ac:name="code">[\s\S]*?<ac:parameter\s+ac:name="language">([\s\S]*?)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g,
288
+ (_, lang, code) => `\n\`\`\`${lang}\n${code}\n\`\`\`\n`);
289
+
290
+ // 标题
291
+ md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n# $1\n');
292
+ md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n');
293
+ md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n### $1\n');
294
+ md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '\n#### $1\n');
295
+ md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '\n##### $1\n');
296
+ md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '\n###### $1\n');
297
+
298
+ // 粗体、斜体、删除线、行内代码
299
+ md = md.replace(/<strong>([\s\S]*?)<\/strong>/gi, '**$1**');
300
+ md = md.replace(/<b>([\s\S]*?)<\/b>/gi, '**$1**');
301
+ md = md.replace(/<em>([\s\S]*?)<\/em>/gi, '*$1*');
302
+ md = md.replace(/<i>([\s\S]*?)<\/i>/gi, '*$1*');
303
+ md = md.replace(/<s>([\s\S]*?)<\/s>/gi, '~~$1~~');
304
+ md = md.replace(/<del>([\s\S]*?)<\/del>/gi, '~~$1~~');
305
+ md = md.replace(/<code>([\s\S]*?)<\/code>/gi, '`$1`');
306
+
307
+ // 链接
308
+ md = md.replace(/<a\s+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
309
+
310
+ // 图片:<ac:image><ri:attachment ri:filename="xxx" /></ac:image>
311
+ md = md.replace(/<ac:image>[\s\S]*?<ri:attachment\s+ri:filename="([^"]*)"[\s\S]*?\/>[\s\S]*?<\/ac:image>/gi, '![]($1)');
312
+
313
+ // 普通图片
314
+ md = md.replace(/<img\s+[^>]*src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
315
+
316
+ // 水平线
317
+ md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
318
+
319
+ // 换行
320
+ md = md.replace(/<br\s*\/?>/gi, '\n');
321
+
322
+ // 段落
323
+ md = md.replace(/<p>([\s\S]*?)<\/p>/gi, '\n$1\n');
324
+
325
+ // 引用块
326
+ md = md.replace(/<blockquote>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
327
+ return content.split('\n').map(line => `> ${line}`).join('\n');
328
+ });
329
+
330
+ // 无序列表
331
+ md = md.replace(/<ul>([\s\S]*?)<\/ul>/gi, (_, content) => {
332
+ return content.replace(/<li>([\s\S]*?)<\/li>/gi, '- $1\n');
333
+ });
334
+
335
+ // 有序列表
336
+ md = md.replace(/<ol>([\s\S]*?)<\/ol>/gi, (_, content) => {
337
+ let i = 0;
338
+ return content.replace(/<li>([\s\S]*?)<\/li>/gi, () => `${++i}. $1\n`);
339
+ });
340
+
341
+ // 表格
342
+ md = md.replace(/<table>([\s\S]*?)<\/table>/gi, (_, tableContent) => {
343
+ let rows = [];
344
+ // 表头
345
+ const headerMatch = tableContent.match(/<thead>([\s\S]*?)<\/thead>/i);
346
+ if (headerMatch) {
347
+ const cells = headerMatch[1].match(/<th[^>]*>([\s\S]*?)<\/th>/gi) || [];
348
+ rows.push(cells.map(c => c.replace(/<th[^>]*>([\s\S]*?)<\/th>/i, '$1').trim()));
349
+ rows.push(rows[0].map(() => '---'));
350
+ }
351
+ // 表体
352
+ const bodyMatch = tableContent.match(/<tbody>([\s\S]*?)<\/tbody>/i);
353
+ if (bodyMatch) {
354
+ const trMatches = bodyMatch[1].match(/<tr>([\s\S]*?)<\/tr>/gi) || [];
355
+ for (const tr of trMatches) {
356
+ const cells = tr.match(/<td[^>]*>([\s\S]*?)<\/td>/gi) || [];
357
+ rows.push(cells.map(c => c.replace(/<td[^>]*>([\s\S]*?)<\/td>/i, '$1').trim()));
358
+ }
359
+ }
360
+ return '\n' + rows.map(r => '| ' + r.join(' | ') + ' |').join('\n') + '\n';
361
+ });
362
+
363
+ // 移除剩余 HTML 标签
364
+ md = md.replace(/<[^>]+>/g, '');
365
+
366
+ // 清理多余空行
367
+ md = md.replace(/\n{3,}/g, '\n\n');
368
+
369
+ return md.trim();
370
+ }
371
+
274
372
  // ── 文件上传(附件)────────────────────────────────────────────────────────────
275
373
 
276
374
  /**
package/upload.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  * confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
16
16
  * confluence-sync list-pages [--space <key>]
17
17
  * confluence-sync list-files --page-id <ID>
18
- * confluence-sync get-content --page-id <ID>
18
+ * confluence-sync get-content --page-id <ID> [--markdown]
19
19
  *
20
20
  * 可选配置参数(优先于环境变量):
21
21
  * --base-url Confluence 服务地址
@@ -31,6 +31,7 @@ import {
31
31
  listPages,
32
32
  listAttachments,
33
33
  getPageContent,
34
+ confluenceToMd,
34
35
  defaultSpaceKey,
35
36
  getBaseUrl,
36
37
  configure,
@@ -101,6 +102,7 @@ list-files 选项:
101
102
 
102
103
  get-content 选项:
103
104
  --page-id 页面 ID(必填)
105
+ --markdown 输出为 Markdown 格式(默认输出 Storage HTML)
104
106
 
105
107
  配置参数(优先于环境变量):
106
108
  --base-url Confluence 服务地址
@@ -252,8 +254,13 @@ async function main() {
252
254
  console.log(`标题: ${page.title}`);
253
255
  console.log(`Space: ${page.space?.key}`);
254
256
  console.log(`版本: ${page.version?.number}`);
255
- console.log(`\n── Storage HTML ──────────────────────────────\n`);
256
- console.log(page.body.storage.value);
257
+ if (args.markdown) {
258
+ console.log(`\n── Markdown ──────────────────────────────\n`);
259
+ console.log(confluenceToMd(page.body.storage.value));
260
+ } else {
261
+ console.log(`\n── Storage HTML ──────────────────────────────\n`);
262
+ console.log(page.body.storage.value);
263
+ }
257
264
  break;
258
265
  }
259
266