confluence-cli 1.3.2 → 1.4.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/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +26 -0
- package/bin/confluence.js +1 -1
- package/lib/confluence-client.js +304 -5
- package/package.json +2 -2
- package/tests/confluence-client.test.js +111 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/pchuri/confluence-cli/compare/v1.3.2...v1.4.0) (2025-06-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Enhanced Markdown Support with Bidirectional Conversion ([#5](https://github.com/pchuri/confluence-cli/issues/5)) ([d17771b](https://github.com/pchuri/confluence-cli/commit/d17771b40d8d60ed68c0ac0a3594fed6b9a4e771))
|
|
7
|
+
|
|
1
8
|
## [1.3.2](https://github.com/pchuri/confluence-cli/compare/v1.3.1...v1.3.2) (2025-06-27)
|
|
2
9
|
|
|
3
10
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -126,6 +126,32 @@ node bin/confluence.js read 123456789
|
|
|
126
126
|
- Add comments for complex logic
|
|
127
127
|
- Keep functions small and focused
|
|
128
128
|
|
|
129
|
+
### Markdown Support
|
|
130
|
+
|
|
131
|
+
The CLI includes enhanced markdown support with:
|
|
132
|
+
|
|
133
|
+
- **Native Confluence Storage Format**: Converts markdown to native Confluence elements instead of HTML macros
|
|
134
|
+
- **Confluence Extensions**: Support for admonitions (`[!info]`, `[!warning]`, `[!note]`)
|
|
135
|
+
- **Bidirectional Conversion**: Convert from markdown to storage format and back
|
|
136
|
+
- **Rich Elements**: Tables, code blocks, lists, links, and formatting
|
|
137
|
+
|
|
138
|
+
Example markdown with Confluence extensions:
|
|
139
|
+
```markdown
|
|
140
|
+
# My Page
|
|
141
|
+
|
|
142
|
+
[!info]
|
|
143
|
+
This is an info admonition that will render as a Confluence info macro.
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
console.log("Code blocks preserve syntax highlighting");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Feature | Status |
|
|
150
|
+
|---------|--------|
|
|
151
|
+
| Tables | ✅ |
|
|
152
|
+
| Lists | ✅ |
|
|
153
|
+
```
|
|
154
|
+
|
|
129
155
|
### File Structure
|
|
130
156
|
|
|
131
157
|
```
|
package/bin/confluence.js
CHANGED
|
@@ -23,7 +23,7 @@ program
|
|
|
23
23
|
program
|
|
24
24
|
.command('read <pageId>')
|
|
25
25
|
.description('Read a Confluence page by ID or URL')
|
|
26
|
-
.option('-f, --format <format>', 'Output format (html, text)', 'text')
|
|
26
|
+
.option('-f, --format <format>', 'Output format (html, text, markdown)', 'text')
|
|
27
27
|
.action(async (pageId, options) => {
|
|
28
28
|
const analytics = new Analytics();
|
|
29
29
|
try {
|
package/lib/confluence-client.js
CHANGED
|
@@ -8,6 +8,7 @@ class ConfluenceClient {
|
|
|
8
8
|
this.token = config.token;
|
|
9
9
|
this.domain = config.domain;
|
|
10
10
|
this.markdown = new MarkdownIt();
|
|
11
|
+
this.setupConfluenceMarkdownExtensions();
|
|
11
12
|
|
|
12
13
|
this.client = axios.create({
|
|
13
14
|
baseURL: this.baseURL,
|
|
@@ -62,6 +63,10 @@ class ConfluenceClient {
|
|
|
62
63
|
return htmlContent;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if (format === 'markdown') {
|
|
67
|
+
return this.storageToMarkdown(htmlContent);
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
// Convert HTML to text
|
|
66
71
|
return convert(htmlContent, {
|
|
67
72
|
wordwrap: 80,
|
|
@@ -139,11 +144,94 @@ class ConfluenceClient {
|
|
|
139
144
|
* Convert markdown to Confluence storage format
|
|
140
145
|
*/
|
|
141
146
|
markdownToStorage(markdown) {
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
// Convert markdown to HTML first
|
|
148
|
+
const html = this.markdown.render(markdown);
|
|
149
|
+
|
|
150
|
+
// Convert HTML to native Confluence storage format elements
|
|
151
|
+
return this.htmlToConfluenceStorage(html);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Convert HTML to native Confluence storage format
|
|
156
|
+
*/
|
|
157
|
+
htmlToConfluenceStorage(html) {
|
|
158
|
+
let storage = html;
|
|
159
|
+
|
|
160
|
+
// Convert headings to native Confluence format
|
|
161
|
+
storage = storage.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, '<h$1>$2</h$1>');
|
|
162
|
+
|
|
163
|
+
// Convert paragraphs
|
|
164
|
+
storage = storage.replace(/<p>(.*?)<\/p>/g, '<p>$1</p>');
|
|
165
|
+
|
|
166
|
+
// Convert strong/bold text
|
|
167
|
+
storage = storage.replace(/<strong>(.*?)<\/strong>/g, '<strong>$1</strong>');
|
|
168
|
+
|
|
169
|
+
// Convert emphasis/italic text
|
|
170
|
+
storage = storage.replace(/<em>(.*?)<\/em>/g, '<em>$1</em>');
|
|
171
|
+
|
|
172
|
+
// Convert unordered lists
|
|
173
|
+
storage = storage.replace(/<ul>(.*?)<\/ul>/gs, '<ul>$1</ul>');
|
|
174
|
+
storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
|
|
175
|
+
|
|
176
|
+
// Convert ordered lists
|
|
177
|
+
storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
|
|
178
|
+
|
|
179
|
+
// Convert code blocks to Confluence code macro
|
|
180
|
+
storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
|
|
181
|
+
const language = lang || 'text';
|
|
182
|
+
return `<ac:structured-macro ac:name="code">
|
|
183
|
+
<ac:parameter ac:name="language">${language}</ac:parameter>
|
|
184
|
+
<ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
|
|
185
|
+
</ac:structured-macro>`;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Convert inline code
|
|
189
|
+
storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
|
|
190
|
+
|
|
191
|
+
// Convert blockquotes to appropriate macros based on content
|
|
192
|
+
storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
|
|
193
|
+
// Check for admonition patterns
|
|
194
|
+
if (content.includes('<strong>INFO</strong>')) {
|
|
195
|
+
const cleanContent = content.replace(/<p><strong>INFO<\/strong><\/p>\s*/, '');
|
|
196
|
+
return `<ac:structured-macro ac:name="info">
|
|
197
|
+
<ac:rich-text-body>${cleanContent}</ac:rich-text-body>
|
|
198
|
+
</ac:structured-macro>`;
|
|
199
|
+
} else if (content.includes('<strong>WARNING</strong>')) {
|
|
200
|
+
const cleanContent = content.replace(/<p><strong>WARNING<\/strong><\/p>\s*/, '');
|
|
201
|
+
return `<ac:structured-macro ac:name="warning">
|
|
202
|
+
<ac:rich-text-body>${cleanContent}</ac:rich-text-body>
|
|
203
|
+
</ac:structured-macro>`;
|
|
204
|
+
} else if (content.includes('<strong>NOTE</strong>')) {
|
|
205
|
+
const cleanContent = content.replace(/<p><strong>NOTE<\/strong><\/p>\s*/, '');
|
|
206
|
+
return `<ac:structured-macro ac:name="note">
|
|
207
|
+
<ac:rich-text-body>${cleanContent}</ac:rich-text-body>
|
|
208
|
+
</ac:structured-macro>`;
|
|
209
|
+
} else {
|
|
210
|
+
// Default to info macro for regular blockquotes
|
|
211
|
+
return `<ac:structured-macro ac:name="info">
|
|
212
|
+
<ac:rich-text-body>${content}</ac:rich-text-body>
|
|
213
|
+
</ac:structured-macro>`;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Convert tables
|
|
218
|
+
storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
|
|
219
|
+
storage = storage.replace(/<thead>(.*?)<\/thead>/gs, '<thead>$1</thead>');
|
|
220
|
+
storage = storage.replace(/<tbody>(.*?)<\/tbody>/gs, '<tbody>$1</tbody>');
|
|
221
|
+
storage = storage.replace(/<tr>(.*?)<\/tr>/gs, '<tr>$1</tr>');
|
|
222
|
+
storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
|
|
223
|
+
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
224
|
+
|
|
225
|
+
// Convert links
|
|
226
|
+
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>');
|
|
227
|
+
|
|
228
|
+
// Convert horizontal rules
|
|
229
|
+
storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
|
|
230
|
+
|
|
231
|
+
// Clean up any remaining HTML entities and normalize whitespace
|
|
232
|
+
storage = storage.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
233
|
+
|
|
234
|
+
return storage;
|
|
147
235
|
}
|
|
148
236
|
|
|
149
237
|
/**
|
|
@@ -182,6 +270,217 @@ class ConfluenceClient {
|
|
|
182
270
|
return storage;
|
|
183
271
|
}
|
|
184
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Setup Confluence-specific markdown extensions
|
|
275
|
+
*/
|
|
276
|
+
setupConfluenceMarkdownExtensions() {
|
|
277
|
+
// Enable additional markdown-it features
|
|
278
|
+
this.markdown.enable(['table', 'strikethrough', 'linkify']);
|
|
279
|
+
|
|
280
|
+
// Add custom rule for Confluence macros in markdown
|
|
281
|
+
this.markdown.core.ruler.before('normalize', 'confluence_macros', (state) => {
|
|
282
|
+
const src = state.src;
|
|
283
|
+
|
|
284
|
+
// Convert [!info] admonitions to info macro
|
|
285
|
+
state.src = src.replace(/\[!info\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
|
|
286
|
+
return `> **INFO**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Convert [!warning] admonitions to warning macro
|
|
290
|
+
state.src = state.src.replace(/\[!warning\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
|
|
291
|
+
return `> **WARNING**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Convert [!note] admonitions to note macro
|
|
295
|
+
state.src = state.src.replace(/\[!note\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
|
|
296
|
+
return `> **NOTE**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Convert task lists to proper format
|
|
300
|
+
state.src = state.src.replace(/^(\s*)- \[([ x])\] (.+)$/gm, (_, indent, checked, text) => {
|
|
301
|
+
return `${indent}- [${checked}] ${text}`;
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Convert Confluence storage format to markdown
|
|
308
|
+
*/
|
|
309
|
+
storageToMarkdown(storage) {
|
|
310
|
+
let markdown = storage;
|
|
311
|
+
|
|
312
|
+
// Remove table of contents macro
|
|
313
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*\s*\/>/g, '');
|
|
314
|
+
markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
315
|
+
|
|
316
|
+
// Convert Confluence code macros to markdown
|
|
317
|
+
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) => {
|
|
318
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Convert code macros without language parameter
|
|
322
|
+
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) => {
|
|
323
|
+
return `\`\`\`\n${code}\n\`\`\``;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Convert info macro to admonition
|
|
327
|
+
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) => {
|
|
328
|
+
const cleanContent = this.htmlToMarkdown(content);
|
|
329
|
+
return `[!info]\n${cleanContent}`;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Convert warning macro to admonition
|
|
333
|
+
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) => {
|
|
334
|
+
const cleanContent = this.htmlToMarkdown(content);
|
|
335
|
+
return `[!warning]\n${cleanContent}`;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Convert note macro to admonition
|
|
339
|
+
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) => {
|
|
340
|
+
const cleanContent = this.htmlToMarkdown(content);
|
|
341
|
+
return `[!note]\n${cleanContent}`;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Remove other unhandled macros (replace with empty string for now)
|
|
345
|
+
markdown = markdown.replace(/<ac:structured-macro[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
346
|
+
|
|
347
|
+
// Convert links
|
|
348
|
+
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)');
|
|
349
|
+
|
|
350
|
+
// Convert remaining HTML to markdown
|
|
351
|
+
markdown = this.htmlToMarkdown(markdown);
|
|
352
|
+
|
|
353
|
+
return markdown;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Convert basic HTML to markdown
|
|
358
|
+
*/
|
|
359
|
+
htmlToMarkdown(html) {
|
|
360
|
+
let markdown = html;
|
|
361
|
+
|
|
362
|
+
// Convert strong/bold BEFORE removing HTML attributes
|
|
363
|
+
markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**');
|
|
364
|
+
|
|
365
|
+
// Convert emphasis/italic BEFORE removing HTML attributes
|
|
366
|
+
markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*');
|
|
367
|
+
|
|
368
|
+
// Convert code BEFORE removing HTML attributes
|
|
369
|
+
markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`');
|
|
370
|
+
|
|
371
|
+
// Remove HTML attributes from tags (but preserve content formatting)
|
|
372
|
+
markdown = markdown.replace(/<(\w+)[^>]*>/g, '<$1>');
|
|
373
|
+
markdown = markdown.replace(/<\/(\w+)[^>]*>/g, '</$1>');
|
|
374
|
+
|
|
375
|
+
// Convert headings first (they don't contain other elements typically)
|
|
376
|
+
markdown = markdown.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, (_, level, text) => {
|
|
377
|
+
return '\n' + '#'.repeat(parseInt(level)) + ' ' + text.trim() + '\n';
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Convert tables BEFORE paragraphs
|
|
381
|
+
markdown = markdown.replace(/<table>(.*?)<\/table>/gs, (_, content) => {
|
|
382
|
+
const rows = [];
|
|
383
|
+
let isHeader = true;
|
|
384
|
+
|
|
385
|
+
// Extract table rows
|
|
386
|
+
const rowMatches = content.match(/<tr>(.*?)<\/tr>/gs);
|
|
387
|
+
if (rowMatches) {
|
|
388
|
+
rowMatches.forEach(rowMatch => {
|
|
389
|
+
const cells = [];
|
|
390
|
+
const cellContent = rowMatch.replace(/<tr>(.*?)<\/tr>/s, '$1');
|
|
391
|
+
|
|
392
|
+
// Extract cells (th or td)
|
|
393
|
+
const cellMatches = cellContent.match(/<t[hd]>(.*?)<\/t[hd]>/gs);
|
|
394
|
+
if (cellMatches) {
|
|
395
|
+
cellMatches.forEach(cellMatch => {
|
|
396
|
+
let cellText = cellMatch.replace(/<t[hd]>(.*?)<\/t[hd]>/s, '$1');
|
|
397
|
+
// Clean up cell content - remove nested HTML but preserve text and some formatting
|
|
398
|
+
cellText = cellText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
|
|
399
|
+
cellText = cellText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
400
|
+
cells.push(cellText || ' ');
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (cells.length > 0) {
|
|
405
|
+
rows.push('| ' + cells.join(' | ') + ' |');
|
|
406
|
+
|
|
407
|
+
if (isHeader) {
|
|
408
|
+
rows.push('| ' + cells.map(() => '---').join(' | ') + ' |');
|
|
409
|
+
isHeader = false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return rows.length > 0 ? '\n' + rows.join('\n') + '\n' : '';
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Convert unordered lists BEFORE paragraphs
|
|
419
|
+
markdown = markdown.replace(/<ul>(.*?)<\/ul>/gs, (_, content) => {
|
|
420
|
+
let listItems = '';
|
|
421
|
+
const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
|
|
422
|
+
if (itemMatches) {
|
|
423
|
+
itemMatches.forEach(itemMatch => {
|
|
424
|
+
let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
|
|
425
|
+
// Clean up nested HTML but preserve some formatting
|
|
426
|
+
itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
|
|
427
|
+
itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
428
|
+
if (itemText) {
|
|
429
|
+
listItems += '- ' + itemText + '\n';
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return '\n' + listItems;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Convert ordered lists BEFORE paragraphs
|
|
437
|
+
markdown = markdown.replace(/<ol>(.*?)<\/ol>/gs, (_, content) => {
|
|
438
|
+
let listItems = '';
|
|
439
|
+
let counter = 1;
|
|
440
|
+
const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
|
|
441
|
+
if (itemMatches) {
|
|
442
|
+
itemMatches.forEach(itemMatch => {
|
|
443
|
+
let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
|
|
444
|
+
// Clean up nested HTML but preserve some formatting
|
|
445
|
+
itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
|
|
446
|
+
itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
447
|
+
if (itemText) {
|
|
448
|
+
listItems += `${counter++}. ${itemText}\n`;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return '\n' + listItems;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Convert paragraphs (after lists and tables)
|
|
456
|
+
markdown = markdown.replace(/<p>(.*?)<\/p>/g, (_, content) => {
|
|
457
|
+
return content.trim() + '\n';
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Convert line breaks
|
|
461
|
+
markdown = markdown.replace(/<br\s*\/?>/g, '\n');
|
|
462
|
+
|
|
463
|
+
// Convert horizontal rules
|
|
464
|
+
markdown = markdown.replace(/<hr\s*\/?>/g, '\n---\n');
|
|
465
|
+
|
|
466
|
+
// Remove any remaining HTML tags
|
|
467
|
+
markdown = markdown.replace(/<[^>]+>/g, ' ');
|
|
468
|
+
|
|
469
|
+
// Clean up whitespace and HTML entities
|
|
470
|
+
markdown = markdown.replace(/ /g, ' ');
|
|
471
|
+
markdown = markdown.replace(/</g, '<');
|
|
472
|
+
markdown = markdown.replace(/>/g, '>');
|
|
473
|
+
markdown = markdown.replace(/&/g, '&');
|
|
474
|
+
markdown = markdown.replace(/"/g, '"');
|
|
475
|
+
|
|
476
|
+
// Clean up extra whitespace
|
|
477
|
+
markdown = markdown.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
|
478
|
+
markdown = markdown.replace(/[ \t]+/g, ' ');
|
|
479
|
+
markdown = markdown.trim();
|
|
480
|
+
|
|
481
|
+
return markdown;
|
|
482
|
+
}
|
|
483
|
+
|
|
185
484
|
/**
|
|
186
485
|
* Create a new Confluence page
|
|
187
486
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"wiki",
|
|
20
20
|
"documentation"
|
|
21
21
|
],
|
|
22
|
-
"author": "
|
|
22
|
+
"author": "pchuri",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"axios": "^1.6.2",
|
|
@@ -28,14 +28,121 @@ describe('ConfluenceClient', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe('markdownToStorage', () => {
|
|
31
|
-
test('should convert markdown to Confluence storage format', () => {
|
|
32
|
-
const markdown = '# Hello World\n\nThis is a **test** page.';
|
|
31
|
+
test('should convert basic markdown to native Confluence storage format', () => {
|
|
32
|
+
const markdown = '# Hello World\n\nThis is a **test** page with *italic* text.';
|
|
33
33
|
const result = client.markdownToStorage(markdown);
|
|
34
34
|
|
|
35
|
-
expect(result).toContain('<
|
|
36
|
-
expect(result).toContain('
|
|
35
|
+
expect(result).toContain('<h1>Hello World</h1>');
|
|
36
|
+
expect(result).toContain('<p>This is a <strong>test</strong> page with <em>italic</em> text.</p>');
|
|
37
|
+
expect(result).not.toContain('<ac:structured-macro ac:name="html">');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should convert code blocks to Confluence code macro', () => {
|
|
41
|
+
const markdown = '```javascript\nconsole.log("Hello World");\n```';
|
|
42
|
+
const result = client.markdownToStorage(markdown);
|
|
43
|
+
|
|
44
|
+
expect(result).toContain('<ac:structured-macro ac:name="code">');
|
|
45
|
+
expect(result).toContain('<ac:parameter ac:name="language">javascript</ac:parameter>');
|
|
46
|
+
expect(result).toContain('console.log("Hello World");');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should convert lists to native Confluence format', () => {
|
|
50
|
+
const markdown = '- Item 1\n- Item 2\n\n1. First\n2. Second';
|
|
51
|
+
const result = client.markdownToStorage(markdown);
|
|
52
|
+
|
|
53
|
+
expect(result).toContain('<ul>');
|
|
54
|
+
expect(result).toContain('<li><p>Item 1</p></li>');
|
|
55
|
+
expect(result).toContain('<ol>');
|
|
56
|
+
expect(result).toContain('<li><p>First</p></li>');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should convert Confluence admonitions', () => {
|
|
60
|
+
const markdown = '[!info]\nThis is an info message';
|
|
61
|
+
const result = client.markdownToStorage(markdown);
|
|
62
|
+
|
|
63
|
+
expect(result).toContain('<ac:structured-macro ac:name="info">');
|
|
64
|
+
expect(result).toContain('This is an info message');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should convert tables to native Confluence format', () => {
|
|
68
|
+
const markdown = '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |';
|
|
69
|
+
const result = client.markdownToStorage(markdown);
|
|
70
|
+
|
|
71
|
+
expect(result).toContain('<table>');
|
|
72
|
+
expect(result).toContain('<th><p>Header 1</p></th>');
|
|
73
|
+
expect(result).toContain('<td><p>Cell 1</p></td>');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should convert links to Confluence link format', () => {
|
|
77
|
+
const markdown = '[Example Link](https://example.com)';
|
|
78
|
+
const result = client.markdownToStorage(markdown);
|
|
79
|
+
|
|
80
|
+
expect(result).toContain('<ac:link>');
|
|
81
|
+
expect(result).toContain('ri:value="https://example.com"');
|
|
82
|
+
expect(result).toContain('Example Link');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('storageToMarkdown', () => {
|
|
87
|
+
test('should convert Confluence storage format to markdown', () => {
|
|
88
|
+
const storage = '<h1>Hello World</h1><p>This is a <strong>test</strong> page.</p>';
|
|
89
|
+
const result = client.storageToMarkdown(storage);
|
|
90
|
+
|
|
91
|
+
expect(result).toContain('# Hello World');
|
|
37
92
|
expect(result).toContain('**test**');
|
|
38
93
|
});
|
|
94
|
+
|
|
95
|
+
test('should convert Confluence code macro to markdown', () => {
|
|
96
|
+
const storage = '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">javascript</ac:parameter><ac:plain-text-body><![CDATA[console.log("Hello");]]></ac:plain-text-body></ac:structured-macro>';
|
|
97
|
+
const result = client.storageToMarkdown(storage);
|
|
98
|
+
|
|
99
|
+
expect(result).toContain('```javascript');
|
|
100
|
+
expect(result).toContain('console.log("Hello");');
|
|
101
|
+
expect(result).toContain('```');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should convert Confluence macros to admonitions', () => {
|
|
105
|
+
const storage = '<ac:structured-macro ac:name="info"><ac:rich-text-body><p>This is info</p></ac:rich-text-body></ac:structured-macro>';
|
|
106
|
+
const result = client.storageToMarkdown(storage);
|
|
107
|
+
|
|
108
|
+
expect(result).toContain('[!info]');
|
|
109
|
+
expect(result).toContain('This is info');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should convert Confluence links to markdown', () => {
|
|
113
|
+
const storage = '<ac:link><ri:url ri:value="https://example.com" /><ac:plain-text-link-body><![CDATA[Example]]></ac:plain-text-link-body></ac:link>';
|
|
114
|
+
const result = client.storageToMarkdown(storage);
|
|
115
|
+
|
|
116
|
+
expect(result).toContain('[Example](https://example.com)');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('htmlToMarkdown', () => {
|
|
121
|
+
test('should convert basic HTML to markdown', () => {
|
|
122
|
+
const html = '<h2>Title</h2><p>Some <strong>bold</strong> and <em>italic</em> text.</p>';
|
|
123
|
+
const result = client.htmlToMarkdown(html);
|
|
124
|
+
|
|
125
|
+
expect(result).toContain('## Title');
|
|
126
|
+
expect(result).toContain('**bold**');
|
|
127
|
+
expect(result).toContain('*italic*');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should convert HTML lists to markdown', () => {
|
|
131
|
+
const html = '<ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul>';
|
|
132
|
+
const result = client.htmlToMarkdown(html);
|
|
133
|
+
|
|
134
|
+
expect(result).toContain('- Item 1');
|
|
135
|
+
expect(result).toContain('- Item 2');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should convert HTML tables to markdown', () => {
|
|
139
|
+
const html = '<table><tr><th><p>Header</p></th></tr><tr><td><p>Cell</p></td></tr></table>';
|
|
140
|
+
const result = client.htmlToMarkdown(html);
|
|
141
|
+
|
|
142
|
+
expect(result).toContain('| Header |');
|
|
143
|
+
expect(result).toContain('| --- |');
|
|
144
|
+
expect(result).toContain('| Cell |');
|
|
145
|
+
});
|
|
39
146
|
});
|
|
40
147
|
|
|
41
148
|
describe('page creation and updates', () => {
|