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,303 +1,112 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional, List, Tuple, Callable
4
+ from typing import Optional
3
5
 
4
- from notionary.blocks import (
5
- NotionBlockElement,
6
- NotionBlockResult,
7
- ElementPromptContent,
8
- ElementPromptBuilder,
9
- )
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
8
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
9
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
10
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
11
+ from notionary.blocks.toggle.toggle_models import CreateToggleBlock, ToggleBlock
12
+ from notionary.blocks.types import BlockColor
10
13
 
11
14
 
12
- class ToggleElement(NotionBlockElement):
15
+ class ToggleElement(BaseBlockElement):
13
16
  """
14
- Improved ToggleElement class using pipe syntax instead of indentation.
17
+ Simplified ToggleElement class that works with the stack-based converter.
18
+ Children are automatically handled by the StackBasedMarkdownConverter.
15
19
  """
16
20
 
17
- TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+(.+)$")
18
- PIPE_CONTENT_PATTERN = re.compile(r"^\|\s?(.*)$")
19
-
20
- TRANSCRIPT_TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+Transcript$")
21
-
22
- @classmethod
23
- def match_markdown(cls, text: str) -> bool:
24
- """Check if the text is a markdown toggle."""
25
- return bool(ToggleElement.TOGGLE_PATTERN.match(text.strip()))
21
+ # Updated pattern for ultra-simplified +++ Title syntax (no quotes!)
22
+ TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+(.+)$", re.IGNORECASE)
23
+ TRANSCRIPT_TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+Transcript$", re.IGNORECASE)
26
24
 
27
25
  @classmethod
28
- def match_notion(cls, block: Dict[str, Any]) -> bool:
26
+ def match_notion(cls, block: Block) -> bool:
29
27
  """Check if the block is a Notion toggle block."""
30
- return block.get("type") == "toggle"
28
+ return block.type == BlockType.TOGGLE
31
29
 
32
30
  @classmethod
33
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
34
- """Convert markdown toggle line to Notion toggle block."""
35
- toggle_match = ToggleElement.TOGGLE_PATTERN.match(text.strip())
36
- if not toggle_match:
37
- return None
38
-
39
- # Extract toggle title
40
- title = toggle_match.group(1)
41
-
42
- return {
43
- "type": "toggle",
44
- "toggle": {
45
- "rich_text": [{"type": "text", "text": {"content": title}}],
46
- "color": "default",
47
- "children": [],
48
- },
49
- }
50
-
51
- @classmethod
52
- def extract_nested_content(
53
- cls, lines: List[str], start_index: int
54
- ) -> Tuple[List[str], int]:
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
55
32
  """
56
- Extracts the nested content lines of a toggle block using pipe syntax.
57
-
58
- Args:
59
- lines: All lines of text.
60
- start_index: Starting index to look for nested content.
61
-
62
- Returns:
63
- Tuple of (nested_content_lines, next_line_index)
33
+ Convert markdown toggle line to Notion ToggleBlock.
34
+ Children are automatically handled by the StackBasedMarkdownConverter.
64
35
  """
65
- nested_content = []
66
- current_index = start_index
67
-
68
- while current_index < len(lines):
69
- current_line = lines[current_index]
70
-
71
- # Case 1: Empty line - could be part of the content if next line is a pipe line
72
- if not current_line.strip():
73
- if ToggleElement.is_next_line_pipe_content(lines, current_index):
74
- nested_content.append("")
75
- current_index += 1
76
- continue
77
- else:
78
- # Empty line not followed by pipe ends the block
79
- break
80
-
81
- # Case 2: Pipe-prefixed line - part of the nested content
82
- pipe_content = ToggleElement.extract_pipe_content(current_line)
83
- if pipe_content is not None:
84
- nested_content.append(pipe_content)
85
- current_index += 1
86
- continue
87
-
88
- # Case 3: Regular line - end of nested content
89
- break
36
+ if not (match := cls.TOGGLE_PATTERN.match(text.strip())):
37
+ return None
90
38
 
91
- return nested_content, current_index
39
+ title = match.group(1).strip()
40
+ rich_text = await TextInlineFormatter.parse_inline_formatting(title)
92
41
 
93
- @classmethod
94
- def is_next_line_pipe_content(cls, lines: List[str], current_index: int) -> bool:
95
- """Checks if the next line starts with a pipe prefix."""
96
- next_index = current_index + 1
97
- return (
98
- next_index < len(lines)
99
- and ToggleElement.PIPE_CONTENT_PATTERN.match(lines[next_index]) is not None
42
+ # Create toggle block with empty children - they will be populated automatically
43
+ toggle_content = ToggleBlock(
44
+ rich_text=rich_text, color=BlockColor.DEFAULT, children=[]
100
45
  )
101
46
 
102
- @classmethod
103
- def extract_pipe_content(cls, line: str) -> Optional[str]:
104
- """
105
- Extracts content from a line with pipe prefix.
106
-
107
- Returns:
108
- The content without the pipe, or None if not a pipe-prefixed line.
109
- """
110
- pipe_match = ToggleElement.PIPE_CONTENT_PATTERN.match(line)
111
- if pipe_match:
112
- return pipe_match.group(1)
113
- return None
47
+ return CreateToggleBlock(toggle=toggle_content)
114
48
 
115
49
  @classmethod
116
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
50
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
117
51
  """
118
- Converts a Notion toggle block into markdown using pipe-prefixed lines.
52
+ Converts a Notion toggle block into markdown using the ultra-simplified +++ syntax.
119
53
  """
120
- if block.get("type") != "toggle":
54
+ if block.type != BlockType.TOGGLE:
55
+ return None
56
+
57
+ if not block.toggle:
121
58
  return None
122
59
 
123
- toggle_data = block.get("toggle", {})
60
+ toggle_data = block.toggle
124
61
 
125
62
  # Extract title from rich_text
126
- title = ToggleElement._extract_text_content(toggle_data.get("rich_text", []))
63
+ title = cls._extract_text_content(toggle_data.rich_text or [])
127
64
 
128
- # Create toggle line
65
+ # Create toggle line with ultra-simplified syntax (no quotes!)
129
66
  toggle_line = f"+++ {title}"
130
67
 
131
68
  # Process children if available
132
- children = toggle_data.get("children", [])
69
+ children = toggle_data.children or []
133
70
  if not children:
134
- return toggle_line
135
-
136
- # Add a placeholder line for each child using pipe syntax
137
- child_lines = ["| [Nested content]" for _ in children]
71
+ return toggle_line + "\n+++"
138
72
 
139
- return toggle_line + "\n" + "\n".join(child_lines)
73
+ # Add a placeholder line for each child
74
+ child_lines = ["[Nested content]" for _ in children]
140
75
 
141
- @classmethod
142
- def is_multiline(cls) -> bool:
143
- """Toggle blocks can span multiple lines due to nested content."""
144
- return True
76
+ return toggle_line + "\n" + "\n".join(child_lines) + "\n+++"
145
77
 
146
78
  @classmethod
147
- def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
79
+ def _extract_text_content(cls, rich_text: list[RichTextObject]) -> str:
148
80
  """Extracts plain text content from Notion rich_text blocks."""
149
81
  result = ""
150
82
  for text_obj in rich_text:
151
- if text_obj.get("type") == "text":
152
- result += text_obj.get("text", {}).get("content", "")
153
- elif "plain_text" in text_obj:
154
- result += text_obj.get("plain_text", "")
155
- return result
156
-
157
- @classmethod
158
- def find_matches(
159
- cls,
160
- text: str,
161
- process_nested_content: Callable = None,
162
- context_aware: bool = True,
163
- ) -> List[Tuple[int, int, Dict[str, Any]]]:
164
- """
165
- Finds all toggle elements in markdown using pipe syntax for nested content.
166
-
167
- Args:
168
- text: The markdown input.
169
- process_nested_content: Optional function to parse nested content into blocks.
170
- context_aware: Whether to skip contextually irrelevant transcript toggles.
171
-
172
- Returns:
173
- List of (start_pos, end_pos, block) tuples.
174
- """
175
- if not text:
176
- return []
177
-
178
- toggle_blocks = []
179
- lines = text.split("\n")
180
- current_line_index = 0
181
-
182
- while current_line_index < len(lines):
183
- current_line = lines[current_line_index]
184
-
185
- # Check if the current line is a toggle
186
- if not cls._is_toggle_line(current_line):
187
- current_line_index += 1
188
- continue
189
-
190
- # Skip transcript toggles if required by context
191
- if cls._should_skip_transcript_toggle(
192
- current_line, lines, current_line_index, context_aware
83
+ if hasattr(text_obj, "plain_text"):
84
+ result += text_obj.plain_text or ""
85
+ elif (
86
+ hasattr(text_obj, "type")
87
+ and text_obj.type == "text"
88
+ and hasattr(text_obj, "text")
193
89
  ):
194
- current_line_index += 1
195
- continue
196
-
197
- # Create toggle block and determine character positions
198
- start_position = cls._calculate_start_position(lines, current_line_index)
199
- toggle_block = cls.markdown_to_notion(current_line)
200
-
201
- if not toggle_block:
202
- current_line_index += 1
203
- continue
204
-
205
- # Extract nested content
206
- nested_content, next_line_index = cls.extract_nested_content(
207
- lines, current_line_index + 1
208
- )
209
- end_position = cls._calculate_end_position(
210
- start_position, current_line, nested_content
211
- )
212
-
213
- # Process nested content if needed
214
- cls._process_nested_content_if_needed(
215
- nested_content, process_nested_content, toggle_block
216
- )
217
-
218
- # Save result
219
- toggle_blocks.append((start_position, end_position, toggle_block))
220
- current_line_index = next_line_index
221
-
222
- return toggle_blocks
223
-
224
- @classmethod
225
- def _is_toggle_line(cls, line: str) -> bool:
226
- """Checks whether the given line is a markdown toggle."""
227
- return bool(ToggleElement.TOGGLE_PATTERN.match(line.strip()))
228
-
229
- @classmethod
230
- def _should_skip_transcript_toggle(
231
- cls, line: str, lines: List[str], current_index: int, context_aware: bool
232
- ) -> bool:
233
- """Determines if a transcript toggle should be skipped based on the surrounding context."""
234
- is_transcript_toggle = cls.TRANSCRIPT_TOGGLE_PATTERN.match(line.strip())
235
-
236
- if not (context_aware and is_transcript_toggle):
237
- return False
238
-
239
- # Only keep transcript toggles that follow a list item
240
- has_list_item_before = current_index > 0 and lines[
241
- current_index - 1
242
- ].strip().startswith("- ")
243
- return not has_list_item_before
244
-
245
- @classmethod
246
- def _calculate_start_position(cls, lines: List[str], current_index: int) -> int:
247
- """Calculates the character start position of a line within the full text."""
248
- start_pos = 0
249
- for index in range(current_index):
250
- start_pos += len(lines[index]) + 1 # +1 for line break
251
- return start_pos
252
-
253
- @classmethod
254
- def _calculate_end_position(
255
- cls, start_pos: int, current_line: str, nested_content: List[str]
256
- ) -> int:
257
- """Calculates the end position of a toggle block including nested lines."""
258
- line_length = len(current_line)
259
- nested_content_length = sum(
260
- len(line) + 1 for line in nested_content
261
- ) # +1 for each line break
262
- return start_pos + line_length + nested_content_length
90
+ result += text_obj.text.content or ""
91
+ # Fallback for dict-style access (backward compatibility)
92
+ elif isinstance(text_obj, dict):
93
+ if text_obj.get("type") == "text":
94
+ result += text_obj.get("text", {}).get("content", "")
95
+ elif "plain_text" in text_obj:
96
+ result += text_obj.get("plain_text", "")
97
+ return result
263
98
 
264
99
  @classmethod
265
- def _process_nested_content_if_needed(
266
- cls,
267
- nested_content: List[str],
268
- process_function: Optional[Callable],
269
- toggle_block: Dict[str, Any],
270
- ) -> None:
271
- """Processes nested content using the provided function if applicable."""
272
- if not (nested_content and process_function):
273
- return
274
-
275
- nested_text = "\n".join(nested_content)
276
- nested_blocks = process_function(nested_text)
277
-
278
- if nested_blocks:
279
- toggle_block["toggle"]["children"] = nested_blocks
280
-
281
100
  @classmethod
282
- def get_llm_prompt_content(cls) -> ElementPromptContent:
283
- """
284
- Returns structured LLM prompt metadata for the toggle element with pipe syntax examples.
285
- """
286
- return (
287
- ElementPromptBuilder()
288
- .with_description(
289
- "Toggle elements are collapsible sections that help organize and hide detailed information."
290
- )
291
- .with_usage_guidelines(
292
- "Use toggles for supplementary information that's not essential for the first reading, "
293
- "such as details, examples, or technical information."
294
- )
295
- .with_syntax("+++ Toggle Title\n| Toggle content with pipe prefix")
296
- .with_examples(
297
- [
298
- "+++ Key Findings\n| The research demonstrates **three main conclusions**:\n| 1. First important point\n| 2. Second important point",
299
- "+++ FAQ\n| **Q: When should I use toggles?**\n| *A: Use toggles for supplementary information.*",
300
- ]
301
- )
302
- .build()
101
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
102
+ """Get system prompt information for toggle blocks."""
103
+ return BlockElementMarkdownInformation(
104
+ block_type=cls.__name__,
105
+ description="Toggle blocks create collapsible sections with expandable content",
106
+ syntax_examples=[
107
+ "+++Title\nContent goes here\n+++",
108
+ "+++Details\nMore information\nAdditional content\n+++",
109
+ "+++FAQ\nFrequently asked questions\n+++",
110
+ ],
111
+ usage_guidelines="Use for collapsible content sections. Start with +++Title, add content, end with +++. Great for FAQs, details, or organizing long content.",
303
112
  )
@@ -1,35 +1,45 @@
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 ToggleMarkdownBlockParams(BaseModel):
9
9
  title: str
10
- content: Optional[List[str]] = None
10
+ children: list[MarkdownNode]
11
+ model_config = {"arbitrary_types_allowed": True}
11
12
 
12
13
 
13
14
  class ToggleMarkdownNode(MarkdownNode):
14
15
  """
15
- Programmatic interface for creating Notion-style Markdown toggle blocks
16
- with pipe-prefixed nested content.
16
+ Clean programmatic interface for creating Notion-style Markdown toggle blocks
17
+ with the simplified +++ "Title" syntax.
18
+
17
19
  Example:
18
- +++ Details
19
- | Here are the details.
20
- | You can add more lines.
20
+ +++ "Advanced Details"
21
+ Content here
22
+ More content
23
+ +++
21
24
  """
22
25
 
23
- def __init__(self, title: str, content: Optional[List[str]] = None):
26
+ def __init__(self, title: str, children: list[MarkdownNode]):
24
27
  self.title = title
25
- self.content = content or []
28
+ self.children = children
26
29
 
27
30
  @classmethod
28
31
  def from_params(cls, params: ToggleMarkdownBlockParams) -> ToggleMarkdownNode:
29
- return cls(title=params.title, content=params.content)
32
+ return cls(title=params.title, children=params.children)
30
33
 
31
34
  def to_markdown(self) -> str:
32
- result = f"+++ {self.title}"
33
- if self.content:
34
- result += "\n" + "\n".join([f"| {line}" for line in self.content])
35
- return result
35
+ result = f"+++{self.title}"
36
+
37
+ if not self.children:
38
+ result += "\n+++"
39
+ return result
40
+
41
+ # Convert children to markdown
42
+ content_parts = [child.to_markdown() for child in self.children]
43
+ content_text = "\n\n".join(content_parts)
44
+
45
+ return result + "\n" + content_text + "\n+++"
@@ -0,0 +1,17 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing_extensions import Literal
3
+
4
+ from notionary.blocks.models import Block
5
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
6
+ from notionary.blocks.types import BlockColor
7
+
8
+
9
+ class ToggleBlock(BaseModel):
10
+ rich_text: list[RichTextObject]
11
+ color: BlockColor = BlockColor.DEFAULT
12
+ children: list[Block] = Field(default_factory=list)
13
+
14
+
15
+ class CreateToggleBlock(BaseModel):
16
+ type: Literal["toggle"] = "toggle"
17
+ toggle: ToggleBlock
@@ -1,9 +1,13 @@
1
- from .toggleable_heading_element import ToggleableHeadingElement
2
- from .toggleable_heading_markdown_node import (
1
+ from notionary.blocks.toggleable_heading.toggleable_heading_element import (
2
+ ToggleableHeadingElement,
3
+ )
4
+ from notionary.blocks.toggleable_heading.toggleable_heading_markdown_node import (
5
+ ToggleableHeadingMarkdownBlockParams,
3
6
  ToggleableHeadingMarkdownNode,
4
7
  )
5
8
 
6
9
  __all__ = [
7
10
  "ToggleableHeadingElement",
8
11
  "ToggleableHeadingMarkdownNode",
12
+ "ToggleableHeadingMarkdownBlockParams",
9
13
  ]