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.
Files changed (65) hide show
  1. docspan/__init__.py +3 -0
  2. docspan/__main__.py +0 -0
  3. docspan/backends/__init__.py +19 -0
  4. docspan/backends/base.py +85 -0
  5. docspan/backends/confluence/__init__.py +0 -0
  6. docspan/backends/confluence/adf/__init__.py +14 -0
  7. docspan/backends/confluence/adf/comparator.py +427 -0
  8. docspan/backends/confluence/adf/converter.py +119 -0
  9. docspan/backends/confluence/adf/converters.py +1449 -0
  10. docspan/backends/confluence/adf/interfaces.py +191 -0
  11. docspan/backends/confluence/adf/nodes.py +2085 -0
  12. docspan/backends/confluence/adf/parser.py +400 -0
  13. docspan/backends/confluence/adf/validators.py +161 -0
  14. docspan/backends/confluence/adf/visitors.py +495 -0
  15. docspan/backends/confluence/backend.py +227 -0
  16. docspan/backends/confluence/client.py +44 -0
  17. docspan/backends/confluence/config/__init__.py +21 -0
  18. docspan/backends/confluence/config/loader.py +107 -0
  19. docspan/backends/confluence/config/models.py +167 -0
  20. docspan/backends/confluence/config/validation.py +297 -0
  21. docspan/backends/confluence/markdown/__init__.py +22 -0
  22. docspan/backends/confluence/markdown/ast.py +819 -0
  23. docspan/backends/confluence/markdown/extensions/__init__.py +5 -0
  24. docspan/backends/confluence/markdown/extensions/frontmatter.py +80 -0
  25. docspan/backends/confluence/markdown/extensions/mermaid.py +64 -0
  26. docspan/backends/confluence/markdown/extensions/wikilinks.py +179 -0
  27. docspan/backends/confluence/markdown/inline_parser.py +495 -0
  28. docspan/backends/confluence/markdown/parser.py +1006 -0
  29. docspan/backends/confluence/models/__init__.py +18 -0
  30. docspan/backends/confluence/models/markdown_file.py +402 -0
  31. docspan/backends/confluence/models/page.py +212 -0
  32. docspan/backends/confluence/models/path_utils.py +34 -0
  33. docspan/backends/confluence/models/results.py +28 -0
  34. docspan/backends/confluence/models/sync_status.py +382 -0
  35. docspan/backends/confluence/services/__init__.py +0 -0
  36. docspan/backends/confluence/services/confluence/__init__.py +40 -0
  37. docspan/backends/confluence/services/confluence/attachment_client.py +147 -0
  38. docspan/backends/confluence/services/confluence/base_client.py +420 -0
  39. docspan/backends/confluence/services/confluence/client.py +376 -0
  40. docspan/backends/confluence/services/confluence/comment_client.py +682 -0
  41. docspan/backends/confluence/services/confluence/crawler.py +587 -0
  42. docspan/backends/confluence/services/confluence/label_client.py +130 -0
  43. docspan/backends/confluence/services/confluence/page_client.py +1288 -0
  44. docspan/backends/confluence/services/confluence/space_client.py +179 -0
  45. docspan/backends/confluence/services/confluence/url_parser.py +106 -0
  46. docspan/backends/google_docs/__init__.py +0 -0
  47. docspan/backends/google_docs/auth.py +143 -0
  48. docspan/backends/google_docs/backend.py +140 -0
  49. docspan/backends/google_docs/client.py +665 -0
  50. docspan/backends/google_docs/converter.py +471 -0
  51. docspan/backends/google_docs/docs_request_builder.py +232 -0
  52. docspan/backends/google_docs/docs_structure_parser.py +120 -0
  53. docspan/backends/google_docs/markdown_to_paragraph_parser.py +145 -0
  54. docspan/cli/__init__.py +0 -0
  55. docspan/cli/main.py +408 -0
  56. docspan/config.py +62 -0
  57. docspan/core/__init__.py +49 -0
  58. docspan/core/merge.py +30 -0
  59. docspan/core/orchestrator.py +332 -0
  60. docspan/core/paths.py +8 -0
  61. docspan/core/state.py +53 -0
  62. docspan-0.1.0.dist-info/METADATA +273 -0
  63. docspan-0.1.0.dist-info/RECORD +65 -0
  64. docspan-0.1.0.dist-info/WHEEL +4 -0
  65. 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)