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