confluence-cli 2.1.0 → 2.1.2

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.
@@ -2,6 +2,11 @@ const MarkdownIt = require('markdown-it');
2
2
  const { htmlToMarkdown } = require('./html-to-markdown');
3
3
 
4
4
  const VALID_LINK_STYLES = ['smart', 'plain', 'wiki'];
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';
5
10
 
6
11
  class MacroConverter {
7
12
  constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null, linkStyle = null } = {}) {
@@ -23,23 +28,32 @@ class MacroConverter {
23
28
  this.markdown.enable(['table', 'strikethrough', 'linkify']);
24
29
 
25
30
  this.markdown.core.ruler.before('normalize', 'confluence_macros', (state) => {
26
- const src = state.src;
27
-
28
- state.src = src.replace(/\[!info\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
29
- return `> **INFO**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
30
- });
31
-
32
- state.src = state.src.replace(/\[!warning\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
33
- 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}`;
34
38
  });
35
39
 
36
- state.src = state.src.replace(/\[!note\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
37
- return `> **NOTE**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
38
- });
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
+ }
39
51
 
40
- state.src = state.src.replace(/^(\s*)- \[([ x])\] (.+)$/gm, (_, indent, checked, text) => {
41
- return `${indent}- [${checked}] ${text}`;
42
- });
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);
43
57
  });
44
58
  }
45
59
 
@@ -89,27 +103,36 @@ class MacroConverter {
89
103
  );
90
104
 
91
105
  storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
92
- if (content.includes('<strong>INFO</strong>')) {
93
- const cleanContent = content.replace(/<p><strong>INFO<\/strong><\/p>\s*/, '');
94
- return `<ac:structured-macro ac:name="info">
95
- <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
96
- </ac:structured-macro>`;
97
- } else if (content.includes('<strong>WARNING</strong>')) {
98
- const cleanContent = content.replace(/<p><strong>WARNING<\/strong><\/p>\s*/, '');
99
- return `<ac:structured-macro ac:name="warning">
100
- <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
101
- </ac:structured-macro>`;
102
- } else if (content.includes('<strong>NOTE</strong>')) {
103
- const cleanContent = content.replace(/<p><strong>NOTE<\/strong><\/p>\s*/, '');
104
- return `<ac:structured-macro ac:name="note">
105
- <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
106
- </ac:structured-macro>`;
107
- } else {
106
+ // Detect the marker only when it sits at the very start of the first
107
+ // paragraph, immediately followed by a `</p>` close (separated form) or
108
+ // a `\n` (same-line body form). This is the same anchor condition the
109
+ // strip step uses below, so detection and stripping stay in sync.
110
+ // Without this anchor, a quotation that merely *mentions* `**INFO**` —
111
+ // e.g. `> Use **INFO** at the start.` — would be silently wrapped in an
112
+ // info macro, surprising the author.
113
+ const marker = CALLOUT_MARKERS.find((m) =>
114
+ new RegExp(`<p><strong>${m.toUpperCase()}<\\/strong>(<\\/p>|\\s*\\n)`).test(content)
115
+ );
116
+ if (!marker) {
108
117
  // Plain blockquote — `> …` is a quotation, not an alert. Use the
109
118
  // `> **INFO**` / `> **WARNING**` / `> **NOTE**` markers above to
110
119
  // produce a Confluence info / warning / note macro instead.
111
120
  return `<blockquote>${content}</blockquote>`;
112
121
  }
122
+ // Strip the leading `<strong>MARKER</strong>`. markdown-it produces two
123
+ // shapes depending on whether a blank `>` line separates marker and body:
124
+ // case A (separated): `<p><strong>MARKER</strong></p>\n<p>body</p>`
125
+ // case B (same-line): `<p><strong>MARKER</strong>\nbody</p>`
126
+ // The original cleanup only handled case A, so case B leaked the marker
127
+ // into the rendered macro body. README's recommended `> **INFO**\n> body`
128
+ // form parses as case B — exactly the form that broke.
129
+ const cleanContent = content.replace(
130
+ new RegExp(`<p><strong>${marker.toUpperCase()}<\\/strong>(<\\/p>\\s*|\\s*\\n)`),
131
+ (_, tail) => tail.startsWith('</p>') ? '' : '<p>'
132
+ );
133
+ return `<ac:structured-macro ac:name="${marker}">
134
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
135
+ </ac:structured-macro>`;
113
136
  });
114
137
 
115
138
  storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "2.1.0",
9
+ "version": "2.1.2",
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.0",
3
+ "version": "2.1.2",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {