@bestend/confluence-cli 1.15.5 → 1.15.8
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/.github/workflows/publish.yml +7 -5
- package/bin/confluence.js +20 -3
- package/lib/confluence-client.js +34 -11
- package/package.json +2 -1
|
@@ -19,14 +19,16 @@ jobs:
|
|
|
19
19
|
node-version: '24.x'
|
|
20
20
|
registry-url: 'https://registry.npmjs.org'
|
|
21
21
|
|
|
22
|
-
- name:
|
|
22
|
+
- name: Set package version from tag (idempotent)
|
|
23
23
|
run: |
|
|
24
24
|
TAG="${GITHUB_REF_NAME#v}"
|
|
25
25
|
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if [ "$TAG" = "$PKG_VERSION" ]; then
|
|
27
|
+
echo "package.json already at $PKG_VERSION (matches tag)"
|
|
28
|
+
else
|
|
29
|
+
echo "Setting package.json version to ${TAG}"
|
|
30
|
+
npm pkg set version="${TAG}"
|
|
31
|
+
npm install --package-lock-only
|
|
30
32
|
fi
|
|
31
33
|
|
|
32
34
|
- name: Install dependencies
|
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,10 +25,32 @@ class ConfluenceClient {
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
sanitizeStorageContent(content) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
sanitizeApiPath(rawPath) {
|
|
@@ -860,7 +883,7 @@ class ConfluenceClient {
|
|
|
860
883
|
.replace(/</g, '<')
|
|
861
884
|
.replace(/>/g, '>')
|
|
862
885
|
.replace(/"/g, '"')
|
|
863
|
-
.replace(/'/g,
|
|
886
|
+
.replace(/'/g, '\'');
|
|
864
887
|
decoded = decoded.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
865
888
|
|
|
866
889
|
return `<ac:structured-macro ac:name="code">
|
|
@@ -1352,7 +1375,7 @@ class ConfluenceClient {
|
|
|
1352
1375
|
/**
|
|
1353
1376
|
* Create a new Confluence page
|
|
1354
1377
|
*/
|
|
1355
|
-
async createPage(title, spaceKey, content, format = 'storage') {
|
|
1378
|
+
async createPage(title, spaceKey, content, format = 'storage', options = {}) {
|
|
1356
1379
|
let storageContent = content;
|
|
1357
1380
|
|
|
1358
1381
|
if (format === 'markdown') {
|
|
@@ -1363,7 +1386,7 @@ class ConfluenceClient {
|
|
|
1363
1386
|
}
|
|
1364
1387
|
|
|
1365
1388
|
if (format === 'storage') {
|
|
1366
|
-
storageContent = this.sanitizeStorageContent(storageContent);
|
|
1389
|
+
storageContent = this.sanitizeStorageContent(storageContent, options);
|
|
1367
1390
|
}
|
|
1368
1391
|
|
|
1369
1392
|
const pageData = {
|
|
@@ -1387,7 +1410,7 @@ class ConfluenceClient {
|
|
|
1387
1410
|
/**
|
|
1388
1411
|
* Create a new Confluence page as a child of another page
|
|
1389
1412
|
*/
|
|
1390
|
-
async createChildPage(title, spaceKey, parentId, content, format = 'storage') {
|
|
1413
|
+
async createChildPage(title, spaceKey, parentId, content, format = 'storage', options = {}) {
|
|
1391
1414
|
let storageContent = content;
|
|
1392
1415
|
|
|
1393
1416
|
if (format === 'markdown') {
|
|
@@ -1398,7 +1421,7 @@ class ConfluenceClient {
|
|
|
1398
1421
|
}
|
|
1399
1422
|
|
|
1400
1423
|
if (format === 'storage') {
|
|
1401
|
-
storageContent = this.sanitizeStorageContent(storageContent);
|
|
1424
|
+
storageContent = this.sanitizeStorageContent(storageContent, options);
|
|
1402
1425
|
}
|
|
1403
1426
|
|
|
1404
1427
|
const pageData = {
|
|
@@ -1427,7 +1450,7 @@ class ConfluenceClient {
|
|
|
1427
1450
|
/**
|
|
1428
1451
|
* Update an existing Confluence page
|
|
1429
1452
|
*/
|
|
1430
|
-
async updatePage(pageId, title, content, format = 'storage') {
|
|
1453
|
+
async updatePage(pageId, title, content, format = 'storage', options = {}) {
|
|
1431
1454
|
// First, get the current page to get the version number and existing content
|
|
1432
1455
|
const currentPage = await this.client.get(`/content/${pageId}`, {
|
|
1433
1456
|
params: {
|
|
@@ -1449,7 +1472,7 @@ class ConfluenceClient {
|
|
|
1449
1472
|
}
|
|
1450
1473
|
|
|
1451
1474
|
if (format === 'storage') {
|
|
1452
|
-
storageContent = this.sanitizeStorageContent(storageContent);
|
|
1475
|
+
storageContent = this.sanitizeStorageContent(storageContent, options);
|
|
1453
1476
|
}
|
|
1454
1477
|
} else {
|
|
1455
1478
|
// If no new content, use the existing content
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bestend/confluence-cli",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.8",
|
|
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",
|