confluence-cli 2.1.1 → 2.1.3

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/README.md CHANGED
@@ -789,7 +789,7 @@ A blockquote whose first line is `**INFO**`, `**WARNING**`, or `**NOTE**` become
789
789
  > Side note for the reader.
790
790
  ```
791
791
 
792
- The reverse direction emits the equivalent shorthand (`[!info]` / `[!warning]` / `[!note]` followed by the body), which markdown→storage then re-expands.
792
+ The reverse direction emits the same `> **INFO**` / `> **WARNING**` / `> **NOTE**` blockquote form, so multi-paragraph bodies round-trip cleanly. The bare `[!info]` / `[!warning]` / `[!note]` shorthand is still accepted on input for backwards compatibility.
793
793
 
794
794
  A blockquote without one of these markers stays a **plain blockquote** (`<blockquote>…</blockquote>`) — `> …` is treated as a quotation, not an alert. Use the markers above when you want a callout.
795
795
 
@@ -3,6 +3,10 @@ const { htmlToMarkdown } = require('./html-to-markdown');
3
3
 
4
4
  const VALID_LINK_STYLES = ['smart', 'plain', 'wiki'];
5
5
  const CALLOUT_MARKERS = ['info', 'warning', 'note'];
6
+ // U+E000 (Unicode Private Use Area) is used as the stash placeholder
7
+ // delimiter. Declared as an explicit escape so the byte is visible in source
8
+ // and survives editor / formatter / lint passes that strip invisible chars.
9
+ const STASH_DELIM = '\uE000';
6
10
 
7
11
  class MacroConverter {
8
12
  constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null, linkStyle = null } = {}) {
@@ -24,23 +28,32 @@ class MacroConverter {
24
28
  this.markdown.enable(['table', 'strikethrough', 'linkify']);
25
29
 
26
30
  this.markdown.core.ruler.before('normalize', 'confluence_macros', (state) => {
27
- const src = state.src;
28
-
29
- state.src = src.replace(/\[!info\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
30
- return `> **INFO**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
31
- });
32
-
33
- state.src = state.src.replace(/\[!warning\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
34
- return `> **WARNING**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
31
+ // Stash fenced code blocks and inline code so the admonition rewrite
32
+ // below cannot transform `[!info]` tokens that the author intended as
33
+ // literal text inside code.
34
+ const stash = [];
35
+ state.src = state.src.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]+`/g, (m) => {
36
+ stash.push(m);
37
+ return `${STASH_DELIM}${stash.length - 1}${STASH_DELIM}`;
35
38
  });
36
39
 
37
- state.src = state.src.replace(/\[!note\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
38
- return `> **NOTE**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
39
- });
40
+ // Anchor `[!info]` to the start of a line (string start or after a
41
+ // newline) so prose mid-paragraph, headings on the same line, and
42
+ // `> [!info]` GitHub-style alerts are left alone. The latter would
43
+ // otherwise expand to a nested blockquote that the storage handler's
44
+ // lazy regex cannot balance, producing malformed XML.
45
+ for (const m of CALLOUT_MARKERS) {
46
+ const re = new RegExp(`(^|\\n)\\[!${m}\\]\\s*([\\s\\S]*?)(?=\\n\\s*\\n|\\n\\s*\\[!|$)`, 'g');
47
+ state.src = state.src.replace(re, (_, pre, content) =>
48
+ `${pre}> **${m.toUpperCase()}**\n> ${content.trim().replace(/\n/g, '\n> ')}`
49
+ );
50
+ }
40
51
 
41
- state.src = state.src.replace(/^(\s*)- \[([ x])\] (.+)$/gm, (_, indent, checked, text) => {
42
- return `${indent}- [${checked}] ${text}`;
43
- });
52
+ // Fall back to the original match if the index is out of range so a
53
+ // literal U+E000<digits>U+E000 in user prose survives untouched instead
54
+ // of becoming the string "undefined".
55
+ const restoreRe = new RegExp(`${STASH_DELIM}(\\d+)${STASH_DELIM}`, 'g');
56
+ state.src = state.src.replace(restoreRe, (m, i) => stash[+i] ?? m);
44
57
  });
45
58
  }
46
59
 
@@ -281,20 +294,34 @@ class MacroConverter {
281
294
  return `\n\`\`\`\n${code}\n\`\`\`\n`;
282
295
  });
283
296
 
284
- markdown = markdown.replace(/<ac:structured-macro ac:name="info"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
285
- const cleanContent = htmlToMarkdown(content);
286
- return `[!info]\n${cleanContent}`;
287
- });
288
-
289
- markdown = markdown.replace(/<ac:structured-macro ac:name="warning"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
290
- const cleanContent = htmlToMarkdown(content);
291
- return `[!warning]\n${cleanContent}`;
292
- });
293
-
294
- markdown = markdown.replace(/<ac:structured-macro ac:name="note"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
295
- const cleanContent = htmlToMarkdown(content);
296
- return `[!note]\n${cleanContent}`;
297
- });
297
+ // Emit the README-recommended `> **MARKER**` blockquote form rather than
298
+ // the bare `[!marker]` shorthand. The bare form has no explicit body
299
+ // terminator, so a blank line inside a multi-paragraph body is
300
+ // indistinguishable from the body ending — round-tripping such a body
301
+ // through markdownToStorage drops every paragraph after the first.
302
+ // The `>` prefix gives the blockquote-based handler an unambiguous body
303
+ // boundary, so multi-paragraph callouts survive download → re-upload.
304
+ // markdownToStorage still accepts the bare `[!marker]` form on input for
305
+ // backwards compatibility with already-downloaded files.
306
+ //
307
+ // Wrap the output with leading + trailing `\n` to combine with the single
308
+ // `\n` that htmlToMarkdown emits around adjacent `<p>` tags. Without
309
+ // these, a following `<p>after</p>` lazy-continues into the blockquote
310
+ // body and lands inside the macro on re-upload (blockquote semantics —
311
+ // unlike code fences, blockquotes have no closing delimiter).
312
+ for (const marker of CALLOUT_MARKERS) {
313
+ const re = new RegExp(`<ac:structured-macro ac:name="${marker}"[^>]*>[\\s\\S]*?<ac:rich-text-body>([\\s\\S]*?)<\\/ac:rich-text-body>[\\s\\S]*?<\\/ac:structured-macro>`, 'g');
314
+ markdown = markdown.replace(re, (_, content) => {
315
+ const body = htmlToMarkdown(content).trim();
316
+ const quotedBody = body
317
+ .split('\n')
318
+ .map((line) => (line.length === 0 ? '>' : `> ${line}`))
319
+ .join('\n');
320
+ const header = `> **${marker.toUpperCase()}**`;
321
+ const inner = body.length === 0 ? header : `${header}\n${quotedBody}`;
322
+ return `\n${inner}\n`;
323
+ });
324
+ }
298
325
 
299
326
  // anchor macro → **ANCHOR: id** marker (round-trip with markdownToStorage).
300
327
  // Must run before the generic <ac:structured-macro> catch-all below, which
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "2.1.1",
9
+ "version": "2.1.3",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "axios": "^1.15.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {