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 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.storageToMarkdown(input);
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');
@@ -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
  }
@@ -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) => text.replace(PASSTHROUGH_TAG_RE, (m) => {
96
- htmlStash.push(m);
97
- return `${STASH_DELIM}H${htmlStash.length - 1}${STASH_DELIM}`;
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;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.5.0",
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.5.0",
9
+ "version": "2.6.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "axios": "~1.15.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {