@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 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)}`);
@@ -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, '&amp;');
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.4",
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",