confluence-cli 1.31.1 → 1.32.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/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 +2 -2
- 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;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.32.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "^1.15.0",
|
package/package.json
CHANGED
|
@@ -149,15 +149,16 @@ confluence --profile staging init --domain "staging.example.com" --auth-type bea
|
|
|
149
149
|
Read page content. Outputs to stdout.
|
|
150
150
|
|
|
151
151
|
```sh
|
|
152
|
-
confluence read <pageId> [--format html|text|markdown]
|
|
152
|
+
confluence read <pageId> [--format html|text|storage|markdown]
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
| Option | Default | Description |
|
|
156
156
|
|---|---|---|
|
|
157
|
-
| `--format` | `text` | Output format: `html`, `text`, or `markdown` |
|
|
157
|
+
| `--format` | `text` | Output format: `html`, `text`, `storage`, or `markdown` |
|
|
158
158
|
|
|
159
159
|
```sh
|
|
160
160
|
confluence read 123456789
|
|
161
|
+
confluence read 123456789 --format storage
|
|
161
162
|
confluence read 123456789 --format markdown
|
|
162
163
|
```
|
|
163
164
|
|
|
@@ -165,14 +166,15 @@ confluence read 123456789 --format markdown
|
|
|
165
166
|
|
|
166
167
|
### `info <pageId>`
|
|
167
168
|
|
|
168
|
-
Get page metadata
|
|
169
|
+
Get page metadata. Use `--format json` for machine-readable output.
|
|
169
170
|
|
|
170
171
|
```sh
|
|
171
|
-
confluence info <pageId>
|
|
172
|
+
confluence info <pageId> [--format text|json]
|
|
172
173
|
```
|
|
173
174
|
|
|
174
175
|
```sh
|
|
175
176
|
confluence info 123456789
|
|
177
|
+
confluence info 123456789 --format json
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
---
|