notionary 0.2.19__py3-none-any.whl → 0.2.22__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 (220) hide show
  1. notionary/__init__.py +8 -4
  2. notionary/base_notion_client.py +3 -1
  3. notionary/blocks/__init__.py +2 -91
  4. notionary/blocks/_bootstrap.py +271 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +69 -106
  7. notionary/blocks/audio/audio_markdown_node.py +13 -5
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +42 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +49 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
  13. notionary/blocks/bookmark/bookmark_models.py +15 -0
  14. notionary/blocks/breadcrumbs/__init__.py +17 -0
  15. notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
  16. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
  17. notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
  18. notionary/blocks/bulleted_list/__init__.py +12 -2
  19. notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
  20. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
  21. notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
  22. notionary/blocks/callout/__init__.py +9 -2
  23. notionary/blocks/callout/callout_element.py +53 -86
  24. notionary/blocks/callout/callout_markdown_node.py +3 -1
  25. notionary/blocks/callout/callout_models.py +33 -0
  26. notionary/blocks/child_database/__init__.py +14 -0
  27. notionary/blocks/child_database/child_database_element.py +61 -0
  28. notionary/blocks/child_database/child_database_models.py +12 -0
  29. notionary/blocks/child_page/__init__.py +9 -0
  30. notionary/blocks/child_page/child_page_element.py +94 -0
  31. notionary/blocks/child_page/child_page_models.py +12 -0
  32. notionary/blocks/{shared/block_client.py → client.py} +54 -54
  33. notionary/blocks/code/__init__.py +6 -2
  34. notionary/blocks/code/code_element.py +96 -181
  35. notionary/blocks/code/code_markdown_node.py +64 -13
  36. notionary/blocks/code/code_models.py +94 -0
  37. notionary/blocks/column/__init__.py +25 -1
  38. notionary/blocks/column/column_element.py +44 -312
  39. notionary/blocks/column/column_list_element.py +52 -0
  40. notionary/blocks/column/column_list_markdown_node.py +50 -0
  41. notionary/blocks/column/column_markdown_node.py +59 -0
  42. notionary/blocks/column/column_models.py +26 -0
  43. notionary/blocks/divider/__init__.py +9 -2
  44. notionary/blocks/divider/divider_element.py +18 -49
  45. notionary/blocks/divider/divider_markdown_node.py +2 -1
  46. notionary/blocks/divider/divider_models.py +12 -0
  47. notionary/blocks/embed/__init__.py +9 -2
  48. notionary/blocks/embed/embed_element.py +65 -111
  49. notionary/blocks/embed/embed_markdown_node.py +3 -1
  50. notionary/blocks/embed/embed_models.py +14 -0
  51. notionary/blocks/equation/__init__.py +14 -0
  52. notionary/blocks/equation/equation_element.py +133 -0
  53. notionary/blocks/equation/equation_element_markdown_node.py +35 -0
  54. notionary/blocks/equation/equation_models.py +11 -0
  55. notionary/blocks/file/__init__.py +25 -0
  56. notionary/blocks/file/file_element.py +112 -0
  57. notionary/blocks/file/file_element_markdown_node.py +37 -0
  58. notionary/blocks/file/file_element_models.py +39 -0
  59. notionary/blocks/guards.py +22 -0
  60. notionary/blocks/heading/__init__.py +16 -2
  61. notionary/blocks/heading/heading_element.py +83 -69
  62. notionary/blocks/heading/heading_markdown_node.py +2 -1
  63. notionary/blocks/heading/heading_models.py +29 -0
  64. notionary/blocks/image_block/__init__.py +13 -0
  65. notionary/blocks/image_block/image_element.py +89 -0
  66. notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
  67. notionary/blocks/image_block/image_models.py +10 -0
  68. notionary/blocks/mixins/captions/__init__.py +4 -0
  69. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  70. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  71. notionary/blocks/models.py +174 -0
  72. notionary/blocks/numbered_list/__init__.py +12 -2
  73. notionary/blocks/numbered_list/numbered_list_element.py +48 -56
  74. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  75. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  76. notionary/blocks/paragraph/__init__.py +12 -2
  77. notionary/blocks/paragraph/paragraph_element.py +40 -66
  78. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  79. notionary/blocks/paragraph/paragraph_models.py +16 -0
  80. notionary/blocks/pdf/__init__.py +13 -0
  81. notionary/blocks/pdf/pdf_element.py +97 -0
  82. notionary/blocks/pdf/pdf_markdown_node.py +37 -0
  83. notionary/blocks/pdf/pdf_models.py +11 -0
  84. notionary/blocks/quote/__init__.py +11 -2
  85. notionary/blocks/quote/quote_element.py +45 -62
  86. notionary/blocks/quote/quote_markdown_node.py +6 -3
  87. notionary/blocks/quote/quote_models.py +18 -0
  88. notionary/blocks/registry/__init__.py +4 -0
  89. notionary/blocks/registry/block_registry.py +60 -121
  90. notionary/blocks/registry/block_registry_builder.py +115 -59
  91. notionary/blocks/rich_text/__init__.py +33 -0
  92. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  93. notionary/blocks/rich_text/rich_text_models.py +221 -0
  94. notionary/blocks/rich_text/text_inline_formatter.py +456 -0
  95. notionary/blocks/syntax_prompt_builder.py +137 -0
  96. notionary/blocks/table/__init__.py +16 -2
  97. notionary/blocks/table/table_element.py +136 -228
  98. notionary/blocks/table/table_markdown_node.py +2 -1
  99. notionary/blocks/table/table_models.py +28 -0
  100. notionary/blocks/table_of_contents/__init__.py +19 -0
  101. notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
  102. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  103. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  104. notionary/blocks/todo/__init__.py +9 -2
  105. notionary/blocks/todo/todo_element.py +52 -92
  106. notionary/blocks/todo/todo_markdown_node.py +2 -1
  107. notionary/blocks/todo/todo_models.py +19 -0
  108. notionary/blocks/toggle/__init__.py +13 -3
  109. notionary/blocks/toggle/toggle_element.py +69 -260
  110. notionary/blocks/toggle/toggle_markdown_node.py +25 -15
  111. notionary/blocks/toggle/toggle_models.py +17 -0
  112. notionary/blocks/toggleable_heading/__init__.py +6 -2
  113. notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
  114. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  115. notionary/blocks/types.py +130 -0
  116. notionary/blocks/video/__init__.py +8 -2
  117. notionary/blocks/video/video_element.py +70 -141
  118. notionary/blocks/video/video_element_models.py +10 -0
  119. notionary/blocks/video/video_markdown_node.py +13 -6
  120. notionary/database/client.py +26 -8
  121. notionary/database/database.py +13 -14
  122. notionary/database/database_filter_builder.py +2 -2
  123. notionary/database/database_provider.py +5 -4
  124. notionary/database/models.py +337 -0
  125. notionary/database/notion_database.py +6 -7
  126. notionary/file_upload/client.py +5 -7
  127. notionary/file_upload/models.py +3 -2
  128. notionary/file_upload/notion_file_upload.py +2 -3
  129. notionary/markdown/markdown_builder.py +729 -0
  130. notionary/markdown/markdown_document_model.py +228 -0
  131. notionary/{blocks → markdown}/markdown_node.py +1 -0
  132. notionary/models/notion_database_response.py +0 -338
  133. notionary/page/client.py +34 -15
  134. notionary/page/models.py +327 -0
  135. notionary/page/notion_page.py +136 -58
  136. notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
  137. notionary/page/page_content_writer.py +177 -0
  138. notionary/page/page_context.py +65 -0
  139. notionary/page/reader/handler/__init__.py +19 -0
  140. notionary/page/reader/handler/base_block_renderer.py +44 -0
  141. notionary/page/reader/handler/block_processing_context.py +35 -0
  142. notionary/page/reader/handler/block_rendering_context.py +48 -0
  143. notionary/page/reader/handler/column_list_renderer.py +51 -0
  144. notionary/page/reader/handler/column_renderer.py +60 -0
  145. notionary/page/reader/handler/line_renderer.py +73 -0
  146. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  147. notionary/page/reader/handler/toggle_renderer.py +69 -0
  148. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  149. notionary/page/reader/page_content_retriever.py +81 -0
  150. notionary/page/search_filter_builder.py +2 -1
  151. notionary/page/writer/handler/__init__.py +24 -0
  152. notionary/page/writer/handler/code_handler.py +72 -0
  153. notionary/page/writer/handler/column_handler.py +141 -0
  154. notionary/page/writer/handler/column_list_handler.py +139 -0
  155. notionary/page/writer/handler/equation_handler.py +74 -0
  156. notionary/page/writer/handler/line_handler.py +35 -0
  157. notionary/page/writer/handler/line_processing_context.py +54 -0
  158. notionary/page/writer/handler/regular_line_handler.py +86 -0
  159. notionary/page/writer/handler/table_handler.py +66 -0
  160. notionary/page/writer/handler/toggle_handler.py +155 -0
  161. notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
  162. notionary/page/writer/markdown_to_notion_converter.py +95 -0
  163. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  164. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  165. notionary/page/writer/notion_text_length_processor.py +150 -0
  166. notionary/telemetry/__init__.py +2 -2
  167. notionary/telemetry/service.py +3 -3
  168. notionary/user/__init__.py +2 -2
  169. notionary/user/base_notion_user.py +2 -1
  170. notionary/user/client.py +2 -3
  171. notionary/user/models.py +1 -0
  172. notionary/user/notion_bot_user.py +4 -5
  173. notionary/user/notion_user.py +3 -4
  174. notionary/user/notion_user_manager.py +23 -95
  175. notionary/util/__init__.py +3 -2
  176. notionary/util/fuzzy.py +2 -1
  177. notionary/util/logging_mixin.py +2 -2
  178. notionary/util/singleton_metaclass.py +1 -1
  179. notionary/workspace.py +6 -5
  180. notionary-0.2.22.dist-info/METADATA +237 -0
  181. notionary-0.2.22.dist-info/RECORD +200 -0
  182. notionary/blocks/document/__init__.py +0 -7
  183. notionary/blocks/document/document_element.py +0 -102
  184. notionary/blocks/document/document_markdown_node.py +0 -31
  185. notionary/blocks/image/__init__.py +0 -7
  186. notionary/blocks/image/image_element.py +0 -151
  187. notionary/blocks/markdown_builder.py +0 -356
  188. notionary/blocks/mention/__init__.py +0 -7
  189. notionary/blocks/mention/mention_element.py +0 -229
  190. notionary/blocks/mention/mention_markdown_node.py +0 -38
  191. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  192. notionary/blocks/prompts/element_prompt_content.py +0 -41
  193. notionary/blocks/shared/models.py +0 -713
  194. notionary/blocks/shared/notion_block_element.py +0 -37
  195. notionary/blocks/shared/text_inline_formatter.py +0 -262
  196. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  197. notionary/database/models/page_result.py +0 -10
  198. notionary/models/notion_block_response.py +0 -264
  199. notionary/models/notion_page_response.py +0 -78
  200. notionary/models/search_response.py +0 -0
  201. notionary/page/__init__.py +0 -0
  202. notionary/page/content/markdown_whitespace_processor.py +0 -80
  203. notionary/page/content/notion_text_length_utils.py +0 -87
  204. notionary/page/content/page_content_retriever.py +0 -60
  205. notionary/page/formatting/line_processor.py +0 -153
  206. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  207. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  208. notionary/page/notion_to_markdown_converter.py +0 -179
  209. notionary/page/properites/property_value_extractor.py +0 -0
  210. notionary/user/notion_user_provider.py +0 -1
  211. notionary-0.2.19.dist-info/METADATA +0 -225
  212. notionary-0.2.19.dist-info/RECORD +0 -150
  213. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  214. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  215. /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
  216. /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
  217. /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
  218. /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
  219. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  220. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -0,0 +1,729 @@
1
+ """
2
+ Clean Fluent Markdown Builder
3
+ ============================
4
+
5
+ A direct, chainable builder for all MarkdownNode types without overengineering.
6
+ Maps 1:1 to the available blocks with clear, expressive method names.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable, Optional, Self
12
+
13
+ from notionary.blocks.audio import AudioMarkdownBlockParams, AudioMarkdownNode
14
+ from notionary.blocks.bookmark import BookmarkMarkdownBlockParams, BookmarkMarkdownNode
15
+ from notionary.blocks.breadcrumbs import BreadcrumbMarkdownNode
16
+ from notionary.blocks.bulleted_list import (
17
+ BulletedListMarkdownBlockParams,
18
+ BulletedListMarkdownNode,
19
+ )
20
+ from notionary.blocks.callout import CalloutMarkdownBlockParams, CalloutMarkdownNode
21
+ from notionary.blocks.code import CodeBlock, CodeLanguage, CodeMarkdownNode
22
+ from notionary.blocks.column import (
23
+ ColumnListMarkdownBlockParams,
24
+ ColumnListMarkdownNode,
25
+ ColumnMarkdownNode,
26
+ )
27
+ from notionary.blocks.divider import DividerMarkdownBlockParams, DividerMarkdownNode
28
+ from notionary.blocks.embed import EmbedMarkdownBlockParams, EmbedMarkdownNode
29
+ from notionary.blocks.equation import EquationMarkdownBlockParams, EquationMarkdownNode
30
+ from notionary.blocks.file import FileMarkdownNode, FileMarkdownNodeParams
31
+ from notionary.blocks.heading import HeadingMarkdownBlockParams, HeadingMarkdownNode
32
+ from notionary.blocks.image_block import ImageMarkdownBlockParams, ImageMarkdownNode
33
+ from notionary.blocks.numbered_list import (
34
+ NumberedListMarkdownBlockParams,
35
+ NumberedListMarkdownNode,
36
+ )
37
+ from notionary.blocks.paragraph import (
38
+ ParagraphMarkdownBlockParams,
39
+ ParagraphMarkdownNode,
40
+ )
41
+ from notionary.blocks.pdf import PdfMarkdownNode, PdfMarkdownNodeParams
42
+ from notionary.blocks.quote import QuoteMarkdownBlockParams, QuoteMarkdownNode
43
+ from notionary.blocks.table import TableMarkdownBlockParams, TableMarkdownNode
44
+ from notionary.blocks.table_of_contents import (
45
+ TableOfContentsMarkdownBlockParams,
46
+ TableOfContentsMarkdownNode,
47
+ )
48
+ from notionary.blocks.todo import TodoMarkdownBlockParams, TodoMarkdownNode
49
+ from notionary.blocks.toggle import ToggleMarkdownBlockParams, ToggleMarkdownNode
50
+ from notionary.blocks.toggleable_heading import (
51
+ ToggleableHeadingMarkdownBlockParams,
52
+ ToggleableHeadingMarkdownNode,
53
+ )
54
+ from notionary.blocks.types import BlockType, MarkdownBlockType
55
+ from notionary.blocks.video import VideoMarkdownBlockParams, VideoMarkdownNode
56
+ from notionary.markdown.markdown_document_model import (
57
+ MarkdownBlock,
58
+ MarkdownDocumentModel,
59
+ )
60
+ from notionary.markdown.markdown_node import MarkdownNode
61
+
62
+
63
+ class MarkdownBuilder:
64
+ """
65
+ Fluent interface builder for creating Notion content with clean, direct methods.
66
+ """
67
+
68
+ def __init__(self) -> None:
69
+ self.children: list[MarkdownNode] = []
70
+
71
+ self._block_processors: dict[str, Callable[[Any], None]] = {
72
+ MarkdownBlockType.HEADING_1: self._add_heading,
73
+ MarkdownBlockType.HEADING_2: self._add_heading,
74
+ MarkdownBlockType.HEADING_3: self._add_heading,
75
+ MarkdownBlockType.PARAGRAPH: self._add_paragraph,
76
+ MarkdownBlockType.QUOTE: self._add_quote,
77
+ MarkdownBlockType.BULLETED_LIST_ITEM: self._add_bulleted_list,
78
+ MarkdownBlockType.NUMBERED_LIST_ITEM: self._add_numbered_list,
79
+ MarkdownBlockType.TO_DO: self._add_todo,
80
+ MarkdownBlockType.CALLOUT: self._add_callout,
81
+ MarkdownBlockType.CODE: self._add_code,
82
+ MarkdownBlockType.IMAGE: self._add_image,
83
+ MarkdownBlockType.VIDEO: self._add_video,
84
+ MarkdownBlockType.AUDIO: self._add_audio,
85
+ MarkdownBlockType.FILE: self._add_file,
86
+ MarkdownBlockType.PDF: self._add_pdf,
87
+ MarkdownBlockType.BOOKMARK: self._add_bookmark,
88
+ MarkdownBlockType.EMBED: self._add_embed,
89
+ MarkdownBlockType.TABLE: self._add_table,
90
+ MarkdownBlockType.DIVIDER: self._add_divider,
91
+ MarkdownBlockType.EQUATION: self._add_equation,
92
+ MarkdownBlockType.TABLE_OF_CONTENTS: self._add_table_of_contents,
93
+ MarkdownBlockType.TOGGLE: self._add_toggle,
94
+ MarkdownBlockType.COLUMN_LIST: self._add_columns,
95
+ MarkdownBlockType.BREADCRUMB: self._add_breadcrumb,
96
+ MarkdownBlockType.HEADING: self._add_heading,
97
+ MarkdownBlockType.BULLETED_LIST: self._add_bulleted_list,
98
+ MarkdownBlockType.NUMBERED_LIST: self._add_numbered_list,
99
+ MarkdownBlockType.TODO: self._add_todo,
100
+ MarkdownBlockType.TOGGLEABLE_HEADING: self._add_toggleable_heading,
101
+ MarkdownBlockType.COLUMNS: self._add_columns,
102
+ MarkdownBlockType.SPACE: self._add_space,
103
+ }
104
+
105
+ @classmethod
106
+ def from_model(cls, model: MarkdownDocumentModel) -> Self:
107
+ """Create MarkdownBuilder from a Pydantic model."""
108
+ builder = cls()
109
+ builder._process_blocks(model.blocks)
110
+ return builder
111
+
112
+ def h1(self, text: str) -> Self:
113
+ """
114
+ Add an H1 heading.
115
+
116
+ Args:
117
+ text: The heading text content
118
+ """
119
+ self.children.append(HeadingMarkdownNode(text=text, level=1))
120
+ return self
121
+
122
+ def h2(self, text: str) -> Self:
123
+ """
124
+ Add an H2 heading.
125
+
126
+ Args:
127
+ text: The heading text content
128
+ """
129
+ self.children.append(HeadingMarkdownNode(text=text, level=2))
130
+ return self
131
+
132
+ def h3(self, text: str) -> Self:
133
+ """
134
+ Add an H3 heading.
135
+
136
+ Args:
137
+ text: The heading text content
138
+ """
139
+ self.children.append(HeadingMarkdownNode(text=text, level=3))
140
+ return self
141
+
142
+ def heading(self, text: str, level: int = 2) -> Self:
143
+ """
144
+ Add a heading with specified level.
145
+
146
+ Args:
147
+ text: The heading text content
148
+ level: Heading level (1-3), defaults to 2
149
+ """
150
+ self.children.append(HeadingMarkdownNode(text=text, level=level))
151
+ return self
152
+
153
+ def paragraph(self, text: str) -> Self:
154
+ """
155
+ Add a paragraph block.
156
+
157
+ Args:
158
+ text: The paragraph text content
159
+ """
160
+ self.children.append(ParagraphMarkdownNode(text=text))
161
+ return self
162
+
163
+ def text(self, content: str) -> Self:
164
+ """
165
+ Add a text paragraph (alias for paragraph).
166
+
167
+ Args:
168
+ content: The text content
169
+ """
170
+ return self.paragraph(content)
171
+
172
+ def quote(self, text: str) -> Self:
173
+ """
174
+ Add a blockquote.
175
+
176
+ Args:
177
+ text: Quote text content
178
+ author: Optional quote author/attribution
179
+ """
180
+ self.children.append(QuoteMarkdownNode(text=text))
181
+ return self
182
+
183
+ def divider(self) -> Self:
184
+ """Add a horizontal divider."""
185
+ self.children.append(DividerMarkdownNode())
186
+ return self
187
+
188
+ def numbered_list(self, items: list[str]) -> Self:
189
+ """
190
+ Add a numbered list.
191
+
192
+ Args:
193
+ items: List of text items for the numbered list
194
+ """
195
+ self.children.append(NumberedListMarkdownNode(texts=items))
196
+ return self
197
+
198
+ def bulleted_list(self, items: list[str]) -> Self:
199
+ """
200
+ Add a bulleted list.
201
+
202
+ Args:
203
+ items: List of text items for the bulleted list
204
+ """
205
+ self.children.append(BulletedListMarkdownNode(texts=items))
206
+ return self
207
+
208
+ def todo(self, text: str, checked: bool = False) -> Self:
209
+ """
210
+ Add a single todo item.
211
+
212
+ Args:
213
+ text: The todo item text
214
+ checked: Whether the todo item is completed, defaults to False
215
+ """
216
+ self.children.append(TodoMarkdownNode(text=text, checked=checked))
217
+ return self
218
+
219
+ def todo_list(
220
+ self, items: list[str], completed: Optional[list[bool]] = None
221
+ ) -> Self:
222
+ """
223
+ Add multiple todo items.
224
+
225
+ Args:
226
+ items: List of todo item texts
227
+ completed: List of completion states for each item, defaults to all False
228
+ """
229
+ if completed is None:
230
+ completed = [False] * len(items)
231
+
232
+ for i, item in enumerate(items):
233
+ is_done = completed[i] if i < len(completed) else False
234
+ self.children.append(TodoMarkdownNode(text=item, checked=is_done))
235
+ return self
236
+
237
+ def callout(self, text: str, emoji: Optional[str] = None) -> Self:
238
+ """
239
+ Add a callout block.
240
+
241
+ Args:
242
+ text: The callout text content
243
+ emoji: Optional emoji for the callout icon
244
+ """
245
+ self.children.append(CalloutMarkdownNode(text=text, emoji=emoji))
246
+ return self
247
+
248
+ def toggle(
249
+ self, title: str, builder_func: Callable[["MarkdownBuilder"], "MarkdownBuilder"]
250
+ ) -> Self:
251
+ """
252
+ Add a toggle block with content built using the builder API.
253
+
254
+ Args:
255
+ title: The toggle title/header text
256
+ builder_func: Function that receives a MarkdownBuilder and returns it configured
257
+
258
+ Example:
259
+ builder.toggle("Advanced Settings", lambda t:
260
+ t.h3("Configuration")
261
+ .paragraph("Settings description")
262
+ .table(["Setting", "Value"], [["Debug", "True"]])
263
+ .callout("Important note", "⚠️")
264
+ )
265
+ """
266
+ toggle_builder = MarkdownBuilder()
267
+ builder_func(toggle_builder)
268
+ self.children.append(
269
+ ToggleMarkdownNode(title=title, children=toggle_builder.children)
270
+ )
271
+ return self
272
+
273
+ def toggleable_heading(
274
+ self,
275
+ text: str,
276
+ level: int,
277
+ builder_func: Callable[["MarkdownBuilder"], "MarkdownBuilder"],
278
+ ) -> Self:
279
+ """
280
+ Add a toggleable heading with content built using the builder API.
281
+
282
+ Args:
283
+ text: The heading text content
284
+ level: Heading level (1-3)
285
+ builder_func: Function that receives a MarkdownBuilder and returns it configured
286
+
287
+ Example:
288
+ builder.toggleable_heading("Advanced Section", 2, lambda t:
289
+ t.paragraph("Introduction to this section")
290
+ .numbered_list(["Step 1", "Step 2", "Step 3"])
291
+ .code("example_code()", "python")
292
+ .table(["Feature", "Status"], [["API", "Ready"]])
293
+ )
294
+ """
295
+ toggle_builder = MarkdownBuilder()
296
+ builder_func(toggle_builder)
297
+ self.children.append(
298
+ ToggleableHeadingMarkdownNode(
299
+ text=text, level=level, children=toggle_builder.children
300
+ )
301
+ )
302
+ return self
303
+
304
+ def image(
305
+ self, url: str, caption: Optional[str] = None, alt: Optional[str] = None
306
+ ) -> Self:
307
+ """
308
+ Add an image.
309
+
310
+ Args:
311
+ url: Image URL or file path
312
+ caption: Optional image caption text
313
+ alt: Optional alternative text for accessibility
314
+ """
315
+ self.children.append(ImageMarkdownNode(url=url, caption=caption, alt=alt))
316
+ return self
317
+
318
+ def video(self, url: str, caption: Optional[str] = None) -> Self:
319
+ """
320
+ Add a video.
321
+
322
+ Args:
323
+ url: Video URL or file path
324
+ caption: Optional video caption text
325
+ """
326
+ self.children.append(VideoMarkdownNode(url=url, caption=caption))
327
+ return self
328
+
329
+ def audio(self, url: str, caption: Optional[str] = None) -> Self:
330
+ """
331
+ Add audio content.
332
+
333
+ Args:
334
+ url: Audio file URL or path
335
+ caption: Optional audio caption text
336
+ """
337
+ self.children.append(AudioMarkdownNode(url=url, caption=caption))
338
+ return self
339
+
340
+ def file(self, url: str, caption: Optional[str] = None) -> Self:
341
+ """
342
+ Add a file.
343
+
344
+ Args:
345
+ url: File URL or path
346
+ caption: Optional file caption text
347
+ """
348
+ self.children.append(FileMarkdownNode(url=url, caption=caption))
349
+ return self
350
+
351
+ def pdf(self, url: str, caption: Optional[str] = None) -> Self:
352
+ """
353
+ Add a PDF document.
354
+
355
+ Args:
356
+ url: PDF URL or file path
357
+ caption: Optional PDF caption text
358
+ """
359
+ self.children.append(PdfMarkdownNode(url=url, caption=caption))
360
+ return self
361
+
362
+ def bookmark(
363
+ self, url: str, title: Optional[str] = None, caption: Optional[str] = None
364
+ ) -> Self:
365
+ """
366
+ Add a bookmark.
367
+
368
+ Args:
369
+ url: Bookmark URL
370
+ title: Optional bookmark title
371
+ description: Optional bookmark description text
372
+ """
373
+ self.children.append(
374
+ BookmarkMarkdownNode(url=url, title=title, caption=caption)
375
+ )
376
+ return self
377
+
378
+ def embed(self, url: str, caption: Optional[str] = None) -> Self:
379
+ """
380
+ Add an embed.
381
+
382
+ Args:
383
+ url: URL to embed (e.g., YouTube, Twitter, etc.)
384
+ caption: Optional embed caption text
385
+ """
386
+ self.children.append(EmbedMarkdownNode(url=url, caption=caption))
387
+ return self
388
+
389
+ def code(
390
+ self, code: str, language: Optional[str] = None, caption: Optional[str] = None
391
+ ) -> Self:
392
+ """
393
+ Add a code block.
394
+
395
+ Args:
396
+ code: The source code content
397
+ language: Optional programming language for syntax highlighting
398
+ caption: Optional code block caption text
399
+ """
400
+ self.children.append(
401
+ CodeMarkdownNode(code=code, language=language, caption=caption)
402
+ )
403
+ return self
404
+
405
+ def mermaid(self, diagram: str, caption: Optional[str] = None) -> Self:
406
+ """
407
+ Add a Mermaid diagram block.
408
+
409
+ Args:
410
+ diagram: The Mermaid diagram source code
411
+ caption: Optional diagram caption text
412
+ """
413
+ self.children.append(
414
+ CodeMarkdownNode(
415
+ code=diagram, language=CodeLanguage.MERMAID.value, caption=caption
416
+ )
417
+ )
418
+ return self
419
+
420
+ def table(self, headers: list[str], rows: list[list[str]]) -> Self:
421
+ """
422
+ Add a table.
423
+
424
+ Args:
425
+ headers: List of column header texts
426
+ rows: List of rows, where each row is a list of cell texts
427
+ """
428
+ self.children.append(TableMarkdownNode(headers=headers, rows=rows))
429
+ return self
430
+
431
+ def add_custom(self, node: MarkdownNode) -> Self:
432
+ """
433
+ Add a custom MarkdownNode.
434
+
435
+ Args:
436
+ node: A custom MarkdownNode instance
437
+ """
438
+ self.children.append(node)
439
+ return self
440
+
441
+ def breadcrumb(self) -> Self:
442
+ """Add a breadcrumb navigation block."""
443
+ self.children.append(BreadcrumbMarkdownNode())
444
+ return self
445
+
446
+ def equation(self, expression: str) -> Self:
447
+ """
448
+ Add a LaTeX equation block.
449
+
450
+ Args:
451
+ expression: LaTeX mathematical expression
452
+
453
+ Example:
454
+ builder.equation("E = mc^2")
455
+ builder.equation("f(x) = \\sin(x) + \\cos(x)")
456
+ builder.equation("x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}")
457
+ """
458
+ self.children.append(EquationMarkdownNode(expression=expression))
459
+ return self
460
+
461
+ def table_of_contents(self, color: Optional[str] = None) -> Self:
462
+ """
463
+ Add a table of contents.
464
+
465
+ Args:
466
+ color: Optional color for the table of contents (e.g., "blue", "blue_background")
467
+ """
468
+ self.children.append(TableOfContentsMarkdownNode(color=color))
469
+ return self
470
+
471
+ def columns(
472
+ self,
473
+ *builder_funcs: Callable[["MarkdownBuilder"], "MarkdownBuilder"],
474
+ width_ratios: Optional[list[float]] = None,
475
+ ) -> Self:
476
+ """
477
+ Add multiple columns in a layout.
478
+
479
+ Args:
480
+ *builder_funcs: Multiple functions, each building one column
481
+ width_ratios: Optional list of width ratios (0.0 to 1.0).
482
+ If None, columns have equal width.
483
+ Length must match number of builder_funcs.
484
+
485
+ Examples:
486
+ # Equal width (original API unchanged):
487
+ builder.columns(
488
+ lambda col: col.h2("Left").paragraph("Left content"),
489
+ lambda col: col.h2("Right").paragraph("Right content")
490
+ )
491
+
492
+ # Custom ratios:
493
+ builder.columns(
494
+ lambda col: col.h2("Main").paragraph("70% width"),
495
+ lambda col: col.h2("Sidebar").paragraph("30% width"),
496
+ width_ratios=[0.7, 0.3]
497
+ )
498
+
499
+ # Three columns with custom ratios:
500
+ builder.columns(
501
+ lambda col: col.h3("Nav").paragraph("Navigation"),
502
+ lambda col: col.h2("Main").paragraph("Main content"),
503
+ lambda col: col.h3("Ads").paragraph("Advertisement"),
504
+ width_ratios=[0.2, 0.6, 0.2]
505
+ )
506
+ """
507
+ if len(builder_funcs) < 2:
508
+ raise ValueError("Column layout requires at least 2 columns")
509
+
510
+ if width_ratios is not None:
511
+ if len(width_ratios) != len(builder_funcs):
512
+ raise ValueError(
513
+ f"width_ratios length ({len(width_ratios)}) must match number of columns ({len(builder_funcs)})"
514
+ )
515
+
516
+ ratio_sum = sum(width_ratios)
517
+ if not (0.9 <= ratio_sum <= 1.1): # Allow small floating point errors
518
+ raise ValueError(f"width_ratios should sum to 1.0, got {ratio_sum}")
519
+
520
+ # Create all columns
521
+ columns = []
522
+ for i, builder_func in enumerate(builder_funcs):
523
+ width_ratio = width_ratios[i] if width_ratios else None
524
+
525
+ col_builder = MarkdownBuilder()
526
+ builder_func(col_builder)
527
+
528
+ column_node = ColumnMarkdownNode(
529
+ children=col_builder.children, width_ratio=width_ratio
530
+ )
531
+ columns.append(column_node)
532
+
533
+ self.children.append(ColumnListMarkdownNode(columns=columns))
534
+ return self
535
+
536
+ def column_with_nodes(
537
+ self, *nodes: MarkdownNode, width_ratio: Optional[float] = None
538
+ ) -> Self:
539
+ """
540
+ Add a column with pre-built MarkdownNode objects.
541
+
542
+ Args:
543
+ *nodes: MarkdownNode objects to include in the column
544
+ width_ratio: Optional width ratio (0.0 to 1.0)
545
+
546
+ Examples:
547
+ # Original API (unchanged):
548
+ builder.column_with_nodes(
549
+ HeadingMarkdownNode(text="Title", level=2),
550
+ ParagraphMarkdownNode(text="Content")
551
+ )
552
+
553
+ # New API with ratio:
554
+ builder.column_with_nodes(
555
+ HeadingMarkdownNode(text="Sidebar", level=2),
556
+ ParagraphMarkdownNode(text="Narrow content"),
557
+ width_ratio=0.25
558
+ )
559
+ """
560
+ from notionary.blocks.column.column_markdown_node import ColumnMarkdownNode
561
+
562
+ column_node = ColumnMarkdownNode(children=list(nodes), width_ratio=width_ratio)
563
+ self.children.append(column_node)
564
+ return self
565
+
566
+ def _column(
567
+ self, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder]
568
+ ) -> ColumnMarkdownNode:
569
+ """
570
+ Internal helper to create a single column.
571
+ Use columns() instead for public API.
572
+ """
573
+ col_builder = MarkdownBuilder()
574
+ builder_func(col_builder)
575
+ return ColumnMarkdownNode(children=col_builder.children)
576
+
577
+ def space(self) -> Self:
578
+ """Add vertical spacing."""
579
+ return self.paragraph("")
580
+
581
+ def build(self) -> str:
582
+ """Build and return the final markdown string."""
583
+ return "\n\n".join(
584
+ child.to_markdown() for child in self.children if child is not None
585
+ )
586
+
587
+ def _add_heading(self, params: HeadingMarkdownBlockParams) -> None:
588
+ """Add a heading block."""
589
+ self.children.append(HeadingMarkdownNode.from_params(params))
590
+
591
+ def _add_paragraph(self, params: ParagraphMarkdownBlockParams) -> None:
592
+ """Add a paragraph block."""
593
+ self.children.append(ParagraphMarkdownNode.from_params(params))
594
+
595
+ def _add_quote(self, params: QuoteMarkdownBlockParams) -> None:
596
+ """Add a quote block."""
597
+ self.children.append(QuoteMarkdownNode.from_params(params))
598
+
599
+ def _add_bulleted_list(self, params: BulletedListMarkdownBlockParams) -> None:
600
+ """Add a bulleted list block."""
601
+ self.children.append(BulletedListMarkdownNode.from_params(params))
602
+
603
+ def _add_numbered_list(self, params: NumberedListMarkdownBlockParams) -> None:
604
+ """Add a numbered list block."""
605
+ self.children.append(NumberedListMarkdownNode.from_params(params))
606
+
607
+ def _add_todo(self, params: TodoMarkdownBlockParams) -> None:
608
+ """Add a todo block."""
609
+ self.children.append(TodoMarkdownNode.from_params(params))
610
+
611
+ def _add_callout(self, params: CalloutMarkdownBlockParams) -> None:
612
+ """Add a callout block."""
613
+ self.children.append(CalloutMarkdownNode.from_params(params))
614
+
615
+ def _add_code(self, params: CodeBlock) -> None:
616
+ """Add a code block."""
617
+ self.children.append(CodeMarkdownNode.from_params(params))
618
+
619
+ def _add_image(self, params: ImageMarkdownBlockParams) -> None:
620
+ """Add an image block."""
621
+ self.children.append(ImageMarkdownNode.from_params(params))
622
+
623
+ def _add_video(self, params: VideoMarkdownBlockParams) -> None:
624
+ """Add a video block."""
625
+ self.children.append(VideoMarkdownNode.from_params(params))
626
+
627
+ def _add_audio(self, params: AudioMarkdownBlockParams) -> None:
628
+ """Add an audio block."""
629
+ self.children.append(AudioMarkdownNode.from_params(params))
630
+
631
+ def _add_file(self, params: FileMarkdownNodeParams) -> None:
632
+ """Add a file block."""
633
+ self.children.append(FileMarkdownNode.from_params(params))
634
+
635
+ def _add_pdf(self, params: PdfMarkdownNodeParams) -> None:
636
+ """Add a PDF block."""
637
+ self.children.append(PdfMarkdownNode.from_params(params))
638
+
639
+ def _add_bookmark(self, params: BookmarkMarkdownBlockParams) -> None:
640
+ """Add a bookmark block."""
641
+ self.children.append(BookmarkMarkdownNode.from_params(params))
642
+
643
+ def _add_embed(self, params: EmbedMarkdownBlockParams) -> None:
644
+ """Add an embed block."""
645
+ self.children.append(EmbedMarkdownNode.from_params(params))
646
+
647
+ def _add_table(self, params: TableMarkdownBlockParams) -> None:
648
+ """Add a table block."""
649
+ self.children.append(TableMarkdownNode.from_params(params))
650
+
651
+ def _add_divider(self, params: DividerMarkdownBlockParams) -> None:
652
+ """Add a divider block."""
653
+ self.children.append(DividerMarkdownNode.from_params(params))
654
+
655
+ def _add_equation(self, params: EquationMarkdownBlockParams) -> None:
656
+ """Add an equation block."""
657
+ self.children.append(EquationMarkdownNode.from_params(params))
658
+
659
+ def _add_table_of_contents(
660
+ self, params: TableOfContentsMarkdownBlockParams
661
+ ) -> None:
662
+ """Add a table of contents block."""
663
+ self.children.append(TableOfContentsMarkdownNode.from_params(params))
664
+
665
+ def _add_toggle(self, params: ToggleMarkdownBlockParams) -> None:
666
+ """Add a toggle block."""
667
+ child_builder = MarkdownBuilder()
668
+ child_builder._process_blocks(params.children)
669
+ self.children.append(
670
+ ToggleMarkdownNode(title=params.title, children=child_builder.children)
671
+ )
672
+
673
+ def _add_toggleable_heading(
674
+ self, params: ToggleableHeadingMarkdownBlockParams
675
+ ) -> None:
676
+ """Add a toggleable heading block."""
677
+ # Create nested builder for children
678
+ child_builder = MarkdownBuilder()
679
+ child_builder._process_blocks(params.children)
680
+ self.children.append(
681
+ ToggleableHeadingMarkdownNode(
682
+ text=params.text, level=params.level, children=child_builder.children
683
+ )
684
+ )
685
+
686
+ def _add_columns(self, params: ColumnListMarkdownBlockParams) -> None:
687
+ """Add a columns block."""
688
+ column_nodes = []
689
+
690
+ for i, column_blocks in enumerate(params.columns):
691
+ width_ratio = (
692
+ params.width_ratios[i]
693
+ if params.width_ratios and i < len(params.width_ratios)
694
+ else None
695
+ )
696
+
697
+ col_builder = MarkdownBuilder()
698
+ col_builder._process_blocks(column_blocks)
699
+
700
+ # Erstelle ColumnMarkdownNode
701
+ column_nodes.append(
702
+ ColumnMarkdownNode(
703
+ children=col_builder.children, width_ratio=width_ratio
704
+ )
705
+ )
706
+
707
+ self.children.append(ColumnListMarkdownNode(columns=column_nodes))
708
+
709
+ def _add_breadcrumb(self, params) -> None:
710
+ """Add a breadcrumb block."""
711
+ self.children.append(BreadcrumbMarkdownNode())
712
+
713
+ def _add_space(self, params) -> None:
714
+ """Add a space block."""
715
+ self.children.append(ParagraphMarkdownNode(text=""))
716
+
717
+ def _process_blocks(self, blocks: list[MarkdownBlock]) -> None:
718
+ """Process blocks using explicit mapping - type-safe and clear."""
719
+ for block in blocks:
720
+ processor = self._block_processors.get(block.type)
721
+ if processor:
722
+ processor(block.params)
723
+ else:
724
+ # More explicit error handling
725
+ available_types = ", ".join(sorted(self._block_processors.keys()))
726
+ raise ValueError(
727
+ f"Unsupported block type '{block.type}'. "
728
+ f"Available types: {available_types}"
729
+ )