atl-fetch 1.0.0 → 1.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.
@@ -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
- * ├── changelog.json # 変更履歴
56
- * ├── comments.json # コメント一覧
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
- lines.push(data.descriptionPlainText ?? '(No description)');
217
- lines.push('');
218
- // Comments
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
- for (const comment of data.comments) {
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
  }
@@ -24,6 +24,14 @@ export declare const convertAdfToPlainText: (adf: unknown) => string;
24
24
  * @returns プレーンテキスト
25
25
  */
26
26
  export declare const convertStorageFormatToPlainText: (storageFormat: string | null | undefined) => string;
27
+ /**
28
+ * ADF(Atlassian Document Format)を Markdown に変換する
29
+ *
30
+ * @param adf ADF ドキュメント(オブジェクトまたは JSON 文字列)
31
+ * @param attachmentPaths 添付ファイル ID → ローカルパスのマッピング
32
+ * @returns Markdown 文字列
33
+ */
34
+ export declare const convertAdfToMarkdown: (adf: unknown, attachmentPaths?: AttachmentPathMapping) => string;
27
35
  /**
28
36
  * Confluence Storage Format(XHTML)を Markdown に変換する
29
37
  *