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.
- package/README.md +20 -7
- 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 +4 -22
- package/dist/services/text-converter/text-converter.js +7 -887
- package/dist/services/text-converter/types.d.ts +40 -0
- package/dist/services/text-converter/types.js +16 -0
- package/dist/types/result.d.ts +104 -0
- package/dist/types/result.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADF(Atlassian Document Format)→ PlainText 変換
|
|
3
|
+
*/
|
|
4
|
+
import { isAdfDocument } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* ADF ノードからプレーンテキストを抽出する
|
|
7
|
+
*
|
|
8
|
+
* @param node ADF ノード
|
|
9
|
+
* @returns 抽出されたプレーンテキスト
|
|
10
|
+
*/
|
|
11
|
+
export const extractTextFromAdfNode = (node) => {
|
|
12
|
+
// テキストノードの場合
|
|
13
|
+
if (node.type === 'text' && node.text !== undefined) {
|
|
14
|
+
return node.text;
|
|
15
|
+
}
|
|
16
|
+
// 硬い改行の場合
|
|
17
|
+
if (node.type === 'hardBreak') {
|
|
18
|
+
return '\n';
|
|
19
|
+
}
|
|
20
|
+
// メンションの場合
|
|
21
|
+
if (node.type === 'mention' && node.attrs !== undefined) {
|
|
22
|
+
const text = node.attrs['text'];
|
|
23
|
+
if (typeof text === 'string') {
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
return '@ユーザー';
|
|
27
|
+
}
|
|
28
|
+
// 絵文字の場合
|
|
29
|
+
if (node.type === 'emoji' && node.attrs !== undefined) {
|
|
30
|
+
const text = node.attrs['text'];
|
|
31
|
+
const shortName = node.attrs['shortName'];
|
|
32
|
+
if (typeof text === 'string') {
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
if (typeof shortName === 'string') {
|
|
36
|
+
return shortName;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
// メディアの場合
|
|
41
|
+
if (node.type === 'media') {
|
|
42
|
+
return '[添付ファイル]';
|
|
43
|
+
}
|
|
44
|
+
// mediaSingle の場合(メディアコンテナ)
|
|
45
|
+
if (node.type === 'mediaSingle' && node.content !== undefined) {
|
|
46
|
+
return node.content.map(extractTextFromAdfNode).join('');
|
|
47
|
+
}
|
|
48
|
+
// 子ノードがある場合は再帰的に処理
|
|
49
|
+
if (node.content !== undefined && Array.isArray(node.content)) {
|
|
50
|
+
const texts = node.content.map(extractTextFromAdfNode);
|
|
51
|
+
// パラグラフや見出しの後には改行を追加
|
|
52
|
+
if (node.type === 'paragraph' || node.type === 'heading') {
|
|
53
|
+
return texts.join('');
|
|
54
|
+
}
|
|
55
|
+
// リストアイテムの後には改行を追加
|
|
56
|
+
if (node.type === 'listItem') {
|
|
57
|
+
return `${texts.join('')}\n`;
|
|
58
|
+
}
|
|
59
|
+
// テーブルセルとヘッダーはタブで区切る
|
|
60
|
+
if (node.type === 'tableCell' || node.type === 'tableHeader') {
|
|
61
|
+
return `${texts.join('')}\t`;
|
|
62
|
+
}
|
|
63
|
+
// テーブル行は改行で区切る
|
|
64
|
+
if (node.type === 'tableRow') {
|
|
65
|
+
return `${texts.join('').trimEnd()}\n`;
|
|
66
|
+
}
|
|
67
|
+
return texts.join('');
|
|
68
|
+
}
|
|
69
|
+
return '';
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* ADF ドキュメントのトップレベルコンテンツを処理する
|
|
73
|
+
*
|
|
74
|
+
* @param content トップレベルのコンテンツ配列
|
|
75
|
+
* @returns プレーンテキスト
|
|
76
|
+
*/
|
|
77
|
+
export const processAdfContent = (content) => {
|
|
78
|
+
const results = [];
|
|
79
|
+
for (const node of content) {
|
|
80
|
+
const text = extractTextFromAdfNode(node);
|
|
81
|
+
if (text !== '') {
|
|
82
|
+
results.push(text);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// パラグラフや見出しは改行で結合
|
|
86
|
+
return results.join('\n');
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* ADF(Atlassian Document Format)をプレーンテキストに変換する
|
|
90
|
+
*
|
|
91
|
+
* @param adf ADF ドキュメント(オブジェクトまたは JSON 文字列)
|
|
92
|
+
* @returns プレーンテキスト
|
|
93
|
+
*/
|
|
94
|
+
export const convertAdfToPlainText = (adf) => {
|
|
95
|
+
// null または undefined の場合は空文字列を返す
|
|
96
|
+
if (adf === null || adf === undefined) {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
// 文字列の場合は JSON としてパースを試みる
|
|
100
|
+
if (typeof adf === 'string') {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(adf);
|
|
103
|
+
if (isAdfDocument(parsed)) {
|
|
104
|
+
return processAdfContent(parsed.content);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// JSON パースに失敗した場合は元の文字列を返す
|
|
109
|
+
return adf;
|
|
110
|
+
}
|
|
111
|
+
// パースできたが ADF 形式でない場合は元の文字列を返す
|
|
112
|
+
return adf;
|
|
113
|
+
}
|
|
114
|
+
// オブジェクトの場合は ADF ドキュメントとして処理
|
|
115
|
+
if (isAdfDocument(adf)) {
|
|
116
|
+
return processAdfContent(adf.content);
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 変換ユーティリティ
|
|
3
|
+
*
|
|
4
|
+
* TurndownService の設定と前処理を提供する
|
|
5
|
+
*/
|
|
6
|
+
import TurndownService from 'turndown';
|
|
7
|
+
import type { AttachmentPathMapping } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* 前処理: 無視する要素を削除し、Confluence 固有タグを処理
|
|
10
|
+
*
|
|
11
|
+
* @param html HTML 文字列
|
|
12
|
+
* @param attachmentPaths 添付ファイルマッピング
|
|
13
|
+
* @returns 前処理済み HTML
|
|
14
|
+
*/
|
|
15
|
+
export declare const preprocessHtmlForMarkdown: (html: string, attachmentPaths?: AttachmentPathMapping) => string;
|
|
16
|
+
/**
|
|
17
|
+
* TurndownService インスタンスを作成する(共通設定)
|
|
18
|
+
* Jira ADF と Confluence Storage Format の両方で使用する
|
|
19
|
+
*
|
|
20
|
+
* @returns 設定済みの TurndownService インスタンス
|
|
21
|
+
*/
|
|
22
|
+
export declare const createTurndownService: () => TurndownService;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 変換ユーティリティ
|
|
3
|
+
*
|
|
4
|
+
* TurndownService の設定と前処理を提供する
|
|
5
|
+
*/
|
|
6
|
+
import TurndownService from 'turndown';
|
|
7
|
+
import { gfm } from 'turndown-plugin-gfm';
|
|
8
|
+
/**
|
|
9
|
+
* 前処理: 無視する要素を削除し、Confluence 固有タグを処理
|
|
10
|
+
*
|
|
11
|
+
* @param html HTML 文字列
|
|
12
|
+
* @param attachmentPaths 添付ファイルマッピング
|
|
13
|
+
* @returns 前処理済み HTML
|
|
14
|
+
*/
|
|
15
|
+
export const preprocessHtmlForMarkdown = (html, attachmentPaths) => {
|
|
16
|
+
let result = html;
|
|
17
|
+
// colgroup/col を削除
|
|
18
|
+
result = result.replace(/<colgroup[\s\S]*?<\/colgroup>/gi, '');
|
|
19
|
+
result = result.replace(/<col[^>]*\/?>/gi, '');
|
|
20
|
+
// data-highlight-colour 属性を削除
|
|
21
|
+
result = result.replace(/\s*data-highlight-colour="[^"]*"/gi, '');
|
|
22
|
+
// ac:local-id, local-id 属性を削除
|
|
23
|
+
result = result.replace(/\s*(ac:)?local-id="[^"]*"/gi, '');
|
|
24
|
+
// ac:inline-comment-marker を内容のみに置換
|
|
25
|
+
result = result.replace(/<ac:inline-comment-marker[^>]*>([\s\S]*?)<\/ac:inline-comment-marker>/gi, '$1');
|
|
26
|
+
// CDATA セクションを処理
|
|
27
|
+
result = result.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
|
|
28
|
+
// --------------------------------------------------
|
|
29
|
+
// Confluence 固有タグを標準 HTML タグに変換(turndown が認識できる形式へ)
|
|
30
|
+
// --------------------------------------------------
|
|
31
|
+
// ac:image + ac:caption を <img> + <figcaption> に変換(キャプション付きを先に処理)
|
|
32
|
+
result = result.replace(/<ac:image[^>]*>[\s\S]*?<ri:attachment[^>]*ri:filename="([^"]*)"[^>]*\/?>[\s\S]*?<ac:caption>([^<]*)<\/ac:caption>[\s\S]*?<\/ac:image>/gi, (_match, filename, caption) => {
|
|
33
|
+
const localPath = attachmentPaths?.[filename] || filename;
|
|
34
|
+
return `<figure><img src="${localPath}" alt="${filename}"><figcaption>${caption}</figcaption></figure>`;
|
|
35
|
+
});
|
|
36
|
+
// ac:image を <img> に変換(キャプションなしの残り)
|
|
37
|
+
result = result.replace(/<ac:image[^>]*>[\s\S]*?<ri:attachment[^>]*ri:filename="([^"]*)"[^>]*\/?>[\s\S]*?<\/ac:image>/gi, (_match, filename) => {
|
|
38
|
+
const localPath = attachmentPaths?.[filename] || filename;
|
|
39
|
+
return `<img src="${localPath}" alt="${filename}">`;
|
|
40
|
+
});
|
|
41
|
+
// ac:structured-macro name="code" を <pre><code> に変換
|
|
42
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="code"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi, (_match, innerContent) => {
|
|
43
|
+
// language パラメータを抽出
|
|
44
|
+
const langMatch = innerContent.match(/<ac:parameter[^>]*ac:name="language"[^>]*>([^<]*)<\/ac:parameter>/i);
|
|
45
|
+
const lang = langMatch?.[1] || '';
|
|
46
|
+
// plain-text-body の内容を抽出
|
|
47
|
+
const bodyMatch = innerContent.match(/<ac:plain-text-body[^>]*>([\s\S]*?)<\/ac:plain-text-body>/i);
|
|
48
|
+
const code = bodyMatch?.[1] || '';
|
|
49
|
+
// turndown が認識できる形式に変換
|
|
50
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
51
|
+
return `<pre><code${langClass}>${code}</code></pre>`;
|
|
52
|
+
});
|
|
53
|
+
// ac:structured-macro name="info/note/tip/warning" を GitHub Alerts 形式の blockquote に変換
|
|
54
|
+
const alertMacros = ['info', 'note', 'tip', 'warning'];
|
|
55
|
+
const alertTypeMap = {
|
|
56
|
+
info: 'NOTE',
|
|
57
|
+
note: 'NOTE',
|
|
58
|
+
tip: 'TIP',
|
|
59
|
+
warning: 'WARNING',
|
|
60
|
+
};
|
|
61
|
+
for (const macroName of alertMacros) {
|
|
62
|
+
const pattern = new RegExp(`<ac:structured-macro[^>]*ac:name="${macroName}"[^>]*>[\\s\\S]*?<ac:rich-text-body>([\\s\\S]*?)<\\/ac:rich-text-body>[\\s\\S]*?<\\/ac:structured-macro>`, 'gi');
|
|
63
|
+
result = result.replace(pattern, (_match, content) => {
|
|
64
|
+
const alertType = alertTypeMap[macroName] || 'NOTE';
|
|
65
|
+
// 専用のマーカー属性を持つ blockquote に変換
|
|
66
|
+
return `<blockquote data-github-alert="${alertType}">${content}</blockquote>`;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// ac:structured-macro name="toc" を [TOC] マーカーに変換(markdown-toc 互換形式)
|
|
70
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="toc"[^>]*>[\s\S]*?<\/ac:structured-macro>/gi, '<p>[TOC]</p>');
|
|
71
|
+
// ac:structured-macro name="anchor" を <a id="name"></a> に変換
|
|
72
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="anchor"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi, (_match, innerContent) => {
|
|
73
|
+
// パラメータの値を抽出(ac:name="" または ac:name="..." 両方対応)
|
|
74
|
+
const paramMatch = innerContent.match(/<ac:parameter[^>]*>([^<]*)<\/ac:parameter>/i);
|
|
75
|
+
const anchorName = paramMatch?.[1]?.trim() || '';
|
|
76
|
+
return anchorName ? `<a id="${anchorName}"></a>` : '';
|
|
77
|
+
});
|
|
78
|
+
// ac:structured-macro name="expand" を <details><summary>...</summary>...</details> に変換
|
|
79
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="expand"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi, (_match, innerContent) => {
|
|
80
|
+
// title パラメータを抽出
|
|
81
|
+
const titleMatch = innerContent.match(/<ac:parameter[^>]*ac:name="title"[^>]*>([^<]*)<\/ac:parameter>/i);
|
|
82
|
+
const title = titleMatch?.[1] || '展開';
|
|
83
|
+
// rich-text-body の内容を抽出
|
|
84
|
+
const bodyMatch = innerContent.match(/<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>/i);
|
|
85
|
+
const body = bodyMatch?.[1] || '';
|
|
86
|
+
return `<details><summary>${title}</summary>${body}</details>`;
|
|
87
|
+
});
|
|
88
|
+
// ac:structured-macro name="excerpt" を処理(hidden=true は削除、それ以外は内容を出力)
|
|
89
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="excerpt"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi, (_match, innerContent) => {
|
|
90
|
+
// hidden パラメータをチェック
|
|
91
|
+
const hiddenMatch = innerContent.match(/<ac:parameter[^>]*ac:name="hidden"[^>]*>([^<]*)<\/ac:parameter>/i);
|
|
92
|
+
const isHidden = hiddenMatch?.[1]?.toLowerCase() === 'true';
|
|
93
|
+
if (isHidden) {
|
|
94
|
+
return ''; // hidden=true の場合は削除
|
|
95
|
+
}
|
|
96
|
+
// rich-text-body の内容を抽出
|
|
97
|
+
const bodyMatch = innerContent.match(/<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>/i);
|
|
98
|
+
return bodyMatch?.[1] || '';
|
|
99
|
+
});
|
|
100
|
+
// ac:structured-macro name="excerpt-include" をプレースホルダーに変換
|
|
101
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="excerpt-include"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi, (_match, innerContent) => {
|
|
102
|
+
// ページ名を抽出
|
|
103
|
+
const pageMatch = innerContent.match(/ri:content-title="([^"]*)"/i);
|
|
104
|
+
const pageName = pageMatch?.[1] || '不明なページ';
|
|
105
|
+
return `<p>[抜粋: ${pageName}]</p>`;
|
|
106
|
+
});
|
|
107
|
+
// ac:structured-macro name="toc-zone" を [TOC] + 内容に変換
|
|
108
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="toc-zone"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi, (_match, innerContent) => {
|
|
109
|
+
// rich-text-body の内容を抽出
|
|
110
|
+
const bodyMatch = innerContent.match(/<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>/i);
|
|
111
|
+
const body = bodyMatch?.[1] || '';
|
|
112
|
+
return `<p>[TOC]</p>${body}`;
|
|
113
|
+
});
|
|
114
|
+
// ac:link + ri:page を Markdown リンクに変換(CDATA 処理後のパターンも対応)
|
|
115
|
+
result = result.replace(/<ac:link[^>]*>[\s\S]*?<ri:page[^>]*ri:content-title="([^"]*)"[^>]*\/>[\s\S]*?<ac:plain-text-link-body[^>]*>([^<]*)<\/ac:plain-text-link-body>[\s\S]*?<\/ac:link>/gi, (_match, pageTitle, linkText) => {
|
|
116
|
+
return `<a href="${pageTitle}">${linkText}</a>`;
|
|
117
|
+
});
|
|
118
|
+
// ac:link + ri:page(リンクテキストなし)を処理
|
|
119
|
+
result = result.replace(/<ac:link[^>]*>[\s\S]*?<ri:page[^>]*ri:content-title="([^"]*)"[^>]*\/>[\s\S]*?<\/ac:link>/gi, (_match, pageTitle) => {
|
|
120
|
+
return `<a href="${pageTitle}">${pageTitle}</a>`;
|
|
121
|
+
});
|
|
122
|
+
// ac:structured-macro name="column" を td タグに変換(先に処理)
|
|
123
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="column"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/gi, (_match, columnBody) => {
|
|
124
|
+
return `<td>${columnBody}</td>`;
|
|
125
|
+
});
|
|
126
|
+
// ac:structured-macro name="section" をテーブルに変換(column 処理後)
|
|
127
|
+
result = result.replace(/<ac:structured-macro[^>]*ac:name="section"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/gi, (_match, sectionBody) => {
|
|
128
|
+
// td タグがあればテーブルとして出力
|
|
129
|
+
if (sectionBody.includes('<td>')) {
|
|
130
|
+
return `<table><tr>${sectionBody.trim()}</tr></table>`;
|
|
131
|
+
}
|
|
132
|
+
return sectionBody;
|
|
133
|
+
});
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* TurndownService インスタンスを作成する(共通設定)
|
|
138
|
+
* Jira ADF と Confluence Storage Format の両方で使用する
|
|
139
|
+
*
|
|
140
|
+
* @returns 設定済みの TurndownService インスタンス
|
|
141
|
+
*/
|
|
142
|
+
export const createTurndownService = () => {
|
|
143
|
+
const turndownService = new TurndownService({
|
|
144
|
+
bulletListMarker: '-',
|
|
145
|
+
codeBlockStyle: 'fenced',
|
|
146
|
+
emDelimiter: '*',
|
|
147
|
+
headingStyle: 'atx',
|
|
148
|
+
strongDelimiter: '**',
|
|
149
|
+
});
|
|
150
|
+
// GFM プラグイン(テーブル、取り消し線など)を使用
|
|
151
|
+
turndownService.use(gfm);
|
|
152
|
+
// カスタムルール: キャプション付き画像(<figure>)
|
|
153
|
+
turndownService.addRule('figureWithCaption', {
|
|
154
|
+
filter: (node) => {
|
|
155
|
+
return node.nodeName === 'FIGURE';
|
|
156
|
+
},
|
|
157
|
+
replacement: (_content, node) => {
|
|
158
|
+
const element = node;
|
|
159
|
+
const img = element.querySelector('img');
|
|
160
|
+
const figcaption = element.querySelector('figcaption');
|
|
161
|
+
if (img) {
|
|
162
|
+
const src = img.getAttribute('src') || '';
|
|
163
|
+
const alt = img.getAttribute('alt') || '';
|
|
164
|
+
let result = ``;
|
|
165
|
+
if (figcaption) {
|
|
166
|
+
const captionText = figcaption.textContent?.trim() || '';
|
|
167
|
+
if (captionText) {
|
|
168
|
+
result += `\n\n*${captionText}*`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
return '';
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
// カスタムルール: GitHub Alerts(<blockquote data-github-alert="...">)
|
|
177
|
+
turndownService.addRule('githubAlerts', {
|
|
178
|
+
filter: (node) => {
|
|
179
|
+
if (node.nodeName !== 'BLOCKQUOTE')
|
|
180
|
+
return false;
|
|
181
|
+
return node.hasAttribute('data-github-alert');
|
|
182
|
+
},
|
|
183
|
+
replacement: (content, node) => {
|
|
184
|
+
const element = node;
|
|
185
|
+
const alertType = element.getAttribute('data-github-alert') || 'NOTE';
|
|
186
|
+
// 内容を再帰的に Markdown 変換
|
|
187
|
+
const innerMarkdown = content.trim();
|
|
188
|
+
// 各行に > プレフィックス付加
|
|
189
|
+
const lines = innerMarkdown.split('\n');
|
|
190
|
+
const quotedContent = lines.map((line) => `> ${line}`).join('\n');
|
|
191
|
+
return `\n> [!${alertType}]\n${quotedContent}\n`;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// カスタムルール: 色変更テキスト(HTML のまま出力)
|
|
195
|
+
turndownService.addRule('coloredText', {
|
|
196
|
+
filter: (node) => {
|
|
197
|
+
if (node.nodeName !== 'SPAN')
|
|
198
|
+
return false;
|
|
199
|
+
const style = node.getAttribute('style') || '';
|
|
200
|
+
return style.includes('color:') || style.includes('color :');
|
|
201
|
+
},
|
|
202
|
+
replacement: (_content, node) => {
|
|
203
|
+
// HTML のまま出力
|
|
204
|
+
return node.outerHTML;
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
// カスタムルール: アンカータグ(id 属性付き)を HTML のまま出力
|
|
208
|
+
turndownService.addRule('anchorTag', {
|
|
209
|
+
filter: (node) => {
|
|
210
|
+
if (node.nodeName !== 'A')
|
|
211
|
+
return false;
|
|
212
|
+
return node.hasAttribute('id');
|
|
213
|
+
},
|
|
214
|
+
replacement: (_content, node) => {
|
|
215
|
+
return node.outerHTML;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
// カスタムルール: 背景色テキスト(HTML のまま出力)
|
|
219
|
+
turndownService.addRule('highlightedText', {
|
|
220
|
+
filter: (node) => {
|
|
221
|
+
if (node.nodeName !== 'SPAN')
|
|
222
|
+
return false;
|
|
223
|
+
const style = node.getAttribute('style') || '';
|
|
224
|
+
return style.includes('background-color');
|
|
225
|
+
},
|
|
226
|
+
replacement: (_content, node) => {
|
|
227
|
+
// HTML のまま出力
|
|
228
|
+
return node.outerHTML;
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
// カスタムルール: date ノード用(<time> タグから日付を抽出)
|
|
232
|
+
turndownService.addRule('dateNode', {
|
|
233
|
+
filter: 'time',
|
|
234
|
+
replacement: (_, node) => {
|
|
235
|
+
return node.getAttribute('datetime') || '';
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
// カスタムルール: expand 用(<details> タグを HTML のまま出力)
|
|
239
|
+
turndownService.addRule('expandDetails', {
|
|
240
|
+
filter: 'details',
|
|
241
|
+
replacement: (content, node) => {
|
|
242
|
+
const element = node;
|
|
243
|
+
const summary = element.querySelector('summary')?.textContent || '展開';
|
|
244
|
+
return `\n<details>\n<summary>${summary}</summary>\n\n${content.trim()}\n\n</details>\n`;
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
// カスタムルール: border スタイル付き画像(HTML のまま出力)
|
|
248
|
+
turndownService.addRule('borderedImage', {
|
|
249
|
+
filter: (node) => {
|
|
250
|
+
if (node.nodeName !== 'IMG')
|
|
251
|
+
return false;
|
|
252
|
+
const style = node.getAttribute('style') || '';
|
|
253
|
+
return style.includes('border');
|
|
254
|
+
},
|
|
255
|
+
replacement: (_content, node) => {
|
|
256
|
+
return node.outerHTML;
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
// カスタムルール: 全テーブルを HTML のまま出力(シンプル・確実)
|
|
260
|
+
// セル内リスト、画像、複数段落、コードなど全ケースで安定
|
|
261
|
+
// Confluence の複雑なテーブルも確実に表示
|
|
262
|
+
// Markdown 対応ビューアでも HTML テーブルは表示可能
|
|
263
|
+
turndownService.addRule('allTables', {
|
|
264
|
+
filter: 'table',
|
|
265
|
+
replacement: (_content, node) => {
|
|
266
|
+
return '\n\n' + node.outerHTML + '\n\n';
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return turndownService;
|
|
270
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confluence Storage Format → PlainText 変換
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* HTML エンティティをデコードする
|
|
6
|
+
*
|
|
7
|
+
* @param text エンコードされた文字列
|
|
8
|
+
* @returns デコードされた文字列
|
|
9
|
+
*/
|
|
10
|
+
export declare const decodeHtmlEntities: (text: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* CDATA セクションからテキストを抽出する
|
|
13
|
+
*
|
|
14
|
+
* @param html HTML 文字列
|
|
15
|
+
* @returns CDATA セクションを処理した文字列
|
|
16
|
+
*/
|
|
17
|
+
export declare const extractCdata: (html: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Confluence マクロのパラメータからテキストを抽出する
|
|
20
|
+
*
|
|
21
|
+
* @param html HTML 文字列
|
|
22
|
+
* @returns 処理された文字列
|
|
23
|
+
*/
|
|
24
|
+
export declare const extractMacroParameters: (html: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* 画像タグをプレースホルダーに変換する
|
|
27
|
+
*
|
|
28
|
+
* @param html HTML 文字列
|
|
29
|
+
* @returns 処理された文字列
|
|
30
|
+
*/
|
|
31
|
+
export declare const convertImagesToPlaceholders: (html: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* ユーザーリンクをプレースホルダーに変換する
|
|
34
|
+
*
|
|
35
|
+
* @param html HTML 文字列
|
|
36
|
+
* @returns 処理された文字列
|
|
37
|
+
*/
|
|
38
|
+
export declare const convertUserLinksToPlaceholders: (html: string) => string;
|
|
39
|
+
/**
|
|
40
|
+
* ブロック要素のタグを処理して改行を適切に挿入する
|
|
41
|
+
*
|
|
42
|
+
* @param html HTML 文字列
|
|
43
|
+
* @returns 処理された文字列
|
|
44
|
+
*/
|
|
45
|
+
export declare const processBlockElements: (html: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* HTML タグを除去する
|
|
48
|
+
*
|
|
49
|
+
* @param html HTML 文字列
|
|
50
|
+
* @returns タグを除去した文字列
|
|
51
|
+
*/
|
|
52
|
+
export declare const stripHtmlTags: (html: string) => string;
|
|
53
|
+
/**
|
|
54
|
+
* 連続する空白を正規化する
|
|
55
|
+
*
|
|
56
|
+
* @param text テキスト
|
|
57
|
+
* @returns 正規化されたテキスト
|
|
58
|
+
*/
|
|
59
|
+
export declare const normalizeWhitespace: (text: string) => string;
|
|
60
|
+
/**
|
|
61
|
+
* Confluence の Storage Format(XHTML)をプレーンテキストに変換する
|
|
62
|
+
*
|
|
63
|
+
* @param storageFormat Storage Format 文字列
|
|
64
|
+
* @returns プレーンテキスト
|
|
65
|
+
*/
|
|
66
|
+
export declare const convertStorageFormatToPlainText: (storageFormat: string | null | undefined) => string;
|