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
@@ -1,270 +1,115 @@
1
- import re
2
- from typing import Dict, Any, Optional, List, Tuple, Callable
1
+ from __future__ import annotations
3
2
 
4
- from notionary.blocks import (
5
- ElementPromptContent,
6
- ElementPromptBuilder,
7
- NotionBlockElement,
8
- NotionBlockResult,
3
+ import re
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.heading.heading_models import (
8
+ CreateHeading1Block,
9
+ CreateHeading2Block,
10
+ CreateHeading3Block,
11
+ HeadingBlock,
9
12
  )
10
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
13
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
14
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
15
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
11
16
 
12
17
 
13
- class ToggleableHeadingElement(NotionBlockElement):
14
- """Handles conversion between Markdown collapsible headings and Notion toggleable heading blocks with pipe syntax."""
18
+ class ToggleableHeadingElement(BaseBlockElement):
19
+ """
20
+ Simplified ToggleableHeadingElement that works with the stack-based converter.
21
+ Children are automatically handled by the StackBasedMarkdownConverter.
22
+ """
15
23
 
16
- PATTERN = re.compile(r"^\+(?P<level>#{1,3})\s+(?P<content>.+)$")
17
- PIPE_CONTENT_PATTERN = re.compile(r"^\|\s?(.*)$")
18
-
19
- @staticmethod
20
- def match_markdown(text: str) -> bool:
21
- """Check if text is a markdown collapsible heading."""
22
- return bool(ToggleableHeadingElement.PATTERN.match(text))
24
+ # Updated pattern for simplified +++# Title syntax (no quotes!)
25
+ PATTERN = re.compile(r"^[+]{3}(?P<level>#{1,3})\s+(.+)$", re.IGNORECASE)
23
26
 
24
27
  @staticmethod
25
- def match_notion(block: Dict[str, Any]) -> bool:
28
+ def match_notion(block: Block) -> bool:
26
29
  """Check if block is a Notion toggleable heading."""
27
- block_type: str = block.get("type", "")
28
- if not block_type.startswith("heading_") or block_type[-1] not in "123":
30
+ # Use BlockType enum for matching heading blocks
31
+ if block.type not in (
32
+ BlockType.HEADING_1,
33
+ BlockType.HEADING_2,
34
+ BlockType.HEADING_3,
35
+ ):
29
36
  return False
30
37
 
31
- # Check if it has the is_toggleable property set to true
32
- heading_data = block.get(block_type, {})
33
- return heading_data.get("is_toggleable", False) is True
34
-
35
- @staticmethod
36
- def markdown_to_notion(text: str) -> NotionBlockResult:
37
- """Convert markdown collapsible heading to Notion toggleable heading block."""
38
- header_match = ToggleableHeadingElement.PATTERN.match(text)
39
- if not header_match:
40
- return None
41
-
42
- level = len(header_match.group(1))
43
- content = header_match.group(2)
44
-
45
- return {
46
- "type": f"heading_{level}",
47
- f"heading_{level}": {
48
- "rich_text": TextInlineFormatter.parse_inline_formatting(content),
49
- "is_toggleable": True,
50
- "color": "default",
51
- "children": [], # Will be populated with nested content if needed
52
- },
53
- }
54
-
55
- @staticmethod
56
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
57
- """Convert Notion toggleable heading block to markdown collapsible heading with pipe syntax."""
58
- block_type = block.get("type", "")
59
-
60
- if not block_type.startswith("heading_"):
61
- return None
62
-
63
- try:
64
- level = int(block_type[-1])
65
- if not 1 <= level <= 3:
66
- return None
67
- except ValueError:
68
- return None
69
-
70
- heading_data = block.get(block_type, {})
71
-
72
- # Check if it's toggleable
73
- if not heading_data.get("is_toggleable", False):
74
- return None
75
-
76
- rich_text = heading_data.get("rich_text", [])
77
- text = TextInlineFormatter.extract_text_with_formatting(rich_text)
78
- prefix = "#" * level
79
- return f"+{prefix} {text or ''}"
80
-
81
- @staticmethod
82
- def is_multiline() -> bool:
83
- """Collapsible headings can have children, so they're multiline elements."""
84
- return True
38
+ if block.heading_1 and block.heading_1.is_toggleable:
39
+ return True
40
+ if block.heading_2 and block.heading_2.is_toggleable:
41
+ return True
42
+ if block.heading_3 and block.heading_3.is_toggleable:
43
+ return True
85
44
 
86
45
  @classmethod
87
- def find_matches(
88
- cls,
89
- text: str,
90
- process_nested_content: Callable = None,
91
- context_aware: bool = True,
92
- ) -> List[Tuple[int, int, Dict[str, Any]]]:
46
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
93
47
  """
94
- Find all collapsible heading matches in the text with pipe syntax for nested content.
95
- Improved version with reduced cognitive complexity.
96
-
97
- Args:
98
- text: The text to process
99
- process_nested_content: Optional callback function to process nested content
100
- context_aware: Whether to consider context when finding collapsible headings
101
-
102
- Returns:
103
- List of (start_pos, end_pos, block) tuples
48
+ Convert markdown collapsible heading to a toggleable Notion HeadingBlock.
49
+ Children are automatically handled by the StackBasedMarkdownConverter.
104
50
  """
105
- if not text:
106
- return []
107
-
108
- collapsible_blocks = []
109
- lines = text.split("\n")
110
- line_index = 0
111
-
112
- while line_index < len(lines):
113
- current_line = lines[line_index]
114
-
115
- # Skip non-collapsible heading lines
116
- if not cls._is_collapsible_heading(current_line):
117
- line_index += 1
118
- continue
119
-
120
- # Process collapsible heading
121
- start_position = cls._calculate_line_position(lines, line_index)
122
- heading_block = cls.markdown_to_notion(current_line)
123
-
124
- if not heading_block:
125
- line_index += 1
126
- continue
51
+ if not (match := cls.PATTERN.match(text.strip())):
52
+ return None
127
53
 
128
- # Extract and process nested content
129
- nested_content, next_line_index = cls._extract_nested_content(
130
- lines, line_index + 1
131
- )
132
- end_position = cls._calculate_block_end_position(
133
- start_position, current_line, nested_content
134
- )
54
+ level = len(match.group("level")) # Count # characters
55
+ content = match.group(2).strip() # group(2) is the title (no quotes needed)
135
56
 
136
- cls._process_nested_content(
137
- heading_block, nested_content, process_nested_content
138
- )
57
+ if level < 1 or level > 3 or not content:
58
+ return None
139
59
 
140
- # Add block to results
141
- collapsible_blocks.append((start_position, end_position, heading_block))
142
- line_index = next_line_index
60
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
143
61
 
144
- return collapsible_blocks
62
+ heading_content = HeadingBlock(
63
+ rich_text=rich_text, color="default", is_toggleable=True, children=[]
64
+ )
145
65
 
146
- @classmethod
147
- def _is_collapsible_heading(cls, line: str) -> bool:
148
- """Check if a line represents a collapsible heading."""
149
- return bool(cls.PATTERN.match(line))
66
+ if level == 1:
67
+ return CreateHeading1Block(heading_1=heading_content)
68
+ elif level == 2:
69
+ return CreateHeading2Block(heading_2=heading_content)
70
+ else:
71
+ return CreateHeading3Block(heading_3=heading_content)
150
72
 
151
73
  @staticmethod
152
- def _calculate_line_position(lines: List[str], current_index: int) -> int:
153
- """Calculate the character position of a line in the text."""
154
- position = 0
155
- for i in range(current_index):
156
- position += len(lines[i]) + 1 # +1 for newline
157
- return position
158
-
159
- @classmethod
160
- def _extract_nested_content(
161
- cls, lines: List[str], start_index: int
162
- ) -> Tuple[List[str], int]:
163
- """
164
- Extract nested content with pipe syntax from lines following a collapsible heading.
165
-
166
- Args:
167
- lines: All text lines
168
- start_index: Index to start looking for nested content
169
-
170
- Returns:
171
- Tuple of (nested_content, next_line_index)
172
- """
173
- nested_content = []
174
- current_index = start_index
175
-
176
- while current_index < len(lines):
177
- current_line = lines[current_index]
178
-
179
- # Case 1: Empty line - check if it's followed by pipe content
180
- if not current_line.strip():
181
- if cls._is_next_line_pipe_content(lines, current_index):
182
- nested_content.append("")
183
- current_index += 1
184
- continue
185
-
186
- # Case 2: Pipe content line - part of nested content
187
- pipe_content = cls._extract_pipe_content(current_line)
188
- if pipe_content is not None:
189
- nested_content.append(pipe_content)
190
- current_index += 1
191
- continue
192
-
193
- # Case 3: Another collapsible heading - ends current heading's content
194
- if cls.PATTERN.match(current_line):
195
- break
196
-
197
- # Case 4: Any other line - ends nested content
198
- break
199
-
200
- return nested_content, current_index
201
-
202
- @classmethod
203
- def _is_next_line_pipe_content(cls, lines: List[str], current_index: int) -> bool:
204
- """Check if the next line uses pipe syntax for nested content."""
205
- next_index = current_index + 1
206
- if next_index >= len(lines):
207
- return False
208
- return bool(cls.PIPE_CONTENT_PATTERN.match(lines[next_index]))
209
-
210
- @classmethod
211
- def _extract_pipe_content(cls, line: str) -> Optional[str]:
212
- """Extract content from a line with pipe prefix."""
213
- pipe_match = cls.PIPE_CONTENT_PATTERN.match(line)
214
- if not pipe_match:
74
+ async def notion_to_markdown(block: Block) -> Optional[str]:
75
+ """Convert Notion toggleable heading block to markdown collapsible heading."""
76
+ # Only handle heading blocks via BlockType enum
77
+ if block.type not in (
78
+ BlockType.HEADING_1,
79
+ BlockType.HEADING_2,
80
+ BlockType.HEADING_3,
81
+ ):
215
82
  return None
216
- return pipe_match.group(1)
217
83
 
218
- @staticmethod
219
- def _calculate_block_end_position(
220
- start_position: int, heading_line: str, nested_content: List[str]
221
- ) -> int:
222
- """Calculate the end position of a collapsible heading block including nested content."""
223
- block_length = len(heading_line)
224
- if nested_content:
225
- # Add length of each nested content line plus newline
226
- nested_length = sum(len(line) + 1 for line in nested_content)
227
- block_length += nested_length
228
- return start_position + block_length
84
+ # Determine heading level from enum
85
+ if block.type == BlockType.HEADING_1:
86
+ level = 1
87
+ elif block.type == BlockType.HEADING_2:
88
+ level = 2
89
+ else:
90
+ level = 3
229
91
 
230
- @classmethod
231
- def _process_nested_content(
232
- cls,
233
- heading_block: Dict[str, Any],
234
- nested_content: List[str],
235
- processor: Optional[Callable],
236
- ) -> None:
237
- """Process nested content with the provided callback function if available."""
238
- if not (nested_content and processor):
239
- return
92
+ heading_content = getattr(block, block.type.value)
93
+ if not isinstance(heading_content, HeadingBlock):
94
+ return None
240
95
 
241
- nested_text = "\n".join(nested_content)
242
- nested_blocks = processor(nested_text)
96
+ text = await TextInlineFormatter.extract_text_with_formatting(
97
+ heading_content.rich_text
98
+ )
99
+ prefix = "#" * level
243
100
 
244
- if nested_blocks:
245
- block_type = heading_block["type"]
246
- heading_block[block_type]["children"] = nested_blocks
101
+ return f'+++{prefix} {text or ""}'
247
102
 
248
103
  @classmethod
249
- def get_llm_prompt_content(cls) -> ElementPromptContent:
250
- """
251
- Returns structured LLM prompt metadata for the collapsible heading element with pipe syntax.
252
- """
253
- return (
254
- ElementPromptBuilder()
255
- .with_description(
256
- "Collapsible headings combine heading structure with toggleable visibility."
257
- )
258
- .with_usage_guidelines(
259
- "Use when you want to create a structured section that can be expanded or collapsed."
260
- )
261
- .with_syntax("+# Collapsible Heading\n| Content with pipe prefix")
262
- .with_examples(
263
- [
264
- "+# Main Collapsible Section\n| Content under the section",
265
- "+## Subsection\n| This content is hidden until expanded",
266
- "+### Detailed Information\n| Technical details go here",
267
- ]
268
- )
269
- .build()
104
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
105
+ """Get system prompt information for toggleable heading blocks."""
106
+ return BlockElementMarkdownInformation(
107
+ block_type=cls.__name__,
108
+ description="Toggleable heading blocks create collapsible sections with heading-style titles",
109
+ syntax_examples=[
110
+ "+++# Main Section\nContent goes here\n+++",
111
+ "+++## Subsection\nSubsection content\n+++",
112
+ "+++### Details\nDetailed information\n+++",
113
+ ],
114
+ usage_guidelines="Use for collapsible sections with heading structure. Combines heading levels (1-3) with toggle functionality. Great for organizing hierarchical expandable content.",
270
115
  )
@@ -1,43 +1,51 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional, List
4
3
  from pydantic import BaseModel
5
- from notionary.blocks.markdown_node import MarkdownNode
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
6
6
 
7
7
 
8
8
  class ToggleableHeadingMarkdownBlockParams(BaseModel):
9
9
  text: str
10
- level: int = 1
11
- content: Optional[List[str]] = None
10
+ level: int
11
+ children: list[MarkdownNode]
12
+ model_config = {"arbitrary_types_allowed": True}
12
13
 
13
14
 
14
15
  class ToggleableHeadingMarkdownNode(MarkdownNode):
15
16
  """
16
- Programmatic interface for creating collapsible Markdown headings (toggleable headings).
17
- Pipe-prefixed lines are used for the collapsible content.
18
- Example:
19
- +# Section
20
- | Hidden content
21
- +## Subsection
22
- | Details
17
+ Clean programmatic interface for creating collapsible Markdown headings (toggleable headings)
18
+ with pipe-prefixed nested content using MarkdownNode children.
19
+
20
+ Example syntax for a level-2 toggleable heading:
21
+ +++## Advanced Section
22
+ some content
23
+ +++
23
24
  """
24
25
 
25
- def __init__(self, text: str, level: int = 1, content: Optional[list[str]] = None):
26
+ def __init__(self, text: str, level: int, children: list[MarkdownNode]):
26
27
  if not (1 <= level <= 3):
27
28
  raise ValueError("Only heading levels 1-3 are supported (H1, H2, H3)")
28
29
  self.text = text
29
30
  self.level = level
30
- self.content = content or []
31
+ self.children = children
31
32
 
32
33
  @classmethod
33
34
  def from_params(
34
35
  cls, params: ToggleableHeadingMarkdownBlockParams
35
36
  ) -> ToggleableHeadingMarkdownNode:
36
- return cls(text=params.text, level=params.level, content=params.content)
37
+ return cls(text=params.text, level=params.level, children=params.children)
37
38
 
38
39
  def to_markdown(self) -> str:
39
- prefix = "+" + ("#" * self.level)
40
+ prefix = "+++" + ("#" * self.level)
40
41
  result = f"{prefix} {self.text}"
41
- if self.content:
42
- result += "\n" + "\n".join([f"| {line}" for line in self.content])
43
- return result
42
+
43
+ if not self.children:
44
+ result += "\n+++"
45
+ return result
46
+
47
+ # Convert children to markdown
48
+ content_parts = [child.to_markdown() for child in self.children]
49
+ content_text = "\n\n".join(content_parts)
50
+
51
+ return result + "\n" + content_text + "\n+++"
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+ from typing import Protocol, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from notionary.blocks.models import BlockCreateRequest
9
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
10
+
11
+
12
+ class BlockColor(str, Enum):
13
+ BLUE = "blue"
14
+ BLUE_BACKGROUND = "blue_background"
15
+ BROWN = "brown"
16
+ BROWN_BACKGROUND = "brown_background"
17
+ DEFAULT = "default"
18
+ GRAY = "gray"
19
+ GRAY_BACKGROUND = "gray_background"
20
+ GREEN = "green"
21
+ GREEN_BACKGROUND = "green_background"
22
+ ORANGE = "orange"
23
+ ORANGE_BACKGROUND = "orange_background"
24
+ YELLOW = "yellow"
25
+ YELLOW_BACKGROUND = "yellow_background"
26
+ PINK = "pink"
27
+ PINK_BACKGROUND = "pink_background"
28
+ PURPLE = "purple"
29
+ PURPLE_BACKGROUND = "purple_background"
30
+ RED = "red"
31
+ RED_BACKGROUND = "red_background"
32
+ DEFAULT_BACKGROUND = "default_background"
33
+
34
+
35
+ class BlockType(str, Enum):
36
+ BOOKMARK = "bookmark"
37
+ BREADCRUMB = "breadcrumb"
38
+ BULLETED_LIST_ITEM = "bulleted_list_item"
39
+ CALLOUT = "callout"
40
+ CHILD_DATABASE = "child_database"
41
+ CHILD_PAGE = "child_page"
42
+ COLUMN = "column"
43
+ COLUMN_LIST = "column_list"
44
+ CODE = "code"
45
+ DIVIDER = "divider"
46
+ EMBED = "embed"
47
+ EQUATION = "equation"
48
+ FILE = "file"
49
+ HEADING_1 = "heading_1"
50
+ HEADING_2 = "heading_2"
51
+ HEADING_3 = "heading_3"
52
+ IMAGE = "image"
53
+ LINK_PREVIEW = "link_preview"
54
+ LINK_TO_PAGE = "link_to_page"
55
+ NUMBERED_LIST_ITEM = "numbered_list_item"
56
+ PARAGRAPH = "paragraph"
57
+ PDF = "pdf"
58
+ QUOTE = "quote"
59
+ SYNCED_BLOCK = "synced_block"
60
+ TABLE = "table"
61
+ TABLE_OF_CONTENTS = "table_of_contents"
62
+ TABLE_ROW = "table_row"
63
+ TO_DO = "to_do"
64
+ TOGGLE = "toggle"
65
+ UNSUPPORTED = "unsupported"
66
+ VIDEO = "video"
67
+ AUDIO = "audio"
68
+
69
+
70
+ class MarkdownBlockType(str, Enum):
71
+ """
72
+ Extended block types for the MarkdownBuilder.
73
+ Includes all BlockType values and adds user-friendly aliases
74
+ for blocks with no direct Notion API counterpart.
75
+ """
76
+
77
+ # All BlockType values
78
+ BOOKMARK = "bookmark"
79
+ BREADCRUMB = "breadcrumb"
80
+ BULLETED_LIST_ITEM = "bulleted_list_item"
81
+ CALLOUT = "callout"
82
+ CHILD_DATABASE = "child_database"
83
+ CHILD_PAGE = "child_page"
84
+ COLUMN = "column"
85
+ COLUMN_LIST = "column_list"
86
+ CODE = "code"
87
+ DIVIDER = "divider"
88
+ EMBED = "embed"
89
+ EQUATION = "equation"
90
+ FILE = "file"
91
+ HEADING_1 = "heading_1"
92
+ HEADING_2 = "heading_2"
93
+ HEADING_3 = "heading_3"
94
+ IMAGE = "image"
95
+ LINK_PREVIEW = "link_preview"
96
+ LINK_TO_PAGE = "link_to_page"
97
+ NUMBERED_LIST_ITEM = "numbered_list_item"
98
+ PARAGRAPH = "paragraph"
99
+ PDF = "pdf"
100
+ QUOTE = "quote"
101
+ SYNCED_BLOCK = "synced_block"
102
+ TABLE = "table"
103
+ TABLE_OF_CONTENTS = "table_of_contents"
104
+ TABLE_ROW = "table_row"
105
+ TO_DO = "to_do"
106
+ TOGGLE = "toggle"
107
+ UNSUPPORTED = "unsupported"
108
+ VIDEO = "video"
109
+ AUDIO = "audio"
110
+
111
+ # Markdown-specific aliases
112
+ HEADING = "heading"
113
+ BULLETED_LIST = "bulleted_list"
114
+ NUMBERED_LIST = "numbered_list"
115
+ TODO = "todo"
116
+ TOGGLEABLE_HEADING = "toggleable_heading"
117
+ COLUMNS = "columns"
118
+ SPACE = "space"
119
+
120
+
121
+ class HasRichText(Protocol):
122
+ """Protocol for objects that have a rich_text attribute."""
123
+
124
+ rich_text: list[RichTextObject]
125
+
126
+
127
+ class HasChildren(Protocol):
128
+ """Protocol for objects that have children blocks."""
129
+
130
+ children: list[BlockCreateRequest]
@@ -1,7 +1,13 @@
1
- from .video_element import VideoElement
2
- from .video_markdown_node import VideoMarkdownNode
1
+ from notionary.blocks.video.video_element import VideoElement
2
+ from notionary.blocks.video.video_element_models import CreateVideoBlock
3
+ from notionary.blocks.video.video_markdown_node import (
4
+ VideoMarkdownBlockParams,
5
+ VideoMarkdownNode,
6
+ )
3
7
 
4
8
  __all__ = [
5
9
  "VideoElement",
10
+ "CreateVideoBlock",
6
11
  "VideoMarkdownNode",
12
+ "VideoMarkdownBlockParams",
7
13
  ]