confluence-cli 2.5.0 → 2.6.1
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 +3 -17
- package/lib/html-to-storage.js +51 -0
- package/lib/macro-converter.js +16 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/bin/confluence.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
3
5
|
const { program } = require('commander');
|
|
4
6
|
const chalk = require('chalk');
|
|
5
7
|
const inquirer = require('inquirer');
|
|
@@ -195,8 +197,6 @@ program
|
|
|
195
197
|
.option('--dest <directory>', 'Target directory', './.claude/skills/confluence')
|
|
196
198
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
197
199
|
.action(async (options) => {
|
|
198
|
-
const fs = require('fs');
|
|
199
|
-
const path = require('path');
|
|
200
200
|
|
|
201
201
|
const skillSrc = path.join(__dirname, '..', 'plugins', 'confluence', 'skills', 'confluence', 'SKILL.md');
|
|
202
202
|
|
|
@@ -255,7 +255,6 @@ program
|
|
|
255
255
|
let content = '';
|
|
256
256
|
|
|
257
257
|
if (options.file) {
|
|
258
|
-
const fs = require('fs');
|
|
259
258
|
if (!fs.existsSync(options.file)) {
|
|
260
259
|
throw new Error(`File not found: ${options.file}`);
|
|
261
260
|
}
|
|
@@ -308,7 +307,6 @@ program
|
|
|
308
307
|
let content = '';
|
|
309
308
|
|
|
310
309
|
if (options.file) {
|
|
311
|
-
const fs = require('fs');
|
|
312
310
|
if (!fs.existsSync(options.file)) {
|
|
313
311
|
throw new Error(`File not found: ${options.file}`);
|
|
314
312
|
}
|
|
@@ -362,7 +360,6 @@ program
|
|
|
362
360
|
let content = null; // Use null to indicate no content change
|
|
363
361
|
|
|
364
362
|
if (options.file) {
|
|
365
|
-
const fs = require('fs');
|
|
366
363
|
if (!fs.existsSync(options.file)) {
|
|
367
364
|
throw new Error(`File not found: ${options.file}`);
|
|
368
365
|
}
|
|
@@ -615,7 +612,6 @@ program
|
|
|
615
612
|
console.log('');
|
|
616
613
|
|
|
617
614
|
if (options.output) {
|
|
618
|
-
const fs = require('fs');
|
|
619
615
|
fs.writeFileSync(options.output, pageData.content);
|
|
620
616
|
console.log(chalk.green(`✅ Content saved to: ${options.output}`));
|
|
621
617
|
console.log(chalk.yellow('💡 Edit the file and use "confluence update" to save changes'));
|
|
@@ -718,8 +714,6 @@ program
|
|
|
718
714
|
}
|
|
719
715
|
|
|
720
716
|
if (options.download) {
|
|
721
|
-
const fs = require('fs');
|
|
722
|
-
const path = require('path');
|
|
723
717
|
const destDir = path.resolve(options.dest || '.');
|
|
724
718
|
fs.mkdirSync(destDir, { recursive: true });
|
|
725
719
|
|
|
@@ -795,8 +789,6 @@ program
|
|
|
795
789
|
throw new Error('At least one --file option is required.');
|
|
796
790
|
}
|
|
797
791
|
|
|
798
|
-
const fs = require('fs');
|
|
799
|
-
const path = require('path');
|
|
800
792
|
const config = getConfig(getProfileName());
|
|
801
793
|
assertWritable(config);
|
|
802
794
|
const client = new ConfluenceClient(config);
|
|
@@ -998,7 +990,6 @@ program
|
|
|
998
990
|
|
|
999
991
|
let value;
|
|
1000
992
|
if (options.file) {
|
|
1001
|
-
const fs = require('fs');
|
|
1002
993
|
const raw = fs.readFileSync(options.file, 'utf-8');
|
|
1003
994
|
try {
|
|
1004
995
|
value = JSON.parse(raw);
|
|
@@ -1248,7 +1239,6 @@ program
|
|
|
1248
1239
|
let content = '';
|
|
1249
1240
|
|
|
1250
1241
|
if (options.file) {
|
|
1251
|
-
const fs = require('fs');
|
|
1252
1242
|
if (!fs.existsSync(options.file)) {
|
|
1253
1243
|
throw new Error(`File not found: ${options.file}`);
|
|
1254
1244
|
}
|
|
@@ -1409,8 +1399,6 @@ program
|
|
|
1409
1399
|
try {
|
|
1410
1400
|
const config = getConfig(getProfileName());
|
|
1411
1401
|
const client = new ConfluenceClient(config);
|
|
1412
|
-
const fs = require('fs');
|
|
1413
|
-
const path = require('path');
|
|
1414
1402
|
|
|
1415
1403
|
if (options.recursive) {
|
|
1416
1404
|
await exportRecursive(client, fs, path, pageId, options);
|
|
@@ -1512,7 +1500,6 @@ function sanitizeFilename(filename) {
|
|
|
1512
1500
|
if (!filename || typeof filename !== 'string') {
|
|
1513
1501
|
return 'unnamed';
|
|
1514
1502
|
}
|
|
1515
|
-
const path = require('path');
|
|
1516
1503
|
const stripped = path.basename(filename.replace(/\\/g, '/'));
|
|
1517
1504
|
const cleaned = stripped
|
|
1518
1505
|
// eslint-disable-next-line no-control-regex
|
|
@@ -2186,7 +2173,6 @@ program
|
|
|
2186
2173
|
process.exit(1);
|
|
2187
2174
|
}
|
|
2188
2175
|
|
|
2189
|
-
const fs = require('fs');
|
|
2190
2176
|
let input;
|
|
2191
2177
|
if (options.inputFile) {
|
|
2192
2178
|
input = fs.readFileSync(options.inputFile, 'utf-8');
|
|
@@ -2214,7 +2200,7 @@ program
|
|
|
2214
2200
|
const { convert: htmlToText } = require('html-to-text');
|
|
2215
2201
|
output = htmlToText(input, { wordwrap: 130 });
|
|
2216
2202
|
} else if (options.inputFormat === 'html' && options.outputFormat === 'markdown') {
|
|
2217
|
-
output = converter.
|
|
2203
|
+
output = converter.htmlToMarkdown(input);
|
|
2218
2204
|
} else if (options.inputFormat === 'markdown' && options.outputFormat === 'text') {
|
|
2219
2205
|
const html = converter.markdown.render(input);
|
|
2220
2206
|
const { convert: htmlToText } = require('html-to-text');
|
package/lib/html-to-storage.js
CHANGED
|
@@ -21,6 +21,8 @@ class HtmlDepthExceededError extends Error {
|
|
|
21
21
|
// whatever shape the source had (markdown-it emits them without a slash).
|
|
22
22
|
const VOID_TAGS = new Set(['hr']);
|
|
23
23
|
const CALLOUT_MARKERS = ['info', 'warning', 'note'];
|
|
24
|
+
// these tags are wrapped into Confluence HTML macro
|
|
25
|
+
const HTML_MACRO_TAGS = new Set(['svg', 'div']);
|
|
24
26
|
|
|
25
27
|
// Phrasing-content tags that trigger the `<li>` / `<th>` / `<td>` `<p>`-wrap
|
|
26
28
|
// quirk: if an item contains only inline children and no text-node newline,
|
|
@@ -218,6 +220,36 @@ function convertBlockquote(node, ctx) {
|
|
|
218
220
|
</ac:structured-macro>`;
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
// `<details><summary>` becomes expand macro. If no summary child is found,
|
|
224
|
+
// falls through to plain HTML.
|
|
225
|
+
function convertDetails(node, ctx) {
|
|
226
|
+
const children = node.children || [];
|
|
227
|
+
let summaryNode = null;
|
|
228
|
+
let bodyNodes = [];
|
|
229
|
+
|
|
230
|
+
for (const child of children) {
|
|
231
|
+
if (child.type === 'tag' && child.name === 'summary') {
|
|
232
|
+
summaryNode = child;
|
|
233
|
+
} else if (!isWhitespaceOnly(child)) {
|
|
234
|
+
bodyNodes.push(child);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!summaryNode) {
|
|
239
|
+
return `<details${renderAttrs(node.attribs)}>${walkChildren(node, ctx)}</details>`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const titleHtml = walkChildren(summaryNode, ctx);
|
|
243
|
+
const cleanTitle = titleHtml.replace(/<[^>]+>/g, '').trim();
|
|
244
|
+
|
|
245
|
+
const bodyHtml = bodyNodes
|
|
246
|
+
.map((c) => walkNode(c, ctx))
|
|
247
|
+
.join('')
|
|
248
|
+
.trim();
|
|
249
|
+
|
|
250
|
+
return `<ac:structured-macro ac:name="expand"><ac:parameter ac:name="title">${cleanTitle}</ac:parameter><ac:rich-text-body>${bodyHtml}</ac:rich-text-body></ac:structured-macro>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
221
253
|
// Strict `<pre><code>` adjacency only — `<pre>` with whitespace siblings or
|
|
222
254
|
// any other shape falls through as plain `<pre>`. The body needs manual
|
|
223
255
|
// entity decode because the parser keeps entities raw and CDATA is opaque
|
|
@@ -243,6 +275,20 @@ function convertCodeBlock(node, ctx) {
|
|
|
243
275
|
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${body}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
244
276
|
}
|
|
245
277
|
|
|
278
|
+
// Wrap allowlisted HTML tags (svg, div) in Confluence HTML macro with CDATA.
|
|
279
|
+
// Used for embedding custom HTML that Confluence doesn't natively support.
|
|
280
|
+
function convertHtmlBlock(node, ctx) {
|
|
281
|
+
const { randomUUID } = require('crypto');
|
|
282
|
+
const inner = walkChildren(node, ctx);
|
|
283
|
+
const attrsStr = renderAttrs(node.attribs);
|
|
284
|
+
const openTag = `<${node.name}${attrsStr}>`;
|
|
285
|
+
const closeTag = `</${node.name}>`;
|
|
286
|
+
const htmlContent = openTag + inner + closeTag;
|
|
287
|
+
const safeContent = htmlContent.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
288
|
+
const macroId = randomUUID();
|
|
289
|
+
return `<ac:structured-macro ac:name="html" ac:schema-version="1" ac:macro-id="${macroId}"><ac:plain-text-body><![CDATA[${safeContent}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
290
|
+
}
|
|
291
|
+
|
|
246
292
|
// Re-escape literal `"` inside attribute values. htmlparser2 with
|
|
247
293
|
// `decodeEntities: false` keeps source-escaped entities intact, but a
|
|
248
294
|
// single-quoted source attribute (`<a title='he said "hi"'>`) lands a
|
|
@@ -359,6 +405,8 @@ function dispatchTag(node, ctx) {
|
|
|
359
405
|
return convertLink(node, ctx);
|
|
360
406
|
case 'blockquote':
|
|
361
407
|
return convertBlockquote(node, ctx);
|
|
408
|
+
case 'details':
|
|
409
|
+
return convertDetails(node, ctx);
|
|
362
410
|
case 'table':
|
|
363
411
|
case 'thead':
|
|
364
412
|
case 'tbody':
|
|
@@ -375,6 +423,9 @@ function dispatchTag(node, ctx) {
|
|
|
375
423
|
if (VOID_TAGS.has(node.name)) {
|
|
376
424
|
return `<${node.name}${renderAttrs(node.attribs)} />`;
|
|
377
425
|
}
|
|
426
|
+
if (HTML_MACRO_TAGS.has(node.name)) {
|
|
427
|
+
return convertHtmlBlock(node, ctx);
|
|
428
|
+
}
|
|
378
429
|
return `<${node.name}${renderAttrs(node.attribs)}>${walkChildren(node, ctx)}</${node.name}>`;
|
|
379
430
|
}
|
|
380
431
|
}
|
package/lib/macro-converter.js
CHANGED
|
@@ -21,7 +21,9 @@ const STASH_DELIM = '\uE000';
|
|
|
21
21
|
// The body alternation `"[^"]*"|'[^']*'|[^>]` makes the match quote-aware
|
|
22
22
|
// so a literal `>` inside a quoted attribute value (e.g.
|
|
23
23
|
// `<mark title="1>0">`) does not terminate the tag prematurely.
|
|
24
|
-
const PASSTHROUGH_TAG_RE = /<\/?(?:u|sub|sup|mark)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^>])*>/gi;
|
|
24
|
+
const PASSTHROUGH_TAG_RE = /<\/?(?:u|sub|sup|mark|details|summary)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^>])*>/gi;
|
|
25
|
+
// Block-level HTML elements that should pass through WITHOUT markdown processing of their content.
|
|
26
|
+
const PASSTHROUGH_BLOCK_RE = /<(svg|div)(?:\s[^>]*)?>[\s\S]*?<\/\1>/gi;
|
|
25
27
|
// Single-backtick inline code spans. Block-level code (fenced + indented) is
|
|
26
28
|
// detected via MarkdownIt's tokenizer in `_findCodeRanges` because a regex
|
|
27
29
|
// can't reliably distinguish a 4-space-indented code block from a list-item
|
|
@@ -92,10 +94,19 @@ class MacroConverter {
|
|
|
92
94
|
_renderMarkdownToHtml(markdown) {
|
|
93
95
|
const codeRanges = this._findCodeRanges(markdown);
|
|
94
96
|
const htmlStash = [];
|
|
95
|
-
const stashHtml = (text) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const stashHtml = (text) => {
|
|
98
|
+
// block-level HTML (svg, div with all content) must be stashed before inline tags to avoid matching the closing tag as inline HTML
|
|
99
|
+
let result = text.replace(PASSTHROUGH_BLOCK_RE, (m) => {
|
|
100
|
+
htmlStash.push(m);
|
|
101
|
+
return `${STASH_DELIM}H${htmlStash.length - 1}${STASH_DELIM}`;
|
|
102
|
+
});
|
|
103
|
+
// Then stash inline HTML tags
|
|
104
|
+
result = result.replace(PASSTHROUGH_TAG_RE, (m) => {
|
|
105
|
+
htmlStash.push(m);
|
|
106
|
+
return `${STASH_DELIM}H${htmlStash.length - 1}${STASH_DELIM}`;
|
|
107
|
+
});
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
99
110
|
|
|
100
111
|
let src = '';
|
|
101
112
|
let pos = 0;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.6.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "~1.15.2",
|