confluence-cli 2.3.0 → 2.4.0
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 +8 -1
- package/bin/confluence.js +6 -4
- package/lib/confluence-client.js +48 -10
- package/lib/macro-converter.js +89 -4
- package/lib/storage-walker.js +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -466,7 +466,14 @@ confluence export 123456789 --skip-attachments
|
|
|
466
466
|
|
|
467
467
|
### List Spaces
|
|
468
468
|
```bash
|
|
469
|
+
# Default: up to 500 spaces (paginated automatically across requests)
|
|
469
470
|
confluence spaces
|
|
471
|
+
|
|
472
|
+
# Increase the cap when your tenant has more than 500 spaces
|
|
473
|
+
confluence spaces --limit 2000
|
|
474
|
+
|
|
475
|
+
# Fetch every space, regardless of how many pages it takes
|
|
476
|
+
confluence spaces --all
|
|
470
477
|
```
|
|
471
478
|
|
|
472
479
|
### List Child Pages
|
|
@@ -691,7 +698,7 @@ confluence stats
|
|
|
691
698
|
| `read <pageId_or_url>` | Read page content | `--format <html\|text\|storage\|markdown>` |
|
|
692
699
|
| `info <pageId_or_url>` | Get page information | `--format <text\|json>` |
|
|
693
700
|
| `search <query>` | Search for pages | `--limit <number>` |
|
|
694
|
-
| `spaces` | List available spaces | `--limit <number
|
|
701
|
+
| `spaces` | List available spaces | `--limit <number>`, `--all` |
|
|
695
702
|
| `find <title>` | Find a page by its title | `--space <spaceKey>` |
|
|
696
703
|
| `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree\|json>`, `--show-url`, `--show-id` |
|
|
697
704
|
| `create <title> <spaceKey>` | Create a new page or folder | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>`, `--type <page\|folder>` |
|
package/bin/confluence.js
CHANGED
|
@@ -154,15 +154,17 @@ program
|
|
|
154
154
|
program
|
|
155
155
|
.command('spaces')
|
|
156
156
|
.description('List Confluence spaces')
|
|
157
|
-
.option('-l, --limit <limit>', '
|
|
157
|
+
.option('-l, --limit <limit>', 'Maximum total spaces to return across paginated requests', '500')
|
|
158
|
+
.option('--all', 'Fetch every space, paginating through all results (overrides --limit)')
|
|
158
159
|
.action(async (options) => {
|
|
159
160
|
const analytics = new Analytics();
|
|
160
161
|
try {
|
|
161
162
|
const config = getConfig(getProfileName());
|
|
162
163
|
const client = new ConfluenceClient(config);
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
const maxResults = options.all ? null : parseInt(options.limit);
|
|
165
|
+
const spaces = await client.getSpaces(maxResults);
|
|
166
|
+
|
|
167
|
+
console.log(chalk.blue(`Available spaces (${spaces.length}):`));
|
|
166
168
|
spaces.forEach(space => {
|
|
167
169
|
console.log(`${chalk.green(space.key)} - ${space.name}`);
|
|
168
170
|
});
|
package/lib/confluence-client.js
CHANGED
|
@@ -437,20 +437,58 @@ class ConfluenceClient {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
/**
|
|
440
|
-
*
|
|
440
|
+
* List a single page of spaces with pagination metadata
|
|
441
441
|
*/
|
|
442
|
-
async
|
|
442
|
+
async listSpaces(options = {}) {
|
|
443
|
+
const limit = this.parsePositiveInt(options.limit, 500);
|
|
444
|
+
const start = this.parsePositiveInt(options.start, 0);
|
|
445
|
+
|
|
443
446
|
const response = await this.client.get('/space', {
|
|
444
|
-
params: {
|
|
445
|
-
limit
|
|
446
|
-
}
|
|
447
|
+
params: { limit, start }
|
|
447
448
|
});
|
|
448
449
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
450
|
+
const results = Array.isArray(response.data?.results)
|
|
451
|
+
? response.data.results.map(space => ({
|
|
452
|
+
key: space.key,
|
|
453
|
+
name: space.name,
|
|
454
|
+
type: space.type
|
|
455
|
+
}))
|
|
456
|
+
: [];
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
results,
|
|
460
|
+
nextStart: this.parseNextStart(response.data?._links?.next)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get spaces, paginating through results until maxResults is reached or
|
|
466
|
+
* the server stops returning a `_links.next`. Pass `null` to fetch every space.
|
|
467
|
+
*/
|
|
468
|
+
async getSpaces(maxResults = 500, options = {}) {
|
|
469
|
+
const cap = maxResults === null || maxResults === undefined
|
|
470
|
+
? null
|
|
471
|
+
: this.parsePositiveInt(maxResults, 500);
|
|
472
|
+
const pageSize = this.parsePositiveInt(options.pageSize, 500);
|
|
473
|
+
let start = this.parsePositiveInt(options.start, 0);
|
|
474
|
+
const spaces = [];
|
|
475
|
+
|
|
476
|
+
let hasNext = true;
|
|
477
|
+
while (hasNext) {
|
|
478
|
+
if (cap !== null && spaces.length >= cap) break;
|
|
479
|
+
const requestLimit = cap === null
|
|
480
|
+
? pageSize
|
|
481
|
+
: Math.min(pageSize, cap - spaces.length);
|
|
482
|
+
const page = await this.listSpaces({ limit: requestLimit, start });
|
|
483
|
+
spaces.push(...page.results);
|
|
484
|
+
|
|
485
|
+
hasNext = page.nextStart !== null && page.nextStart !== undefined;
|
|
486
|
+
if (hasNext) {
|
|
487
|
+
start = page.nextStart;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return cap !== null ? spaces.slice(0, cap) : spaces;
|
|
454
492
|
}
|
|
455
493
|
|
|
456
494
|
/**
|
package/lib/macro-converter.js
CHANGED
|
@@ -9,6 +9,25 @@ const CALLOUT_MARKERS = ['info', 'warning', 'note'];
|
|
|
9
9
|
// and survives editor / formatter / lint passes that strip invisible chars.
|
|
10
10
|
const STASH_DELIM = '\uE000';
|
|
11
11
|
|
|
12
|
+
// Inline HTML tags that markdown has no native syntax for. The walker emits
|
|
13
|
+
// them as raw HTML on storage\u2192markdown, so they must round-trip back through
|
|
14
|
+
// markdownToStorage; without this whitelist, MarkdownIt (html: false) escapes
|
|
15
|
+
// them to literal `<...>` and the formatting is lost.
|
|
16
|
+
//
|
|
17
|
+
// The lookahead `(?=[\s/>])` after the tag name rejects strings that look
|
|
18
|
+
// like markdown autolinks (e.g. `<u@example.com>`, `<sub:foo>`) \u2014 a plain
|
|
19
|
+
// `\b` word boundary would let these through and break linkify.
|
|
20
|
+
//
|
|
21
|
+
// The body alternation `"[^"]*"|'[^']*'|[^>]` makes the match quote-aware
|
|
22
|
+
// so a literal `>` inside a quoted attribute value (e.g.
|
|
23
|
+
// `<mark title="1>0">`) does not terminate the tag prematurely.
|
|
24
|
+
const PASSTHROUGH_TAG_RE = /<\/?(?:u|sub|sup|mark)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^>])*>/gi;
|
|
25
|
+
// Single-backtick inline code spans. Block-level code (fenced + indented) is
|
|
26
|
+
// detected via MarkdownIt's tokenizer in `_findCodeRanges` because a regex
|
|
27
|
+
// can't reliably distinguish a 4-space-indented code block from a list-item
|
|
28
|
+
// continuation that happens to align to four spaces.
|
|
29
|
+
const INLINE_CODE_RE = /`[^`\n]+`/g;
|
|
30
|
+
|
|
12
31
|
class MacroConverter {
|
|
13
32
|
constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null, linkStyle = null } = {}) {
|
|
14
33
|
this._isCloud = isCloud;
|
|
@@ -57,13 +76,79 @@ class MacroConverter {
|
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
markdownToStorage(markdown) {
|
|
60
|
-
|
|
61
|
-
return this.htmlToConfluenceStorage(html);
|
|
79
|
+
return this.htmlToConfluenceStorage(this._renderMarkdownToHtml(markdown));
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
markdownToNativeStorage(markdown) {
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
return this.htmlToConfluenceStorage(this._renderMarkdownToHtml(markdown));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pre-stashes whitelisted inline HTML so MarkdownIt won't escape it, renders,
|
|
87
|
+
// then restores. Code regions (fenced, indented, and inline) are skipped so a
|
|
88
|
+
// literal `<u>` typed inside a code block survives MarkdownIt's escape and
|
|
89
|
+
// round-trips as text rather than as a real tag — which would otherwise be
|
|
90
|
+
// smuggled past the escape and dropped by `convertCodeBlock`'s text-only
|
|
91
|
+
// child collection in html-to-storage.
|
|
92
|
+
_renderMarkdownToHtml(markdown) {
|
|
93
|
+
const codeRanges = this._findCodeRanges(markdown);
|
|
94
|
+
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
|
+
});
|
|
99
|
+
|
|
100
|
+
let src = '';
|
|
101
|
+
let pos = 0;
|
|
102
|
+
for (const [start, end] of codeRanges) {
|
|
103
|
+
src += stashHtml(markdown.slice(pos, start));
|
|
104
|
+
src += markdown.slice(start, end);
|
|
105
|
+
pos = end;
|
|
106
|
+
}
|
|
107
|
+
src += stashHtml(markdown.slice(pos));
|
|
108
|
+
|
|
109
|
+
const html = this.markdown.render(src);
|
|
110
|
+
return html.replace(
|
|
111
|
+
new RegExp(`${STASH_DELIM}H(\\d+)${STASH_DELIM}`, 'g'),
|
|
112
|
+
(m, i) => htmlStash[+i] ?? m,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Returns merged, sorted character ranges covering all code regions in the
|
|
117
|
+
// markdown source — fenced and indented blocks via MarkdownIt's tokenizer
|
|
118
|
+
// (which correctly distinguishes them from list-item continuations) and
|
|
119
|
+
// single-backtick inline spans via regex (MarkdownIt parses these into
|
|
120
|
+
// `code_inline` tokens but does not expose source positions for them).
|
|
121
|
+
_findCodeRanges(markdown) {
|
|
122
|
+
const tokens = this.markdown.parse(markdown, {});
|
|
123
|
+
const lineStarts = [0];
|
|
124
|
+
for (let i = 0; i < markdown.length; i++) {
|
|
125
|
+
if (markdown[i] === '\n') lineStarts.push(i + 1);
|
|
126
|
+
}
|
|
127
|
+
const lineToChar = (n) => (n < lineStarts.length ? lineStarts[n] : markdown.length);
|
|
128
|
+
|
|
129
|
+
const ranges = [];
|
|
130
|
+
for (const tok of tokens) {
|
|
131
|
+
if ((tok.type === 'code_block' || tok.type === 'fence') && tok.map) {
|
|
132
|
+
ranges.push([lineToChar(tok.map[0]), lineToChar(tok.map[1])]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
INLINE_CODE_RE.lastIndex = 0;
|
|
136
|
+
let m;
|
|
137
|
+
while ((m = INLINE_CODE_RE.exec(markdown)) !== null) {
|
|
138
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
142
|
+
const merged = [];
|
|
143
|
+
for (const r of ranges) {
|
|
144
|
+
const last = merged[merged.length - 1];
|
|
145
|
+
if (last && r[0] <= last[1]) {
|
|
146
|
+
last[1] = Math.max(last[1], r[1]);
|
|
147
|
+
} else {
|
|
148
|
+
merged.push([r[0], r[1]]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return merged;
|
|
67
152
|
}
|
|
68
153
|
|
|
69
154
|
htmlToConfluenceStorage(html) {
|
package/lib/storage-walker.js
CHANGED
|
@@ -200,6 +200,7 @@ class StorageWalker {
|
|
|
200
200
|
case 'blockquote':
|
|
201
201
|
return this.handleBlockquote(node);
|
|
202
202
|
case 'details': case 'summary':
|
|
203
|
+
case 'u': case 'sub': case 'sup': case 'mark':
|
|
203
204
|
return `<${tag}>` + this.walkNodes(node.children) + `</${tag}>`;
|
|
204
205
|
case 'ac:structured-macro':
|
|
205
206
|
return this.handleMacro(node);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.4.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "^1.15.0",
|