@ecubelabs/atlassian-mcp 1.4.0 → 1.6.0-next.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/dist/confluence-tools/get-template-by-id.js +34 -0
- package/dist/confluence-tools/get-templates.js +51 -0
- package/dist/confluence-tools/index.js +2 -0
- package/dist/confluence-tools-register.js +3 -1
- package/dist/index.js +1 -0
- package/dist/libs/base-client.js +35 -2
- package/dist/libs/confluence-client.js +34 -2
- package/package.json +3 -2
- package/dist/libs/confluence-storage-format-utils.js +0 -468
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const registerGetTemplateById = (server, confluenceService) => {
|
|
3
|
+
server.tool('get-template-by-id', 'Get a specific content template by its ID. Returns detailed template information including name, description, space, labels, and optionally the template body content.', {
|
|
4
|
+
contentTemplateId: z.string().describe('The ID of the content template to retrieve'),
|
|
5
|
+
expand: z
|
|
6
|
+
.array(z.enum(['body', 'body.storage', 'body.view']))
|
|
7
|
+
.optional()
|
|
8
|
+
.describe('Properties to expand in the response. Use "body" or "body.storage" to include template content in storage format.'),
|
|
9
|
+
}, async ({ contentTemplateId, expand }) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await confluenceService.getTemplateById(contentTemplateId, {
|
|
12
|
+
expand,
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: 'text',
|
|
18
|
+
text: JSON.stringify(result),
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: `Failed to retrieve template: ${error instanceof Error ? error.message : String(error)}`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const registerGetTemplates = (server, confluenceService) => {
|
|
3
|
+
server.tool('get-templates', 'Get list of content templates from Confluence. Returns global templates if no spaceKey is provided, or space-specific templates when spaceKey is specified.', {
|
|
4
|
+
spaceKey: z
|
|
5
|
+
.string()
|
|
6
|
+
.optional()
|
|
7
|
+
.describe('The key of the space to retrieve templates from. If not specified, global templates will be returned.'),
|
|
8
|
+
start: z
|
|
9
|
+
.number()
|
|
10
|
+
.min(0)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('The starting index of the returned templates (for pagination). Default: 0'),
|
|
13
|
+
limit: z
|
|
14
|
+
.number()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(100)
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Maximum number of templates to return per page. Default: 25, Max: 100'),
|
|
19
|
+
expand: z
|
|
20
|
+
.array(z.enum(['body', 'body.storage', 'body.view']))
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Properties to expand in the response. Use "body" or "body.storage" to include template content.'),
|
|
23
|
+
}, async ({ spaceKey, start, limit, expand }) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await confluenceService.getTemplates({
|
|
26
|
+
spaceKey,
|
|
27
|
+
start,
|
|
28
|
+
limit,
|
|
29
|
+
expand,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: JSON.stringify(result),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `Failed to retrieve templates: ${error instanceof Error ? error.message : String(error)}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -12,6 +12,8 @@ export { registerGetSpaceById } from './get-space-by-id.js';
|
|
|
12
12
|
export { registerFindSpaceByName } from './find-space-by-name.js';
|
|
13
13
|
export { registerGetRecentlyUpdatedPages } from './get-recently-updated-pages.js';
|
|
14
14
|
export { registerGetRecentlyCreatedPages } from './get-recently-created-pages.js';
|
|
15
|
+
export { registerGetTemplates } from './get-templates.js';
|
|
16
|
+
export { registerGetTemplateById } from './get-template-by-id.js';
|
|
15
17
|
// Write tools
|
|
16
18
|
export { registerCreatePage } from './create-page.js';
|
|
17
19
|
export { registerUpdatePage } from './update-page.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfluenceService } from './libs/confluence-client.js';
|
|
2
|
-
import { registerGetPages, registerGetPagesForLabel, registerGetPageById, registerGetLabels, registerFindPagesByLabelName, registerFindLabelByName, registerGetPageChildren, registerGetPageChildrenTree, registerGetSpaces, registerGetSpaceById, registerFindSpaceByName, registerGetRecentlyUpdatedPages, registerGetRecentlyCreatedPages, registerCreatePage, registerUpdatePage, registerDeletePage, registerAddLabelsToPage, registerRemoveLabelFromPage, registerConfluenceStorageFormatHelp, } from './confluence-tools/index.js';
|
|
2
|
+
import { registerGetPages, registerGetPagesForLabel, registerGetPageById, registerGetLabels, registerFindPagesByLabelName, registerFindLabelByName, registerGetPageChildren, registerGetPageChildrenTree, registerGetSpaces, registerGetSpaceById, registerFindSpaceByName, registerGetRecentlyUpdatedPages, registerGetRecentlyCreatedPages, registerGetTemplates, registerGetTemplateById, registerCreatePage, registerUpdatePage, registerDeletePage, registerAddLabelsToPage, registerRemoveLabelFromPage, registerConfluenceStorageFormatHelp, } from './confluence-tools/index.js';
|
|
3
3
|
export const registerConfluenceTools = (server) => {
|
|
4
4
|
// Initialize Confluence service
|
|
5
5
|
const confluenceService = new ConfluenceService();
|
|
@@ -17,6 +17,8 @@ export const registerConfluenceTools = (server) => {
|
|
|
17
17
|
registerFindSpaceByName(server, confluenceService);
|
|
18
18
|
registerGetRecentlyUpdatedPages(server, confluenceService);
|
|
19
19
|
registerGetRecentlyCreatedPages(server, confluenceService);
|
|
20
|
+
registerGetTemplates(server, confluenceService);
|
|
21
|
+
registerGetTemplateById(server, confluenceService);
|
|
20
22
|
// Register write tools
|
|
21
23
|
registerCreatePage(server, confluenceService);
|
|
22
24
|
registerUpdatePage(server, confluenceService);
|
package/dist/index.js
CHANGED
package/dist/libs/base-client.js
CHANGED
|
@@ -8,11 +8,13 @@ export class BaseApiService {
|
|
|
8
8
|
config;
|
|
9
9
|
serviceName;
|
|
10
10
|
client;
|
|
11
|
+
v1Client; // v1 API용 클라이언트 (Confluence 전용)
|
|
11
12
|
rateLimiter;
|
|
12
13
|
constructor(config, serviceName) {
|
|
13
14
|
this.config = config;
|
|
14
15
|
this.serviceName = serviceName;
|
|
15
16
|
this.client = this.setupAxiosClient();
|
|
17
|
+
this.v1Client = this.setupV1AxiosClient();
|
|
16
18
|
this.rateLimiter = pLimit(5); // 동시 요청 5개 제한
|
|
17
19
|
this.setupInterceptors();
|
|
18
20
|
this.setupRetry();
|
|
@@ -37,6 +39,24 @@ export class BaseApiService {
|
|
|
37
39
|
},
|
|
38
40
|
});
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Confluence v1 REST API용 클라이언트 설정
|
|
44
|
+
* v1 API 엔드포인트: /wiki/rest/api/...
|
|
45
|
+
*/
|
|
46
|
+
setupV1AxiosClient() {
|
|
47
|
+
const auth = Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString('base64');
|
|
48
|
+
const baseURL = `${this.config.host}/wiki/rest/api`;
|
|
49
|
+
return axios.create({
|
|
50
|
+
baseURL,
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Basic ${auth}`,
|
|
53
|
+
Accept: 'application/json',
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
},
|
|
56
|
+
timeout: 30000,
|
|
57
|
+
paramsSerializer: (params) => this.serializeConfluenceParams(params),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
40
60
|
serializeConfluenceParams(params) {
|
|
41
61
|
const searchParams = new URLSearchParams();
|
|
42
62
|
Object.entries(params).forEach(([key, value]) => {
|
|
@@ -69,9 +89,20 @@ export class BaseApiService {
|
|
|
69
89
|
}, (error) => {
|
|
70
90
|
return this.handleError(error);
|
|
71
91
|
});
|
|
92
|
+
// v1Client에도 동일한 인터셉터 설정
|
|
93
|
+
this.v1Client.interceptors.request.use((config) => {
|
|
94
|
+
return config;
|
|
95
|
+
}, (error) => {
|
|
96
|
+
return Promise.reject(error);
|
|
97
|
+
});
|
|
98
|
+
this.v1Client.interceptors.response.use((response) => {
|
|
99
|
+
return response;
|
|
100
|
+
}, (error) => {
|
|
101
|
+
return this.handleError(error);
|
|
102
|
+
});
|
|
72
103
|
}
|
|
73
104
|
setupRetry() {
|
|
74
|
-
|
|
105
|
+
const retryConfig = {
|
|
75
106
|
retries: 3,
|
|
76
107
|
retryDelay: axiosRetry.exponentialDelay,
|
|
77
108
|
retryCondition: (error) => {
|
|
@@ -79,7 +110,9 @@ export class BaseApiService {
|
|
|
79
110
|
error.response?.status === 429 || // Rate limit
|
|
80
111
|
error.response?.status === 503); // Service unavailable
|
|
81
112
|
},
|
|
82
|
-
}
|
|
113
|
+
};
|
|
114
|
+
axiosRetry(this.client, retryConfig);
|
|
115
|
+
axiosRetry(this.v1Client, retryConfig);
|
|
83
116
|
}
|
|
84
117
|
async handleError(error) {
|
|
85
118
|
const { response } = error;
|
|
@@ -334,7 +334,7 @@ export class ConfluenceService extends BaseApiService {
|
|
|
334
334
|
* 참고: 라벨 관리는 v1 API를 사용 (/wiki/rest/api/content)
|
|
335
335
|
*/
|
|
336
336
|
async addLabelsToPage(pageId, labels) {
|
|
337
|
-
return this.makeRequest(() => this.
|
|
337
|
+
return this.makeRequest(() => this.v1Client.post(`/content/${pageId}/label`, labels, {
|
|
338
338
|
headers: {
|
|
339
339
|
'Content-Type': 'application/json',
|
|
340
340
|
},
|
|
@@ -349,6 +349,38 @@ export class ConfluenceService extends BaseApiService {
|
|
|
349
349
|
const params = {
|
|
350
350
|
name: labelName,
|
|
351
351
|
};
|
|
352
|
-
return this.makeRequest(() => this.
|
|
352
|
+
return this.makeRequest(() => this.v1Client.delete(`/content/${pageId}/label`, { params }));
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* 템플릿 목록 조회
|
|
356
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-page-get
|
|
357
|
+
* 참고: 템플릿 API는 v1 API를 사용 (/wiki/rest/api/template)
|
|
358
|
+
*/
|
|
359
|
+
async getTemplates(options) {
|
|
360
|
+
const params = {};
|
|
361
|
+
if (options) {
|
|
362
|
+
const { spaceKey, start, limit, expand } = options;
|
|
363
|
+
if (spaceKey)
|
|
364
|
+
params.spaceKey = spaceKey;
|
|
365
|
+
if (start !== undefined)
|
|
366
|
+
params.start = start;
|
|
367
|
+
if (limit !== undefined)
|
|
368
|
+
params.limit = limit;
|
|
369
|
+
if (expand)
|
|
370
|
+
params.expand = expand.join(',');
|
|
371
|
+
}
|
|
372
|
+
return this.makeRequest(() => this.v1Client.get('/template/page', { params }));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 특정 템플릿 상세 조회
|
|
376
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-contentTemplateId-get
|
|
377
|
+
* 참고: 템플릿 API는 v1 API를 사용 (/wiki/rest/api/template)
|
|
378
|
+
*/
|
|
379
|
+
async getTemplateById(contentTemplateId, options) {
|
|
380
|
+
const params = {};
|
|
381
|
+
if (options?.expand) {
|
|
382
|
+
params.expand = options.expand.join(',');
|
|
383
|
+
}
|
|
384
|
+
return this.makeRequest(() => this.v1Client.get(`/template/page/${contentTemplateId}`, { params }));
|
|
353
385
|
}
|
|
354
386
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecubelabs/atlassian-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0-next.1",
|
|
4
4
|
"bin": "./dist/index.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/Ecube-Labs/skynet.git"
|
|
@@ -37,5 +37,6 @@
|
|
|
37
37
|
"semantic-release-yarn": "^3.0.2",
|
|
38
38
|
"ts-node": "^10.9.2",
|
|
39
39
|
"typescript": "^5.8.2"
|
|
40
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"stableVersion": "1.0.0"
|
|
41
42
|
}
|
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Confluence Storage Format Utilities
|
|
3
|
-
*
|
|
4
|
-
* This module provides helper functions to create Confluence storage format (XHTML-based) content
|
|
5
|
-
* without manually writing XML. It supports common elements like headings, text formatting, lists,
|
|
6
|
-
* tables, macros, images, and links.
|
|
7
|
-
*
|
|
8
|
-
* @see https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Confluence Storage Format Builder
|
|
12
|
-
* Provides a fluent API for building Confluence storage format content
|
|
13
|
-
*/
|
|
14
|
-
export class ConfluenceStorageFormatBuilder {
|
|
15
|
-
content = [];
|
|
16
|
-
/**
|
|
17
|
-
* Add a heading
|
|
18
|
-
* @param level Heading level (1-6)
|
|
19
|
-
* @param text Heading text
|
|
20
|
-
*/
|
|
21
|
-
heading(level, text) {
|
|
22
|
-
this.content.push(`<h${level}>${this.escapeHtml(text)}</h${level}>`);
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Add a paragraph
|
|
27
|
-
*/
|
|
28
|
-
paragraph(text) {
|
|
29
|
-
this.content.push(`<p>${this.escapeHtml(text)}</p>`);
|
|
30
|
-
return this;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Add bold text
|
|
34
|
-
*/
|
|
35
|
-
bold(text) {
|
|
36
|
-
return `<strong>${this.escapeHtml(text)}</strong>`;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Add italic text
|
|
40
|
-
*/
|
|
41
|
-
italic(text) {
|
|
42
|
-
return `<em>${this.escapeHtml(text)}</em>`;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Add underlined text
|
|
46
|
-
*/
|
|
47
|
-
underline(text) {
|
|
48
|
-
return `<u>${this.escapeHtml(text)}</u>`;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Add strikethrough text
|
|
52
|
-
*/
|
|
53
|
-
strikethrough(text) {
|
|
54
|
-
return `<s>${this.escapeHtml(text)}</s>`;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Add a line break
|
|
58
|
-
*/
|
|
59
|
-
lineBreak() {
|
|
60
|
-
this.content.push('<br />');
|
|
61
|
-
return this;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Add a horizontal rule
|
|
65
|
-
*/
|
|
66
|
-
horizontalRule() {
|
|
67
|
-
this.content.push('<hr />');
|
|
68
|
-
return this;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Add an unordered list
|
|
72
|
-
*/
|
|
73
|
-
unorderedList(items) {
|
|
74
|
-
const listItems = items.map((item) => `<li>${this.escapeHtml(item)}</li>`).join('');
|
|
75
|
-
this.content.push(`<ul>${listItems}</ul>`);
|
|
76
|
-
return this;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Add an ordered list
|
|
80
|
-
*/
|
|
81
|
-
orderedList(items) {
|
|
82
|
-
const listItems = items.map((item) => `<li>${this.escapeHtml(item)}</li>`).join('');
|
|
83
|
-
this.content.push(`<ol>${listItems}</ol>`);
|
|
84
|
-
return this;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Add a blockquote
|
|
88
|
-
*/
|
|
89
|
-
blockquote(text) {
|
|
90
|
-
this.content.push(`<blockquote><p>${this.escapeHtml(text)}</p></blockquote>`);
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Add preformatted text
|
|
95
|
-
*/
|
|
96
|
-
preformatted(text) {
|
|
97
|
-
this.content.push(`<pre>${this.escapeHtml(text)}</pre>`);
|
|
98
|
-
return this;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Add a table
|
|
102
|
-
*/
|
|
103
|
-
table(rows) {
|
|
104
|
-
const tableRows = rows
|
|
105
|
-
.map((row) => {
|
|
106
|
-
const cells = row.cells
|
|
107
|
-
.map((cell) => {
|
|
108
|
-
const tag = cell.isHeader ? 'th' : 'td';
|
|
109
|
-
const attrs = [];
|
|
110
|
-
if (cell.rowspan && cell.rowspan > 1) {
|
|
111
|
-
attrs.push(`rowspan="${cell.rowspan}"`);
|
|
112
|
-
}
|
|
113
|
-
if (cell.colspan && cell.colspan > 1) {
|
|
114
|
-
attrs.push(`colspan="${cell.colspan}"`);
|
|
115
|
-
}
|
|
116
|
-
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
117
|
-
return `<${tag}${attrStr}>${this.escapeHtml(cell.content)}</${tag}>`;
|
|
118
|
-
})
|
|
119
|
-
.join('');
|
|
120
|
-
return `<tr>${cells}</tr>`;
|
|
121
|
-
})
|
|
122
|
-
.join('');
|
|
123
|
-
this.content.push(`<table><tbody>${tableRows}</tbody></table>`);
|
|
124
|
-
return this;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Add a code block macro
|
|
128
|
-
* @param code Code content
|
|
129
|
-
* @param language Programming language for syntax highlighting (e.g., 'java', 'python', 'javascript')
|
|
130
|
-
* @param options Additional options
|
|
131
|
-
*/
|
|
132
|
-
codeBlock(code, language, options) {
|
|
133
|
-
const params = [];
|
|
134
|
-
if (language) {
|
|
135
|
-
params.push({ name: 'language', value: language });
|
|
136
|
-
}
|
|
137
|
-
if (options?.title) {
|
|
138
|
-
params.push({ name: 'title', value: options.title });
|
|
139
|
-
}
|
|
140
|
-
if (options?.linenumbers) {
|
|
141
|
-
params.push({ name: 'linenumbers', value: 'true' });
|
|
142
|
-
}
|
|
143
|
-
if (options?.collapse) {
|
|
144
|
-
params.push({ name: 'collapse', value: 'true' });
|
|
145
|
-
}
|
|
146
|
-
if (options?.firstline) {
|
|
147
|
-
params.push({ name: 'firstline', value: String(options.firstline) });
|
|
148
|
-
}
|
|
149
|
-
this.content.push(this.createMacro('code', params, code, true));
|
|
150
|
-
return this;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Add an info panel macro
|
|
154
|
-
*/
|
|
155
|
-
infoPanel(content, title, showIcon = true) {
|
|
156
|
-
const params = [];
|
|
157
|
-
if (title) {
|
|
158
|
-
params.push({ name: 'title', value: title });
|
|
159
|
-
}
|
|
160
|
-
params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
|
|
161
|
-
this.content.push(this.createMacro('info', params, content));
|
|
162
|
-
return this;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Add a note panel macro
|
|
166
|
-
*/
|
|
167
|
-
notePanel(content, title, showIcon = true) {
|
|
168
|
-
const params = [];
|
|
169
|
-
if (title) {
|
|
170
|
-
params.push({ name: 'title', value: title });
|
|
171
|
-
}
|
|
172
|
-
params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
|
|
173
|
-
this.content.push(this.createMacro('note', params, content));
|
|
174
|
-
return this;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Add a warning panel macro
|
|
178
|
-
*/
|
|
179
|
-
warningPanel(content, title, showIcon = true) {
|
|
180
|
-
const params = [];
|
|
181
|
-
if (title) {
|
|
182
|
-
params.push({ name: 'title', value: title });
|
|
183
|
-
}
|
|
184
|
-
params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
|
|
185
|
-
this.content.push(this.createMacro('warning', params, content));
|
|
186
|
-
return this;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Add a tip panel macro
|
|
190
|
-
*/
|
|
191
|
-
tipPanel(content, title, showIcon = true) {
|
|
192
|
-
const params = [];
|
|
193
|
-
if (title) {
|
|
194
|
-
params.push({ name: 'title', value: title });
|
|
195
|
-
}
|
|
196
|
-
params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
|
|
197
|
-
this.content.push(this.createMacro('tip', params, content));
|
|
198
|
-
return this;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Add a custom panel macro
|
|
202
|
-
*/
|
|
203
|
-
panel(content, options) {
|
|
204
|
-
const params = [];
|
|
205
|
-
if (options?.title) {
|
|
206
|
-
params.push({ name: 'title', value: options.title });
|
|
207
|
-
}
|
|
208
|
-
if (options?.borderStyle) {
|
|
209
|
-
params.push({ name: 'borderStyle', value: options.borderStyle });
|
|
210
|
-
}
|
|
211
|
-
if (options?.borderColor) {
|
|
212
|
-
params.push({ name: 'borderColor', value: options.borderColor });
|
|
213
|
-
}
|
|
214
|
-
if (options?.borderWidth) {
|
|
215
|
-
params.push({ name: 'borderWidth', value: String(options.borderWidth) });
|
|
216
|
-
}
|
|
217
|
-
if (options?.bgColor) {
|
|
218
|
-
params.push({ name: 'bgColor', value: options.bgColor });
|
|
219
|
-
}
|
|
220
|
-
if (options?.titleBGColor) {
|
|
221
|
-
params.push({ name: 'titleBGColor', value: options.titleBGColor });
|
|
222
|
-
}
|
|
223
|
-
if (options?.titleColor) {
|
|
224
|
-
params.push({ name: 'titleColor', value: options.titleColor });
|
|
225
|
-
}
|
|
226
|
-
this.content.push(this.createMacro('panel', params, content));
|
|
227
|
-
return this;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Add a link to a Confluence page
|
|
231
|
-
*/
|
|
232
|
-
pageLink(options) {
|
|
233
|
-
const linkBody = options.linkText
|
|
234
|
-
? `<ac:plain-text-link-body><![CDATA[${options.linkText}]]></ac:plain-text-link-body>`
|
|
235
|
-
: '';
|
|
236
|
-
const link = `<ac:link><ri:page ri:content-title="${this.escapeAttr(options.pageTitle)}" ri:space-key="${this.escapeAttr(options.spaceKey)}" />${linkBody}</ac:link>`;
|
|
237
|
-
this.content.push(link);
|
|
238
|
-
return this;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Add an external link
|
|
242
|
-
*/
|
|
243
|
-
externalLink(url, linkText) {
|
|
244
|
-
const linkBody = linkText ? `<ac:plain-text-link-body><![CDATA[${linkText}]]></ac:plain-text-link-body>` : '';
|
|
245
|
-
const link = `<ac:link><ri:url ri:value="${this.escapeAttr(url)}" />${linkBody}</ac:link>`;
|
|
246
|
-
this.content.push(link);
|
|
247
|
-
return this;
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Add a link to an attachment
|
|
251
|
-
*/
|
|
252
|
-
attachmentLink(filename, linkText) {
|
|
253
|
-
const linkBody = linkText ? `<ac:plain-text-link-body><![CDATA[${linkText}]]></ac:plain-text-link-body>` : '';
|
|
254
|
-
const link = `<ac:link><ri:attachment ri:filename="${this.escapeAttr(filename)}" />${linkBody}</ac:link>`;
|
|
255
|
-
this.content.push(link);
|
|
256
|
-
return this;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Add an image from attachment
|
|
260
|
-
*/
|
|
261
|
-
imageFromAttachment(filename, options) {
|
|
262
|
-
const attrs = [];
|
|
263
|
-
if (options?.width) {
|
|
264
|
-
attrs.push(`ac:width="${options.width}"`);
|
|
265
|
-
}
|
|
266
|
-
if (options?.height) {
|
|
267
|
-
attrs.push(`ac:height="${options.height}"`);
|
|
268
|
-
}
|
|
269
|
-
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
270
|
-
const image = `<ac:image${attrStr}><ri:attachment ri:filename="${this.escapeAttr(filename)}" /></ac:image>`;
|
|
271
|
-
this.content.push(image);
|
|
272
|
-
return this;
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Add an image from URL
|
|
276
|
-
*/
|
|
277
|
-
imageFromUrl(url, options) {
|
|
278
|
-
const attrs = [];
|
|
279
|
-
if (options?.width) {
|
|
280
|
-
attrs.push(`ac:width="${options.width}"`);
|
|
281
|
-
}
|
|
282
|
-
if (options?.height) {
|
|
283
|
-
attrs.push(`ac:height="${options.height}"`);
|
|
284
|
-
}
|
|
285
|
-
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
286
|
-
const image = `<ac:image${attrStr}><ri:url ri:value="${this.escapeAttr(url)}" /></ac:image>`;
|
|
287
|
-
this.content.push(image);
|
|
288
|
-
return this;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Add a task list
|
|
292
|
-
*/
|
|
293
|
-
taskList(tasks) {
|
|
294
|
-
const taskItems = tasks
|
|
295
|
-
.map((task) => {
|
|
296
|
-
const status = task.completed ? 'complete' : 'incomplete';
|
|
297
|
-
return `<ac:task><ac:task-status>${status}</ac:task-status><ac:task-body>${this.escapeHtml(task.text)}</ac:task-body></ac:task>`;
|
|
298
|
-
})
|
|
299
|
-
.join('');
|
|
300
|
-
this.content.push(`<ac:task-list>${taskItems}</ac:task-list>`);
|
|
301
|
-
return this;
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Add a table of contents macro
|
|
305
|
-
*/
|
|
306
|
-
tableOfContents(options) {
|
|
307
|
-
const params = [];
|
|
308
|
-
if (options?.maxLevel) {
|
|
309
|
-
params.push({ name: 'maxLevel', value: String(options.maxLevel) });
|
|
310
|
-
}
|
|
311
|
-
if (options?.minLevel) {
|
|
312
|
-
params.push({ name: 'minLevel', value: String(options.minLevel) });
|
|
313
|
-
}
|
|
314
|
-
if (options?.exclude) {
|
|
315
|
-
params.push({ name: 'exclude', value: options.exclude });
|
|
316
|
-
}
|
|
317
|
-
if (options?.type) {
|
|
318
|
-
params.push({ name: 'type', value: options.type });
|
|
319
|
-
}
|
|
320
|
-
if (options?.outline !== undefined) {
|
|
321
|
-
params.push({ name: 'outline', value: String(options.outline) });
|
|
322
|
-
}
|
|
323
|
-
if (options?.style) {
|
|
324
|
-
params.push({ name: 'style', value: options.style });
|
|
325
|
-
}
|
|
326
|
-
this.content.push(this.createMacro('toc', params));
|
|
327
|
-
return this;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Add a layout with sections and cells
|
|
331
|
-
*/
|
|
332
|
-
layout(sections) {
|
|
333
|
-
const sectionElements = sections
|
|
334
|
-
.map((section) => {
|
|
335
|
-
const cells = section.cells
|
|
336
|
-
.map((cellContent) => `<ac:layout-cell>${cellContent}</ac:layout-cell>`)
|
|
337
|
-
.join('');
|
|
338
|
-
return `<ac:layout-section ac:type="${this.escapeAttr(section.type)}">${cells}</ac:layout-section>`;
|
|
339
|
-
})
|
|
340
|
-
.join('');
|
|
341
|
-
this.content.push(`<ac:layout>${sectionElements}</ac:layout>`);
|
|
342
|
-
return this;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Add raw storage format content
|
|
346
|
-
*/
|
|
347
|
-
raw(storageFormat) {
|
|
348
|
-
this.content.push(storageFormat);
|
|
349
|
-
return this;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Build the final storage format string
|
|
353
|
-
*/
|
|
354
|
-
build() {
|
|
355
|
-
return this.content.join('\n');
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Create a structured macro
|
|
359
|
-
*/
|
|
360
|
-
createMacro(name, parameters, body, isPlainTextBody = false) {
|
|
361
|
-
const params = parameters
|
|
362
|
-
.map((p) => `<ac:parameter ac:name="${this.escapeAttr(p.name)}">${this.escapeHtml(p.value)}</ac:parameter>`)
|
|
363
|
-
.join('');
|
|
364
|
-
let bodyContent = '';
|
|
365
|
-
if (body) {
|
|
366
|
-
if (isPlainTextBody) {
|
|
367
|
-
bodyContent = `<ac:plain-text-body><![CDATA[${body}]]></ac:plain-text-body>`;
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
bodyContent = `<ac:rich-text-body>${this.escapeHtml(body)}</ac:rich-text-body>`;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return `<ac:structured-macro ac:name="${this.escapeAttr(name)}">${params}${bodyContent}</ac:structured-macro>`;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Escape HTML special characters
|
|
377
|
-
*/
|
|
378
|
-
escapeHtml(text) {
|
|
379
|
-
return text
|
|
380
|
-
.replace(/&/g, '&')
|
|
381
|
-
.replace(/</g, '<')
|
|
382
|
-
.replace(/>/g, '>')
|
|
383
|
-
.replace(/"/g, '"')
|
|
384
|
-
.replace(/'/g, ''');
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Escape XML attribute values
|
|
388
|
-
*/
|
|
389
|
-
escapeAttr(text) {
|
|
390
|
-
return text
|
|
391
|
-
.replace(/&/g, '&')
|
|
392
|
-
.replace(/</g, '<')
|
|
393
|
-
.replace(/>/g, '>')
|
|
394
|
-
.replace(/"/g, '"')
|
|
395
|
-
.replace(/'/g, ''');
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Static helper functions for common Confluence storage format patterns
|
|
400
|
-
*/
|
|
401
|
-
export class ConfluenceStorageFormat {
|
|
402
|
-
/**
|
|
403
|
-
* Create a new builder instance
|
|
404
|
-
*/
|
|
405
|
-
static builder() {
|
|
406
|
-
return new ConfluenceStorageFormatBuilder();
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Create a heading
|
|
410
|
-
*/
|
|
411
|
-
static heading(level, text) {
|
|
412
|
-
return new ConfluenceStorageFormatBuilder().heading(level, text).build();
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Create a paragraph
|
|
416
|
-
*/
|
|
417
|
-
static paragraph(text) {
|
|
418
|
-
return new ConfluenceStorageFormatBuilder().paragraph(text).build();
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Create a code block
|
|
422
|
-
*/
|
|
423
|
-
static codeBlock(code, language, options) {
|
|
424
|
-
return new ConfluenceStorageFormatBuilder().codeBlock(code, language, options).build();
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Create an info panel
|
|
428
|
-
*/
|
|
429
|
-
static infoPanel(content, title) {
|
|
430
|
-
return new ConfluenceStorageFormatBuilder().infoPanel(content, title).build();
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Create a note panel
|
|
434
|
-
*/
|
|
435
|
-
static notePanel(content, title) {
|
|
436
|
-
return new ConfluenceStorageFormatBuilder().notePanel(content, title).build();
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Create a warning panel
|
|
440
|
-
*/
|
|
441
|
-
static warningPanel(content, title) {
|
|
442
|
-
return new ConfluenceStorageFormatBuilder().warningPanel(content, title).build();
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Create a tip panel
|
|
446
|
-
*/
|
|
447
|
-
static tipPanel(content, title) {
|
|
448
|
-
return new ConfluenceStorageFormatBuilder().tipPanel(content, title).build();
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Create a table
|
|
452
|
-
*/
|
|
453
|
-
static table(rows) {
|
|
454
|
-
return new ConfluenceStorageFormatBuilder().table(rows).build();
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Create an unordered list
|
|
458
|
-
*/
|
|
459
|
-
static unorderedList(items) {
|
|
460
|
-
return new ConfluenceStorageFormatBuilder().unorderedList(items).build();
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Create an ordered list
|
|
464
|
-
*/
|
|
465
|
-
static orderedList(items) {
|
|
466
|
-
return new ConfluenceStorageFormatBuilder().orderedList(items).build();
|
|
467
|
-
}
|
|
468
|
-
}
|