atl-fetch 1.1.0 → 1.2.1
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 +20 -7
- package/dist/services/fetch/fetch-service.js +4 -5
- package/dist/services/jira/jira-service.js +2 -0
- package/dist/services/storage/storage-service.d.ts +5 -3
- package/dist/services/storage/storage-service.js +297 -36
- package/dist/services/text-converter/adf-to-html.d.ts +37 -0
- package/dist/services/text-converter/adf-to-html.js +476 -0
- package/dist/services/text-converter/adf-to-plain-text.d.ts +25 -0
- package/dist/services/text-converter/adf-to-plain-text.js +119 -0
- package/dist/services/text-converter/markdown-utils.d.ts +22 -0
- package/dist/services/text-converter/markdown-utils.js +270 -0
- package/dist/services/text-converter/storage-to-plain-text.d.ts +66 -0
- package/dist/services/text-converter/storage-to-plain-text.js +238 -0
- package/dist/services/text-converter/text-converter.d.ts +8 -18
- package/dist/services/text-converter/text-converter.js +23 -630
- package/dist/services/text-converter/types.d.ts +40 -0
- package/dist/services/text-converter/types.js +16 -0
- package/dist/types/jira.d.ts +5 -1
- package/dist/types/result.d.ts +104 -0
- package/dist/types/result.js +119 -0
- package/dist/types/storage.d.ts +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,18 @@ Atlassian Cloud(Jira / Confluence)から情報を取得する Node.js CLI
|
|
|
25
25
|
- Node.js 22.0.0 以上
|
|
26
26
|
- pnpm(推奨)または npm
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## 使い方
|
|
29
|
+
|
|
30
|
+
### npx で即時実行(インストール不要)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 環境変数を設定して実行
|
|
34
|
+
ATLASSIAN_EMAIL="your-email@example.com" \
|
|
35
|
+
ATLASSIAN_API_TOKEN="your-api-token" \
|
|
36
|
+
npx atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### グローバルインストール
|
|
29
40
|
|
|
30
41
|
```bash
|
|
31
42
|
# npm
|
|
@@ -50,21 +61,23 @@ API トークンは [Atlassian Account Settings](https://id.atlassian.com/manage
|
|
|
50
61
|
|
|
51
62
|
```bash
|
|
52
63
|
# Jira Issue を取得
|
|
53
|
-
atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123
|
|
64
|
+
npx atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123
|
|
54
65
|
|
|
55
66
|
# Confluence ページを取得
|
|
56
|
-
atl-fetch https://your-domain.atlassian.net/wiki/spaces/SPACE/pages/123456/Page+Title
|
|
67
|
+
npx atl-fetch https://your-domain.atlassian.net/wiki/spaces/SPACE/pages/123456/Page+Title
|
|
57
68
|
|
|
58
69
|
# Markdown 形式で出力
|
|
59
|
-
atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 --format markdown
|
|
70
|
+
npx atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 --format markdown
|
|
60
71
|
|
|
61
72
|
# 添付ファイルをダウンロード
|
|
62
|
-
atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 --download --dir ./output
|
|
73
|
+
npx atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 --download --dir ./output
|
|
63
74
|
|
|
64
75
|
# ファイルに保存(リダイレクト)
|
|
65
|
-
atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 > result.json
|
|
76
|
+
npx atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 > result.json
|
|
66
77
|
```
|
|
67
78
|
|
|
79
|
+
**Note**: グローバルインストール済みの場合は `npx` を省略して `atl-fetch` として実行できます。
|
|
80
|
+
|
|
68
81
|
## CLI オプション
|
|
69
82
|
|
|
70
83
|
| オプション | 短縮形 | 説明 | デフォルト |
|
|
@@ -88,7 +101,7 @@ atl-fetch https://your-domain.atlassian.net/browse/PROJECT-123 > result.json
|
|
|
88
101
|
認証情報の設定状態を確認します。
|
|
89
102
|
|
|
90
103
|
```bash
|
|
91
|
-
atl-fetch auth check
|
|
104
|
+
npx atl-fetch auth check
|
|
92
105
|
```
|
|
93
106
|
|
|
94
107
|
出力例(設定済み):
|
|
@@ -3,7 +3,7 @@ import { fetchConfluencePage } from '../confluence/confluence-service.js';
|
|
|
3
3
|
import { fetchJiraIssue } from '../jira/jira-service.js';
|
|
4
4
|
import { formatConfluencePage, formatJiraIssue, writeToFile } from '../output/output-service.js';
|
|
5
5
|
import { saveConfluencePage, saveConfluenceVersions, saveJiraIssue } from '../storage/storage-service.js';
|
|
6
|
-
import {
|
|
6
|
+
import { convertStorageFormatToPlainText } from '../text-converter/text-converter.js';
|
|
7
7
|
import { parseUrl } from '../url-parser/url-parser.js';
|
|
8
8
|
/**
|
|
9
9
|
* Jira エラーを FetchError に変換する
|
|
@@ -230,15 +230,14 @@ export async function fetchAndSave(url, options) {
|
|
|
230
230
|
return err(mapJiraErrorToFetchError(issueResult.error));
|
|
231
231
|
}
|
|
232
232
|
const issue = issueResult.value;
|
|
233
|
-
// ADF をプレーンテキストに変換
|
|
234
|
-
const descriptionPlainText = issue.description !== null ? convertAdfToPlainText(issue.description) : null;
|
|
235
233
|
// Jira Issue をディレクトリ構造で保存
|
|
234
|
+
// description は ADF 形式で保存、descriptionPlainText は後方互換性のため維持
|
|
236
235
|
const saveResult = await saveJiraIssue({
|
|
237
236
|
attachments: issue.attachments,
|
|
238
237
|
changelog: issue.changelog,
|
|
239
238
|
comments: issue.comments,
|
|
240
|
-
description: issue.
|
|
241
|
-
descriptionPlainText,
|
|
239
|
+
description: issue.descriptionAdf,
|
|
240
|
+
descriptionPlainText: issue.description,
|
|
242
241
|
key: issue.key,
|
|
243
242
|
summary: issue.summary,
|
|
244
243
|
}, {
|
|
@@ -108,6 +108,7 @@ function mapApiCommentToJiraComment(apiComment) {
|
|
|
108
108
|
return {
|
|
109
109
|
author: apiComment.author.displayName,
|
|
110
110
|
body: extractTextFromAdf(apiComment.body),
|
|
111
|
+
bodyAdf: apiComment.body,
|
|
111
112
|
created: apiComment.created,
|
|
112
113
|
id: apiComment.id,
|
|
113
114
|
updated: apiComment.updated,
|
|
@@ -254,6 +255,7 @@ export async function fetchJiraIssue(organization, issueKey) {
|
|
|
254
255
|
changelog: (apiResponse.changelog?.histories ?? []).map(mapApiChangelogEntryToJiraChangelogEntry),
|
|
255
256
|
comments: (apiResponse.fields.comment?.comments ?? []).map(mapApiCommentToJiraComment),
|
|
256
257
|
description: apiResponse.fields.description ? extractTextFromAdf(apiResponse.fields.description) : null,
|
|
258
|
+
descriptionAdf: apiResponse.fields.description,
|
|
257
259
|
key: apiResponse.key,
|
|
258
260
|
summary: apiResponse.fields.summary,
|
|
259
261
|
};
|
|
@@ -16,9 +16,11 @@ import type { ConfluenceSaveData, ConfluenceSaveResult, ConfluenceStorageOptions
|
|
|
16
16
|
* ├── manifest.json # 取得メタデータ
|
|
17
17
|
* ├── issue.json # Issue 全データ(JSON 形式)
|
|
18
18
|
* ├── description.txt # 説明文のプレーンテキスト
|
|
19
|
-
* ├── content.md # Markdown
|
|
20
|
-
* ├──
|
|
21
|
-
* ├──
|
|
19
|
+
* ├── content.md # Markdown 形式(Description + Attachments)
|
|
20
|
+
* ├── comments.md # コメント一覧(Markdown 形式)
|
|
21
|
+
* ├── changelog.md # 変更履歴(Markdown 形式)
|
|
22
|
+
* ├── changelog.json # 変更履歴(JSON 形式)
|
|
23
|
+
* ├── comments.json # コメント一覧(JSON 形式)
|
|
22
24
|
* ├── attachments.json # 添付ファイル一覧メタデータ
|
|
23
25
|
* └── attachments/ # 添付ファイル実体
|
|
24
26
|
* └── {id}_{filename}
|
|
@@ -10,7 +10,7 @@ import { ensureDir, writeFileContent } from '../../ports/file/file-port.js';
|
|
|
10
10
|
import { downloadConfluenceAttachment } from '../confluence/confluence-service.js';
|
|
11
11
|
import { diffText } from '../diff/diff-service.js';
|
|
12
12
|
import { downloadJiraAttachment } from '../jira/jira-service.js';
|
|
13
|
-
import { convertStorageFormatToMarkdown } from '../text-converter/text-converter.js';
|
|
13
|
+
import { convertAdfToMarkdown, convertStorageFormatToMarkdown } from '../text-converter/text-converter.js';
|
|
14
14
|
/**
|
|
15
15
|
* 現在時刻を ISO 8601 形式で取得する
|
|
16
16
|
*
|
|
@@ -51,9 +51,11 @@ const createJiraManifest = (data, options, attachmentResults) => {
|
|
|
51
51
|
* ├── manifest.json # 取得メタデータ
|
|
52
52
|
* ├── issue.json # Issue 全データ(JSON 形式)
|
|
53
53
|
* ├── description.txt # 説明文のプレーンテキスト
|
|
54
|
-
* ├── content.md # Markdown
|
|
55
|
-
* ├──
|
|
56
|
-
* ├──
|
|
54
|
+
* ├── content.md # Markdown 形式(Description + Attachments)
|
|
55
|
+
* ├── comments.md # コメント一覧(Markdown 形式)
|
|
56
|
+
* ├── changelog.md # 変更履歴(Markdown 形式)
|
|
57
|
+
* ├── changelog.json # 変更履歴(JSON 形式)
|
|
58
|
+
* ├── comments.json # コメント一覧(JSON 形式)
|
|
57
59
|
* ├── attachments.json # 添付ファイル一覧メタデータ
|
|
58
60
|
* └── attachments/ # 添付ファイル実体
|
|
59
61
|
* └── {id}_{filename}
|
|
@@ -150,7 +152,7 @@ export const saveJiraIssue = async (data, options) => {
|
|
|
150
152
|
path: descPath,
|
|
151
153
|
});
|
|
152
154
|
}
|
|
153
|
-
// content.md を保存(Markdown
|
|
155
|
+
// content.md を保存(Markdown 形式:Description + Attachments)
|
|
154
156
|
const markdownContent = formatJiraIssueAsMarkdown(data, attachmentResults);
|
|
155
157
|
const markdownPath = join(issueDir, 'content.md');
|
|
156
158
|
const markdownWriteResult = await writeFileContent(markdownPath, markdownContent);
|
|
@@ -161,6 +163,28 @@ export const saveJiraIssue = async (data, options) => {
|
|
|
161
163
|
path: markdownPath,
|
|
162
164
|
});
|
|
163
165
|
}
|
|
166
|
+
// comments.md を保存(Markdown 形式)
|
|
167
|
+
const commentsMarkdownContent = formatCommentsAsMarkdown(data, attachmentResults);
|
|
168
|
+
const commentsMarkdownPath = join(issueDir, 'comments.md');
|
|
169
|
+
const commentsMarkdownWriteResult = await writeFileContent(commentsMarkdownPath, commentsMarkdownContent);
|
|
170
|
+
if (commentsMarkdownWriteResult.isErr()) {
|
|
171
|
+
return err({
|
|
172
|
+
kind: 'FILE_WRITE_FAILED',
|
|
173
|
+
message: `comments.md の書き込みに失敗しました: ${commentsMarkdownWriteResult.error.message}`,
|
|
174
|
+
path: commentsMarkdownPath,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// changelog.md を保存(Markdown 形式)
|
|
178
|
+
const changelogMarkdownContent = formatChangelogAsMarkdown(data);
|
|
179
|
+
const changelogMarkdownPath = join(issueDir, 'changelog.md');
|
|
180
|
+
const changelogMarkdownWriteResult = await writeFileContent(changelogMarkdownPath, changelogMarkdownContent);
|
|
181
|
+
if (changelogMarkdownWriteResult.isErr()) {
|
|
182
|
+
return err({
|
|
183
|
+
kind: 'FILE_WRITE_FAILED',
|
|
184
|
+
message: `changelog.md の書き込みに失敗しました: ${changelogMarkdownWriteResult.error.message}`,
|
|
185
|
+
path: changelogMarkdownPath,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
164
188
|
// changelog.json を保存
|
|
165
189
|
const changelogPath = join(issueDir, 'changelog.json');
|
|
166
190
|
const changelogWriteResult = await writeFileContent(changelogPath, JSON.stringify(data.changelog, null, 2));
|
|
@@ -199,54 +223,38 @@ export const saveJiraIssue = async (data, options) => {
|
|
|
199
223
|
/**
|
|
200
224
|
* Jira Issue を Markdown 形式にフォーマットする(ファイル保存用)
|
|
201
225
|
*
|
|
226
|
+
* ADF (Atlassian Document Format) を Markdown に変換して保存する。
|
|
227
|
+
* Confluence と同様に構造を保持した Markdown を生成する。
|
|
228
|
+
*
|
|
202
229
|
* @param data 保存データ
|
|
203
230
|
* @param attachmentResults 添付ファイルのダウンロード結果
|
|
204
231
|
* @returns Markdown 文字列
|
|
205
232
|
*/
|
|
206
233
|
const formatJiraIssueAsMarkdown = (data, attachmentResults) => {
|
|
234
|
+
// 添付ファイル ID → savedPath のマッピングを生成
|
|
235
|
+
const attachmentPaths = {};
|
|
236
|
+
for (const att of attachmentResults) {
|
|
237
|
+
if (att.status === 'success' && att.savedPath !== undefined) {
|
|
238
|
+
attachmentPaths[att.id] = att.savedPath;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
207
241
|
const lines = [];
|
|
208
242
|
// Title
|
|
209
243
|
lines.push(`# ${data.key}`);
|
|
210
244
|
lines.push('');
|
|
211
245
|
lines.push(`**${data.summary}**`);
|
|
212
246
|
lines.push('');
|
|
213
|
-
// Description
|
|
247
|
+
// Description - ADF を Markdown に変換
|
|
214
248
|
lines.push('## Description');
|
|
215
249
|
lines.push('');
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
lines.push('## Comments');
|
|
220
|
-
lines.push('');
|
|
221
|
-
if (data.comments.length === 0) {
|
|
222
|
-
lines.push('No comments');
|
|
250
|
+
if (data.description !== null && data.description !== undefined) {
|
|
251
|
+
const descriptionMarkdown = convertAdfToMarkdown(data.description, attachmentPaths);
|
|
252
|
+
lines.push(descriptionMarkdown || '(No description)');
|
|
223
253
|
}
|
|
224
254
|
else {
|
|
225
|
-
|
|
226
|
-
lines.push(`### ${comment.author} (${comment.created})`);
|
|
227
|
-
lines.push('');
|
|
228
|
-
lines.push(comment.body);
|
|
229
|
-
lines.push('');
|
|
230
|
-
}
|
|
255
|
+
lines.push('(No description)');
|
|
231
256
|
}
|
|
232
|
-
// Changelog
|
|
233
|
-
lines.push('## Changelog');
|
|
234
257
|
lines.push('');
|
|
235
|
-
if (data.changelog.length === 0) {
|
|
236
|
-
lines.push('No changelog');
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
for (const entry of data.changelog) {
|
|
240
|
-
lines.push(`### ${entry.author} (${entry.created})`);
|
|
241
|
-
lines.push('');
|
|
242
|
-
for (const item of entry.items) {
|
|
243
|
-
const from = item.fromString ?? '(empty)';
|
|
244
|
-
const to = item.toString ?? '(empty)';
|
|
245
|
-
lines.push(`- **${item.field}**: ${from} → ${to}`);
|
|
246
|
-
}
|
|
247
|
-
lines.push('');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
258
|
// Attachments
|
|
251
259
|
lines.push('## Attachments');
|
|
252
260
|
lines.push('');
|
|
@@ -272,6 +280,66 @@ const formatJiraIssueAsMarkdown = (data, attachmentResults) => {
|
|
|
272
280
|
}
|
|
273
281
|
return lines.join('\n');
|
|
274
282
|
};
|
|
283
|
+
/**
|
|
284
|
+
* Jira コメントを Markdown 形式にフォーマットする(ファイル保存用)
|
|
285
|
+
*
|
|
286
|
+
* @param data 保存データ
|
|
287
|
+
* @param attachmentResults 添付ファイルのダウンロード結果
|
|
288
|
+
* @returns Markdown 文字列
|
|
289
|
+
*/
|
|
290
|
+
const formatCommentsAsMarkdown = (data, attachmentResults) => {
|
|
291
|
+
// 添付ファイル ID → savedPath のマッピングを生成
|
|
292
|
+
const attachmentPaths = {};
|
|
293
|
+
for (const att of attachmentResults) {
|
|
294
|
+
if (att.status === 'success' && att.savedPath !== undefined) {
|
|
295
|
+
attachmentPaths[att.id] = att.savedPath;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const lines = [];
|
|
299
|
+
lines.push(`# ${data.key} - Comments`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
if (data.comments.length === 0) {
|
|
302
|
+
lines.push('No comments');
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
for (const comment of data.comments) {
|
|
306
|
+
lines.push(`## ${comment.author} (${comment.created})`);
|
|
307
|
+
lines.push('');
|
|
308
|
+
// コメント本文も ADF 形式なので Markdown に変換
|
|
309
|
+
const commentMarkdown = convertAdfToMarkdown(comment.bodyAdf, attachmentPaths);
|
|
310
|
+
lines.push(commentMarkdown || comment.body);
|
|
311
|
+
lines.push('');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Jira 変更履歴を Markdown 形式にフォーマットする(ファイル保存用)
|
|
318
|
+
*
|
|
319
|
+
* @param data 保存データ
|
|
320
|
+
* @returns Markdown 文字列
|
|
321
|
+
*/
|
|
322
|
+
const formatChangelogAsMarkdown = (data) => {
|
|
323
|
+
const lines = [];
|
|
324
|
+
lines.push(`# ${data.key} - Changelog`);
|
|
325
|
+
lines.push('');
|
|
326
|
+
if (data.changelog.length === 0) {
|
|
327
|
+
lines.push('No changelog');
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
for (const entry of data.changelog) {
|
|
331
|
+
lines.push(`## ${entry.author} (${entry.created})`);
|
|
332
|
+
lines.push('');
|
|
333
|
+
for (const item of entry.items) {
|
|
334
|
+
const from = item.fromString ?? '(empty)';
|
|
335
|
+
const to = item.toString ?? '(empty)';
|
|
336
|
+
lines.push(`- **${item.field}**: ${from} → ${to}`);
|
|
337
|
+
}
|
|
338
|
+
lines.push('');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return lines.join('\n');
|
|
342
|
+
};
|
|
275
343
|
/**
|
|
276
344
|
* Confluence ページ用の Manifest を生成する
|
|
277
345
|
*
|
|
@@ -735,4 +803,197 @@ if (import.meta.vitest) {
|
|
|
735
803
|
});
|
|
736
804
|
});
|
|
737
805
|
});
|
|
806
|
+
describe('formatCommentsAsMarkdown (in-source testing)', () => {
|
|
807
|
+
// テストの目的: コメントがない場合に "No comments" が出力されること
|
|
808
|
+
it('Given: コメントがない JiraSaveData, When: formatCommentsAsMarkdown を呼び出す, Then: "No comments" が出力される', () => {
|
|
809
|
+
const data = {
|
|
810
|
+
attachments: [],
|
|
811
|
+
changelog: [],
|
|
812
|
+
comments: [],
|
|
813
|
+
description: null,
|
|
814
|
+
descriptionPlainText: null,
|
|
815
|
+
key: 'TEST-001',
|
|
816
|
+
summary: 'テスト',
|
|
817
|
+
};
|
|
818
|
+
const result = formatCommentsAsMarkdown(data, []);
|
|
819
|
+
expect(result).toContain('# TEST-001 - Comments');
|
|
820
|
+
expect(result).toContain('No comments');
|
|
821
|
+
});
|
|
822
|
+
// テストの目的: コメントが正しくフォーマットされること
|
|
823
|
+
it('Given: コメントを含む JiraSaveData, When: formatCommentsAsMarkdown を呼び出す, Then: コメントが Markdown 形式でフォーマットされる', () => {
|
|
824
|
+
const data = {
|
|
825
|
+
attachments: [],
|
|
826
|
+
changelog: [],
|
|
827
|
+
comments: [
|
|
828
|
+
{
|
|
829
|
+
author: 'TestUser',
|
|
830
|
+
body: 'テストコメント',
|
|
831
|
+
bodyAdf: null, // bodyAdf が null の場合は body が使用される
|
|
832
|
+
created: '2024-01-15T10:30:00.000Z',
|
|
833
|
+
id: 'cmt-1',
|
|
834
|
+
updated: '2024-01-15T10:30:00.000Z',
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
description: null,
|
|
838
|
+
descriptionPlainText: null,
|
|
839
|
+
key: 'TEST-002',
|
|
840
|
+
summary: 'テスト',
|
|
841
|
+
};
|
|
842
|
+
const result = formatCommentsAsMarkdown(data, []);
|
|
843
|
+
expect(result).toContain('# TEST-002 - Comments');
|
|
844
|
+
expect(result).toContain('## TestUser');
|
|
845
|
+
expect(result).toContain('2024-01-15T10:30:00.000Z');
|
|
846
|
+
expect(result).toContain('テストコメント');
|
|
847
|
+
});
|
|
848
|
+
// テストの目的: 複数のコメントが正しくフォーマットされること
|
|
849
|
+
it('Given: 複数のコメントを含む JiraSaveData, When: formatCommentsAsMarkdown を呼び出す, Then: すべてのコメントが Markdown 形式でフォーマットされる', () => {
|
|
850
|
+
const data = {
|
|
851
|
+
attachments: [],
|
|
852
|
+
changelog: [],
|
|
853
|
+
comments: [
|
|
854
|
+
{
|
|
855
|
+
author: 'User1',
|
|
856
|
+
body: 'コメント1',
|
|
857
|
+
bodyAdf: null,
|
|
858
|
+
created: '2024-01-15T10:00:00.000Z',
|
|
859
|
+
id: 'cmt-1',
|
|
860
|
+
updated: '2024-01-15T10:00:00.000Z',
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
author: 'User2',
|
|
864
|
+
body: 'コメント2',
|
|
865
|
+
bodyAdf: null,
|
|
866
|
+
created: '2024-01-16T11:00:00.000Z',
|
|
867
|
+
id: 'cmt-2',
|
|
868
|
+
updated: '2024-01-16T11:00:00.000Z',
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
description: null,
|
|
872
|
+
descriptionPlainText: null,
|
|
873
|
+
key: 'TEST-003',
|
|
874
|
+
summary: 'テスト',
|
|
875
|
+
};
|
|
876
|
+
const result = formatCommentsAsMarkdown(data, []);
|
|
877
|
+
expect(result).toContain('## User1');
|
|
878
|
+
expect(result).toContain('## User2');
|
|
879
|
+
expect(result).toContain('コメント1');
|
|
880
|
+
expect(result).toContain('コメント2');
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
describe('formatChangelogAsMarkdown (in-source testing)', () => {
|
|
884
|
+
// テストの目的: 変更履歴がない場合に "No changelog" が出力されること
|
|
885
|
+
it('Given: 変更履歴がない JiraSaveData, When: formatChangelogAsMarkdown を呼び出す, Then: "No changelog" が出力される', () => {
|
|
886
|
+
const data = {
|
|
887
|
+
attachments: [],
|
|
888
|
+
changelog: [],
|
|
889
|
+
comments: [],
|
|
890
|
+
description: null,
|
|
891
|
+
descriptionPlainText: null,
|
|
892
|
+
key: 'TEST-001',
|
|
893
|
+
summary: 'テスト',
|
|
894
|
+
};
|
|
895
|
+
const result = formatChangelogAsMarkdown(data);
|
|
896
|
+
expect(result).toContain('# TEST-001 - Changelog');
|
|
897
|
+
expect(result).toContain('No changelog');
|
|
898
|
+
});
|
|
899
|
+
// テストの目的: 変更履歴が正しくフォーマットされること
|
|
900
|
+
it('Given: 変更履歴を含む JiraSaveData, When: formatChangelogAsMarkdown を呼び出す, Then: 変更履歴が Markdown 形式でフォーマットされる', () => {
|
|
901
|
+
const data = {
|
|
902
|
+
attachments: [],
|
|
903
|
+
changelog: [
|
|
904
|
+
{
|
|
905
|
+
author: 'ChangeUser',
|
|
906
|
+
created: '2024-01-15T10:00:00.000Z',
|
|
907
|
+
id: 'cl-1',
|
|
908
|
+
items: [{ field: 'status', fromString: 'Open', toString: 'In Progress' }],
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
comments: [],
|
|
912
|
+
description: null,
|
|
913
|
+
descriptionPlainText: null,
|
|
914
|
+
key: 'TEST-002',
|
|
915
|
+
summary: 'テスト',
|
|
916
|
+
};
|
|
917
|
+
const result = formatChangelogAsMarkdown(data);
|
|
918
|
+
expect(result).toContain('# TEST-002 - Changelog');
|
|
919
|
+
expect(result).toContain('## ChangeUser');
|
|
920
|
+
expect(result).toContain('2024-01-15T10:00:00.000Z');
|
|
921
|
+
expect(result).toContain('**status**');
|
|
922
|
+
expect(result).toContain('Open');
|
|
923
|
+
expect(result).toContain('In Progress');
|
|
924
|
+
});
|
|
925
|
+
// テストの目的: null 値が (empty) として表示されること
|
|
926
|
+
it('Given: fromString が null の変更履歴, When: formatChangelogAsMarkdown を呼び出す, Then: (empty) が表示される', () => {
|
|
927
|
+
const data = {
|
|
928
|
+
attachments: [],
|
|
929
|
+
changelog: [
|
|
930
|
+
{
|
|
931
|
+
author: 'ChangeUser',
|
|
932
|
+
created: '2024-01-15T10:00:00.000Z',
|
|
933
|
+
id: 'cl-1',
|
|
934
|
+
items: [{ field: 'assignee', fromString: null, toString: 'Developer' }],
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
comments: [],
|
|
938
|
+
description: null,
|
|
939
|
+
descriptionPlainText: null,
|
|
940
|
+
key: 'TEST-003',
|
|
941
|
+
summary: 'テスト',
|
|
942
|
+
};
|
|
943
|
+
const result = formatChangelogAsMarkdown(data);
|
|
944
|
+
expect(result).toContain('(empty)');
|
|
945
|
+
expect(result).toContain('Developer');
|
|
946
|
+
});
|
|
947
|
+
// テストの目的: toString が null の場合も (empty) として表示されること
|
|
948
|
+
it('Given: toString が null の変更履歴, When: formatChangelogAsMarkdown を呼び出す, Then: (empty) が表示される', () => {
|
|
949
|
+
const data = {
|
|
950
|
+
attachments: [],
|
|
951
|
+
changelog: [
|
|
952
|
+
{
|
|
953
|
+
author: 'ChangeUser',
|
|
954
|
+
created: '2024-01-15T10:00:00.000Z',
|
|
955
|
+
id: 'cl-1',
|
|
956
|
+
items: [{ field: 'assignee', fromString: 'Developer', toString: null }],
|
|
957
|
+
},
|
|
958
|
+
],
|
|
959
|
+
comments: [],
|
|
960
|
+
description: null,
|
|
961
|
+
descriptionPlainText: null,
|
|
962
|
+
key: 'TEST-004',
|
|
963
|
+
summary: 'テスト',
|
|
964
|
+
};
|
|
965
|
+
const result = formatChangelogAsMarkdown(data);
|
|
966
|
+
expect(result).toContain('Developer');
|
|
967
|
+
expect(result).toContain('(empty)');
|
|
968
|
+
});
|
|
969
|
+
// テストの目的: 複数の変更項目が正しくフォーマットされること
|
|
970
|
+
it('Given: 複数の変更項目を含む変更履歴, When: formatChangelogAsMarkdown を呼び出す, Then: すべての項目が Markdown 形式でフォーマットされる', () => {
|
|
971
|
+
const data = {
|
|
972
|
+
attachments: [],
|
|
973
|
+
changelog: [
|
|
974
|
+
{
|
|
975
|
+
author: 'ChangeUser',
|
|
976
|
+
created: '2024-01-15T10:00:00.000Z',
|
|
977
|
+
id: 'cl-1',
|
|
978
|
+
items: [
|
|
979
|
+
{ field: 'status', fromString: 'Open', toString: 'In Progress' },
|
|
980
|
+
{ field: 'priority', fromString: 'Low', toString: 'High' },
|
|
981
|
+
],
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
comments: [],
|
|
985
|
+
description: null,
|
|
986
|
+
descriptionPlainText: null,
|
|
987
|
+
key: 'TEST-005',
|
|
988
|
+
summary: 'テスト',
|
|
989
|
+
};
|
|
990
|
+
const result = formatChangelogAsMarkdown(data);
|
|
991
|
+
expect(result).toContain('**status**');
|
|
992
|
+
expect(result).toContain('**priority**');
|
|
993
|
+
expect(result).toContain('Open');
|
|
994
|
+
expect(result).toContain('In Progress');
|
|
995
|
+
expect(result).toContain('Low');
|
|
996
|
+
expect(result).toContain('High');
|
|
997
|
+
});
|
|
998
|
+
});
|
|
738
999
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADF(Atlassian Document Format)→ HTML 変換
|
|
3
|
+
*
|
|
4
|
+
* Markdown 変換の中間形式として HTML を生成する
|
|
5
|
+
*/
|
|
6
|
+
import type { AdfMark, AdfNode, AttachmentPathMapping } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* HTML 特殊文字をエスケープする
|
|
9
|
+
*
|
|
10
|
+
* @param text エスケープ対象の文字列
|
|
11
|
+
* @returns エスケープ済み文字列
|
|
12
|
+
*/
|
|
13
|
+
export declare const escapeHtml: (text: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* ADF マークを HTML タグで囲む
|
|
16
|
+
*
|
|
17
|
+
* @param text 対象のテキスト
|
|
18
|
+
* @param marks 適用するマーク配列
|
|
19
|
+
* @returns マークを適用した HTML
|
|
20
|
+
*/
|
|
21
|
+
export declare const applyMarksToHtml: (text: string, marks: readonly AdfMark[]) => string;
|
|
22
|
+
/**
|
|
23
|
+
* ADF ノードを HTML に変換する
|
|
24
|
+
*
|
|
25
|
+
* @param node ADF ノード
|
|
26
|
+
* @param attachmentPaths 添付ファイル ID → ローカルパスのマッピング
|
|
27
|
+
* @returns HTML 文字列
|
|
28
|
+
*/
|
|
29
|
+
export declare const convertAdfNodeToHtml: (node: AdfNode, attachmentPaths?: AttachmentPathMapping) => string;
|
|
30
|
+
/**
|
|
31
|
+
* ADF ドキュメントを HTML に変換する
|
|
32
|
+
*
|
|
33
|
+
* @param content ADF ドキュメントのコンテンツ配列
|
|
34
|
+
* @param attachmentPaths 添付ファイル ID → ローカルパスのマッピング
|
|
35
|
+
* @returns HTML 文字列
|
|
36
|
+
*/
|
|
37
|
+
export declare const convertAdfContentToHtml: (content: readonly AdfNode[] | undefined, attachmentPaths?: AttachmentPathMapping) => string;
|