@bestend/confluence-cli 1.15.4 → 1.15.7
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/bin/confluence.js +20 -3
- package/lib/confluence-client.js +44 -3
- package/package.json +2 -1
package/bin/confluence.js
CHANGED
|
@@ -146,6 +146,8 @@ program
|
|
|
146
146
|
.option('-f, --file <file>', 'Read content from file')
|
|
147
147
|
.option('-c, --content <content>', 'Page content as string')
|
|
148
148
|
.option('--format <format>', 'Content format (storage, html, markdown)', 'storage')
|
|
149
|
+
.option('--validate-storage', 'Validate storage content (XML well-formed) before sending')
|
|
150
|
+
.option('--no-sanitize-storage', 'Disable auto-escaping raw ampersands in storage format')
|
|
149
151
|
.action(async (title, spaceKey, options) => {
|
|
150
152
|
const analytics = new Analytics();
|
|
151
153
|
try {
|
|
@@ -166,7 +168,10 @@ program
|
|
|
166
168
|
throw new Error('Either --file or --content option is required');
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
const result = await client.createPage(title, spaceKey, content, options.format
|
|
171
|
+
const result = await client.createPage(title, spaceKey, content, options.format, {
|
|
172
|
+
validateStorage: options.validateStorage,
|
|
173
|
+
sanitizeStorage: options.sanitizeStorage
|
|
174
|
+
});
|
|
170
175
|
|
|
171
176
|
console.log(chalk.green('✅ Page created successfully!'));
|
|
172
177
|
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
@@ -189,6 +194,10 @@ program
|
|
|
189
194
|
.option('-f, --file <file>', 'Read content from file')
|
|
190
195
|
.option('-c, --content <content>', 'Page content as string')
|
|
191
196
|
.option('--format <format>', 'Content format (storage, html, markdown)', 'storage')
|
|
197
|
+
.option('--validate-storage', 'Validate storage content (XML well-formed) before sending')
|
|
198
|
+
.option('--no-sanitize-storage', 'Disable auto-escaping raw ampersands in storage format')
|
|
199
|
+
.option('--validate-storage', 'Validate storage content (XML well-formed) before sending')
|
|
200
|
+
.option('--no-sanitize-storage', 'Disable auto-escaping raw ampersands in storage format')
|
|
192
201
|
.action(async (title, parentId, options) => {
|
|
193
202
|
const analytics = new Analytics();
|
|
194
203
|
try {
|
|
@@ -213,7 +222,10 @@ program
|
|
|
213
222
|
throw new Error('Either --file or --content option is required');
|
|
214
223
|
}
|
|
215
224
|
|
|
216
|
-
const result = await client.createChildPage(title, spaceKey, parentId, content, options.format
|
|
225
|
+
const result = await client.createChildPage(title, spaceKey, parentId, content, options.format, {
|
|
226
|
+
validateStorage: options.validateStorage,
|
|
227
|
+
sanitizeStorage: options.sanitizeStorage
|
|
228
|
+
});
|
|
217
229
|
|
|
218
230
|
console.log(chalk.green('✅ Child page created successfully!'));
|
|
219
231
|
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
@@ -238,6 +250,8 @@ program
|
|
|
238
250
|
.option('-f, --file <file>', 'Read content from file')
|
|
239
251
|
.option('-c, --content <content>', 'Page content as string')
|
|
240
252
|
.option('--format <format>', 'Content format (storage, html, markdown)', 'storage')
|
|
253
|
+
.option('--validate-storage', 'Validate storage content (XML well-formed) before sending')
|
|
254
|
+
.option('--no-sanitize-storage', 'Disable auto-escaping raw ampersands in storage format')
|
|
241
255
|
.action(async (pageId, options) => {
|
|
242
256
|
const analytics = new Analytics();
|
|
243
257
|
try {
|
|
@@ -261,7 +275,10 @@ program
|
|
|
261
275
|
content = options.content;
|
|
262
276
|
}
|
|
263
277
|
|
|
264
|
-
const result = await client.updatePage(pageId, options.title, content, options.format
|
|
278
|
+
const result = await client.updatePage(pageId, options.title, content, options.format, {
|
|
279
|
+
validateStorage: options.validateStorage,
|
|
280
|
+
sanitizeStorage: options.sanitizeStorage
|
|
281
|
+
});
|
|
265
282
|
|
|
266
283
|
console.log(chalk.green('✅ Page updated successfully!'));
|
|
267
284
|
console.log(`Title: ${chalk.blue(result.title)}`);
|
package/lib/confluence-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const { convert } = require('html-to-text');
|
|
3
3
|
const MarkdownIt = require('markdown-it');
|
|
4
|
+
const { XMLValidator } = require('fast-xml-parser');
|
|
4
5
|
|
|
5
6
|
class ConfluenceClient {
|
|
6
7
|
constructor(config) {
|
|
@@ -24,6 +25,34 @@ class ConfluenceClient {
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
sanitizeStorageContent(content, options = {}) {
|
|
29
|
+
const sanitize = options.sanitizeStorage !== false;
|
|
30
|
+
const validate = options.validateStorage === true;
|
|
31
|
+
|
|
32
|
+
let result = content;
|
|
33
|
+
|
|
34
|
+
if (sanitize) {
|
|
35
|
+
// Escape unescaped ampersands that would break XML (except already-escaped entities)
|
|
36
|
+
result = result.replace(/&(?![a-zA-Z]+;|#\d+;|#x[0-9a-fA-F]+;)/g, '&');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (validate) {
|
|
40
|
+
const validation = XMLValidator.validate(result, {
|
|
41
|
+
allowBooleanAttributes: true,
|
|
42
|
+
suppressEmptyNodeCheck: true,
|
|
43
|
+
ignoreAttributes: false
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (validation !== true) {
|
|
47
|
+
const { err } = validation;
|
|
48
|
+
const location = err?.line ? ` (line ${err.line}${err.col ? `, col ${err.col}` : ''})` : '';
|
|
49
|
+
throw new Error(`Storage content is not well-formed XML: ${err.msg}${location}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
sanitizeApiPath(rawPath) {
|
|
28
57
|
const fallback = '/rest/api';
|
|
29
58
|
const value = (rawPath || '').trim();
|
|
@@ -1346,7 +1375,7 @@ class ConfluenceClient {
|
|
|
1346
1375
|
/**
|
|
1347
1376
|
* Create a new Confluence page
|
|
1348
1377
|
*/
|
|
1349
|
-
async createPage(title, spaceKey, content, format = 'storage') {
|
|
1378
|
+
async createPage(title, spaceKey, content, format = 'storage', options = {}) {
|
|
1350
1379
|
let storageContent = content;
|
|
1351
1380
|
|
|
1352
1381
|
if (format === 'markdown') {
|
|
@@ -1356,6 +1385,10 @@ class ConfluenceClient {
|
|
|
1356
1385
|
storageContent = content;
|
|
1357
1386
|
}
|
|
1358
1387
|
|
|
1388
|
+
if (format === 'storage') {
|
|
1389
|
+
storageContent = this.sanitizeStorageContent(storageContent, options);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1359
1392
|
const pageData = {
|
|
1360
1393
|
type: 'page',
|
|
1361
1394
|
title: title,
|
|
@@ -1377,7 +1410,7 @@ class ConfluenceClient {
|
|
|
1377
1410
|
/**
|
|
1378
1411
|
* Create a new Confluence page as a child of another page
|
|
1379
1412
|
*/
|
|
1380
|
-
async createChildPage(title, spaceKey, parentId, content, format = 'storage') {
|
|
1413
|
+
async createChildPage(title, spaceKey, parentId, content, format = 'storage', options = {}) {
|
|
1381
1414
|
let storageContent = content;
|
|
1382
1415
|
|
|
1383
1416
|
if (format === 'markdown') {
|
|
@@ -1387,6 +1420,10 @@ class ConfluenceClient {
|
|
|
1387
1420
|
storageContent = content;
|
|
1388
1421
|
}
|
|
1389
1422
|
|
|
1423
|
+
if (format === 'storage') {
|
|
1424
|
+
storageContent = this.sanitizeStorageContent(storageContent, options);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1390
1427
|
const pageData = {
|
|
1391
1428
|
type: 'page',
|
|
1392
1429
|
title: title,
|
|
@@ -1413,7 +1450,7 @@ class ConfluenceClient {
|
|
|
1413
1450
|
/**
|
|
1414
1451
|
* Update an existing Confluence page
|
|
1415
1452
|
*/
|
|
1416
|
-
async updatePage(pageId, title, content, format = 'storage') {
|
|
1453
|
+
async updatePage(pageId, title, content, format = 'storage', options = {}) {
|
|
1417
1454
|
// First, get the current page to get the version number and existing content
|
|
1418
1455
|
const currentPage = await this.client.get(`/content/${pageId}`, {
|
|
1419
1456
|
params: {
|
|
@@ -1433,6 +1470,10 @@ class ConfluenceClient {
|
|
|
1433
1470
|
} else { // 'storage' format
|
|
1434
1471
|
storageContent = content;
|
|
1435
1472
|
}
|
|
1473
|
+
|
|
1474
|
+
if (format === 'storage') {
|
|
1475
|
+
storageContent = this.sanitizeStorageContent(storageContent, options);
|
|
1476
|
+
}
|
|
1436
1477
|
} else {
|
|
1437
1478
|
// If no new content, use the existing content
|
|
1438
1479
|
storageContent = currentPage.data.body.storage.value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bestend/confluence-cli",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.7",
|
|
4
4
|
"description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"axios": "^1.12.0",
|
|
27
27
|
"chalk": "^4.1.2",
|
|
28
28
|
"commander": "^11.1.0",
|
|
29
|
+
"fast-xml-parser": "^4.5.3",
|
|
29
30
|
"html-to-text": "^9.0.5",
|
|
30
31
|
"inquirer": "^8.2.6",
|
|
31
32
|
"markdown-it": "^14.1.0",
|