atl-fetch 1.2.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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Confluence Storage Format → PlainText 変換
3
+ */
4
+ /**
5
+ * HTML エンティティをデコードする
6
+ *
7
+ * @param text エンコードされた文字列
8
+ * @returns デコードされた文字列
9
+ */
10
+ export const decodeHtmlEntities = (text) => {
11
+ return text
12
+ .replace(/ /g, ' ')
13
+ .replace(/&/g, '&')
14
+ .replace(/&lt;/g, '<')
15
+ .replace(/&gt;/g, '>')
16
+ .replace(/&quot;/g, '"')
17
+ .replace(/&#39;/g, "'")
18
+ .replace(/&#x27;/g, "'")
19
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 10)));
20
+ };
21
+ /**
22
+ * CDATA セクションからテキストを抽出する
23
+ *
24
+ * @param html HTML 文字列
25
+ * @returns CDATA セクションを処理した文字列
26
+ */
27
+ export const extractCdata = (html) => {
28
+ return html.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
29
+ };
30
+ /**
31
+ * Confluence マクロのパラメータからテキストを抽出する
32
+ *
33
+ * @param html HTML 文字列
34
+ * @returns 処理された文字列
35
+ */
36
+ export const extractMacroParameters = (html) => {
37
+ // ac:parameter タグから title などのテキストを抽出
38
+ return html.replace(/<ac:parameter[^>]*ac:name="title"[^>]*>([^<]*)<\/ac:parameter>/g, '$1');
39
+ };
40
+ /**
41
+ * 画像タグをプレースホルダーに変換する
42
+ *
43
+ * @param html HTML 文字列
44
+ * @returns 処理された文字列
45
+ */
46
+ export const convertImagesToPlaceholders = (html) => {
47
+ // ac:image タグと ri:attachment から画像ファイル名を抽出
48
+ return html.replace(/<ac:image[^>]*>[\s\S]*?<ri:attachment\s+ri:filename="([^"]*)"[^>]*\/>[\s\S]*?<\/ac:image>/g, '[画像: $1]');
49
+ };
50
+ /**
51
+ * ユーザーリンクをプレースホルダーに変換する
52
+ *
53
+ * @param html HTML 文字列
54
+ * @returns 処理された文字列
55
+ */
56
+ export const convertUserLinksToPlaceholders = (html) => {
57
+ // ri:user タグをプレースホルダーに変換
58
+ return html.replace(/<ac:link[^>]*>[\s\S]*?<ri:user[^>]*\/>[\s\S]*?<\/ac:link>/g, '[ユーザー]');
59
+ };
60
+ /**
61
+ * ブロック要素のタグを処理して改行を適切に挿入する
62
+ *
63
+ * @param html HTML 文字列
64
+ * @returns 処理された文字列
65
+ */
66
+ export const processBlockElements = (html) => {
67
+ // 閉じタグの前後に改行マーカーを追加
68
+ let result = html;
69
+ // パラグラフと見出しの後に改行
70
+ result = result.replace(/<\/(p|h[1-6])>/gi, '</$1>\n');
71
+ // リストアイテムの後に改行
72
+ result = result.replace(/<\/li>/gi, '</li>\n');
73
+ // テーブル行の後に改行
74
+ result = result.replace(/<\/tr>/gi, '</tr>\n');
75
+ // テーブルセルの後にタブ
76
+ result = result.replace(/<\/(td|th)>/gi, '\t</$1>');
77
+ // br タグを改行に変換
78
+ result = result.replace(/<br\s*\/?>/gi, '\n');
79
+ // blockquote の後に改行
80
+ result = result.replace(/<\/blockquote>/gi, '</blockquote>\n');
81
+ return result;
82
+ };
83
+ /**
84
+ * HTML タグを除去する
85
+ *
86
+ * @param html HTML 文字列
87
+ * @returns タグを除去した文字列
88
+ */
89
+ export const stripHtmlTags = (html) => {
90
+ return html.replace(/<[^>]*>/g, '');
91
+ };
92
+ /**
93
+ * 連続する空白を正規化する
94
+ *
95
+ * @param text テキスト
96
+ * @returns 正規化されたテキスト
97
+ */
98
+ export const normalizeWhitespace = (text) => {
99
+ // 行ごとに処理
100
+ const lines = text.split('\n');
101
+ const normalizedLines = lines.map((line) => {
102
+ // 行内の連続する空白を単一スペースに
103
+ return line.replace(/[ \t]+/g, ' ').trim();
104
+ });
105
+ // 空行を除去して結合
106
+ return normalizedLines.filter((line) => line !== '').join('\n');
107
+ };
108
+ /**
109
+ * Confluence の Storage Format(XHTML)をプレーンテキストに変換する
110
+ *
111
+ * @param storageFormat Storage Format 文字列
112
+ * @returns プレーンテキスト
113
+ */
114
+ export const convertStorageFormatToPlainText = (storageFormat) => {
115
+ // null または undefined の場合は空文字列を返す
116
+ if (storageFormat === null || storageFormat === undefined || storageFormat === '') {
117
+ return '';
118
+ }
119
+ let result = storageFormat;
120
+ // CDATA セクションを処理
121
+ result = extractCdata(result);
122
+ // マクロパラメータからテキストを抽出
123
+ result = extractMacroParameters(result);
124
+ // 画像をプレースホルダーに変換
125
+ result = convertImagesToPlaceholders(result);
126
+ // ユーザーリンクをプレースホルダーに変換
127
+ result = convertUserLinksToPlaceholders(result);
128
+ // ブロック要素を処理
129
+ result = processBlockElements(result);
130
+ // HTML タグを除去
131
+ result = stripHtmlTags(result);
132
+ // HTML エンティティをデコード
133
+ result = decodeHtmlEntities(result);
134
+ // 空白を正規化
135
+ result = normalizeWhitespace(result);
136
+ return result;
137
+ };
138
+ // ============================================================
139
+ // In-source Testing(プライベート関数のテスト)
140
+ // ============================================================
141
+ if (import.meta.vitest) {
142
+ const { describe, expect, it } = import.meta.vitest;
143
+ describe('decodeHtmlEntities (in-source testing)', () => {
144
+ // テストの目的: 各エンティティが正確に変換されること
145
+ describe('個別エンティティの変換検証', () => {
146
+ it('Given: &nbsp;, When: decodeHtmlEntities を呼び出す, Then: スペースに変換される', () => {
147
+ expect(decodeHtmlEntities('&nbsp;')).toBe(' ');
148
+ });
149
+ it('Given: &amp;, When: decodeHtmlEntities を呼び出す, Then: & に変換される', () => {
150
+ expect(decodeHtmlEntities('&amp;')).toBe('&');
151
+ });
152
+ it('Given: &lt;, When: decodeHtmlEntities を呼び出す, Then: < に変換される', () => {
153
+ expect(decodeHtmlEntities('&lt;')).toBe('<');
154
+ });
155
+ it('Given: &gt;, When: decodeHtmlEntities を呼び出す, Then: > に変換される', () => {
156
+ expect(decodeHtmlEntities('&gt;')).toBe('>');
157
+ });
158
+ it('Given: &quot;, When: decodeHtmlEntities を呼び出す, Then: " に変換される', () => {
159
+ expect(decodeHtmlEntities('&quot;')).toBe('"');
160
+ });
161
+ it("Given: &#39;, When: decodeHtmlEntities を呼び出す, Then: ' に変換される", () => {
162
+ expect(decodeHtmlEntities('&#39;')).toBe("'");
163
+ });
164
+ it("Given: &#x27;, When: decodeHtmlEntities を呼び出す, Then: ' に変換される", () => {
165
+ expect(decodeHtmlEntities('&#x27;')).toBe("'");
166
+ });
167
+ it('Given: 数値文字参照 &#65;, When: decodeHtmlEntities を呼び出す, Then: A に変換される', () => {
168
+ expect(decodeHtmlEntities('&#65;')).toBe('A');
169
+ });
170
+ it('Given: 数値文字参照 &#12354;, When: decodeHtmlEntities を呼び出す, Then: あ に変換される', () => {
171
+ expect(decodeHtmlEntities('&#12354;')).toBe('あ');
172
+ });
173
+ });
174
+ // テストの目的: 複数のエンティティが正しい順序で変換されること
175
+ describe('変換順序の検証', () => {
176
+ it('Given: &amp;nbsp;, When: decodeHtmlEntities を呼び出す, Then: &nbsp; に変換される(&amp; が先に処理される)', () => {
177
+ // &amp;nbsp; → &nbsp; になること(&nbsp; → スペース にはならない)
178
+ expect(decodeHtmlEntities('&amp;nbsp;')).toBe('&nbsp;');
179
+ });
180
+ it('Given: &amp;lt;, When: decodeHtmlEntities を呼び出す, Then: < に変換される(連鎖的に置換される)', () => {
181
+ // &amp; -> & になり、その後 &lt; -> < になる
182
+ expect(decodeHtmlEntities('&amp;lt;')).toBe('<');
183
+ });
184
+ });
185
+ });
186
+ describe('convertImagesToPlaceholders (in-source testing)', () => {
187
+ // テストの目的: 画像タグが '[画像: ファイル名]' に変換されること
188
+ describe('画像プレースホルダーの検証', () => {
189
+ it('Given: ac:image タグ, When: convertImagesToPlaceholders を呼び出す, Then: [画像: filename] 形式になる', () => {
190
+ const html = '<ac:image><ri:attachment ri:filename="test.png"/></ac:image>';
191
+ const result = convertImagesToPlaceholders(html);
192
+ expect(result).toBe('[画像: test.png]');
193
+ });
194
+ it('Given: 日本語ファイル名の画像, When: convertImagesToPlaceholders を呼び出す, Then: ファイル名がそのまま含まれる', () => {
195
+ const html = '<ac:image><ri:attachment ri:filename="テスト画像.png"/></ac:image>';
196
+ const result = convertImagesToPlaceholders(html);
197
+ expect(result).toBe('[画像: テスト画像.png]');
198
+ });
199
+ it('Given: 空のファイル名, When: convertImagesToPlaceholders を呼び出す, Then: [画像: ] となる', () => {
200
+ const html = '<ac:image><ri:attachment ri:filename=""/></ac:image>';
201
+ const result = convertImagesToPlaceholders(html);
202
+ expect(result).toBe('[画像: ]');
203
+ });
204
+ });
205
+ });
206
+ describe('convertUserLinksToPlaceholders (in-source testing)', () => {
207
+ // テストの目的: ユーザーリンクが '[ユーザー]' に変換されること
208
+ describe('ユーザープレースホルダーの検証', () => {
209
+ it('Given: ri:user タグ, When: convertUserLinksToPlaceholders を呼び出す, Then: [ユーザー] になる', () => {
210
+ const html = '<ac:link><ri:user ri:account-id="123"/></ac:link>';
211
+ const result = convertUserLinksToPlaceholders(html);
212
+ expect(result).toBe('[ユーザー]');
213
+ });
214
+ it('Given: 複数のユーザーリンク, When: convertUserLinksToPlaceholders を呼び出す, Then: それぞれ [ユーザー] になる', () => {
215
+ const html = '<ac:link><ri:user ri:account-id="1"/></ac:link>と<ac:link><ri:user ri:account-id="2"/></ac:link>';
216
+ const result = convertUserLinksToPlaceholders(html);
217
+ expect(result).toBe('[ユーザー]と[ユーザー]');
218
+ });
219
+ });
220
+ });
221
+ describe('normalizeWhitespace (in-source testing)', () => {
222
+ // テストの目的: 連続空白が単一スペースに正規化されること
223
+ describe('空白正規化の検証', () => {
224
+ it('Given: 連続スペース, When: normalizeWhitespace を呼び出す, Then: 単一スペースになる', () => {
225
+ expect(normalizeWhitespace('a b')).toBe('a b');
226
+ });
227
+ it('Given: 連続タブ, When: normalizeWhitespace を呼び出す, Then: 単一スペースになる', () => {
228
+ expect(normalizeWhitespace('a\t\t\tb')).toBe('a b');
229
+ });
230
+ it('Given: 空行, When: normalizeWhitespace を呼び出す, Then: 空行が削除される', () => {
231
+ expect(normalizeWhitespace('a\n\n\nb')).toBe('a\nb');
232
+ });
233
+ it('Given: 先頭末尾の空白, When: normalizeWhitespace を呼び出す, Then: トリムされる', () => {
234
+ expect(normalizeWhitespace(' a ')).toBe('a');
235
+ });
236
+ });
237
+ });
238
+ }
@@ -2,28 +2,11 @@
2
2
  * テキスト変換サービス
3
3
  *
4
4
  * Jira の ADF(Atlassian Document Format)と Confluence の Storage Format を
5
- * プレーンテキストに変換する機能を提供する。
6
- * また、Confluence の Storage Format を Markdown に変換する機能も提供する。
5
+ * プレーンテキストや Markdown に変換する機能を提供する。
7
6
  */
8
- /**
9
- * 添付ファイルパスのマッピング型
10
- * filename -> savedPath
11
- */
12
- type AttachmentPathMapping = Record<string, string>;
13
- /**
14
- * ADF(Atlassian Document Format)をプレーンテキストに変換する
15
- *
16
- * @param adf ADF ドキュメント(オブジェクトまたは JSON 文字列)
17
- * @returns プレーンテキスト
18
- */
19
- export declare const convertAdfToPlainText: (adf: unknown) => string;
20
- /**
21
- * Confluence の Storage Format(XHTML)をプレーンテキストに変換する
22
- *
23
- * @param storageFormat Storage Format 文字列
24
- * @returns プレーンテキスト
25
- */
26
- export declare const convertStorageFormatToPlainText: (storageFormat: string | null | undefined) => string;
7
+ import type { AttachmentPathMapping } from './types.js';
8
+ export { convertAdfToPlainText } from './adf-to-plain-text.js';
9
+ export { convertStorageFormatToPlainText } from './storage-to-plain-text.js';
27
10
  /**
28
11
  * ADF(Atlassian Document Format)を Markdown に変換する
29
12
  *
@@ -40,4 +23,3 @@ export declare const convertAdfToMarkdown: (adf: unknown, attachmentPaths?: Atta
40
23
  * @returns Markdown 文字列
41
24
  */
42
25
  export declare const convertStorageFormatToMarkdown: (storageFormat: string | null | undefined, attachmentPaths?: AttachmentPathMapping) => string;
43
- export {};