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
@@ -1,333 +1,59 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Optional, Callable
4
+ from turtle import width
3
5
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import (
6
- ElementPromptContent,
7
- ElementPromptBuilder,
8
- NotionBlockResult,
9
- )
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.column.column_models import ColumnBlock, CreateColumnBlock
8
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
10
9
 
11
10
 
12
- class ColumnElement(NotionBlockElement):
11
+ class ColumnElement(BaseBlockElement):
13
12
  """
14
- Handles conversion between custom Markdown column syntax and Notion column blocks.
15
-
16
- Markdown column syntax:
17
- ::: columns
18
- ::: column
19
- Content for first column
20
- :::
21
- ::: column
22
- Content for second column
23
- :::
24
- :::
13
+ Handles individual `::: column` blocks with optional width ratio.
14
+ Content is automatically added by the stack processor.
25
15
 
26
- This creates a column layout in Notion with the specified content in each column.
16
+ Supported syntax:
17
+ - `::: column` (equal width)
18
+ - `::: column 0.5` (50% width)
19
+ - `::: column 0.25` (25% width)
27
20
  """
28
21
 
29
- COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
30
- COLUMN_START = re.compile(r"^:::\s*column\s*$")
31
- BLOCK_END = re.compile(r"^:::\s*$")
32
-
33
- _converter_callback = None
34
-
35
- @classmethod
36
- def set_converter_callback(
37
- cls, callback: Callable[[str], list[dict[str, any]]]
38
- ) -> None:
39
- """
40
- Setze die Callback-Funktion, die zum Konvertieren von Markdown zu Notion-Blöcken verwendet wird.
41
-
42
- Args:
43
- callback: Funktion, die Markdown-Text annimmt und eine Liste von Notion-Blöcken zurückgibt
44
- """
45
- cls._converter_callback = callback
46
-
47
- @staticmethod
48
- def match_markdown(text: str) -> bool:
49
- """Check if text starts a columns block."""
50
- return bool(ColumnElement.COLUMNS_START.match(text.strip()))
51
-
52
- @staticmethod
53
- def match_notion(block: dict[str, any]) -> bool:
54
- """Check if block is a Notion column_list."""
55
- return block.get("type") == "column_list"
56
-
57
- @staticmethod
58
- def markdown_to_notion(text: str) -> NotionBlockResult:
59
- """
60
- Convert markdown column syntax to Notion column blocks.
61
-
62
- Note: This only processes the first line (columns start).
63
- The full column content needs to be processed separately.
64
- """
65
- if not ColumnElement.COLUMNS_START.match(text.strip()):
66
- return None
67
-
68
- # Create an empty column_list block
69
- # Child columns will be added by the column processor
70
- return [{"type": "column_list", "column_list": {"children": []}}]
71
-
72
- @staticmethod
73
- def notion_to_markdown(block: dict[str, any]) -> Optional[str]:
74
- """Convert Notion column_list block to markdown column syntax."""
75
- if block.get("type") != "column_list":
76
- return None
77
-
78
- column_children = block.get("column_list", {}).get("children", [])
79
-
80
- # Start the columns block
81
- result = ["::: columns"]
82
-
83
- # Process each column
84
- for column_block in column_children:
85
- if column_block.get("type") == "column":
86
- result.append("::: column")
87
-
88
- for _ in column_block.get("column", {}).get("children", []):
89
- result.append(" [Column content]") # Placeholder
90
-
91
- result.append(":::")
92
-
93
- # End the columns block
94
- result.append(":::")
95
-
96
- return "\n".join(result)
97
-
98
- @staticmethod
99
- def is_multiline() -> bool:
100
- """Column blocks span multiple lines."""
101
- return True
102
-
103
- @classmethod
104
- def find_matches(
105
- cls, text: str, converter_callback: Optional[Callable] = None
106
- ) -> list[tuple[int, int, dict[str, any]]]:
107
- """
108
- Find all column block matches in the text and return their positions and blocks.
109
-
110
- Args:
111
- text: The input markdown text
112
- converter_callback: Optional callback to convert nested content
113
-
114
- Returns:
115
- List of tuples (start_pos, end_pos, block)
116
- """
117
- # Wenn ein Callback übergeben wurde, nutze diesen, sonst die gespeicherte Referenz
118
- converter = converter_callback or cls._converter_callback
119
- if not converter:
120
- raise ValueError(
121
- "No converter callback provided for ColumnElement. Call set_converter_callback first or provide converter_callback parameter."
122
- )
123
-
124
- matches = []
125
- lines = text.split("\n")
126
- i = 0
127
-
128
- while i < len(lines):
129
- # Skip non-column lines
130
- if not ColumnElement.COLUMNS_START.match(lines[i].strip()):
131
- i += 1
132
- continue
133
-
134
- # Process a column block and add to matches
135
- column_block_info = cls._process_column_block(
136
- lines=lines, start_index=i, converter_callback=converter
137
- )
138
- matches.append(column_block_info)
139
-
140
- # Skip to the end of the processed column block
141
- i = column_block_info[3] # i is returned as the 4th element in the tuple
142
-
143
- return [(start, end, block) for start, end, block, _ in matches]
22
+ COLUMN_START = re.compile(r"^:::\s*column(?:\s+(0?\.\d+|1\.0?))?\s*$")
144
23
 
145
24
  @classmethod
146
- def _process_column_block(
147
- cls, lines: list[str], start_index: int, converter_callback: Callable
148
- ) -> tuple[int, int, dict[str, any], int]:
149
- """
150
- Process a complete column block structure from the given starting line.
151
-
152
- Args:
153
- lines: All lines of the text
154
- start_index: Index of the column block start line
155
- converter_callback: Callback function to convert markdown to notion blocks
156
-
157
- Returns:
158
- Tuple of (start_pos, end_pos, block, next_line_index)
159
- """
160
- columns_start = start_index
161
- columns_blocks = cls.markdown_to_notion(lines[start_index].strip())
162
- columns_block = columns_blocks[0] if columns_blocks else None
163
- columns_children = []
164
-
165
- next_index = cls._collect_columns(
166
- lines, start_index + 1, columns_children, converter_callback
167
- )
168
-
169
- # Add columns to the main block
170
- if columns_children and columns_block:
171
- columns_block["column_list"]["children"] = columns_children
172
-
173
- # Calculate positions
174
- start_pos = sum(len(lines[j]) + 1 for j in range(columns_start))
175
- end_pos = sum(len(lines[j]) + 1 for j in range(next_index))
176
-
177
- return (start_pos, end_pos, columns_block, next_index)
25
+ def match_notion(cls, block: Block) -> bool:
26
+ """Check if block is a Notion column."""
27
+ return block.type == BlockType.COLUMN and block.column
178
28
 
179
29
  @classmethod
180
- def _collect_columns(
181
- cls,
182
- lines: list[str],
183
- start_index: int,
184
- columns_children: list[dict[str, any]],
185
- converter_callback: Callable,
186
- ) -> int:
187
- """
188
- Collect all columns within a column block structure.
189
-
190
- Args:
191
- lines: All lines of the text
192
- start_index: Index to start collecting from
193
- columns_children: List to append collected columns to
194
- converter_callback: Callback function to convert column content
195
-
196
- Returns:
197
- Next line index after all columns have been processed
198
- """
199
- i = start_index
200
- in_column = False
201
- column_content = []
202
-
203
- while i < len(lines):
204
- current_line = lines[i].strip()
205
-
206
- if cls.COLUMNS_START.match(current_line):
207
- break
208
-
209
- if cls.COLUMN_START.match(current_line):
210
- cls._finalize_column(
211
- column_content, columns_children, in_column, converter_callback
212
- )
213
- column_content = []
214
- in_column = True
215
- i += 1
216
- continue
217
-
218
- if cls.BLOCK_END.match(current_line) and in_column:
219
- cls._finalize_column(
220
- column_content, columns_children, in_column, converter_callback
221
- )
222
- column_content = []
223
- in_column = False
224
- i += 1
225
- continue
226
-
227
- if cls.BLOCK_END.match(current_line) and not in_column:
228
- i += 1
229
- break
230
-
231
- if in_column:
232
- column_content.append(lines[i])
233
-
234
- i += 1
235
-
236
- cls._finalize_column(
237
- column_content, columns_children, in_column, converter_callback
238
- )
239
-
240
- return i
241
-
242
- @staticmethod
243
- def _finalize_column(
244
- column_content: list[str],
245
- columns_children: list[dict[str, any]],
246
- in_column: bool,
247
- converter_callback: Callable,
248
- ) -> None:
249
- """
250
- Finalize a column by processing its content and adding it to the columns_children list.
251
-
252
- Args:
253
- column_content: Content lines of the column
254
- columns_children: List to append the column block to
255
- in_column: Whether we're currently in a column (if False, does nothing)
256
- converter_callback: Callback function to convert column content
257
- """
258
- if not (in_column and column_content):
259
- return
30
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ """Convert `::: column [ratio]` to Notion ColumnBlock."""
32
+ if not (match := cls.COLUMN_START.match(text.strip())):
33
+ return None
260
34
 
261
- processed_content = ColumnElement._preprocess_column_content(column_content)
35
+ ratio_str = match.group(1)
36
+ width_ratio = None
262
37
 
263
- column_blocks = converter_callback("\n".join(processed_content))
38
+ if ratio_str:
39
+ try:
40
+ width_ratio = float(ratio_str)
41
+ # Validate ratio is between 0 and 1
42
+ if not (0 < width_ratio <= 1.0):
43
+ width_ratio = None # Invalid ratio, use default
44
+ except ValueError:
45
+ width_ratio = None # Invalid format, use default
264
46
 
265
- # Create column block
266
- column_block = {"type": "column", "column": {"children": column_blocks}}
267
- columns_children.append(column_block)
47
+ column_content = ColumnBlock(width_ratio=width_ratio)
48
+ return CreateColumnBlock(column=column_content)
268
49
 
269
50
  @classmethod
270
- def is_multiline(cls) -> bool:
271
- """Column blocks span multiple lines."""
272
- return True
51
+ def notion_to_markdown(cls, block: Block) -> str:
52
+ """Convert Notion column to markdown."""
53
+ if not cls.match_notion(block):
54
+ return ""
273
55
 
274
- @staticmethod
275
- def _preprocess_column_content(lines: list[str]) -> list[str]:
276
- """Remove all spacer markers from column content."""
277
- return [line for line in lines if line.strip() != "---spacer---"]
56
+ if not block.column.width_ratio:
57
+ return "::: column"
278
58
 
279
- @classmethod
280
- def get_llm_prompt_content(cls) -> ElementPromptContent:
281
- """
282
- Returns structured LLM prompt metadata for the column layout element.
283
- """
284
- return (
285
- ElementPromptBuilder()
286
- .with_description(
287
- "Creates a multi-column layout that displays content side by side."
288
- )
289
- .with_usage_guidelines(
290
- "Use columns sparingly, only for direct comparisons or when parallel presentation significantly improves readability. "
291
- "Best for pros/cons lists, feature comparisons, or pairing images with descriptions."
292
- )
293
- .with_avoidance_guidelines(
294
- "Avoid overusing as it can complicate document structure. Do not use for simple content that works better in linear format."
295
- )
296
- .with_syntax(
297
- "::: columns\n"
298
- "::: column\n"
299
- "Content for first column\n"
300
- ":::\n"
301
- "::: column\n"
302
- "Content for second column\n"
303
- ":::\n"
304
- ":::"
305
- )
306
- .with_examples(
307
- [
308
- "::: columns\n"
309
- "::: column\n"
310
- "## Features\n"
311
- "- Fast response time\n"
312
- "- Intuitive interface\n"
313
- "- Regular updates\n"
314
- ":::\n"
315
- "::: column\n"
316
- "## Benefits\n"
317
- "- Increased productivity\n"
318
- "- Better collaboration\n"
319
- "- Simplified workflows\n"
320
- ":::\n"
321
- ":::",
322
- "::: columns\n"
323
- "::: column\n"
324
- "![Image placeholder](/api/placeholder/400/320)\n"
325
- ":::\n"
326
- "::: column\n"
327
- "This text appears next to the image, creating a media-with-caption style layout.\n"
328
- ":::\n"
329
- ":::",
330
- ]
331
- )
332
- .build()
333
- )
59
+ return f"::: column {block.column.width_ratio}"
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from notionary.blocks.base_block_element import BaseBlockElement
6
+ from notionary.blocks.column.column_models import ColumnListBlock, CreateColumnListBlock
7
+ from notionary.blocks.models import Block, BlockCreateResult
8
+ from notionary.blocks.types import BlockType
9
+
10
+
11
+ class ColumnListElement(BaseBlockElement):
12
+ """
13
+ Handles the `::: columns` container.
14
+ Individual columns are handled by ColumnElement.
15
+ """
16
+
17
+ COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
18
+
19
+ @classmethod
20
+ def match_markdown(cls, text: str) -> bool:
21
+ """Check if text starts a columns container."""
22
+ return bool(cls.COLUMNS_START.match(text.strip()))
23
+
24
+ @classmethod
25
+ def match_notion(cls, block: Block) -> bool:
26
+ """Check if block is a Notion column_list."""
27
+ return block.type == BlockType.COLUMN_LIST and block.column_list
28
+
29
+ @classmethod
30
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ """Convert `::: columns` to Notion ColumnListBlock."""
32
+ if not cls.COLUMNS_START.match(text.strip()):
33
+ return None
34
+
35
+ # Empty ColumnListBlock - children (columns) added by stack processor
36
+ column_list_content = ColumnListBlock()
37
+ return CreateColumnListBlock(column_list=column_list_content)
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.column.column_markdown_node import ColumnMarkdownNode
6
+ from notionary.markdown.markdown_document_model import MarkdownBlock
7
+ from notionary.markdown.markdown_node import MarkdownNode
8
+
9
+
10
+ class ColumnListMarkdownBlockParams(BaseModel):
11
+ columns: list[list[MarkdownBlock]]
12
+ model_config = {"arbitrary_types_allowed": True}
13
+
14
+
15
+ class ColumnListMarkdownNode(MarkdownNode):
16
+ """
17
+ Programmatic interface for creating a Markdown column list container.
18
+ This represents the `::: columns` container that holds multiple columns.
19
+
20
+ Example:
21
+ ::: columns
22
+ ::: column
23
+ Left content
24
+ with nested lines
25
+ :::
26
+
27
+ ::: column 0.3
28
+ Right content (30% width)
29
+ with nested lines
30
+ :::
31
+ :::
32
+ """
33
+
34
+ def __init__(self, columns: list[ColumnMarkdownNode]):
35
+ self.columns = columns
36
+
37
+ @classmethod
38
+ def from_params(
39
+ cls, params: ColumnListMarkdownBlockParams
40
+ ) -> ColumnListMarkdownNode:
41
+ return cls(columns=params.columns)
42
+
43
+ def to_markdown(self) -> str:
44
+ if not self.columns:
45
+ return "::: columns\n:::"
46
+
47
+ column_parts = [column.to_markdown() for column in self.columns]
48
+ columns_content = "\n\n".join(column_parts)
49
+
50
+ return f"::: columns\n{columns_content}\n:::"
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from notionary.markdown.markdown_node import MarkdownNode
8
+
9
+
10
+ class ColumnMarkdownBlockParams(BaseModel):
11
+ children: list[MarkdownNode]
12
+ width_ratio: Optional[float] = None
13
+ model_config = {"arbitrary_types_allowed": True}
14
+
15
+
16
+ class ColumnMarkdownNode(MarkdownNode):
17
+ """
18
+ Programmatic interface for creating a single Markdown column block
19
+ with nested content and optional width ratio.
20
+
21
+ Example:
22
+ ::: column
23
+ # Column Title
24
+
25
+ Some content here
26
+ :::
27
+
28
+ ::: column 0.7
29
+ # Wide Column (70%)
30
+
31
+ This column takes 70% width
32
+ :::
33
+ """
34
+
35
+ def __init__(
36
+ self, children: list[MarkdownNode], width_ratio: Optional[float] = None
37
+ ):
38
+ self.children = children
39
+ self.width_ratio = width_ratio
40
+
41
+ @classmethod
42
+ def from_params(cls, params: ColumnMarkdownBlockParams) -> ColumnMarkdownNode:
43
+ return cls(children=params.children, width_ratio=params.width_ratio)
44
+
45
+ def to_markdown(self) -> str:
46
+ # Start tag with optional width ratio
47
+ if self.width_ratio is not None:
48
+ start_tag = f"::: column {self.width_ratio}"
49
+ else:
50
+ start_tag = "::: column"
51
+
52
+ if not self.children:
53
+ return f"{start_tag}\n:::"
54
+
55
+ # Convert children to markdown
56
+ content_parts = [child.to_markdown() for child in self.children]
57
+ content_text = "\n\n".join(content_parts)
58
+
59
+ return f"{start_tag}\n{content_text}\n:::"
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from notionary.blocks.models import BlockCreateRequest
8
+
9
+
10
+ class ColumnBlock(BaseModel):
11
+ width_ratio: Optional[float] = None
12
+ children: list[BlockCreateRequest] = Field(default_factory=list)
13
+
14
+
15
+ class CreateColumnBlock(BaseModel):
16
+ type: Literal["column"] = "column"
17
+ column: ColumnBlock
18
+
19
+
20
+ class ColumnListBlock(BaseModel):
21
+ children: list[CreateColumnBlock] = Field(default_factory=list)
22
+
23
+
24
+ class CreateColumnListBlock(BaseModel):
25
+ type: Literal["column_list"] = "column_list"
26
+ column_list: ColumnListBlock
@@ -1,7 +1,14 @@
1
- from .divider_element import DividerElement
2
- from .divider_markdown_node import DividerMarkdownNode
1
+ from notionary.blocks.divider.divider_element import DividerElement
2
+ from notionary.blocks.divider.divider_markdown_node import (
3
+ DividerMarkdownBlockParams,
4
+ DividerMarkdownNode,
5
+ )
6
+ from notionary.blocks.divider.divider_models import CreateDividerBlock, DividerBlock
3
7
 
4
8
  __all__ = [
5
9
  "DividerElement",
10
+ "DividerBlock",
11
+ "CreateDividerBlock",
6
12
  "DividerMarkdownNode",
13
+ "DividerMarkdownBlockParams",
7
14
  ]
@@ -1,15 +1,19 @@
1
- import re
2
- from typing import Dict, Any, Optional
1
+ from __future__ import annotations
3
2
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import (
6
- ElementPromptContent,
7
- ElementPromptBuilder,
8
- NotionBlockResult,
3
+ import re
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.divider.divider_models import CreateDividerBlock, DividerBlock
8
+ from notionary.blocks.models import Block, BlockCreateResult
9
+ from notionary.blocks.paragraph.paragraph_models import (
10
+ CreateParagraphBlock,
11
+ ParagraphBlock,
9
12
  )
13
+ from notionary.blocks.types import BlockType
10
14
 
11
15
 
12
- class DividerElement(NotionBlockElement):
16
+ class DividerElement(BaseBlockElement):
13
17
  """
14
18
  Handles conversion between Markdown horizontal dividers and Notion divider blocks.
15
19
 
@@ -20,53 +24,26 @@ class DividerElement(NotionBlockElement):
20
24
  PATTERN = re.compile(r"^\s*-{3,}\s*$")
21
25
 
22
26
  @classmethod
23
- def match_markdown(cls, text: str) -> bool:
24
- """Check if text is a markdown divider."""
25
- return bool(DividerElement.PATTERN.match(text))
27
+ def match_notion(cls, block: Block) -> bool:
28
+ """Check if this element can handle the given Notion block."""
29
+ return block.type == BlockType.DIVIDER and block.divider
26
30
 
27
31
  @classmethod
28
- def match_notion(cls, block: Dict[str, Any]) -> bool:
29
- """Check if block is a Notion divider."""
30
- return block.get("type") == "divider"
31
-
32
- @classmethod
33
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
34
- """Convert markdown divider to Notion divider block."""
35
- if not DividerElement.match_markdown(text):
32
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
33
+ """Convert markdown horizontal rule to Notion divider, with preceding empty paragraph."""
34
+ if not cls.PATTERN.match(text.strip()):
36
35
  return None
37
36
 
38
- empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
39
-
40
- divider_block = {"type": "divider", "divider": {}}
37
+ empty_para = ParagraphBlock(rich_text=[])
38
+ divider = DividerBlock()
41
39
 
42
- return [empty_paragraph, divider_block]
40
+ return [
41
+ CreateParagraphBlock(paragraph=empty_para),
42
+ CreateDividerBlock(divider=divider),
43
+ ]
43
44
 
44
45
  @classmethod
45
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
46
- """Convert Notion divider block to markdown divider."""
47
- if block.get("type") != "divider":
46
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
47
+ if block.type != BlockType.DIVIDER or not block.divider:
48
48
  return None
49
-
50
49
  return "---"
51
-
52
- @classmethod
53
- def is_multiline(cls) -> bool:
54
- return False
55
-
56
- @classmethod
57
- def get_llm_prompt_content(cls) -> ElementPromptContent:
58
- """Returns structured LLM prompt metadata for the divider element."""
59
- return (
60
- ElementPromptBuilder()
61
- .with_description(
62
- "Creates a horizontal divider line to visually separate sections of content."
63
- )
64
- .with_usage_guidelines(
65
- "Use dividers only sparingly and only when the user explicitly asks for them. Dividers create strong visual breaks between content sections, so they should not be used unless specifically requested by the user."
66
- )
67
- .with_syntax("---")
68
- .with_examples(
69
- ["## Section 1\nContent\n\n---\n\n## Section 2\nMore content"]
70
- )
71
- .build()
72
- )
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pydantic import BaseModel
4
- from notionary.blocks.markdown_node import MarkdownNode
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
5
6
 
6
7
 
7
8
  class DividerMarkdownBlockParams(BaseModel):
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DividerBlock(BaseModel):
7
+ pass
8
+
9
+
10
+ class CreateDividerBlock(BaseModel):
11
+ type: Literal["divider"] = "divider"
12
+ divider: DividerBlock