atl-fetch 1.1.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.
- 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/text-converter.d.ts +8 -0
- package/dist/services/text-converter/text-converter.js +293 -20
- package/dist/types/jira.d.ts +5 -1
- package/dist/types/storage.d.ts +3 -3
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -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
|
*
|
|
@@ -135,6 +135,236 @@ export const convertAdfToPlainText = (adf) => {
|
|
|
135
135
|
}
|
|
136
136
|
return '';
|
|
137
137
|
};
|
|
138
|
+
// ============================================================
|
|
139
|
+
// ADF → Markdown 変換(Confluence と同じ TurndownService を使用)
|
|
140
|
+
// ============================================================
|
|
141
|
+
/**
|
|
142
|
+
* HTML 特殊文字をエスケープする
|
|
143
|
+
*
|
|
144
|
+
* @param text エスケープ対象の文字列
|
|
145
|
+
* @returns エスケープ済み文字列
|
|
146
|
+
*/
|
|
147
|
+
const escapeHtml = (text) => {
|
|
148
|
+
return text
|
|
149
|
+
.replace(/&/g, '&')
|
|
150
|
+
.replace(/</g, '<')
|
|
151
|
+
.replace(/>/g, '>')
|
|
152
|
+
.replace(/"/g, '"')
|
|
153
|
+
.replace(/'/g, ''');
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* ADF マークを HTML タグで囲む
|
|
157
|
+
*
|
|
158
|
+
* @param text 対象のテキスト
|
|
159
|
+
* @param marks 適用するマーク配列
|
|
160
|
+
* @returns マークを適用した HTML
|
|
161
|
+
*/
|
|
162
|
+
const applyMarksToHtml = (text, marks) => {
|
|
163
|
+
let result = text;
|
|
164
|
+
for (const mark of marks) {
|
|
165
|
+
switch (mark.type) {
|
|
166
|
+
case 'strong':
|
|
167
|
+
result = `<strong>${result}</strong>`;
|
|
168
|
+
break;
|
|
169
|
+
case 'em':
|
|
170
|
+
result = `<em>${result}</em>`;
|
|
171
|
+
break;
|
|
172
|
+
case 'code':
|
|
173
|
+
result = `<code>${result}</code>`;
|
|
174
|
+
break;
|
|
175
|
+
case 'strike':
|
|
176
|
+
result = `<s>${result}</s>`;
|
|
177
|
+
break;
|
|
178
|
+
case 'underline':
|
|
179
|
+
result = `<u>${result}</u>`;
|
|
180
|
+
break;
|
|
181
|
+
case 'link': {
|
|
182
|
+
const href = mark.attrs?.['href'];
|
|
183
|
+
if (typeof href === 'string') {
|
|
184
|
+
result = `<a href="${escapeHtml(href)}">${result}</a>`;
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case 'textColor': {
|
|
189
|
+
const color = mark.attrs?.['color'];
|
|
190
|
+
if (typeof color === 'string') {
|
|
191
|
+
result = `<span style="color: ${escapeHtml(color)}">${result}</span>`;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'subsup': {
|
|
196
|
+
const subType = mark.attrs?.['type'];
|
|
197
|
+
if (subType === 'sub') {
|
|
198
|
+
result = `<sub>${result}</sub>`;
|
|
199
|
+
}
|
|
200
|
+
else if (subType === 'sup') {
|
|
201
|
+
result = `<sup>${result}</sup>`;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
// 未知のマークタイプは無視
|
|
206
|
+
default:
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* ADF ノードを HTML に変換する
|
|
214
|
+
*
|
|
215
|
+
* @param node ADF ノード
|
|
216
|
+
* @param attachmentPaths 添付ファイル ID → ローカルパスのマッピング
|
|
217
|
+
* @returns HTML 文字列
|
|
218
|
+
*/
|
|
219
|
+
const convertAdfNodeToHtml = (node, attachmentPaths) => {
|
|
220
|
+
// テキストノードの場合
|
|
221
|
+
if (node.type === 'text' && node.text !== undefined) {
|
|
222
|
+
const escapedText = escapeHtml(node.text);
|
|
223
|
+
if (node.marks !== undefined && node.marks.length > 0) {
|
|
224
|
+
return applyMarksToHtml(escapedText, node.marks);
|
|
225
|
+
}
|
|
226
|
+
return escapedText;
|
|
227
|
+
}
|
|
228
|
+
// hardBreak の場合
|
|
229
|
+
if (node.type === 'hardBreak') {
|
|
230
|
+
return '<br>';
|
|
231
|
+
}
|
|
232
|
+
// rule(水平線)の場合
|
|
233
|
+
if (node.type === 'rule') {
|
|
234
|
+
return '<hr>';
|
|
235
|
+
}
|
|
236
|
+
// メンションの場合
|
|
237
|
+
if (node.type === 'mention' && node.attrs !== undefined) {
|
|
238
|
+
const text = node.attrs['text'];
|
|
239
|
+
if (typeof text === 'string') {
|
|
240
|
+
return escapeHtml(text);
|
|
241
|
+
}
|
|
242
|
+
return '@ユーザー';
|
|
243
|
+
}
|
|
244
|
+
// 絵文字の場合
|
|
245
|
+
if (node.type === 'emoji' && node.attrs !== undefined) {
|
|
246
|
+
const text = node.attrs['text'];
|
|
247
|
+
const shortName = node.attrs['shortName'];
|
|
248
|
+
if (typeof text === 'string') {
|
|
249
|
+
return text;
|
|
250
|
+
}
|
|
251
|
+
if (typeof shortName === 'string') {
|
|
252
|
+
return shortName;
|
|
253
|
+
}
|
|
254
|
+
return '';
|
|
255
|
+
}
|
|
256
|
+
// media ノードの場合(添付ファイル)
|
|
257
|
+
if (node.type === 'media' && node.attrs !== undefined) {
|
|
258
|
+
const mediaId = node.attrs['id'];
|
|
259
|
+
const mediaType = node.attrs['type'];
|
|
260
|
+
if (typeof mediaId === 'string' && attachmentPaths?.[mediaId] !== undefined) {
|
|
261
|
+
const localPath = attachmentPaths[mediaId];
|
|
262
|
+
const alt = typeof node.attrs['alt'] === 'string' ? node.attrs['alt'] : mediaId;
|
|
263
|
+
return `<img src="${escapeHtml(localPath)}" alt="${escapeHtml(alt)}">`;
|
|
264
|
+
}
|
|
265
|
+
// 外部リンクの場合
|
|
266
|
+
if (mediaType === 'external' || mediaType === 'link') {
|
|
267
|
+
const url = node.attrs['url'];
|
|
268
|
+
if (typeof url === 'string') {
|
|
269
|
+
return `<img src="${escapeHtml(url)}" alt="">`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// マッピングがない場合はプレースホルダー
|
|
273
|
+
return '[添付ファイル]';
|
|
274
|
+
}
|
|
275
|
+
// mediaSingle(メディアコンテナ)の場合
|
|
276
|
+
if (node.type === 'mediaSingle' && node.content !== undefined) {
|
|
277
|
+
return node.content.map((child) => convertAdfNodeToHtml(child, attachmentPaths)).join('');
|
|
278
|
+
}
|
|
279
|
+
// mediaGroup の場合
|
|
280
|
+
if (node.type === 'mediaGroup' && node.content !== undefined) {
|
|
281
|
+
return node.content.map((child) => convertAdfNodeToHtml(child, attachmentPaths)).join('');
|
|
282
|
+
}
|
|
283
|
+
// inlineCard(インラインリンク)の場合
|
|
284
|
+
if (node.type === 'inlineCard' && node.attrs !== undefined) {
|
|
285
|
+
const url = node.attrs['url'];
|
|
286
|
+
if (typeof url === 'string') {
|
|
287
|
+
return `<a href="${escapeHtml(url)}">${escapeHtml(url)}</a>`;
|
|
288
|
+
}
|
|
289
|
+
return '';
|
|
290
|
+
}
|
|
291
|
+
// blockCard(ブロックリンク)の場合
|
|
292
|
+
if (node.type === 'blockCard' && node.attrs !== undefined) {
|
|
293
|
+
const url = node.attrs['url'];
|
|
294
|
+
if (typeof url === 'string') {
|
|
295
|
+
return `<p><a href="${escapeHtml(url)}">${escapeHtml(url)}</a></p>`;
|
|
296
|
+
}
|
|
297
|
+
return '';
|
|
298
|
+
}
|
|
299
|
+
// 子ノードがある場合
|
|
300
|
+
if (node.content !== undefined && Array.isArray(node.content)) {
|
|
301
|
+
const childrenHtml = node.content.map((child) => convertAdfNodeToHtml(child, attachmentPaths)).join('');
|
|
302
|
+
switch (node.type) {
|
|
303
|
+
case 'doc':
|
|
304
|
+
return childrenHtml;
|
|
305
|
+
case 'paragraph':
|
|
306
|
+
return `<p>${childrenHtml}</p>`;
|
|
307
|
+
case 'heading': {
|
|
308
|
+
const level = typeof node.attrs?.['level'] === 'number' ? node.attrs['level'] : 1;
|
|
309
|
+
const safeLevel = Math.max(1, Math.min(6, level));
|
|
310
|
+
return `<h${safeLevel}>${childrenHtml}</h${safeLevel}>`;
|
|
311
|
+
}
|
|
312
|
+
case 'bulletList':
|
|
313
|
+
return `<ul>${childrenHtml}</ul>`;
|
|
314
|
+
case 'orderedList':
|
|
315
|
+
return `<ol>${childrenHtml}</ol>`;
|
|
316
|
+
case 'listItem':
|
|
317
|
+
return `<li>${childrenHtml}</li>`;
|
|
318
|
+
case 'blockquote':
|
|
319
|
+
return `<blockquote>${childrenHtml}</blockquote>`;
|
|
320
|
+
case 'codeBlock': {
|
|
321
|
+
const language = typeof node.attrs?.['language'] === 'string' ? node.attrs['language'] : '';
|
|
322
|
+
const langClass = language ? ` class="language-${escapeHtml(language)}"` : '';
|
|
323
|
+
// コードブロック内のテキストは子ノードから取得
|
|
324
|
+
const codeText = node.content
|
|
325
|
+
.map((child) => (child.type === 'text' && child.text !== undefined ? child.text : ''))
|
|
326
|
+
.join('');
|
|
327
|
+
return `<pre><code${langClass}>${escapeHtml(codeText)}</code></pre>`;
|
|
328
|
+
}
|
|
329
|
+
case 'table':
|
|
330
|
+
return `<table>${childrenHtml}</table>`;
|
|
331
|
+
case 'tableRow':
|
|
332
|
+
return `<tr>${childrenHtml}</tr>`;
|
|
333
|
+
case 'tableHeader':
|
|
334
|
+
return `<th>${childrenHtml}</th>`;
|
|
335
|
+
case 'tableCell':
|
|
336
|
+
return `<td>${childrenHtml}</td>`;
|
|
337
|
+
case 'panel': {
|
|
338
|
+
// panel タイプを GitHub Alerts 形式に変換
|
|
339
|
+
const panelType = typeof node.attrs?.['panelType'] === 'string' ? node.attrs['panelType'] : 'info';
|
|
340
|
+
const alertTypeMap = {
|
|
341
|
+
error: 'WARNING',
|
|
342
|
+
info: 'NOTE',
|
|
343
|
+
note: 'NOTE',
|
|
344
|
+
success: 'TIP',
|
|
345
|
+
warning: 'WARNING',
|
|
346
|
+
};
|
|
347
|
+
const alertType = alertTypeMap[panelType] || 'NOTE';
|
|
348
|
+
return `<blockquote data-github-alert="${alertType}">${childrenHtml}</blockquote>`;
|
|
349
|
+
}
|
|
350
|
+
// 未知のノードタイプは子ノードの内容を返す
|
|
351
|
+
default:
|
|
352
|
+
return childrenHtml;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// 子ノードもテキストもない場合は空文字列
|
|
356
|
+
return '';
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* ADF ドキュメントを HTML に変換する
|
|
360
|
+
*
|
|
361
|
+
* @param content ADF ドキュメントのコンテンツ配列
|
|
362
|
+
* @param attachmentPaths 添付ファイル ID → ローカルパスのマッピング
|
|
363
|
+
* @returns HTML 文字列
|
|
364
|
+
*/
|
|
365
|
+
const convertAdfContentToHtml = (content, attachmentPaths) => {
|
|
366
|
+
return content.map((node) => convertAdfNodeToHtml(node, attachmentPaths)).join('');
|
|
367
|
+
};
|
|
138
368
|
/**
|
|
139
369
|
* HTML エンティティをデコードする
|
|
140
370
|
*
|
|
@@ -348,20 +578,12 @@ const preprocessHtmlForMarkdown = (html, attachmentPaths) => {
|
|
|
348
578
|
return result;
|
|
349
579
|
};
|
|
350
580
|
/**
|
|
351
|
-
*
|
|
581
|
+
* TurndownService インスタンスを作成する(共通設定)
|
|
582
|
+
* Jira ADF と Confluence Storage Format の両方で使用する
|
|
352
583
|
*
|
|
353
|
-
* @
|
|
354
|
-
* @param attachmentPaths 添付ファイル名 → ローカルパスのマッピング
|
|
355
|
-
* @returns Markdown 文字列
|
|
584
|
+
* @returns 設定済みの TurndownService インスタンス
|
|
356
585
|
*/
|
|
357
|
-
|
|
358
|
-
// null または undefined の場合は空文字列を返す
|
|
359
|
-
if (storageFormat === null || storageFormat === undefined || storageFormat === '') {
|
|
360
|
-
return '';
|
|
361
|
-
}
|
|
362
|
-
// 前処理
|
|
363
|
-
const preprocessedHtml = preprocessHtmlForMarkdown(storageFormat, attachmentPaths);
|
|
364
|
-
// Turndown インスタンス作成
|
|
586
|
+
const createTurndownService = () => {
|
|
365
587
|
const turndownService = new TurndownService({
|
|
366
588
|
bulletListMarker: '-',
|
|
367
589
|
codeBlockStyle: 'fenced',
|
|
@@ -371,9 +593,7 @@ export const convertStorageFormatToMarkdown = (storageFormat, attachmentPaths) =
|
|
|
371
593
|
});
|
|
372
594
|
// GFM プラグイン(テーブル、取り消し線など)を使用
|
|
373
595
|
turndownService.use(gfm);
|
|
374
|
-
// --------------------------------------------------
|
|
375
596
|
// カスタムルール: キャプション付き画像(<figure>)
|
|
376
|
-
// --------------------------------------------------
|
|
377
597
|
turndownService.addRule('figureWithCaption', {
|
|
378
598
|
filter: (node) => {
|
|
379
599
|
return node.nodeName === 'FIGURE';
|
|
@@ -397,9 +617,7 @@ export const convertStorageFormatToMarkdown = (storageFormat, attachmentPaths) =
|
|
|
397
617
|
return '';
|
|
398
618
|
},
|
|
399
619
|
});
|
|
400
|
-
// --------------------------------------------------
|
|
401
620
|
// カスタムルール: GitHub Alerts(<blockquote data-github-alert="...">)
|
|
402
|
-
// --------------------------------------------------
|
|
403
621
|
turndownService.addRule('githubAlerts', {
|
|
404
622
|
filter: (node) => {
|
|
405
623
|
if (node.nodeName !== 'BLOCKQUOTE')
|
|
@@ -417,9 +635,7 @@ export const convertStorageFormatToMarkdown = (storageFormat, attachmentPaths) =
|
|
|
417
635
|
return `\n> [!${alertType}]\n${quotedContent}\n`;
|
|
418
636
|
},
|
|
419
637
|
});
|
|
420
|
-
// --------------------------------------------------
|
|
421
638
|
// カスタムルール: 色変更テキスト(HTML のまま出力)
|
|
422
|
-
// --------------------------------------------------
|
|
423
639
|
turndownService.addRule('coloredText', {
|
|
424
640
|
filter: (node) => {
|
|
425
641
|
if (node.nodeName !== 'SPAN')
|
|
@@ -432,9 +648,7 @@ export const convertStorageFormatToMarkdown = (storageFormat, attachmentPaths) =
|
|
|
432
648
|
return node.outerHTML;
|
|
433
649
|
},
|
|
434
650
|
});
|
|
435
|
-
// --------------------------------------------------
|
|
436
651
|
// カスタムルール: セル結合/セル内改行のあるテーブル(HTML のまま出力)
|
|
437
|
-
// --------------------------------------------------
|
|
438
652
|
turndownService.addRule('complexTable', {
|
|
439
653
|
filter: (node) => {
|
|
440
654
|
if (node.nodeName !== 'TABLE')
|
|
@@ -447,6 +661,65 @@ export const convertStorageFormatToMarkdown = (storageFormat, attachmentPaths) =
|
|
|
447
661
|
return node.outerHTML;
|
|
448
662
|
},
|
|
449
663
|
});
|
|
664
|
+
return turndownService;
|
|
665
|
+
};
|
|
666
|
+
/**
|
|
667
|
+
* ADF(Atlassian Document Format)を Markdown に変換する
|
|
668
|
+
*
|
|
669
|
+
* @param adf ADF ドキュメント(オブジェクトまたは JSON 文字列)
|
|
670
|
+
* @param attachmentPaths 添付ファイル ID → ローカルパスのマッピング
|
|
671
|
+
* @returns Markdown 文字列
|
|
672
|
+
*/
|
|
673
|
+
export const convertAdfToMarkdown = (adf, attachmentPaths) => {
|
|
674
|
+
// null または undefined の場合は空文字列を返す
|
|
675
|
+
if (adf === null || adf === undefined) {
|
|
676
|
+
return '';
|
|
677
|
+
}
|
|
678
|
+
// 文字列の場合は JSON としてパースを試みる
|
|
679
|
+
if (typeof adf === 'string') {
|
|
680
|
+
// 空文字列の場合
|
|
681
|
+
if (adf === '') {
|
|
682
|
+
return '';
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const parsed = JSON.parse(adf);
|
|
686
|
+
if (isAdfDocument(parsed)) {
|
|
687
|
+
const html = convertAdfContentToHtml(parsed.content, attachmentPaths);
|
|
688
|
+
const turndownService = createTurndownService();
|
|
689
|
+
return turndownService.turndown(html).trim();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// JSON パースに失敗した場合は元の文字列を返す
|
|
694
|
+
return adf;
|
|
695
|
+
}
|
|
696
|
+
// パースできたが ADF 形式でない場合は元の文字列を返す
|
|
697
|
+
return adf;
|
|
698
|
+
}
|
|
699
|
+
// オブジェクトの場合は ADF ドキュメントとして処理
|
|
700
|
+
if (isAdfDocument(adf)) {
|
|
701
|
+
const html = convertAdfContentToHtml(adf.content, attachmentPaths);
|
|
702
|
+
const turndownService = createTurndownService();
|
|
703
|
+
return turndownService.turndown(html).trim();
|
|
704
|
+
}
|
|
705
|
+
return '';
|
|
706
|
+
};
|
|
707
|
+
/**
|
|
708
|
+
* Confluence Storage Format(XHTML)を Markdown に変換する
|
|
709
|
+
*
|
|
710
|
+
* @param storageFormat Storage Format 文字列(HTML/XHTML)
|
|
711
|
+
* @param attachmentPaths 添付ファイル名 → ローカルパスのマッピング
|
|
712
|
+
* @returns Markdown 文字列
|
|
713
|
+
*/
|
|
714
|
+
export const convertStorageFormatToMarkdown = (storageFormat, attachmentPaths) => {
|
|
715
|
+
// null または undefined の場合は空文字列を返す
|
|
716
|
+
if (storageFormat === null || storageFormat === undefined || storageFormat === '') {
|
|
717
|
+
return '';
|
|
718
|
+
}
|
|
719
|
+
// 前処理
|
|
720
|
+
const preprocessedHtml = preprocessHtmlForMarkdown(storageFormat, attachmentPaths);
|
|
721
|
+
// 共通の TurndownService を使用
|
|
722
|
+
const turndownService = createTurndownService();
|
|
450
723
|
// Markdown に変換
|
|
451
724
|
const markdown = turndownService.turndown(preprocessedHtml);
|
|
452
725
|
// 末尾の空白を除去
|
package/dist/types/jira.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export interface JiraComment {
|
|
|
6
6
|
readonly id: string;
|
|
7
7
|
/** 作成者の表示名 */
|
|
8
8
|
readonly author: string;
|
|
9
|
-
/**
|
|
9
|
+
/** コメント本文(プレーンテキスト) */
|
|
10
10
|
readonly body: string;
|
|
11
|
+
/** コメント本文(ADF 形式、Markdown 変換用) */
|
|
12
|
+
readonly bodyAdf: unknown;
|
|
11
13
|
/** 作成日時(ISO 8601 形式) */
|
|
12
14
|
readonly created: string;
|
|
13
15
|
/** 更新日時(ISO 8601 形式) */
|
|
@@ -62,6 +64,8 @@ export interface JiraIssue {
|
|
|
62
64
|
readonly summary: string;
|
|
63
65
|
/** Issue 説明(null の場合は説明なし) */
|
|
64
66
|
readonly description: string | null;
|
|
67
|
+
/** Issue 説明(ADF 形式、Markdown 変換用) */
|
|
68
|
+
readonly descriptionAdf: unknown;
|
|
65
69
|
/** コメント一覧 */
|
|
66
70
|
readonly comments: readonly JiraComment[];
|
|
67
71
|
/** 変更履歴一覧 */
|
package/dist/types/storage.d.ts
CHANGED
|
@@ -93,9 +93,9 @@ export interface JiraSaveData {
|
|
|
93
93
|
readonly key: string;
|
|
94
94
|
/** Issue タイトル(要約) */
|
|
95
95
|
readonly summary: string;
|
|
96
|
-
/** Issue 説明(null の場合は説明なし) */
|
|
97
|
-
readonly description:
|
|
98
|
-
/** 説明のプレーンテキスト(null
|
|
96
|
+
/** Issue 説明(ADF 形式、null の場合は説明なし) */
|
|
97
|
+
readonly description: unknown;
|
|
98
|
+
/** 説明のプレーンテキスト(null の場合は説明なし、後方互換性のため維持) */
|
|
99
99
|
readonly descriptionPlainText: string | null;
|
|
100
100
|
/** コメント一覧 */
|
|
101
101
|
readonly comments: readonly JiraComment[];
|