docspan 0.1.0__py3-none-any.whl
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.
- docspan/__init__.py +3 -0
- docspan/__main__.py +0 -0
- docspan/backends/__init__.py +19 -0
- docspan/backends/base.py +85 -0
- docspan/backends/confluence/__init__.py +0 -0
- docspan/backends/confluence/adf/__init__.py +14 -0
- docspan/backends/confluence/adf/comparator.py +427 -0
- docspan/backends/confluence/adf/converter.py +119 -0
- docspan/backends/confluence/adf/converters.py +1449 -0
- docspan/backends/confluence/adf/interfaces.py +191 -0
- docspan/backends/confluence/adf/nodes.py +2085 -0
- docspan/backends/confluence/adf/parser.py +400 -0
- docspan/backends/confluence/adf/validators.py +161 -0
- docspan/backends/confluence/adf/visitors.py +495 -0
- docspan/backends/confluence/backend.py +227 -0
- docspan/backends/confluence/client.py +44 -0
- docspan/backends/confluence/config/__init__.py +21 -0
- docspan/backends/confluence/config/loader.py +107 -0
- docspan/backends/confluence/config/models.py +167 -0
- docspan/backends/confluence/config/validation.py +297 -0
- docspan/backends/confluence/markdown/__init__.py +22 -0
- docspan/backends/confluence/markdown/ast.py +819 -0
- docspan/backends/confluence/markdown/extensions/__init__.py +5 -0
- docspan/backends/confluence/markdown/extensions/frontmatter.py +80 -0
- docspan/backends/confluence/markdown/extensions/mermaid.py +64 -0
- docspan/backends/confluence/markdown/extensions/wikilinks.py +179 -0
- docspan/backends/confluence/markdown/inline_parser.py +495 -0
- docspan/backends/confluence/markdown/parser.py +1006 -0
- docspan/backends/confluence/models/__init__.py +18 -0
- docspan/backends/confluence/models/markdown_file.py +402 -0
- docspan/backends/confluence/models/page.py +212 -0
- docspan/backends/confluence/models/path_utils.py +34 -0
- docspan/backends/confluence/models/results.py +28 -0
- docspan/backends/confluence/models/sync_status.py +382 -0
- docspan/backends/confluence/services/__init__.py +0 -0
- docspan/backends/confluence/services/confluence/__init__.py +40 -0
- docspan/backends/confluence/services/confluence/attachment_client.py +147 -0
- docspan/backends/confluence/services/confluence/base_client.py +420 -0
- docspan/backends/confluence/services/confluence/client.py +376 -0
- docspan/backends/confluence/services/confluence/comment_client.py +682 -0
- docspan/backends/confluence/services/confluence/crawler.py +587 -0
- docspan/backends/confluence/services/confluence/label_client.py +130 -0
- docspan/backends/confluence/services/confluence/page_client.py +1288 -0
- docspan/backends/confluence/services/confluence/space_client.py +179 -0
- docspan/backends/confluence/services/confluence/url_parser.py +106 -0
- docspan/backends/google_docs/__init__.py +0 -0
- docspan/backends/google_docs/auth.py +143 -0
- docspan/backends/google_docs/backend.py +140 -0
- docspan/backends/google_docs/client.py +665 -0
- docspan/backends/google_docs/converter.py +471 -0
- docspan/backends/google_docs/docs_request_builder.py +232 -0
- docspan/backends/google_docs/docs_structure_parser.py +120 -0
- docspan/backends/google_docs/markdown_to_paragraph_parser.py +145 -0
- docspan/cli/__init__.py +0 -0
- docspan/cli/main.py +408 -0
- docspan/config.py +62 -0
- docspan/core/__init__.py +49 -0
- docspan/core/merge.py +30 -0
- docspan/core/orchestrator.py +332 -0
- docspan/core/paths.py +8 -0
- docspan/core/state.py +53 -0
- docspan-0.1.0.dist-info/METADATA +273 -0
- docspan-0.1.0.dist-info/RECORD +65 -0
- docspan-0.1.0.dist-info/WHEEL +4 -0
- docspan-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,2085 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atlassian Document Format node definitions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AdfNode:
|
|
14
|
+
"""
|
|
15
|
+
Represents a node in the ADF structure.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
type: Node type
|
|
19
|
+
attrs: Node attributes
|
|
20
|
+
content: Child nodes
|
|
21
|
+
marks: Formatting marks for text nodes
|
|
22
|
+
text: Text content for text nodes
|
|
23
|
+
storage_format_html: Optional HTML content for storage format
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
type: str
|
|
27
|
+
attrs: Dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
content: List["AdfNode"] = field(default_factory=list)
|
|
29
|
+
marks: List[Dict[str, Any]] = field(default_factory=list)
|
|
30
|
+
text: Optional[str] = None
|
|
31
|
+
storage_format_html: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Convert the node to a dictionary for JSON serialization.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary representation of the node
|
|
39
|
+
"""
|
|
40
|
+
result = {"type": self.type}
|
|
41
|
+
|
|
42
|
+
if self.attrs:
|
|
43
|
+
result["attrs"] = self.attrs
|
|
44
|
+
|
|
45
|
+
if self.content:
|
|
46
|
+
result["content"] = [node.to_dict() for node in self.content]
|
|
47
|
+
|
|
48
|
+
if self.marks:
|
|
49
|
+
result["marks"] = self.marks
|
|
50
|
+
|
|
51
|
+
if self.text is not None:
|
|
52
|
+
result["text"] = self.text
|
|
53
|
+
|
|
54
|
+
# Include storage_format_html if present
|
|
55
|
+
if self.storage_format_html is not None:
|
|
56
|
+
if "attrs" not in result:
|
|
57
|
+
result["attrs"] = {}
|
|
58
|
+
result["attrs"]["storage_format_html"] = self.storage_format_html
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AdfBuilder:
|
|
64
|
+
"""
|
|
65
|
+
Builder for creating ADF documents.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def document(content: List[AdfNode]) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Create an ADF document.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
content: Document content nodes
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
ADF document dictionary
|
|
78
|
+
"""
|
|
79
|
+
return {"version": 1, "type": "doc", "content": [node.to_dict() for node in content]}
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def paragraph(content: List[AdfNode] = None) -> AdfNode:
|
|
83
|
+
"""
|
|
84
|
+
Create a paragraph node.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
content: Paragraph content
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Paragraph node
|
|
91
|
+
"""
|
|
92
|
+
return AdfNode(type="paragraph", content=content or [])
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def text(text: str, marks: List[Dict[str, Any]] = None) -> AdfNode:
|
|
96
|
+
"""
|
|
97
|
+
Create a text node.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
text: Text content
|
|
101
|
+
marks: Text formatting marks
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Text node
|
|
105
|
+
"""
|
|
106
|
+
return AdfNode(type="text", text=text, marks=marks or [])
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def heading(content: List[AdfNode], level: int) -> AdfNode:
|
|
110
|
+
"""
|
|
111
|
+
Create a heading node.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
content: Heading content
|
|
115
|
+
level: Heading level (1-6)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Heading node
|
|
119
|
+
"""
|
|
120
|
+
return AdfNode(type="heading", attrs={"level": level}, content=content)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def code_block(content: str, language: Optional[str] = None) -> AdfNode:
|
|
124
|
+
"""
|
|
125
|
+
Create a code block node.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
content: Code content
|
|
129
|
+
language: Programming language
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Code block node
|
|
133
|
+
"""
|
|
134
|
+
attrs = {}
|
|
135
|
+
if language:
|
|
136
|
+
attrs["language"] = language
|
|
137
|
+
|
|
138
|
+
return AdfNode(type="codeBlock", attrs=attrs, content=[AdfNode(type="text", text=content)])
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def bullet_list(items: List[AdfNode]) -> AdfNode:
|
|
142
|
+
"""
|
|
143
|
+
Create a bullet list node.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
items: List items
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Bullet list node
|
|
150
|
+
"""
|
|
151
|
+
return AdfNode(type="bulletList", content=items)
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def ordered_list(items: List[AdfNode]) -> AdfNode:
|
|
155
|
+
"""
|
|
156
|
+
Create an ordered list node.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
items: List items
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Ordered list node
|
|
163
|
+
"""
|
|
164
|
+
return AdfNode(type="orderedList", content=items)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def list_item(content: List[AdfNode]) -> AdfNode:
|
|
168
|
+
"""
|
|
169
|
+
Create a list item node.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
content: List item content
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List item node
|
|
176
|
+
"""
|
|
177
|
+
return AdfNode(type="listItem", content=content)
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def blockquote(content: List[AdfNode]) -> AdfNode:
|
|
181
|
+
"""
|
|
182
|
+
Create a blockquote node.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
content: Blockquote content
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Blockquote node
|
|
189
|
+
"""
|
|
190
|
+
return AdfNode(type="blockquote", content=content)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def panel(content: List[AdfNode], panel_type: str) -> AdfNode:
|
|
194
|
+
"""
|
|
195
|
+
Create a panel node.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
content: Panel content
|
|
199
|
+
panel_type: Panel type (note, info, warning, error, success)
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Panel node
|
|
203
|
+
"""
|
|
204
|
+
return AdfNode(type="panel", attrs={"panelType": panel_type}, content=content)
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def table(rows: List[List[AdfNode]], headers: bool = False) -> AdfNode:
|
|
208
|
+
"""
|
|
209
|
+
Create a table node.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
rows: Table rows
|
|
213
|
+
headers: Whether the first row contains headers
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Table node
|
|
217
|
+
"""
|
|
218
|
+
attrs = {}
|
|
219
|
+
if headers:
|
|
220
|
+
attrs["isNumberColumnEnabled"] = False
|
|
221
|
+
attrs["layout"] = "default"
|
|
222
|
+
|
|
223
|
+
table_rows = []
|
|
224
|
+
for row in rows:
|
|
225
|
+
cells = []
|
|
226
|
+
for cell_content in row:
|
|
227
|
+
cells.append(AdfNode(type="tableCell", content=[cell_content]))
|
|
228
|
+
table_rows.append(AdfNode(type="tableRow", content=cells))
|
|
229
|
+
|
|
230
|
+
return AdfNode(type="table", attrs=attrs, content=table_rows)
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def link(text: str, href: str, title: Optional[str] = None, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
234
|
+
"""
|
|
235
|
+
Create a link node.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
text: Link text
|
|
239
|
+
href: Link URL
|
|
240
|
+
title: Link title
|
|
241
|
+
additional_marks: Additional marks to apply (e.g., strong, em) from child nodes
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Text node with link mark and any additional marks
|
|
245
|
+
"""
|
|
246
|
+
attrs = {"href": href}
|
|
247
|
+
if title:
|
|
248
|
+
attrs["title"] = title
|
|
249
|
+
|
|
250
|
+
node = AdfNode(type="text", text=text)
|
|
251
|
+
|
|
252
|
+
# Start with any additional marks (e.g., strong, em)
|
|
253
|
+
marks = list(additional_marks) if additional_marks else []
|
|
254
|
+
# Add the link mark
|
|
255
|
+
marks.append({"type": "link", "attrs": attrs})
|
|
256
|
+
|
|
257
|
+
node.marks = marks
|
|
258
|
+
return node
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def image(
|
|
262
|
+
url: str,
|
|
263
|
+
alt: str = "",
|
|
264
|
+
title: Optional[str] = None,
|
|
265
|
+
width: Optional[int] = None,
|
|
266
|
+
height: Optional[int] = None
|
|
267
|
+
) -> AdfNode:
|
|
268
|
+
"""
|
|
269
|
+
Create an image node.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
url: Image URL
|
|
273
|
+
alt: Alternative text
|
|
274
|
+
title: Image title
|
|
275
|
+
width: Image width in pixels
|
|
276
|
+
height: Image height in pixels
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Media node for image
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
>>> AdfBuilder.image("https://example.com/img.jpg", "Photo")
|
|
283
|
+
>>> AdfBuilder.image("https://example.com/img.jpg", "Photo", width=800, height=600)
|
|
284
|
+
|
|
285
|
+
Notes:
|
|
286
|
+
- For layout control (center, wide, wrap), wrap in media_single()
|
|
287
|
+
- Width and height are optional display dimensions
|
|
288
|
+
- Use media_group() for image galleries
|
|
289
|
+
"""
|
|
290
|
+
logger.debug(f"Creating image node with URL: {url}")
|
|
291
|
+
|
|
292
|
+
if not url:
|
|
293
|
+
logger.warning("Empty URL provided for image node, creating placeholder")
|
|
294
|
+
return AdfNode(type="paragraph", content=[
|
|
295
|
+
AdfNode(type="text", text="[Image placeholder - URL was empty]")
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
attrs = {"type": "external", "url": url}
|
|
299
|
+
|
|
300
|
+
if alt:
|
|
301
|
+
attrs["alt"] = alt
|
|
302
|
+
if title:
|
|
303
|
+
attrs["title"] = title
|
|
304
|
+
if width is not None:
|
|
305
|
+
attrs["width"] = width
|
|
306
|
+
if height is not None:
|
|
307
|
+
attrs["height"] = height
|
|
308
|
+
|
|
309
|
+
logger.debug(f"Image node attributes: {attrs}")
|
|
310
|
+
return AdfNode(type="media", attrs=attrs)
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def confluence_image(
|
|
314
|
+
file_id: str,
|
|
315
|
+
alt: str = "",
|
|
316
|
+
title: Optional[str] = None,
|
|
317
|
+
width: Optional[int] = None,
|
|
318
|
+
height: Optional[int] = None,
|
|
319
|
+
collection: str = "contentId",
|
|
320
|
+
occurrence_key: Optional[str] = None
|
|
321
|
+
) -> AdfNode:
|
|
322
|
+
"""
|
|
323
|
+
Create a Confluence image node.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
file_id: Confluence file ID
|
|
327
|
+
alt: Alternative text
|
|
328
|
+
title: Image title
|
|
329
|
+
width: Image width in pixels
|
|
330
|
+
height: Image height in pixels
|
|
331
|
+
collection: Confluence Media Services collection name (default: "contentId")
|
|
332
|
+
occurrence_key: Occurrence key for deletion support
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Media node for Confluence image
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> AdfBuilder.confluence_image("abc123", "Diagram")
|
|
339
|
+
>>> AdfBuilder.confluence_image("abc123", "Diagram", width=800, height=600)
|
|
340
|
+
|
|
341
|
+
Notes:
|
|
342
|
+
- Uses Confluence file ID instead of external URL
|
|
343
|
+
- For layout control, wrap in media_single()
|
|
344
|
+
- occurrence_key enables proper deletion tracking
|
|
345
|
+
"""
|
|
346
|
+
attrs = {"type": "file", "id": file_id, "collection": collection}
|
|
347
|
+
|
|
348
|
+
if alt:
|
|
349
|
+
attrs["alt"] = alt
|
|
350
|
+
if title:
|
|
351
|
+
attrs["title"] = title
|
|
352
|
+
if width is not None:
|
|
353
|
+
attrs["width"] = width
|
|
354
|
+
if height is not None:
|
|
355
|
+
attrs["height"] = height
|
|
356
|
+
if occurrence_key is not None:
|
|
357
|
+
attrs["occurrenceKey"] = occurrence_key
|
|
358
|
+
|
|
359
|
+
return AdfNode(type="media", attrs=attrs)
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def horizontal_rule() -> AdfNode:
|
|
363
|
+
"""
|
|
364
|
+
Create a horizontal rule node.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Rule node
|
|
368
|
+
"""
|
|
369
|
+
return AdfNode(type="rule")
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def inline_card(url: str, title: Optional[str] = None, confluence_metadata: Optional[Dict[str, Any]] = None) -> AdfNode:
|
|
373
|
+
"""
|
|
374
|
+
Create an inline smart card node.
|
|
375
|
+
|
|
376
|
+
Inline cards are used for wiki links and page references within text.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
url: URL to link to (Confluence page URL or external URL)
|
|
380
|
+
title: Optional display title (if different from page title)
|
|
381
|
+
confluence_metadata: Optional Confluence-specific metadata (linkType, contentTitle, etc.)
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
InlineCard node
|
|
385
|
+
|
|
386
|
+
Example:
|
|
387
|
+
>>> AdfBuilder.inline_card(
|
|
388
|
+
... url="https://example.atlassian.net/wiki/spaces/SPACE/pages/12345/Page+Title",
|
|
389
|
+
... title="Page Title",
|
|
390
|
+
... confluence_metadata={
|
|
391
|
+
... "linkType": "page",
|
|
392
|
+
... "contentTitle": "Page Title",
|
|
393
|
+
... "isRenamedTitle": True
|
|
394
|
+
... }
|
|
395
|
+
... )
|
|
396
|
+
"""
|
|
397
|
+
attrs = {"url": url}
|
|
398
|
+
|
|
399
|
+
# Add Confluence metadata if provided
|
|
400
|
+
if confluence_metadata:
|
|
401
|
+
attrs["__confluenceMetadata"] = confluence_metadata
|
|
402
|
+
|
|
403
|
+
return AdfNode(type="inlineCard", attrs=attrs)
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def block_card(url: str, datasource: Optional[Dict[str, Any]] = None) -> AdfNode:
|
|
407
|
+
"""
|
|
408
|
+
Create a block smart card node.
|
|
409
|
+
|
|
410
|
+
Block cards are used for rich data sources like JIRA queries, dashboards, etc.
|
|
411
|
+
They appear as standalone blocks rather than inline elements.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
url: URL to the data source
|
|
415
|
+
datasource: Optional datasource configuration (for JIRA queries, etc.)
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
BlockCard node
|
|
419
|
+
|
|
420
|
+
Example:
|
|
421
|
+
>>> AdfBuilder.block_card(
|
|
422
|
+
... url="https://example.atlassian.net/issues/?jql=project=PROJ",
|
|
423
|
+
... datasource={
|
|
424
|
+
... "id": "datasource-id",
|
|
425
|
+
... "parameters": {"jql": "project = PROJ"},
|
|
426
|
+
... "views": [{"type": "table", "properties": {...}}]
|
|
427
|
+
... }
|
|
428
|
+
... )
|
|
429
|
+
"""
|
|
430
|
+
attrs = {"url": url}
|
|
431
|
+
|
|
432
|
+
# Add datasource configuration if provided
|
|
433
|
+
if datasource:
|
|
434
|
+
attrs["datasource"] = datasource
|
|
435
|
+
|
|
436
|
+
return AdfNode(type="blockCard", attrs=attrs)
|
|
437
|
+
|
|
438
|
+
@staticmethod
|
|
439
|
+
def mention(user_id: str, text: str, access_level: Optional[str] = None) -> AdfNode:
|
|
440
|
+
"""
|
|
441
|
+
Create a user mention node.
|
|
442
|
+
|
|
443
|
+
Mention nodes are used to reference Confluence users in content.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
user_id: Confluence user ID (account ID)
|
|
447
|
+
text: Display text for the mention (e.g., "@John Doe")
|
|
448
|
+
access_level: Optional access level (e.g., "CONTAINER", "APPLICATION")
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Mention node
|
|
452
|
+
|
|
453
|
+
Example:
|
|
454
|
+
>>> AdfBuilder.mention(
|
|
455
|
+
... user_id="6400c3ebc6e77744a1dd3cec",
|
|
456
|
+
... text="@John Doe"
|
|
457
|
+
... )
|
|
458
|
+
|
|
459
|
+
Notes:
|
|
460
|
+
- user_id should be the Confluence account ID (obtained via user resolution)
|
|
461
|
+
- text typically includes @ prefix for display
|
|
462
|
+
- access_level can be used for permission-based mentions
|
|
463
|
+
"""
|
|
464
|
+
attrs = {
|
|
465
|
+
"id": user_id,
|
|
466
|
+
"text": text
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# Add access level if provided
|
|
470
|
+
if access_level:
|
|
471
|
+
attrs["accessLevel"] = access_level
|
|
472
|
+
|
|
473
|
+
return AdfNode(type="mention", attrs=attrs)
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def emoji(short_name: str, emoji_id: Optional[str] = None, text: Optional[str] = None) -> AdfNode:
|
|
477
|
+
"""
|
|
478
|
+
Create an emoji node.
|
|
479
|
+
|
|
480
|
+
Emoji nodes are used to display emoji expressions in content.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
short_name: Emoji short name with colons (e.g., ":smile:", ":tada:")
|
|
484
|
+
emoji_id: Unicode emoji ID/codepoint (e.g., "1f604" for smile)
|
|
485
|
+
text: Unicode emoji character (e.g., "😄")
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Emoji node
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
>>> AdfBuilder.emoji(
|
|
492
|
+
... short_name=":smile:",
|
|
493
|
+
... emoji_id="1f604",
|
|
494
|
+
... text="😄"
|
|
495
|
+
... )
|
|
496
|
+
|
|
497
|
+
Notes:
|
|
498
|
+
- short_name should include colons for consistency
|
|
499
|
+
- emoji_id is the unicode codepoint (hex)
|
|
500
|
+
- text is the actual emoji character
|
|
501
|
+
- If emoji_id or text not provided, uses short_name as fallback
|
|
502
|
+
"""
|
|
503
|
+
attrs = {"shortName": short_name}
|
|
504
|
+
|
|
505
|
+
# Add emoji ID if provided
|
|
506
|
+
if emoji_id:
|
|
507
|
+
attrs["id"] = emoji_id
|
|
508
|
+
|
|
509
|
+
# Add text (emoji character) if provided, otherwise use short_name
|
|
510
|
+
if text:
|
|
511
|
+
attrs["text"] = text
|
|
512
|
+
else:
|
|
513
|
+
# Fallback to short_name if text not provided
|
|
514
|
+
attrs["text"] = short_name
|
|
515
|
+
|
|
516
|
+
return AdfNode(type="emoji", attrs=attrs)
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def expand(title: str, content: List[AdfNode]) -> AdfNode:
|
|
520
|
+
"""
|
|
521
|
+
Create an expand (collapsible) node.
|
|
522
|
+
|
|
523
|
+
Expand nodes are used to create collapsible sections that can be
|
|
524
|
+
expanded or collapsed by the user.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
title: Title shown in the expand header
|
|
528
|
+
content: List of child nodes contained within the expand section
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Expand node
|
|
532
|
+
|
|
533
|
+
Example:
|
|
534
|
+
>>> AdfBuilder.expand(
|
|
535
|
+
... title="Click to expand",
|
|
536
|
+
... content=[
|
|
537
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Hidden content")])
|
|
538
|
+
... ]
|
|
539
|
+
... )
|
|
540
|
+
|
|
541
|
+
Notes:
|
|
542
|
+
- Title can be empty string for expand without title
|
|
543
|
+
- Content should be a list of block nodes (paragraphs, lists, etc.)
|
|
544
|
+
- At least one content node is required for valid ADF
|
|
545
|
+
"""
|
|
546
|
+
attrs = {"title": title}
|
|
547
|
+
return AdfNode(type="expand", attrs=attrs, content=content)
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def hard_break() -> AdfNode:
|
|
551
|
+
"""
|
|
552
|
+
Create a hard break (line break) node.
|
|
553
|
+
|
|
554
|
+
Hard break nodes insert an explicit line break within text content,
|
|
555
|
+
similar to <br> in HTML.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
HardBreak node
|
|
559
|
+
|
|
560
|
+
Example:
|
|
561
|
+
>>> AdfBuilder.paragraph([
|
|
562
|
+
... AdfBuilder.text("First line"),
|
|
563
|
+
... AdfBuilder.hard_break(),
|
|
564
|
+
... AdfBuilder.text("Second line")
|
|
565
|
+
... ])
|
|
566
|
+
|
|
567
|
+
Notes:
|
|
568
|
+
- Used for explicit line breaks within paragraphs
|
|
569
|
+
- Different from starting a new paragraph (which adds spacing)
|
|
570
|
+
"""
|
|
571
|
+
return AdfNode(type="hardBreak")
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def placeholder(text: str) -> AdfNode:
|
|
575
|
+
"""
|
|
576
|
+
Create a placeholder node.
|
|
577
|
+
|
|
578
|
+
Placeholder nodes display placeholder text in the editor,
|
|
579
|
+
typically used for template fields or content hints.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
text: Placeholder text to display
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Placeholder node
|
|
586
|
+
|
|
587
|
+
Example:
|
|
588
|
+
>>> AdfBuilder.placeholder("Enter description here...")
|
|
589
|
+
|
|
590
|
+
Notes:
|
|
591
|
+
- Appears as grayed-out text in the editor
|
|
592
|
+
- Often used in templates or forms
|
|
593
|
+
"""
|
|
594
|
+
return AdfNode(type="placeholder", attrs={"text": text})
|
|
595
|
+
|
|
596
|
+
@staticmethod
|
|
597
|
+
def status(text: str, color: str = "neutral") -> AdfNode:
|
|
598
|
+
"""
|
|
599
|
+
Create a status lozenge node.
|
|
600
|
+
|
|
601
|
+
Status nodes display colored status badges/lozenges, commonly used
|
|
602
|
+
for indicating task states, priorities, or other status information.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
text: Status text to display
|
|
606
|
+
color: Status color (neutral, blue, green, yellow, red, purple)
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Status node
|
|
610
|
+
|
|
611
|
+
Example:
|
|
612
|
+
>>> AdfBuilder.status("In Progress", "blue")
|
|
613
|
+
>>> AdfBuilder.status("Complete", "green")
|
|
614
|
+
>>> AdfBuilder.status("Blocked", "red")
|
|
615
|
+
|
|
616
|
+
Notes:
|
|
617
|
+
- Valid colors: neutral (gray), blue, green, yellow, red, purple
|
|
618
|
+
- Status badges are inline elements
|
|
619
|
+
- Commonly used in project management pages
|
|
620
|
+
"""
|
|
621
|
+
valid_colors = {"neutral", "blue", "green", "yellow", "red", "purple"}
|
|
622
|
+
if color not in valid_colors:
|
|
623
|
+
logging.warning(f"Invalid status color '{color}', using 'neutral'. Valid colors: {valid_colors}")
|
|
624
|
+
color = "neutral"
|
|
625
|
+
|
|
626
|
+
return AdfNode(type="status", attrs={"text": text, "color": color})
|
|
627
|
+
|
|
628
|
+
@staticmethod
|
|
629
|
+
def date(timestamp: int) -> AdfNode:
|
|
630
|
+
"""
|
|
631
|
+
Create a date node.
|
|
632
|
+
|
|
633
|
+
Date nodes display formatted dates in Confluence, with the timestamp
|
|
634
|
+
stored as Unix milliseconds.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
timestamp: Unix timestamp in milliseconds
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
Date node
|
|
641
|
+
|
|
642
|
+
Example:
|
|
643
|
+
>>> import time
|
|
644
|
+
>>> timestamp_ms = int(time.time() * 1000)
|
|
645
|
+
>>> AdfBuilder.date(timestamp_ms)
|
|
646
|
+
|
|
647
|
+
Notes:
|
|
648
|
+
- Timestamp must be in milliseconds (not seconds)
|
|
649
|
+
- Confluence will display the date according to user preferences
|
|
650
|
+
- Dates are inline elements
|
|
651
|
+
"""
|
|
652
|
+
return AdfNode(type="date", attrs={"timestamp": str(timestamp)})
|
|
653
|
+
|
|
654
|
+
# Mark Helper Methods
|
|
655
|
+
|
|
656
|
+
@staticmethod
|
|
657
|
+
def colored_text(text: str, color: str, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
658
|
+
"""
|
|
659
|
+
Create text with a color mark.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
text: Text content
|
|
663
|
+
color: Text color (hex color code like "#ff0000" or CSS color name)
|
|
664
|
+
additional_marks: Additional marks to apply (e.g., strong, em)
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Text node with textColor mark
|
|
668
|
+
|
|
669
|
+
Example:
|
|
670
|
+
>>> AdfBuilder.colored_text("Red text", "#ff0000")
|
|
671
|
+
>>> AdfBuilder.colored_text("Bold red text", "#ff0000",
|
|
672
|
+
... additional_marks=[{"type": "strong"}])
|
|
673
|
+
|
|
674
|
+
Notes:
|
|
675
|
+
- Color should be a valid hex code (#RRGGBB) or CSS color name
|
|
676
|
+
- Multiple marks can be combined
|
|
677
|
+
"""
|
|
678
|
+
marks = list(additional_marks) if additional_marks else []
|
|
679
|
+
marks.append({"type": "textColor", "attrs": {"color": color}})
|
|
680
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def highlighted_text(text: str, bg_color: str, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
684
|
+
"""
|
|
685
|
+
Create text with a background color mark.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
text: Text content
|
|
689
|
+
bg_color: Background color (hex color code like "#ffff00" or CSS color name)
|
|
690
|
+
additional_marks: Additional marks to apply (e.g., strong, em)
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Text node with backgroundColor mark
|
|
694
|
+
|
|
695
|
+
Example:
|
|
696
|
+
>>> AdfBuilder.highlighted_text("Highlighted text", "#ffff00")
|
|
697
|
+
>>> AdfBuilder.highlighted_text("Bold highlighted", "#ffff00",
|
|
698
|
+
... additional_marks=[{"type": "strong"}])
|
|
699
|
+
|
|
700
|
+
Notes:
|
|
701
|
+
- Color should be a valid hex code (#RRGGBB) or CSS color name
|
|
702
|
+
- Commonly used for highlighting important information
|
|
703
|
+
"""
|
|
704
|
+
marks = list(additional_marks) if additional_marks else []
|
|
705
|
+
marks.append({"type": "backgroundColor", "attrs": {"color": bg_color}})
|
|
706
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
707
|
+
|
|
708
|
+
@staticmethod
|
|
709
|
+
def aligned_text(text: str, align: str, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
710
|
+
"""
|
|
711
|
+
Create text with an alignment mark.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
text: Text content
|
|
715
|
+
align: Text alignment (start, center, end)
|
|
716
|
+
additional_marks: Additional marks to apply
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
Text node with alignment mark
|
|
720
|
+
|
|
721
|
+
Example:
|
|
722
|
+
>>> AdfBuilder.aligned_text("Centered text", "center")
|
|
723
|
+
>>> AdfBuilder.aligned_text("Right-aligned bold", "end",
|
|
724
|
+
... additional_marks=[{"type": "strong"}])
|
|
725
|
+
|
|
726
|
+
Notes:
|
|
727
|
+
- Valid alignments: start (left), center, end (right)
|
|
728
|
+
- Alignment applies within the parent block
|
|
729
|
+
"""
|
|
730
|
+
valid_alignments = {"start", "center", "end"}
|
|
731
|
+
if align not in valid_alignments:
|
|
732
|
+
logging.warning(f"Invalid alignment '{align}', using 'start'. Valid: {valid_alignments}")
|
|
733
|
+
align = "start"
|
|
734
|
+
|
|
735
|
+
marks = list(additional_marks) if additional_marks else []
|
|
736
|
+
marks.append({"type": "alignment", "attrs": {"align": align}})
|
|
737
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
738
|
+
|
|
739
|
+
@staticmethod
|
|
740
|
+
def indented_text(text: str, level: int = 1, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
741
|
+
"""
|
|
742
|
+
Create text with an indentation mark.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
text: Text content
|
|
746
|
+
level: Indentation level (1-6)
|
|
747
|
+
additional_marks: Additional marks to apply
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
Text node with indentation mark
|
|
751
|
+
|
|
752
|
+
Example:
|
|
753
|
+
>>> AdfBuilder.indented_text("Indented once", 1)
|
|
754
|
+
>>> AdfBuilder.indented_text("Double indent", 2)
|
|
755
|
+
|
|
756
|
+
Notes:
|
|
757
|
+
- Valid levels: 1-6
|
|
758
|
+
- Each level typically adds ~30px of indentation
|
|
759
|
+
"""
|
|
760
|
+
if not 1 <= level <= 6:
|
|
761
|
+
logger.warning(f"Invalid indentation level {level}, clamping to 1-6")
|
|
762
|
+
level = max(1, min(6, level))
|
|
763
|
+
|
|
764
|
+
marks = list(additional_marks) if additional_marks else []
|
|
765
|
+
marks.append({"type": "indentation", "attrs": {"level": level}})
|
|
766
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
767
|
+
|
|
768
|
+
@staticmethod
|
|
769
|
+
def subscript(text: str, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
770
|
+
"""
|
|
771
|
+
Create subscript text.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
text: Text content
|
|
775
|
+
additional_marks: Additional marks to apply
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
Text node with subsup mark (subscript)
|
|
779
|
+
|
|
780
|
+
Example:
|
|
781
|
+
>>> AdfBuilder.paragraph([
|
|
782
|
+
... AdfBuilder.text("H"),
|
|
783
|
+
... AdfBuilder.subscript("2"),
|
|
784
|
+
... AdfBuilder.text("O")
|
|
785
|
+
... ]) # Creates H₂O
|
|
786
|
+
|
|
787
|
+
Notes:
|
|
788
|
+
- Used for chemical formulas, mathematical notation, etc.
|
|
789
|
+
"""
|
|
790
|
+
marks = list(additional_marks) if additional_marks else []
|
|
791
|
+
marks.append({"type": "subsup", "attrs": {"type": "sub"}})
|
|
792
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
793
|
+
|
|
794
|
+
@staticmethod
|
|
795
|
+
def superscript(text: str, additional_marks: Optional[List[Dict[str, Any]]] = None) -> AdfNode:
|
|
796
|
+
"""
|
|
797
|
+
Create superscript text.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
text: Text content
|
|
801
|
+
additional_marks: Additional marks to apply
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
Text node with subsup mark (superscript)
|
|
805
|
+
|
|
806
|
+
Example:
|
|
807
|
+
>>> AdfBuilder.paragraph([
|
|
808
|
+
... AdfBuilder.text("E=mc"),
|
|
809
|
+
... AdfBuilder.superscript("2")
|
|
810
|
+
... ]) # Creates E=mc²
|
|
811
|
+
|
|
812
|
+
Notes:
|
|
813
|
+
- Used for exponents, footnotes, mathematical notation, etc.
|
|
814
|
+
"""
|
|
815
|
+
marks = list(additional_marks) if additional_marks else []
|
|
816
|
+
marks.append({"type": "subsup", "attrs": {"type": "sup"}})
|
|
817
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
818
|
+
|
|
819
|
+
# Task Management Nodes
|
|
820
|
+
|
|
821
|
+
@staticmethod
|
|
822
|
+
def task_list(items: List[AdfNode], local_id: Optional[str] = None) -> AdfNode:
|
|
823
|
+
"""
|
|
824
|
+
Create a task list (checklist) node.
|
|
825
|
+
|
|
826
|
+
Task lists contain task items that can be checked/unchecked,
|
|
827
|
+
commonly used for to-do lists and checklists.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
items: List of task items
|
|
831
|
+
local_id: Optional local identifier for the task list
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
TaskList node
|
|
835
|
+
|
|
836
|
+
Example:
|
|
837
|
+
>>> AdfBuilder.task_list([
|
|
838
|
+
... AdfBuilder.task_item("Buy groceries", state="TODO"),
|
|
839
|
+
... AdfBuilder.task_item("Finish report", state="DONE")
|
|
840
|
+
... ])
|
|
841
|
+
|
|
842
|
+
Notes:
|
|
843
|
+
- Items should be TaskItem nodes created with task_item()
|
|
844
|
+
- local_id is used for tracking and synchronization
|
|
845
|
+
"""
|
|
846
|
+
attrs = {}
|
|
847
|
+
if local_id:
|
|
848
|
+
attrs["localId"] = local_id
|
|
849
|
+
|
|
850
|
+
return AdfNode(type="taskList", attrs=attrs if attrs else {}, content=items)
|
|
851
|
+
|
|
852
|
+
@staticmethod
|
|
853
|
+
def task_item(content: List[AdfNode], state: str = "TODO", local_id: Optional[str] = None) -> AdfNode:
|
|
854
|
+
"""
|
|
855
|
+
Create a task item node.
|
|
856
|
+
|
|
857
|
+
Task items are individual items within a task list that can be
|
|
858
|
+
marked as done or todo.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
content: Task item content (typically paragraphs with text)
|
|
862
|
+
state: Task state ("TODO" or "DONE")
|
|
863
|
+
local_id: Optional local identifier for the task item
|
|
864
|
+
|
|
865
|
+
Returns:
|
|
866
|
+
TaskItem node
|
|
867
|
+
|
|
868
|
+
Example:
|
|
869
|
+
>>> AdfBuilder.task_item(
|
|
870
|
+
... [AdfBuilder.paragraph([AdfBuilder.text("Complete task")])],
|
|
871
|
+
... state="TODO"
|
|
872
|
+
... )
|
|
873
|
+
>>> AdfBuilder.task_item(
|
|
874
|
+
... [AdfBuilder.paragraph([AdfBuilder.text("Finished task")])],
|
|
875
|
+
... state="DONE",
|
|
876
|
+
... local_id="task-001"
|
|
877
|
+
... )
|
|
878
|
+
|
|
879
|
+
Notes:
|
|
880
|
+
- Valid states: TODO, DONE
|
|
881
|
+
- local_id is recommended for persistent task tracking
|
|
882
|
+
- Content should be block nodes (paragraphs, nested lists, etc.)
|
|
883
|
+
"""
|
|
884
|
+
valid_states = {"TODO", "DONE"}
|
|
885
|
+
if state not in valid_states:
|
|
886
|
+
logging.warning(f"Invalid task state '{state}', using 'TODO'. Valid states: {valid_states}")
|
|
887
|
+
state = "TODO"
|
|
888
|
+
|
|
889
|
+
attrs = {"state": state}
|
|
890
|
+
if local_id:
|
|
891
|
+
attrs["localId"] = local_id
|
|
892
|
+
|
|
893
|
+
return AdfNode(type="taskItem", attrs=attrs, content=content)
|
|
894
|
+
|
|
895
|
+
@staticmethod
|
|
896
|
+
def decision_list(items: List[AdfNode], local_id: Optional[str] = None) -> AdfNode:
|
|
897
|
+
"""
|
|
898
|
+
Create a decision list node.
|
|
899
|
+
|
|
900
|
+
Decision lists contain decision items for tracking decisions
|
|
901
|
+
and their states.
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
items: List of decision items
|
|
905
|
+
local_id: Optional local identifier for the decision list
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
DecisionList node
|
|
909
|
+
|
|
910
|
+
Example:
|
|
911
|
+
>>> AdfBuilder.decision_list([
|
|
912
|
+
... AdfBuilder.decision_item("Approve budget", state="DECIDED"),
|
|
913
|
+
... AdfBuilder.decision_item("Choose vendor", state="PENDING")
|
|
914
|
+
... ])
|
|
915
|
+
|
|
916
|
+
Notes:
|
|
917
|
+
- Items should be DecisionItem nodes
|
|
918
|
+
- Used for decision tracking and documentation
|
|
919
|
+
"""
|
|
920
|
+
attrs = {}
|
|
921
|
+
if local_id:
|
|
922
|
+
attrs["localId"] = local_id
|
|
923
|
+
|
|
924
|
+
return AdfNode(type="decisionList", attrs=attrs if attrs else {}, content=items)
|
|
925
|
+
|
|
926
|
+
@staticmethod
|
|
927
|
+
def decision_item(content: List[AdfNode], state: str = "PENDING", local_id: Optional[str] = None) -> AdfNode:
|
|
928
|
+
"""
|
|
929
|
+
Create a decision item node.
|
|
930
|
+
|
|
931
|
+
Decision items represent individual decisions within a decision list.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
content: Decision item content (typically paragraphs with text)
|
|
935
|
+
state: Decision state ("DECIDED" or "PENDING")
|
|
936
|
+
local_id: Optional local identifier for the decision item
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
DecisionItem node
|
|
940
|
+
|
|
941
|
+
Example:
|
|
942
|
+
>>> AdfBuilder.decision_item(
|
|
943
|
+
... [AdfBuilder.paragraph([AdfBuilder.text("Approve new feature")])],
|
|
944
|
+
... state="DECIDED"
|
|
945
|
+
... )
|
|
946
|
+
|
|
947
|
+
Notes:
|
|
948
|
+
- Valid states: DECIDED, PENDING
|
|
949
|
+
- local_id is recommended for tracking
|
|
950
|
+
"""
|
|
951
|
+
valid_states = {"DECIDED", "PENDING"}
|
|
952
|
+
if state not in valid_states:
|
|
953
|
+
logging.warning(f"Invalid decision state '{state}', using 'PENDING'. Valid states: {valid_states}")
|
|
954
|
+
state = "PENDING"
|
|
955
|
+
|
|
956
|
+
attrs = {"state": state}
|
|
957
|
+
if local_id:
|
|
958
|
+
attrs["localId"] = local_id
|
|
959
|
+
|
|
960
|
+
return AdfNode(type="decisionItem", attrs=attrs, content=content)
|
|
961
|
+
|
|
962
|
+
# Media Nodes
|
|
963
|
+
|
|
964
|
+
@staticmethod
|
|
965
|
+
def media_group(media_items: List[AdfNode]) -> AdfNode:
|
|
966
|
+
"""
|
|
967
|
+
Create a media group node.
|
|
968
|
+
|
|
969
|
+
Media groups contain multiple media items displayed together,
|
|
970
|
+
commonly used for image galleries or multiple file attachments.
|
|
971
|
+
|
|
972
|
+
Args:
|
|
973
|
+
media_items: List of media nodes (created with image() or confluence_image())
|
|
974
|
+
|
|
975
|
+
Returns:
|
|
976
|
+
MediaGroup node
|
|
977
|
+
|
|
978
|
+
Example:
|
|
979
|
+
>>> AdfBuilder.media_group([
|
|
980
|
+
... AdfBuilder.image("https://example.com/image1.jpg", "Image 1"),
|
|
981
|
+
... AdfBuilder.image("https://example.com/image2.jpg", "Image 2")
|
|
982
|
+
... ])
|
|
983
|
+
|
|
984
|
+
Notes:
|
|
985
|
+
- Displays media items in a gallery/grid layout
|
|
986
|
+
- Each item should be a media node
|
|
987
|
+
- Useful for showing multiple related images
|
|
988
|
+
"""
|
|
989
|
+
return AdfNode(type="mediaGroup", content=media_items)
|
|
990
|
+
|
|
991
|
+
# Layout Nodes
|
|
992
|
+
|
|
993
|
+
@staticmethod
|
|
994
|
+
def layout_section(columns: List[AdfNode]) -> AdfNode:
|
|
995
|
+
"""
|
|
996
|
+
Create a layout section node.
|
|
997
|
+
|
|
998
|
+
Layout sections create multi-column layouts in Confluence pages,
|
|
999
|
+
allowing content to be organized side-by-side.
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
columns: List of layout column nodes (created with layout_column())
|
|
1003
|
+
|
|
1004
|
+
Returns:
|
|
1005
|
+
LayoutSection node
|
|
1006
|
+
|
|
1007
|
+
Example:
|
|
1008
|
+
>>> AdfBuilder.layout_section([
|
|
1009
|
+
... AdfBuilder.layout_column([
|
|
1010
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Left column content")])
|
|
1011
|
+
... ], width=50),
|
|
1012
|
+
... AdfBuilder.layout_column([
|
|
1013
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Right column content")])
|
|
1014
|
+
... ], width=50)
|
|
1015
|
+
... ])
|
|
1016
|
+
|
|
1017
|
+
Notes:
|
|
1018
|
+
- Sections typically contain 2-3 columns
|
|
1019
|
+
- Column widths should sum to 100 (percentage)
|
|
1020
|
+
- Nested layouts are supported
|
|
1021
|
+
"""
|
|
1022
|
+
return AdfNode(type="layoutSection", content=columns)
|
|
1023
|
+
|
|
1024
|
+
@staticmethod
|
|
1025
|
+
def layout_column(content: List[AdfNode], width: Optional[int] = None) -> AdfNode:
|
|
1026
|
+
"""
|
|
1027
|
+
Create a layout column node.
|
|
1028
|
+
|
|
1029
|
+
Layout columns are containers within a layout section that hold content
|
|
1030
|
+
in a multi-column layout.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
content: Column content (paragraphs, lists, etc.)
|
|
1034
|
+
width: Column width as percentage (e.g., 50 for 50%)
|
|
1035
|
+
|
|
1036
|
+
Returns:
|
|
1037
|
+
LayoutColumn node
|
|
1038
|
+
|
|
1039
|
+
Example:
|
|
1040
|
+
>>> AdfBuilder.layout_column([
|
|
1041
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Column content")]),
|
|
1042
|
+
... AdfBuilder.bullet_list([...])
|
|
1043
|
+
... ], width=33)
|
|
1044
|
+
|
|
1045
|
+
Notes:
|
|
1046
|
+
- Width is optional; if not provided, columns share space equally
|
|
1047
|
+
- Width is specified as a percentage (integer)
|
|
1048
|
+
- Common widths: 33 (1/3), 50 (1/2), 66 (2/3), 100 (full)
|
|
1049
|
+
"""
|
|
1050
|
+
attrs = {}
|
|
1051
|
+
if width is not None:
|
|
1052
|
+
attrs["width"] = width
|
|
1053
|
+
|
|
1054
|
+
return AdfNode(type="layoutColumn", attrs=attrs if attrs else {}, content=content)
|
|
1055
|
+
|
|
1056
|
+
# Extension Nodes (Macros)
|
|
1057
|
+
|
|
1058
|
+
@staticmethod
|
|
1059
|
+
def bodied_extension(
|
|
1060
|
+
extension_type: str,
|
|
1061
|
+
extension_key: str,
|
|
1062
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
1063
|
+
content: Optional[List[AdfNode]] = None
|
|
1064
|
+
) -> AdfNode:
|
|
1065
|
+
"""
|
|
1066
|
+
Create a bodied extension node.
|
|
1067
|
+
|
|
1068
|
+
Bodied extensions are Confluence macros that contain nested content,
|
|
1069
|
+
like expand macros, info panels, or custom macro blocks.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
extension_type: Extension type identifier (e.g., "com.atlassian.confluence.macro.core")
|
|
1073
|
+
extension_key: Extension key (e.g., "toc", "info", "expand")
|
|
1074
|
+
parameters: Optional macro parameters (configuration)
|
|
1075
|
+
content: Optional nested content within the macro
|
|
1076
|
+
|
|
1077
|
+
Returns:
|
|
1078
|
+
BodiedExtension node
|
|
1079
|
+
|
|
1080
|
+
Example:
|
|
1081
|
+
>>> # Table of Contents macro
|
|
1082
|
+
>>> AdfBuilder.bodied_extension(
|
|
1083
|
+
... extension_type="com.atlassian.confluence.macro.core",
|
|
1084
|
+
... extension_key="toc",
|
|
1085
|
+
... parameters={"maxLevel": "3"}
|
|
1086
|
+
... )
|
|
1087
|
+
>>> # Info panel with content
|
|
1088
|
+
>>> AdfBuilder.bodied_extension(
|
|
1089
|
+
... extension_type="com.atlassian.confluence.macro.core",
|
|
1090
|
+
... extension_key="info",
|
|
1091
|
+
... content=[AdfBuilder.paragraph([AdfBuilder.text("Important information")])]
|
|
1092
|
+
... )
|
|
1093
|
+
|
|
1094
|
+
Notes:
|
|
1095
|
+
- Used for block-level macros with optional content
|
|
1096
|
+
- Parameters are macro-specific configuration options
|
|
1097
|
+
- Content can include paragraphs, lists, and other block elements
|
|
1098
|
+
"""
|
|
1099
|
+
attrs = {
|
|
1100
|
+
"extensionType": extension_type,
|
|
1101
|
+
"extensionKey": extension_key
|
|
1102
|
+
}
|
|
1103
|
+
if parameters:
|
|
1104
|
+
attrs["parameters"] = parameters
|
|
1105
|
+
|
|
1106
|
+
return AdfNode(
|
|
1107
|
+
type="bodiedExtension",
|
|
1108
|
+
attrs=attrs,
|
|
1109
|
+
content=content or []
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
@staticmethod
|
|
1113
|
+
def inline_extension(
|
|
1114
|
+
extension_type: str,
|
|
1115
|
+
extension_key: str,
|
|
1116
|
+
parameters: Optional[Dict[str, Any]] = None
|
|
1117
|
+
) -> AdfNode:
|
|
1118
|
+
"""
|
|
1119
|
+
Create an inline extension node.
|
|
1120
|
+
|
|
1121
|
+
Inline extensions are Confluence macros that appear inline with text,
|
|
1122
|
+
like status badges, user macros, or inline code snippets.
|
|
1123
|
+
|
|
1124
|
+
Args:
|
|
1125
|
+
extension_type: Extension type identifier
|
|
1126
|
+
extension_key: Extension key
|
|
1127
|
+
parameters: Optional macro parameters
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
InlineExtension node
|
|
1131
|
+
|
|
1132
|
+
Example:
|
|
1133
|
+
>>> # Status macro
|
|
1134
|
+
>>> AdfBuilder.inline_extension(
|
|
1135
|
+
... extension_type="com.atlassian.confluence.macro.core",
|
|
1136
|
+
... extension_key="status",
|
|
1137
|
+
... parameters={"colour": "Green", "title": "Complete"}
|
|
1138
|
+
... )
|
|
1139
|
+
>>> # Custom inline macro
|
|
1140
|
+
>>> AdfBuilder.inline_extension(
|
|
1141
|
+
... extension_type="com.example.macros",
|
|
1142
|
+
... extension_key="badge",
|
|
1143
|
+
... parameters={"text": "NEW", "color": "red"}
|
|
1144
|
+
... )
|
|
1145
|
+
|
|
1146
|
+
Notes:
|
|
1147
|
+
- Used for inline macros that don't contain nested content
|
|
1148
|
+
- Appears as part of text flow
|
|
1149
|
+
- Parameters configure the macro's appearance and behavior
|
|
1150
|
+
"""
|
|
1151
|
+
attrs = {
|
|
1152
|
+
"extensionType": extension_type,
|
|
1153
|
+
"extensionKey": extension_key
|
|
1154
|
+
}
|
|
1155
|
+
if parameters:
|
|
1156
|
+
attrs["parameters"] = parameters
|
|
1157
|
+
|
|
1158
|
+
return AdfNode(type="inlineExtension", attrs=attrs)
|
|
1159
|
+
|
|
1160
|
+
# Extension Helper Methods
|
|
1161
|
+
|
|
1162
|
+
@staticmethod
|
|
1163
|
+
def toc_macro(max_level: int = 7, min_level: int = 1, include: Optional[str] = None) -> AdfNode:
|
|
1164
|
+
"""
|
|
1165
|
+
Create a Table of Contents macro.
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
max_level: Maximum heading level to include (1-7)
|
|
1169
|
+
min_level: Minimum heading level to include (1-7)
|
|
1170
|
+
include: Optional regex pattern for headings to include
|
|
1171
|
+
|
|
1172
|
+
Returns:
|
|
1173
|
+
BodiedExtension node configured as TOC macro
|
|
1174
|
+
|
|
1175
|
+
Example:
|
|
1176
|
+
>>> # Standard TOC with levels 1-3
|
|
1177
|
+
>>> AdfBuilder.toc_macro(max_level=3)
|
|
1178
|
+
>>> # TOC for specific sections
|
|
1179
|
+
>>> AdfBuilder.toc_macro(max_level=4, include="^\\d+\\.")
|
|
1180
|
+
|
|
1181
|
+
Notes:
|
|
1182
|
+
- Automatically generates a table of contents from page headings
|
|
1183
|
+
- Level 7 includes all headings
|
|
1184
|
+
"""
|
|
1185
|
+
parameters = {
|
|
1186
|
+
"maxLevel": str(max_level),
|
|
1187
|
+
"minLevel": str(min_level)
|
|
1188
|
+
}
|
|
1189
|
+
if include:
|
|
1190
|
+
parameters["include"] = include
|
|
1191
|
+
|
|
1192
|
+
return AdfBuilder.bodied_extension(
|
|
1193
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1194
|
+
extension_key="toc",
|
|
1195
|
+
parameters=parameters
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
@staticmethod
|
|
1199
|
+
def info_panel(content: List[AdfNode], title: Optional[str] = None) -> AdfNode:
|
|
1200
|
+
"""
|
|
1201
|
+
Create an info panel macro.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
content: Panel content
|
|
1205
|
+
title: Optional panel title
|
|
1206
|
+
|
|
1207
|
+
Returns:
|
|
1208
|
+
BodiedExtension node configured as info panel
|
|
1209
|
+
|
|
1210
|
+
Example:
|
|
1211
|
+
>>> AdfBuilder.info_panel([
|
|
1212
|
+
... AdfBuilder.paragraph([AdfBuilder.text("This is important information")])
|
|
1213
|
+
... ], title="Note")
|
|
1214
|
+
|
|
1215
|
+
Notes:
|
|
1216
|
+
- Displays content in a blue info-styled panel
|
|
1217
|
+
- Similar to panel() but uses extension framework
|
|
1218
|
+
"""
|
|
1219
|
+
parameters = {}
|
|
1220
|
+
if title:
|
|
1221
|
+
parameters["title"] = title
|
|
1222
|
+
|
|
1223
|
+
return AdfBuilder.bodied_extension(
|
|
1224
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1225
|
+
extension_key="info",
|
|
1226
|
+
parameters=parameters if parameters else None,
|
|
1227
|
+
content=content
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
@staticmethod
|
|
1231
|
+
def jira_issue(issue_key: str, server_id: Optional[str] = None) -> AdfNode:
|
|
1232
|
+
"""
|
|
1233
|
+
Create a JIRA issue macro.
|
|
1234
|
+
|
|
1235
|
+
Args:
|
|
1236
|
+
issue_key: JIRA issue key (e.g., "PROJ-123")
|
|
1237
|
+
server_id: Optional JIRA server ID for multiple connections
|
|
1238
|
+
|
|
1239
|
+
Returns:
|
|
1240
|
+
BodiedExtension node configured as JIRA issue macro
|
|
1241
|
+
|
|
1242
|
+
Example:
|
|
1243
|
+
>>> AdfBuilder.jira_issue("PROJ-1234")
|
|
1244
|
+
>>> AdfBuilder.jira_issue("TEAM-567", server_id="abc-123")
|
|
1245
|
+
|
|
1246
|
+
Notes:
|
|
1247
|
+
- Embeds a JIRA issue in the Confluence page
|
|
1248
|
+
- Requires JIRA-Confluence integration
|
|
1249
|
+
"""
|
|
1250
|
+
parameters = {"key": issue_key}
|
|
1251
|
+
if server_id:
|
|
1252
|
+
parameters["serverId"] = server_id
|
|
1253
|
+
|
|
1254
|
+
return AdfBuilder.bodied_extension(
|
|
1255
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1256
|
+
extension_key="jira",
|
|
1257
|
+
parameters=parameters
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
@staticmethod
|
|
1261
|
+
def warning_panel(content: List[AdfNode], title: Optional[str] = None, icon: bool = True) -> AdfNode:
|
|
1262
|
+
"""
|
|
1263
|
+
Create a warning panel macro.
|
|
1264
|
+
|
|
1265
|
+
Args:
|
|
1266
|
+
content: Panel content
|
|
1267
|
+
title: Optional panel title
|
|
1268
|
+
icon: Whether to show warning icon (default: True)
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
BodiedExtension node configured as warning panel
|
|
1272
|
+
|
|
1273
|
+
Example:
|
|
1274
|
+
>>> AdfBuilder.warning_panel([
|
|
1275
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Be careful with this operation")])
|
|
1276
|
+
... ], title="Caution")
|
|
1277
|
+
|
|
1278
|
+
Notes:
|
|
1279
|
+
- Displays content in a warning-styled panel
|
|
1280
|
+
- Used for cautionary information
|
|
1281
|
+
"""
|
|
1282
|
+
parameters = {}
|
|
1283
|
+
if title:
|
|
1284
|
+
parameters["title"] = title
|
|
1285
|
+
if not icon:
|
|
1286
|
+
parameters["icon"] = "false"
|
|
1287
|
+
|
|
1288
|
+
return AdfBuilder.bodied_extension(
|
|
1289
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1290
|
+
extension_key="warning",
|
|
1291
|
+
parameters=parameters if parameters else None,
|
|
1292
|
+
content=content
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
@staticmethod
|
|
1296
|
+
def note_panel(content: List[AdfNode], title: Optional[str] = None, icon: bool = True) -> AdfNode:
|
|
1297
|
+
"""
|
|
1298
|
+
Create a note panel macro.
|
|
1299
|
+
|
|
1300
|
+
Args:
|
|
1301
|
+
content: Panel content
|
|
1302
|
+
title: Optional panel title
|
|
1303
|
+
icon: Whether to show note icon (default: True)
|
|
1304
|
+
|
|
1305
|
+
Returns:
|
|
1306
|
+
BodiedExtension node configured as note panel
|
|
1307
|
+
|
|
1308
|
+
Example:
|
|
1309
|
+
>>> AdfBuilder.note_panel([
|
|
1310
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Remember to save your work")])
|
|
1311
|
+
... ])
|
|
1312
|
+
|
|
1313
|
+
Notes:
|
|
1314
|
+
- Displays content in a note-styled panel
|
|
1315
|
+
- Used for additional information
|
|
1316
|
+
"""
|
|
1317
|
+
parameters = {}
|
|
1318
|
+
if title:
|
|
1319
|
+
parameters["title"] = title
|
|
1320
|
+
if not icon:
|
|
1321
|
+
parameters["icon"] = "false"
|
|
1322
|
+
|
|
1323
|
+
return AdfBuilder.bodied_extension(
|
|
1324
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1325
|
+
extension_key="note",
|
|
1326
|
+
parameters=parameters if parameters else None,
|
|
1327
|
+
content=content
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
@staticmethod
|
|
1331
|
+
def excerpt_macro(content: List[AdfNode], hidden: bool = False) -> AdfNode:
|
|
1332
|
+
"""
|
|
1333
|
+
Create an excerpt macro.
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
content: Excerpt content
|
|
1337
|
+
hidden: Whether excerpt is hidden from page content (default: False)
|
|
1338
|
+
|
|
1339
|
+
Returns:
|
|
1340
|
+
BodiedExtension node configured as excerpt
|
|
1341
|
+
|
|
1342
|
+
Example:
|
|
1343
|
+
>>> AdfBuilder.excerpt_macro([
|
|
1344
|
+
... AdfBuilder.paragraph([AdfBuilder.text("This is the page summary")])
|
|
1345
|
+
... ])
|
|
1346
|
+
|
|
1347
|
+
Notes:
|
|
1348
|
+
- Defines a page excerpt for search results and listings
|
|
1349
|
+
- Hidden excerpts don't appear on the page but show in searches
|
|
1350
|
+
"""
|
|
1351
|
+
parameters = {}
|
|
1352
|
+
if hidden:
|
|
1353
|
+
parameters["hidden"] = "true"
|
|
1354
|
+
|
|
1355
|
+
return AdfBuilder.bodied_extension(
|
|
1356
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1357
|
+
extension_key="excerpt",
|
|
1358
|
+
parameters=parameters if parameters else None,
|
|
1359
|
+
content=content
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
@staticmethod
|
|
1363
|
+
def expand_macro(content: List[AdfNode], title: str = "") -> AdfNode:
|
|
1364
|
+
"""
|
|
1365
|
+
Create an expand (collapsible) macro.
|
|
1366
|
+
|
|
1367
|
+
Args:
|
|
1368
|
+
content: Collapsible content
|
|
1369
|
+
title: Title shown in expand header
|
|
1370
|
+
|
|
1371
|
+
Returns:
|
|
1372
|
+
BodiedExtension node configured as expand
|
|
1373
|
+
|
|
1374
|
+
Example:
|
|
1375
|
+
>>> AdfBuilder.expand_macro([
|
|
1376
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Hidden details here")])
|
|
1377
|
+
... ], title="Click to expand")
|
|
1378
|
+
|
|
1379
|
+
Notes:
|
|
1380
|
+
- Creates a collapsible section
|
|
1381
|
+
- Content is hidden until user clicks to expand
|
|
1382
|
+
"""
|
|
1383
|
+
parameters = {}
|
|
1384
|
+
if title:
|
|
1385
|
+
parameters["title"] = title
|
|
1386
|
+
|
|
1387
|
+
return AdfBuilder.bodied_extension(
|
|
1388
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1389
|
+
extension_key="expand",
|
|
1390
|
+
parameters=parameters if parameters else None,
|
|
1391
|
+
content=content
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
@staticmethod
|
|
1395
|
+
def status_macro(status_text: str, color: str = "Grey", subtle: bool = False) -> AdfNode:
|
|
1396
|
+
"""
|
|
1397
|
+
Create a status lozenge macro.
|
|
1398
|
+
|
|
1399
|
+
Args:
|
|
1400
|
+
status_text: Status text to display
|
|
1401
|
+
color: Status color (Green, Yellow, Red, Blue, Grey) - default: Grey
|
|
1402
|
+
subtle: Use subtle style (default: False)
|
|
1403
|
+
|
|
1404
|
+
Returns:
|
|
1405
|
+
InlineExtension node configured as status
|
|
1406
|
+
|
|
1407
|
+
Example:
|
|
1408
|
+
>>> AdfBuilder.status_macro("Complete", color="Green")
|
|
1409
|
+
>>> AdfBuilder.status_macro("In Progress", color="Blue", subtle=True)
|
|
1410
|
+
|
|
1411
|
+
Notes:
|
|
1412
|
+
- Creates an inline status badge
|
|
1413
|
+
- Colors: Green (success), Yellow (warning), Red (error), Blue (info), Grey (neutral)
|
|
1414
|
+
- Subtle style uses lighter colors
|
|
1415
|
+
"""
|
|
1416
|
+
parameters = {
|
|
1417
|
+
"title": status_text,
|
|
1418
|
+
"colour": color # Confluence uses British spelling
|
|
1419
|
+
}
|
|
1420
|
+
if subtle:
|
|
1421
|
+
parameters["subtle"] = "true"
|
|
1422
|
+
|
|
1423
|
+
return AdfBuilder.inline_extension(
|
|
1424
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1425
|
+
extension_key="status",
|
|
1426
|
+
parameters=parameters
|
|
1427
|
+
)
|
|
1428
|
+
|
|
1429
|
+
@staticmethod
|
|
1430
|
+
def code_block_macro(
|
|
1431
|
+
code: str,
|
|
1432
|
+
language: Optional[str] = None,
|
|
1433
|
+
title: Optional[str] = None,
|
|
1434
|
+
linenumbers: bool = False,
|
|
1435
|
+
theme: Optional[str] = None,
|
|
1436
|
+
collapse: bool = False
|
|
1437
|
+
) -> AdfNode:
|
|
1438
|
+
"""
|
|
1439
|
+
Create an enhanced code block macro with additional features.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
code: Code content
|
|
1443
|
+
language: Programming language for syntax highlighting
|
|
1444
|
+
title: Code block title
|
|
1445
|
+
linenumbers: Show line numbers (default: False)
|
|
1446
|
+
theme: Syntax highlighting theme
|
|
1447
|
+
collapse: Make code block collapsible (default: False)
|
|
1448
|
+
|
|
1449
|
+
Returns:
|
|
1450
|
+
BodiedExtension node configured as code block
|
|
1451
|
+
|
|
1452
|
+
Example:
|
|
1453
|
+
>>> AdfBuilder.code_block_macro(
|
|
1454
|
+
... "def hello():\\n print('Hello')",
|
|
1455
|
+
... language="python",
|
|
1456
|
+
... title="main.py",
|
|
1457
|
+
... linenumbers=True
|
|
1458
|
+
... )
|
|
1459
|
+
|
|
1460
|
+
Notes:
|
|
1461
|
+
- Enhanced version of standard code block
|
|
1462
|
+
- Supports titles, line numbers, themes, and collapse
|
|
1463
|
+
- Use standard code_block() for simple code blocks
|
|
1464
|
+
"""
|
|
1465
|
+
parameters = {}
|
|
1466
|
+
if language:
|
|
1467
|
+
parameters["language"] = language
|
|
1468
|
+
if title:
|
|
1469
|
+
parameters["title"] = title
|
|
1470
|
+
if linenumbers:
|
|
1471
|
+
parameters["linenumbers"] = "true"
|
|
1472
|
+
if theme:
|
|
1473
|
+
parameters["theme"] = theme
|
|
1474
|
+
if collapse:
|
|
1475
|
+
parameters["collapse"] = "true"
|
|
1476
|
+
|
|
1477
|
+
# Create code block with plain text content
|
|
1478
|
+
# The code content is stored as a plain text node
|
|
1479
|
+
code_content = [AdfBuilder.paragraph([AdfBuilder.text(code)])]
|
|
1480
|
+
|
|
1481
|
+
return AdfBuilder.bodied_extension(
|
|
1482
|
+
extension_type="com.atlassian.confluence.macro.core",
|
|
1483
|
+
extension_key="code",
|
|
1484
|
+
parameters=parameters if parameters else None,
|
|
1485
|
+
content=code_content
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
# Advanced Table Features
|
|
1489
|
+
|
|
1490
|
+
@staticmethod
|
|
1491
|
+
def advanced_table(
|
|
1492
|
+
rows: List[List[AdfNode]],
|
|
1493
|
+
headers: bool = False,
|
|
1494
|
+
column_widths: Optional[List[int]] = None,
|
|
1495
|
+
layout: str = "default",
|
|
1496
|
+
numbered_column: bool = False
|
|
1497
|
+
) -> AdfNode:
|
|
1498
|
+
"""
|
|
1499
|
+
Create a table node with advanced features.
|
|
1500
|
+
|
|
1501
|
+
Args:
|
|
1502
|
+
rows: Table rows
|
|
1503
|
+
headers: Whether the first row contains headers
|
|
1504
|
+
column_widths: Optional list of column widths in pixels
|
|
1505
|
+
layout: Table layout ("default", "wide", "full-width")
|
|
1506
|
+
numbered_column: Whether to show a numbered column
|
|
1507
|
+
|
|
1508
|
+
Returns:
|
|
1509
|
+
Table node with advanced attributes
|
|
1510
|
+
|
|
1511
|
+
Example:
|
|
1512
|
+
>>> # Table with custom column widths
|
|
1513
|
+
>>> AdfBuilder.advanced_table(
|
|
1514
|
+
... rows=[[cell1, cell2, cell3]],
|
|
1515
|
+
... column_widths=[100, 200, 300],
|
|
1516
|
+
... layout="wide"
|
|
1517
|
+
... )
|
|
1518
|
+
|
|
1519
|
+
Notes:
|
|
1520
|
+
- column_widths are in pixels
|
|
1521
|
+
- layout options affect table display width
|
|
1522
|
+
- numbered_column adds row numbers automatically
|
|
1523
|
+
"""
|
|
1524
|
+
attrs = {
|
|
1525
|
+
"isNumberColumnEnabled": numbered_column,
|
|
1526
|
+
"layout": layout
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if column_widths:
|
|
1530
|
+
attrs["width"] = column_widths
|
|
1531
|
+
|
|
1532
|
+
table_rows = []
|
|
1533
|
+
for row in rows:
|
|
1534
|
+
cells = []
|
|
1535
|
+
for cell_content in row:
|
|
1536
|
+
cells.append(AdfNode(type="tableCell", content=[cell_content]))
|
|
1537
|
+
table_rows.append(AdfNode(type="tableRow", content=cells))
|
|
1538
|
+
|
|
1539
|
+
return AdfNode(type="table", attrs=attrs, content=table_rows)
|
|
1540
|
+
|
|
1541
|
+
@staticmethod
|
|
1542
|
+
def table_cell(
|
|
1543
|
+
content: List[AdfNode],
|
|
1544
|
+
background_color: Optional[str] = None,
|
|
1545
|
+
colspan: int = 1,
|
|
1546
|
+
rowspan: int = 1
|
|
1547
|
+
) -> AdfNode:
|
|
1548
|
+
"""
|
|
1549
|
+
Create a table cell node with advanced attributes.
|
|
1550
|
+
|
|
1551
|
+
Args:
|
|
1552
|
+
content: Cell content
|
|
1553
|
+
background_color: Optional background color (hex code)
|
|
1554
|
+
colspan: Number of columns to span
|
|
1555
|
+
rowspan: Number of rows to span
|
|
1556
|
+
|
|
1557
|
+
Returns:
|
|
1558
|
+
TableCell node
|
|
1559
|
+
|
|
1560
|
+
Example:
|
|
1561
|
+
>>> AdfBuilder.table_cell(
|
|
1562
|
+
... [AdfBuilder.paragraph([AdfBuilder.text("Cell content")])],
|
|
1563
|
+
... background_color="#ffff00",
|
|
1564
|
+
... colspan=2
|
|
1565
|
+
... )
|
|
1566
|
+
|
|
1567
|
+
Notes:
|
|
1568
|
+
- background_color should be hex format (#RRGGBB)
|
|
1569
|
+
- colspan/rowspan > 1 create merged cells
|
|
1570
|
+
"""
|
|
1571
|
+
attrs = {}
|
|
1572
|
+
if background_color:
|
|
1573
|
+
attrs["background"] = background_color
|
|
1574
|
+
if colspan > 1:
|
|
1575
|
+
attrs["colspan"] = colspan
|
|
1576
|
+
if rowspan > 1:
|
|
1577
|
+
attrs["rowspan"] = rowspan
|
|
1578
|
+
|
|
1579
|
+
return AdfNode(type="tableCell", attrs=attrs if attrs else {}, content=content)
|
|
1580
|
+
|
|
1581
|
+
# Additional Mark Helper Methods
|
|
1582
|
+
|
|
1583
|
+
@staticmethod
|
|
1584
|
+
def annotated_text(
|
|
1585
|
+
text: str,
|
|
1586
|
+
annotation_id: str,
|
|
1587
|
+
annotation_type: str = "inlineComment",
|
|
1588
|
+
additional_marks: Optional[List[Dict[str, Any]]] = None
|
|
1589
|
+
) -> AdfNode:
|
|
1590
|
+
"""
|
|
1591
|
+
Create text with an annotation mark (inline comment).
|
|
1592
|
+
|
|
1593
|
+
Args:
|
|
1594
|
+
text: Text content
|
|
1595
|
+
annotation_id: Unique annotation identifier
|
|
1596
|
+
annotation_type: Type of annotation (default: "inlineComment")
|
|
1597
|
+
additional_marks: Additional marks to apply
|
|
1598
|
+
|
|
1599
|
+
Returns:
|
|
1600
|
+
Text node with annotation mark
|
|
1601
|
+
|
|
1602
|
+
Example:
|
|
1603
|
+
>>> AdfBuilder.annotated_text(
|
|
1604
|
+
... "This needs review",
|
|
1605
|
+
... annotation_id="comment-123",
|
|
1606
|
+
... annotation_type="inlineComment"
|
|
1607
|
+
... )
|
|
1608
|
+
|
|
1609
|
+
Notes:
|
|
1610
|
+
- Used for inline comments and annotations
|
|
1611
|
+
- annotation_id must be unique within the document
|
|
1612
|
+
- Requires Confluence permissions to view/edit
|
|
1613
|
+
"""
|
|
1614
|
+
marks = list(additional_marks) if additional_marks else []
|
|
1615
|
+
marks.append({
|
|
1616
|
+
"type": "annotation",
|
|
1617
|
+
"attrs": {
|
|
1618
|
+
"id": annotation_id,
|
|
1619
|
+
"annotationType": annotation_type
|
|
1620
|
+
}
|
|
1621
|
+
})
|
|
1622
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
1623
|
+
|
|
1624
|
+
@staticmethod
|
|
1625
|
+
def bordered_text(
|
|
1626
|
+
text: str,
|
|
1627
|
+
border_color: Optional[str] = None,
|
|
1628
|
+
border_size: int = 1,
|
|
1629
|
+
additional_marks: Optional[List[Dict[str, Any]]] = None
|
|
1630
|
+
) -> AdfNode:
|
|
1631
|
+
"""
|
|
1632
|
+
Create text with a border mark.
|
|
1633
|
+
|
|
1634
|
+
Args:
|
|
1635
|
+
text: Text content
|
|
1636
|
+
border_color: Border color (hex code)
|
|
1637
|
+
border_size: Border width in pixels
|
|
1638
|
+
additional_marks: Additional marks to apply
|
|
1639
|
+
|
|
1640
|
+
Returns:
|
|
1641
|
+
Text node with border mark
|
|
1642
|
+
|
|
1643
|
+
Example:
|
|
1644
|
+
>>> AdfBuilder.bordered_text("Boxed text", "#000000", 2)
|
|
1645
|
+
|
|
1646
|
+
Notes:
|
|
1647
|
+
- Creates bordered/boxed text effect
|
|
1648
|
+
- border_color should be hex format
|
|
1649
|
+
"""
|
|
1650
|
+
attrs = {"size": border_size}
|
|
1651
|
+
if border_color:
|
|
1652
|
+
attrs["color"] = border_color
|
|
1653
|
+
|
|
1654
|
+
marks = list(additional_marks) if additional_marks else []
|
|
1655
|
+
marks.append({"type": "border", "attrs": attrs})
|
|
1656
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
1657
|
+
|
|
1658
|
+
@staticmethod
|
|
1659
|
+
def breakout_text(
|
|
1660
|
+
text: str,
|
|
1661
|
+
mode: str = "wide",
|
|
1662
|
+
additional_marks: Optional[List[Dict[str, Any]]] = None
|
|
1663
|
+
) -> AdfNode:
|
|
1664
|
+
"""
|
|
1665
|
+
Create text with a breakout mark.
|
|
1666
|
+
|
|
1667
|
+
Args:
|
|
1668
|
+
text: Text content
|
|
1669
|
+
mode: Breakout mode ("wide" or "full-width")
|
|
1670
|
+
additional_marks: Additional marks to apply
|
|
1671
|
+
|
|
1672
|
+
Returns:
|
|
1673
|
+
Text node with breakout mark
|
|
1674
|
+
|
|
1675
|
+
Example:
|
|
1676
|
+
>>> AdfBuilder.breakout_text("Full width content", "full-width")
|
|
1677
|
+
|
|
1678
|
+
Notes:
|
|
1679
|
+
- Makes content break out of normal page width
|
|
1680
|
+
- Valid modes: "wide", "full-width"
|
|
1681
|
+
- Affects layout rendering
|
|
1682
|
+
"""
|
|
1683
|
+
valid_modes = {"wide", "full-width"}
|
|
1684
|
+
if mode not in valid_modes:
|
|
1685
|
+
logging.warning(f"Invalid breakout mode '{mode}', using 'wide'. Valid: {valid_modes}")
|
|
1686
|
+
mode = "wide"
|
|
1687
|
+
|
|
1688
|
+
marks = list(additional_marks) if additional_marks else []
|
|
1689
|
+
marks.append({"type": "breakout", "attrs": {"mode": mode}})
|
|
1690
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
1691
|
+
|
|
1692
|
+
@staticmethod
|
|
1693
|
+
def fragment_text(
|
|
1694
|
+
text: str,
|
|
1695
|
+
local_id: str,
|
|
1696
|
+
name: Optional[str] = None,
|
|
1697
|
+
additional_marks: Optional[List[Dict[str, Any]]] = None
|
|
1698
|
+
) -> AdfNode:
|
|
1699
|
+
"""
|
|
1700
|
+
Create text with a fragment mark.
|
|
1701
|
+
|
|
1702
|
+
Args:
|
|
1703
|
+
text: Text content
|
|
1704
|
+
local_id: Fragment local identifier
|
|
1705
|
+
name: Optional fragment name
|
|
1706
|
+
additional_marks: Additional marks to apply
|
|
1707
|
+
|
|
1708
|
+
Returns:
|
|
1709
|
+
Text node with fragment mark
|
|
1710
|
+
|
|
1711
|
+
Example:
|
|
1712
|
+
>>> AdfBuilder.fragment_text(
|
|
1713
|
+
... "Fragment content",
|
|
1714
|
+
... local_id="fragment-1",
|
|
1715
|
+
... name="Section A"
|
|
1716
|
+
... )
|
|
1717
|
+
|
|
1718
|
+
Notes:
|
|
1719
|
+
- Used for fragment references and linking
|
|
1720
|
+
- local_id must be unique within document
|
|
1721
|
+
"""
|
|
1722
|
+
attrs = {"localId": local_id}
|
|
1723
|
+
if name:
|
|
1724
|
+
attrs["name"] = name
|
|
1725
|
+
|
|
1726
|
+
marks = list(additional_marks) if additional_marks else []
|
|
1727
|
+
marks.append({"type": "fragment", "attrs": attrs})
|
|
1728
|
+
return AdfNode(type="text", text=text, marks=marks)
|
|
1729
|
+
|
|
1730
|
+
# Advanced Media Nodes
|
|
1731
|
+
|
|
1732
|
+
@staticmethod
|
|
1733
|
+
def media_single(
|
|
1734
|
+
media: AdfNode,
|
|
1735
|
+
layout: str = "center",
|
|
1736
|
+
width: Optional[int] = None,
|
|
1737
|
+
width_type: str = "pixel"
|
|
1738
|
+
) -> AdfNode:
|
|
1739
|
+
"""
|
|
1740
|
+
Create a media single node with advanced layout options.
|
|
1741
|
+
|
|
1742
|
+
MediaSingle wraps a media node to control its display layout and sizing.
|
|
1743
|
+
|
|
1744
|
+
Args:
|
|
1745
|
+
media: Media node (from image() or confluence_image())
|
|
1746
|
+
layout: Layout mode ("center", "wrap-left", "wrap-right", "wide", "full-width")
|
|
1747
|
+
width: Optional width value
|
|
1748
|
+
width_type: Width type ("pixel" or "percentage")
|
|
1749
|
+
|
|
1750
|
+
Returns:
|
|
1751
|
+
MediaSingle node
|
|
1752
|
+
|
|
1753
|
+
Example:
|
|
1754
|
+
>>> img = AdfBuilder.image("https://example.com/img.jpg", "Photo")
|
|
1755
|
+
>>> AdfBuilder.media_single(img, layout="wide", width=800)
|
|
1756
|
+
|
|
1757
|
+
Notes:
|
|
1758
|
+
- Provides fine-grained control over media display
|
|
1759
|
+
- Layout options affect how text wraps around media
|
|
1760
|
+
- width_type determines how width value is interpreted
|
|
1761
|
+
"""
|
|
1762
|
+
attrs = {"layout": layout}
|
|
1763
|
+
if width is not None:
|
|
1764
|
+
attrs["width"] = width
|
|
1765
|
+
attrs["widthType"] = width_type
|
|
1766
|
+
|
|
1767
|
+
return AdfNode(type="mediaSingle", attrs=attrs, content=[media])
|
|
1768
|
+
|
|
1769
|
+
@staticmethod
|
|
1770
|
+
def media_inline(
|
|
1771
|
+
media_id: str,
|
|
1772
|
+
collection: str = "contentId",
|
|
1773
|
+
media_type: str = "file",
|
|
1774
|
+
alt: Optional[str] = None
|
|
1775
|
+
) -> AdfNode:
|
|
1776
|
+
"""
|
|
1777
|
+
Create an inline media node.
|
|
1778
|
+
|
|
1779
|
+
MediaInline displays media inline with text, similar to an inline image.
|
|
1780
|
+
|
|
1781
|
+
Args:
|
|
1782
|
+
media_id: Media file ID
|
|
1783
|
+
collection: Collection identifier (default: "contentId")
|
|
1784
|
+
media_type: Type of media ("file", "link", "external")
|
|
1785
|
+
alt: Alternative text
|
|
1786
|
+
|
|
1787
|
+
Returns:
|
|
1788
|
+
MediaInline node
|
|
1789
|
+
|
|
1790
|
+
Example:
|
|
1791
|
+
>>> AdfBuilder.media_inline("file-123", alt="Icon")
|
|
1792
|
+
|
|
1793
|
+
Notes:
|
|
1794
|
+
- Displays inline with text flow
|
|
1795
|
+
- Typically used for icons or small images
|
|
1796
|
+
- Requires Confluence media ID
|
|
1797
|
+
"""
|
|
1798
|
+
attrs = {
|
|
1799
|
+
"id": media_id,
|
|
1800
|
+
"type": media_type,
|
|
1801
|
+
"collection": collection
|
|
1802
|
+
}
|
|
1803
|
+
if alt:
|
|
1804
|
+
attrs["alt"] = alt
|
|
1805
|
+
|
|
1806
|
+
return AdfNode(type="mediaInline", attrs=attrs)
|
|
1807
|
+
|
|
1808
|
+
@staticmethod
|
|
1809
|
+
def caption(content: List[AdfNode]) -> AdfNode:
|
|
1810
|
+
"""
|
|
1811
|
+
Create a caption node for media.
|
|
1812
|
+
|
|
1813
|
+
Captions provide descriptive text for media elements.
|
|
1814
|
+
|
|
1815
|
+
Args:
|
|
1816
|
+
content: Caption content (typically text)
|
|
1817
|
+
|
|
1818
|
+
Returns:
|
|
1819
|
+
Caption node
|
|
1820
|
+
|
|
1821
|
+
Example:
|
|
1822
|
+
>>> AdfBuilder.caption([
|
|
1823
|
+
... AdfBuilder.text("Figure 1: System Architecture")
|
|
1824
|
+
... ])
|
|
1825
|
+
|
|
1826
|
+
Notes:
|
|
1827
|
+
- Used within media nodes to add captions
|
|
1828
|
+
- Appears below the media element
|
|
1829
|
+
"""
|
|
1830
|
+
return AdfNode(type="caption", content=content)
|
|
1831
|
+
|
|
1832
|
+
@staticmethod
|
|
1833
|
+
def rich_media(
|
|
1834
|
+
url: str,
|
|
1835
|
+
layout: str = "center",
|
|
1836
|
+
width: Optional[int] = None,
|
|
1837
|
+
height: Optional[int] = None
|
|
1838
|
+
) -> AdfNode:
|
|
1839
|
+
"""
|
|
1840
|
+
Create a rich media node for embedded content.
|
|
1841
|
+
|
|
1842
|
+
RichMedia embeds external media like videos, interactive content, etc.
|
|
1843
|
+
|
|
1844
|
+
Args:
|
|
1845
|
+
url: URL of the rich media content
|
|
1846
|
+
layout: Layout mode ("center", "wide", "full-width")
|
|
1847
|
+
width: Optional width in pixels
|
|
1848
|
+
height: Optional height in pixels
|
|
1849
|
+
|
|
1850
|
+
Returns:
|
|
1851
|
+
RichMedia node
|
|
1852
|
+
|
|
1853
|
+
Example:
|
|
1854
|
+
>>> AdfBuilder.rich_media(
|
|
1855
|
+
... "https://youtube.com/embed/video123",
|
|
1856
|
+
... layout="wide",
|
|
1857
|
+
... width=800,
|
|
1858
|
+
... height=450
|
|
1859
|
+
... )
|
|
1860
|
+
|
|
1861
|
+
Notes:
|
|
1862
|
+
- Used for video embeds, interactive content
|
|
1863
|
+
- URL should point to embeddable content
|
|
1864
|
+
- Layout affects display width
|
|
1865
|
+
"""
|
|
1866
|
+
attrs = {"url": url, "layout": layout}
|
|
1867
|
+
if width is not None:
|
|
1868
|
+
attrs["width"] = width
|
|
1869
|
+
if height is not None:
|
|
1870
|
+
attrs["height"] = height
|
|
1871
|
+
|
|
1872
|
+
return AdfNode(type="richMedia", attrs=attrs)
|
|
1873
|
+
|
|
1874
|
+
@staticmethod
|
|
1875
|
+
def embed_card(url: str, layout: str = "center") -> AdfNode:
|
|
1876
|
+
"""
|
|
1877
|
+
Create an embed card node.
|
|
1878
|
+
|
|
1879
|
+
EmbedCard embeds external content as a card with preview.
|
|
1880
|
+
|
|
1881
|
+
Args:
|
|
1882
|
+
url: URL to embed
|
|
1883
|
+
layout: Layout mode ("center", "wide", "full-width")
|
|
1884
|
+
|
|
1885
|
+
Returns:
|
|
1886
|
+
EmbedCard node
|
|
1887
|
+
|
|
1888
|
+
Example:
|
|
1889
|
+
>>> AdfBuilder.embed_card(
|
|
1890
|
+
... "https://example.com/content",
|
|
1891
|
+
... layout="wide"
|
|
1892
|
+
... )
|
|
1893
|
+
|
|
1894
|
+
Notes:
|
|
1895
|
+
- Creates a card with URL preview
|
|
1896
|
+
- Shows title, description, thumbnail from URL
|
|
1897
|
+
- Requires embeddable URL
|
|
1898
|
+
"""
|
|
1899
|
+
attrs = {"url": url, "layout": layout}
|
|
1900
|
+
return AdfNode(type="embedCard", attrs=attrs)
|
|
1901
|
+
|
|
1902
|
+
# Advanced Content Nodes
|
|
1903
|
+
|
|
1904
|
+
@staticmethod
|
|
1905
|
+
def nested_expand(title: str, content: List[AdfNode]) -> AdfNode:
|
|
1906
|
+
"""
|
|
1907
|
+
Create a nested expand node.
|
|
1908
|
+
|
|
1909
|
+
NestedExpand is similar to expand but can be nested within other expands.
|
|
1910
|
+
|
|
1911
|
+
Args:
|
|
1912
|
+
title: Expand title
|
|
1913
|
+
content: Nested content
|
|
1914
|
+
|
|
1915
|
+
Returns:
|
|
1916
|
+
NestedExpand node
|
|
1917
|
+
|
|
1918
|
+
Example:
|
|
1919
|
+
>>> AdfBuilder.nested_expand(
|
|
1920
|
+
... "Details",
|
|
1921
|
+
... [AdfBuilder.paragraph([AdfBuilder.text("Nested content")])]
|
|
1922
|
+
... )
|
|
1923
|
+
|
|
1924
|
+
Notes:
|
|
1925
|
+
- Used for nested collapsible sections
|
|
1926
|
+
- Can be placed inside expand nodes
|
|
1927
|
+
- Supports multiple nesting levels
|
|
1928
|
+
"""
|
|
1929
|
+
attrs = {"title": title}
|
|
1930
|
+
return AdfNode(type="nestedExpand", attrs=attrs, content=content)
|
|
1931
|
+
|
|
1932
|
+
@staticmethod
|
|
1933
|
+
def sync_block(local_id: str, resource_id: Optional[str] = None, content: Optional[List[AdfNode]] = None) -> AdfNode:
|
|
1934
|
+
"""
|
|
1935
|
+
Create a sync block node.
|
|
1936
|
+
|
|
1937
|
+
SyncBlock represents synchronized content that can be shared across pages.
|
|
1938
|
+
|
|
1939
|
+
Args:
|
|
1940
|
+
local_id: Local identifier for the sync block
|
|
1941
|
+
resource_id: Optional resource identifier for synchronization
|
|
1942
|
+
content: Block content
|
|
1943
|
+
|
|
1944
|
+
Returns:
|
|
1945
|
+
SyncBlock node
|
|
1946
|
+
|
|
1947
|
+
Example:
|
|
1948
|
+
>>> AdfBuilder.sync_block(
|
|
1949
|
+
... local_id="sync-1",
|
|
1950
|
+
... resource_id="resource-abc",
|
|
1951
|
+
... content=[AdfBuilder.paragraph([AdfBuilder.text("Synced content")])]
|
|
1952
|
+
... )
|
|
1953
|
+
|
|
1954
|
+
Notes:
|
|
1955
|
+
- Used for content that syncs across multiple pages
|
|
1956
|
+
- Requires Confluence synchronization setup
|
|
1957
|
+
- Changes propagate to all instances
|
|
1958
|
+
"""
|
|
1959
|
+
attrs = {"localId": local_id}
|
|
1960
|
+
if resource_id:
|
|
1961
|
+
attrs["resourceId"] = resource_id
|
|
1962
|
+
|
|
1963
|
+
return AdfNode(type="syncBlock", attrs=attrs, content=content or [])
|
|
1964
|
+
|
|
1965
|
+
@staticmethod
|
|
1966
|
+
def extension_frame(extension: AdfNode) -> AdfNode:
|
|
1967
|
+
"""
|
|
1968
|
+
Create an extension frame node.
|
|
1969
|
+
|
|
1970
|
+
ExtensionFrame wraps extension nodes to provide additional context.
|
|
1971
|
+
|
|
1972
|
+
Args:
|
|
1973
|
+
extension: Extension node to wrap
|
|
1974
|
+
|
|
1975
|
+
Returns:
|
|
1976
|
+
ExtensionFrame node
|
|
1977
|
+
|
|
1978
|
+
Example:
|
|
1979
|
+
>>> ext = AdfBuilder.bodied_extension(
|
|
1980
|
+
... "com.example.macros", "custom", {}
|
|
1981
|
+
... )
|
|
1982
|
+
>>> AdfBuilder.extension_frame(ext)
|
|
1983
|
+
|
|
1984
|
+
Notes:
|
|
1985
|
+
- Provides frame/wrapper for extensions
|
|
1986
|
+
- Used for extension isolation
|
|
1987
|
+
- Rarely needed directly (automatic in some cases)
|
|
1988
|
+
"""
|
|
1989
|
+
return AdfNode(type="extensionFrame", content=[extension])
|
|
1990
|
+
|
|
1991
|
+
@staticmethod
|
|
1992
|
+
def multi_bodied_extension(
|
|
1993
|
+
extension_type: str,
|
|
1994
|
+
extension_key: str,
|
|
1995
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
1996
|
+
sections: Optional[List[Dict[str, Any]]] = None
|
|
1997
|
+
) -> AdfNode:
|
|
1998
|
+
"""
|
|
1999
|
+
Create a multi-bodied extension node.
|
|
2000
|
+
|
|
2001
|
+
MultiBodiedExtension supports macros with multiple content sections.
|
|
2002
|
+
|
|
2003
|
+
Args:
|
|
2004
|
+
extension_type: Extension type identifier
|
|
2005
|
+
extension_key: Extension key
|
|
2006
|
+
parameters: Macro parameters
|
|
2007
|
+
sections: List of content sections (each with content)
|
|
2008
|
+
|
|
2009
|
+
Returns:
|
|
2010
|
+
MultiBodiedExtension node
|
|
2011
|
+
|
|
2012
|
+
Example:
|
|
2013
|
+
>>> AdfBuilder.multi_bodied_extension(
|
|
2014
|
+
... extension_type="com.atlassian.confluence.macro.core",
|
|
2015
|
+
... extension_key="multi-section",
|
|
2016
|
+
... parameters={"param1": "value1"},
|
|
2017
|
+
... sections=[
|
|
2018
|
+
... {"content": [AdfBuilder.paragraph([AdfBuilder.text("Section 1")])]},
|
|
2019
|
+
... {"content": [AdfBuilder.paragraph([AdfBuilder.text("Section 2")])]}
|
|
2020
|
+
... ]
|
|
2021
|
+
... )
|
|
2022
|
+
|
|
2023
|
+
Notes:
|
|
2024
|
+
- Used for complex macros with multiple sections
|
|
2025
|
+
- Each section can have different content
|
|
2026
|
+
- Less common than bodied_extension
|
|
2027
|
+
"""
|
|
2028
|
+
attrs = {
|
|
2029
|
+
"extensionType": extension_type,
|
|
2030
|
+
"extensionKey": extension_key
|
|
2031
|
+
}
|
|
2032
|
+
if parameters:
|
|
2033
|
+
attrs["parameters"] = parameters
|
|
2034
|
+
|
|
2035
|
+
# Convert sections to ADF nodes if provided
|
|
2036
|
+
content = []
|
|
2037
|
+
if sections:
|
|
2038
|
+
for section in sections:
|
|
2039
|
+
if "content" in section:
|
|
2040
|
+
# Wrap section content in a structure
|
|
2041
|
+
section_nodes = section["content"] if isinstance(section["content"], list) else [section["content"]]
|
|
2042
|
+
content.extend(section_nodes)
|
|
2043
|
+
|
|
2044
|
+
return AdfNode(type="multiBodiedExtension", attrs=attrs, content=content)
|
|
2045
|
+
|
|
2046
|
+
@staticmethod
|
|
2047
|
+
def block_task_item(content: List[AdfNode], state: str = "TODO", local_id: Optional[str] = None) -> AdfNode:
|
|
2048
|
+
"""
|
|
2049
|
+
Create a block-level task item node.
|
|
2050
|
+
|
|
2051
|
+
BlockTaskItem is similar to taskItem but used for block-level tasks
|
|
2052
|
+
that can contain more complex content.
|
|
2053
|
+
|
|
2054
|
+
Args:
|
|
2055
|
+
content: Task content (can include multiple blocks)
|
|
2056
|
+
state: Task state ("TODO" or "DONE")
|
|
2057
|
+
local_id: Optional local identifier
|
|
2058
|
+
|
|
2059
|
+
Returns:
|
|
2060
|
+
BlockTaskItem node
|
|
2061
|
+
|
|
2062
|
+
Example:
|
|
2063
|
+
>>> AdfBuilder.block_task_item(
|
|
2064
|
+
... [
|
|
2065
|
+
... AdfBuilder.paragraph([AdfBuilder.text("Complex task")]),
|
|
2066
|
+
... AdfBuilder.bullet_list([...])
|
|
2067
|
+
... ],
|
|
2068
|
+
... state="TODO"
|
|
2069
|
+
... )
|
|
2070
|
+
|
|
2071
|
+
Notes:
|
|
2072
|
+
- Supports richer content than regular taskItem
|
|
2073
|
+
- Can contain paragraphs, lists, and other blocks
|
|
2074
|
+
- Used in action items and complex task tracking
|
|
2075
|
+
"""
|
|
2076
|
+
valid_states = {"TODO", "DONE"}
|
|
2077
|
+
if state not in valid_states:
|
|
2078
|
+
logging.warning(f"Invalid task state '{state}', using 'TODO'. Valid states: {valid_states}")
|
|
2079
|
+
state = "TODO"
|
|
2080
|
+
|
|
2081
|
+
attrs = {"state": state}
|
|
2082
|
+
if local_id:
|
|
2083
|
+
attrs["localId"] = local_id
|
|
2084
|
+
|
|
2085
|
+
return AdfNode(type="blockTaskItem", attrs=attrs, content=content)
|