confluence-cli 1.31.1 → 1.32.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 +54 -2
- package/bin/confluence.js +53 -24
- package/lib/confluence-client.js +143 -582
- package/lib/html-to-markdown.js +150 -0
- package/lib/macro-converter.js +298 -0
- package/npm-shrinkwrap.json +499 -4754
- package/package.json +1 -1
- package/plugins/confluence/skills/confluence/SKILL.md +6 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const NAMED_ENTITIES = {
|
|
2
|
+
aring: 'å', auml: 'ä', ouml: 'ö',
|
|
3
|
+
eacute: 'é', egrave: 'è', ecirc: 'ê', euml: 'ë',
|
|
4
|
+
aacute: 'á', agrave: 'à', acirc: 'â', atilde: 'ã',
|
|
5
|
+
oacute: 'ó', ograve: 'ò', ocirc: 'ô', otilde: 'õ',
|
|
6
|
+
uacute: 'ú', ugrave: 'ù', ucirc: 'û', uuml: 'ü',
|
|
7
|
+
iacute: 'í', igrave: 'ì', icirc: 'î', iuml: 'ï',
|
|
8
|
+
ntilde: 'ñ', ccedil: 'ç', szlig: 'ß', yuml: 'ÿ',
|
|
9
|
+
eth: 'ð', thorn: 'þ',
|
|
10
|
+
Aring: 'Å', Auml: 'Ä', Ouml: 'Ö',
|
|
11
|
+
Eacute: 'É', Egrave: 'È', Ecirc: 'Ê', Euml: 'Ë',
|
|
12
|
+
Aacute: 'Á', Agrave: 'À', Acirc: 'Â', Atilde: 'Ã',
|
|
13
|
+
Oacute: 'Ó', Ograve: 'Ò', Ocirc: 'Ô', Otilde: 'Õ',
|
|
14
|
+
Uacute: 'Ú', Ugrave: 'Ù', Ucirc: 'Û', Uuml: 'Ü',
|
|
15
|
+
Iacute: 'Í', Igrave: 'Ì', Icirc: 'Î', Iuml: 'Ï',
|
|
16
|
+
Ntilde: 'Ñ', Ccedil: 'Ç', Szlig: 'SS', Yuml: 'Ÿ',
|
|
17
|
+
Eth: 'Ð', Thorn: 'Þ'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function htmlToMarkdown(html) {
|
|
21
|
+
let markdown = html;
|
|
22
|
+
|
|
23
|
+
markdown = markdown.replace(/<time\s+datetime="([^"]+)"[^>]*(?:\/>|>\s*<\/time>)/g, '$1');
|
|
24
|
+
|
|
25
|
+
markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**');
|
|
26
|
+
|
|
27
|
+
markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*');
|
|
28
|
+
|
|
29
|
+
markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`');
|
|
30
|
+
|
|
31
|
+
markdown = markdown.replace(/<(\w+)[^>]*>/g, '<$1>');
|
|
32
|
+
markdown = markdown.replace(/<\/(\w+)[^>]*>/g, '</$1>');
|
|
33
|
+
|
|
34
|
+
markdown = markdown.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, (_, level, text) => {
|
|
35
|
+
return '\n' + '#'.repeat(parseInt(level)) + ' ' + text.trim() + '\n';
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
markdown = markdown.replace(/<table>(.*?)<\/table>/gs, (_, content) => {
|
|
39
|
+
const rows = [];
|
|
40
|
+
let isHeader = true;
|
|
41
|
+
|
|
42
|
+
const rowMatches = content.match(/<tr>(.*?)<\/tr>/gs);
|
|
43
|
+
if (rowMatches) {
|
|
44
|
+
rowMatches.forEach(rowMatch => {
|
|
45
|
+
const cells = [];
|
|
46
|
+
const cellContent = rowMatch.replace(/<tr>(.*?)<\/tr>/s, '$1');
|
|
47
|
+
|
|
48
|
+
const cellMatches = cellContent.match(/<t[hd]>(.*?)<\/t[hd]>/gs);
|
|
49
|
+
if (cellMatches) {
|
|
50
|
+
cellMatches.forEach(cellMatch => {
|
|
51
|
+
let cellText = cellMatch.replace(/<t[hd]>(.*?)<\/t[hd]>/s, '$1');
|
|
52
|
+
cellText = cellText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
|
|
53
|
+
cellText = cellText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
54
|
+
cells.push(cellText || ' ');
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (cells.length > 0) {
|
|
59
|
+
rows.push('| ' + cells.join(' | ') + ' |');
|
|
60
|
+
|
|
61
|
+
if (isHeader) {
|
|
62
|
+
rows.push('| ' + cells.map(() => '---').join(' | ') + ' |');
|
|
63
|
+
isHeader = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return rows.length > 0 ? '\n' + rows.join('\n') + '\n' : '';
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
markdown = markdown.replace(/<ul>(.*?)<\/ul>/gs, (_, content) => {
|
|
73
|
+
let listItems = '';
|
|
74
|
+
const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
|
|
75
|
+
if (itemMatches) {
|
|
76
|
+
itemMatches.forEach(itemMatch => {
|
|
77
|
+
let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
|
|
78
|
+
itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
|
|
79
|
+
itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
80
|
+
if (itemText) {
|
|
81
|
+
listItems += '- ' + itemText + '\n';
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return '\n' + listItems;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
markdown = markdown.replace(/<ol>(.*?)<\/ol>/gs, (_, content) => {
|
|
89
|
+
let listItems = '';
|
|
90
|
+
let counter = 1;
|
|
91
|
+
const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
|
|
92
|
+
if (itemMatches) {
|
|
93
|
+
itemMatches.forEach(itemMatch => {
|
|
94
|
+
let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
|
|
95
|
+
itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
|
|
96
|
+
itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
97
|
+
if (itemText) {
|
|
98
|
+
listItems += `${counter++}. ${itemText}\n`;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return '\n' + listItems;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
markdown = markdown.replace(/<p>(.*?)<\/p>/gs, (_, content) => {
|
|
106
|
+
return '\n' + content.trim() + '\n';
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
markdown = markdown.replace(/<br\s*\/?>/g, '\n');
|
|
110
|
+
|
|
111
|
+
markdown = markdown.replace(/<hr\s*\/?>/g, '\n---\n');
|
|
112
|
+
|
|
113
|
+
markdown = markdown.replace(/<(?!\/?(details|summary)\b)[^>]+>/g, ' ');
|
|
114
|
+
|
|
115
|
+
markdown = markdown.replace(/ /g, ' ');
|
|
116
|
+
markdown = markdown.replace(/</g, '<');
|
|
117
|
+
markdown = markdown.replace(/>/g, '>');
|
|
118
|
+
markdown = markdown.replace(/&/g, '&');
|
|
119
|
+
markdown = markdown.replace(/"/g, '"');
|
|
120
|
+
markdown = markdown.replace(/'/g, '\'');
|
|
121
|
+
markdown = markdown.replace(/“/g, '"');
|
|
122
|
+
markdown = markdown.replace(/”/g, '"');
|
|
123
|
+
markdown = markdown.replace(/‘/g, '\'');
|
|
124
|
+
markdown = markdown.replace(/’/g, '\'');
|
|
125
|
+
markdown = markdown.replace(/—/g, '—');
|
|
126
|
+
markdown = markdown.replace(/–/g, '–');
|
|
127
|
+
markdown = markdown.replace(/…/g, '...');
|
|
128
|
+
markdown = markdown.replace(/•/g, '•');
|
|
129
|
+
markdown = markdown.replace(/©/g, '©');
|
|
130
|
+
markdown = markdown.replace(/®/g, '®');
|
|
131
|
+
markdown = markdown.replace(/™/g, '™');
|
|
132
|
+
markdown = markdown.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
|
|
133
|
+
markdown = markdown.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
134
|
+
|
|
135
|
+
markdown = markdown.replace(/&([a-zA-Z]+);/g, (match, name) => NAMED_ENTITIES[name] || match);
|
|
136
|
+
|
|
137
|
+
markdown = markdown.replace(/[ \t]+$/gm, '');
|
|
138
|
+
markdown = markdown.replace(/^[ \t]+(?!([`>]|[*+-] |\d+[.)] ))/gm, '');
|
|
139
|
+
markdown = markdown.replace(/^(#{1,6}[^\n]+)\n(?!\n)/gm, '$1\n\n');
|
|
140
|
+
markdown = markdown.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
|
141
|
+
markdown = markdown.replace(/[ \t]+/g, ' ');
|
|
142
|
+
markdown = markdown.trim();
|
|
143
|
+
|
|
144
|
+
return markdown;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
htmlToMarkdown,
|
|
149
|
+
NAMED_ENTITIES
|
|
150
|
+
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
const MarkdownIt = require('markdown-it');
|
|
2
|
+
const { htmlToMarkdown } = require('./html-to-markdown');
|
|
3
|
+
|
|
4
|
+
class MacroConverter {
|
|
5
|
+
constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null } = {}) {
|
|
6
|
+
this._isCloud = isCloud;
|
|
7
|
+
this.webUrlPrefix = webUrlPrefix;
|
|
8
|
+
this.buildUrl = buildUrl || ((pathOrUrl) => pathOrUrl);
|
|
9
|
+
this.markdown = new MarkdownIt();
|
|
10
|
+
this.setupConfluenceMarkdownExtensions();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
isCloud() {
|
|
14
|
+
return this._isCloud;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setupConfluenceMarkdownExtensions() {
|
|
18
|
+
this.markdown.enable(['table', 'strikethrough', 'linkify']);
|
|
19
|
+
|
|
20
|
+
this.markdown.core.ruler.before('normalize', 'confluence_macros', (state) => {
|
|
21
|
+
const src = state.src;
|
|
22
|
+
|
|
23
|
+
state.src = src.replace(/\[!info\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
|
|
24
|
+
return `> **INFO**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
state.src = state.src.replace(/\[!warning\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
|
|
28
|
+
return `> **WARNING**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
state.src = state.src.replace(/\[!note\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
|
|
32
|
+
return `> **NOTE**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
state.src = state.src.replace(/^(\s*)- \[([ x])\] (.+)$/gm, (_, indent, checked, text) => {
|
|
36
|
+
return `${indent}- [${checked}] ${text}`;
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
markdownToStorage(markdown) {
|
|
42
|
+
const html = this.markdown.render(markdown);
|
|
43
|
+
return this.htmlToConfluenceStorage(html);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
markdownToNativeStorage(markdown) {
|
|
47
|
+
const html = this.markdown.render(markdown);
|
|
48
|
+
return this.htmlToConfluenceStorage(html);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
htmlToConfluenceStorage(html) {
|
|
52
|
+
let storage = html;
|
|
53
|
+
|
|
54
|
+
storage = storage.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, '<h$1>$2</h$1>');
|
|
55
|
+
|
|
56
|
+
storage = storage.replace(/<p>(.*?)<\/p>/g, '<p>$1</p>');
|
|
57
|
+
|
|
58
|
+
storage = storage.replace(/<strong>(.*?)<\/strong>/g, '<strong>$1</strong>');
|
|
59
|
+
|
|
60
|
+
storage = storage.replace(/<em>(.*?)<\/em>/g, '<em>$1</em>');
|
|
61
|
+
|
|
62
|
+
storage = storage.replace(/<ul>(.*?)<\/ul>/gs, '<ul>$1</ul>');
|
|
63
|
+
storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
|
|
64
|
+
|
|
65
|
+
storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
|
|
66
|
+
|
|
67
|
+
storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
|
|
68
|
+
const language = lang || 'text';
|
|
69
|
+
const decodedCode = code.replace(/\n$/, '')
|
|
70
|
+
.replace(/"/g, '"')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
73
|
+
.replace(/&/g, '&');
|
|
74
|
+
const safeCode = decodedCode.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
75
|
+
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${safeCode}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
|
|
79
|
+
|
|
80
|
+
storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
|
|
81
|
+
if (content.includes('<strong>INFO</strong>')) {
|
|
82
|
+
const cleanContent = content.replace(/<p><strong>INFO<\/strong><\/p>\s*/, '');
|
|
83
|
+
return `<ac:structured-macro ac:name="info">
|
|
84
|
+
<ac:rich-text-body>${cleanContent}</ac:rich-text-body>
|
|
85
|
+
</ac:structured-macro>`;
|
|
86
|
+
} else if (content.includes('<strong>WARNING</strong>')) {
|
|
87
|
+
const cleanContent = content.replace(/<p><strong>WARNING<\/strong><\/p>\s*/, '');
|
|
88
|
+
return `<ac:structured-macro ac:name="warning">
|
|
89
|
+
<ac:rich-text-body>${cleanContent}</ac:rich-text-body>
|
|
90
|
+
</ac:structured-macro>`;
|
|
91
|
+
} else if (content.includes('<strong>NOTE</strong>')) {
|
|
92
|
+
const cleanContent = content.replace(/<p><strong>NOTE<\/strong><\/p>\s*/, '');
|
|
93
|
+
return `<ac:structured-macro ac:name="note">
|
|
94
|
+
<ac:rich-text-body>${cleanContent}</ac:rich-text-body>
|
|
95
|
+
</ac:structured-macro>`;
|
|
96
|
+
} else {
|
|
97
|
+
return `<ac:structured-macro ac:name="info">
|
|
98
|
+
<ac:rich-text-body>${content}</ac:rich-text-body>
|
|
99
|
+
</ac:structured-macro>`;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
|
|
104
|
+
storage = storage.replace(/<thead>(.*?)<\/thead>/gs, '<thead>$1</thead>');
|
|
105
|
+
storage = storage.replace(/<tbody>(.*?)<\/tbody>/gs, '<tbody>$1</tbody>');
|
|
106
|
+
storage = storage.replace(/<tr>(.*?)<\/tr>/gs, '<tr>$1</tr>');
|
|
107
|
+
storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
|
|
108
|
+
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
109
|
+
|
|
110
|
+
if (this.isCloud()) {
|
|
111
|
+
storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<a href="$1" data-card-appearance="inline">$2</a>');
|
|
112
|
+
} else {
|
|
113
|
+
storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<ac:link><ri:url ri:value="$1" /><ac:plain-text-link-body><![CDATA[$2]]></ac:plain-text-link-body></ac:link>');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
|
|
117
|
+
|
|
118
|
+
return storage;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
detectLanguageLabels(text) {
|
|
122
|
+
const labels = {
|
|
123
|
+
includePage: 'Include Page',
|
|
124
|
+
sharedBlock: 'Shared Block',
|
|
125
|
+
includeSharedBlock: 'Include Shared Block',
|
|
126
|
+
fromPage: 'from page',
|
|
127
|
+
expandDetails: 'Expand Details'
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (/[\u4e00-\u9fa5]/.test(text)) {
|
|
131
|
+
labels.includePage = '包含页面';
|
|
132
|
+
labels.sharedBlock = '共享块';
|
|
133
|
+
labels.includeSharedBlock = '包含共享块';
|
|
134
|
+
labels.fromPage = '来自页面';
|
|
135
|
+
labels.expandDetails = '展开详情';
|
|
136
|
+
} else if (/[\u3040-\u309f\u30a0-\u30ff]/.test(text)) {
|
|
137
|
+
labels.includePage = 'ページを含む';
|
|
138
|
+
labels.sharedBlock = '共有ブロック';
|
|
139
|
+
labels.includeSharedBlock = '共有ブロックを含む';
|
|
140
|
+
labels.fromPage = 'ページから';
|
|
141
|
+
labels.expandDetails = '詳細を表示';
|
|
142
|
+
} else if (/[\uac00-\ud7af]/.test(text)) {
|
|
143
|
+
labels.includePage = '페이지 포함';
|
|
144
|
+
labels.sharedBlock = '공유 블록';
|
|
145
|
+
labels.includeSharedBlock = '공유 블록 포함';
|
|
146
|
+
labels.fromPage = '페이지에서';
|
|
147
|
+
labels.expandDetails = '상세 보기';
|
|
148
|
+
} else if (/[\u0400-\u04ff]/.test(text)) {
|
|
149
|
+
labels.includePage = 'Включить страницу';
|
|
150
|
+
labels.sharedBlock = 'Общий блок';
|
|
151
|
+
labels.includeSharedBlock = 'Включить общий блок';
|
|
152
|
+
labels.fromPage = 'со страницы';
|
|
153
|
+
labels.expandDetails = 'Подробнее';
|
|
154
|
+
} else if ((text.match(/[àâäéèêëïîôùûüÿœæç]/gi) || []).length >= 2) {
|
|
155
|
+
labels.includePage = 'Inclure la page';
|
|
156
|
+
labels.sharedBlock = 'Bloc partagé';
|
|
157
|
+
labels.includeSharedBlock = 'Inclure le bloc partagé';
|
|
158
|
+
labels.fromPage = 'de la page';
|
|
159
|
+
labels.expandDetails = 'Détails';
|
|
160
|
+
} else if ((text.match(/[äöüß]/gi) || []).length >= 2) {
|
|
161
|
+
labels.includePage = 'Seite einbinden';
|
|
162
|
+
labels.sharedBlock = 'Gemeinsamer Block';
|
|
163
|
+
labels.includeSharedBlock = 'Gemeinsamen Block einbinden';
|
|
164
|
+
labels.fromPage = 'von Seite';
|
|
165
|
+
labels.expandDetails = 'Details';
|
|
166
|
+
} else if ((text.match(/[áéíóúñ¿¡]/gi) || []).length >= 2) {
|
|
167
|
+
labels.includePage = 'Incluir página';
|
|
168
|
+
labels.sharedBlock = 'Bloque compartido';
|
|
169
|
+
labels.includeSharedBlock = 'Incluir bloque compartido';
|
|
170
|
+
labels.fromPage = 'de la página';
|
|
171
|
+
labels.expandDetails = 'Detalles';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return labels;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
storageToMarkdown(storage, options = {}) {
|
|
178
|
+
const attachmentsDir = options.attachmentsDir || 'attachments';
|
|
179
|
+
let markdown = storage;
|
|
180
|
+
|
|
181
|
+
const labels = this.detectLanguageLabels(markdown);
|
|
182
|
+
|
|
183
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*\s*\/>/g, '');
|
|
184
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
185
|
+
|
|
186
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="floatmenu"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
187
|
+
|
|
188
|
+
markdown = markdown.replace(/<ac:image[^>]*>\s*<ri:attachment\s+ri:filename="([^"]+)"[^>]*\s*\/>\s*<\/ac:image>/g, (_, filename) => {
|
|
189
|
+
return ``;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
markdown = markdown.replace(/<ac:image[^>]*><ri:attachment\s+ri:filename="([^"]+)"[^>]*><\/ri:attachment><\/ac:image>/g, (_, filename) => {
|
|
193
|
+
return ``;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="mermaid-macro"[^>]*>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, code) => {
|
|
197
|
+
return `\n\`\`\`mermaid\n${code.trim()}\n\`\`\`\n`;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="expand"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
|
|
201
|
+
return `\n<details>\n<summary>${labels.expandDetails}</summary>\n\n${content}\n\n</details>\n`;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:parameter ac:name="language">([^<]*)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, lang, code) => {
|
|
205
|
+
return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, code) => {
|
|
209
|
+
return `\n\`\`\`\n${code}\n\`\`\`\n`;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="info"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
|
|
213
|
+
const cleanContent = htmlToMarkdown(content);
|
|
214
|
+
return `[!info]\n${cleanContent}`;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="warning"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
|
|
218
|
+
const cleanContent = htmlToMarkdown(content);
|
|
219
|
+
return `[!warning]\n${cleanContent}`;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="note"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
|
|
223
|
+
const cleanContent = htmlToMarkdown(content);
|
|
224
|
+
return `[!note]\n${cleanContent}`;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
markdown = markdown.replace(/<ac:task-list>([\s\S]*?)<\/ac:task-list>/g, (_, content) => {
|
|
228
|
+
const tasks = [];
|
|
229
|
+
const taskRegex = /<ac:task>[\s\S]*?<ac:task-status>([^<]*)<\/ac:task-status>[\s\S]*?<ac:task-body>([\s\S]*?)<\/ac:task-body>[\s\S]*?<\/ac:task>/g;
|
|
230
|
+
let match;
|
|
231
|
+
while ((match = taskRegex.exec(content)) !== null) {
|
|
232
|
+
const status = match[1];
|
|
233
|
+
let taskBody = match[2];
|
|
234
|
+
taskBody = taskBody.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
235
|
+
const checkbox = status === 'complete' ? '[x]' : '[ ]';
|
|
236
|
+
if (taskBody) {
|
|
237
|
+
tasks.push(`- ${checkbox} ${taskBody}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return tasks.length > 0 ? '\n' + tasks.join('\n') + '\n' : '';
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="panel"[^>]*>[\s\S]*?<ac:parameter ac:name="title">([^<]*)<\/ac:parameter>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, title, content) => {
|
|
244
|
+
const cleanContent = htmlToMarkdown(content);
|
|
245
|
+
return `\n> **${title}**\n>\n${cleanContent.split('\n').map(line => line ? `> ${line}` : '>').join('\n')}\n`;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="include"[^>]*>[\s\S]*?<ac:parameter ac:name="">[\s\S]*?<ac:link>[\s\S]*?<ri:page\s+ri:space-key="([^"]+)"\s+ri:content-title="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:link>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, spaceKey, title) => {
|
|
249
|
+
if (spaceKey.startsWith('~')) {
|
|
250
|
+
const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`;
|
|
251
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`${this.webUrlPrefix}/${spacePath}`)})\n`;
|
|
252
|
+
} else {
|
|
253
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`${this.webUrlPrefix}/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="(shared-block|include-shared-block)"[^>]*>[\s\S]*?<ac:parameter ac:name="shared-block-key">([^<]*)<\/ac:parameter>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, macroType, blockKey, content) => {
|
|
258
|
+
const cleanContent = htmlToMarkdown(content);
|
|
259
|
+
return `\n> **${labels.sharedBlock}: ${blockKey}**\n>\n${cleanContent.split('\n').map(line => line ? `> ${line}` : '>').join('\n')}\n`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="include-shared-block"[^>]*>[\s\S]*?<ac:parameter ac:name="shared-block-key">([^<]*)<\/ac:parameter>[\s\S]*?<ac:parameter ac:name="page">[\s\S]*?<ac:link>[\s\S]*?<ri:page\s+ri:space-key="([^"]+)"\s+ri:content-title="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:link>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, blockKey, spaceKey, pageTitle) => {
|
|
263
|
+
return `\n> 📄 **${labels.includeSharedBlock}**: ${blockKey} (${labels.fromPage}: ${pageTitle} [link needs manual correction])\n`;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="view-file"[^>]*>[\s\S]*?<ac:parameter ac:name="name">[\s\S]*?<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, filename) => {
|
|
267
|
+
return `\n📎 [${filename}](${attachmentsDir}/${filename})\n`;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="view-file"[^>]*>[\s\S]*?<ac:parameter ac:name="name">[\s\S]*?<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:parameter>[\s\S]*?<ac:parameter ac:name="height">([^<]*)<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, filename, _height) => {
|
|
271
|
+
return `\n📎 [${filename}](${attachmentsDir}/${filename})\n`;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
markdown = markdown.replace(/<ac:layout>/g, '');
|
|
275
|
+
markdown = markdown.replace(/<\/ac:layout>/g, '');
|
|
276
|
+
markdown = markdown.replace(/<ac:layout-section[^>]*>/g, '');
|
|
277
|
+
markdown = markdown.replace(/<\/ac:layout-section>/g, '');
|
|
278
|
+
markdown = markdown.replace(/<ac:layout-cell[^>]*>/g, '');
|
|
279
|
+
markdown = markdown.replace(/<\/ac:layout-cell>/g, '');
|
|
280
|
+
|
|
281
|
+
markdown = markdown.replace(/<ac:structured-macro[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
282
|
+
|
|
283
|
+
markdown = markdown.replace(/<ac:link><ri:url ri:value="([^"]*)" \/><ac:plain-text-link-body><!\[CDATA\[([^\]]*)\]\]><\/ac:plain-text-link-body><\/ac:link>/g, '[$2]($1)');
|
|
284
|
+
|
|
285
|
+
markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*\/>\s*<\/ac:link>/g, '[$1]');
|
|
286
|
+
markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*>\s*<\/ri:page>\s*<\/ac:link>/g, '[$1]');
|
|
287
|
+
|
|
288
|
+
markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<ac:link-body>([\s\S]*?)<\/ac:link-body>[\s\S]*?<\/ac:link>/g, '$1');
|
|
289
|
+
|
|
290
|
+
markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<\/ac:link>/g, '');
|
|
291
|
+
|
|
292
|
+
markdown = htmlToMarkdown(markdown);
|
|
293
|
+
|
|
294
|
+
return markdown;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = MacroConverter;
|