notionary 0.2.19__py3-none-any.whl → 0.2.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. notionary/__init__.py +8 -4
  2. notionary/base_notion_client.py +3 -1
  3. notionary/blocks/__init__.py +2 -91
  4. notionary/blocks/_bootstrap.py +271 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +69 -106
  7. notionary/blocks/audio/audio_markdown_node.py +13 -5
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +42 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +49 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
  13. notionary/blocks/bookmark/bookmark_models.py +15 -0
  14. notionary/blocks/breadcrumbs/__init__.py +17 -0
  15. notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
  16. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
  17. notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
  18. notionary/blocks/bulleted_list/__init__.py +12 -2
  19. notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
  20. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
  21. notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
  22. notionary/blocks/callout/__init__.py +9 -2
  23. notionary/blocks/callout/callout_element.py +53 -86
  24. notionary/blocks/callout/callout_markdown_node.py +3 -1
  25. notionary/blocks/callout/callout_models.py +33 -0
  26. notionary/blocks/child_database/__init__.py +14 -0
  27. notionary/blocks/child_database/child_database_element.py +61 -0
  28. notionary/blocks/child_database/child_database_models.py +12 -0
  29. notionary/blocks/child_page/__init__.py +9 -0
  30. notionary/blocks/child_page/child_page_element.py +94 -0
  31. notionary/blocks/child_page/child_page_models.py +12 -0
  32. notionary/blocks/{shared/block_client.py → client.py} +54 -54
  33. notionary/blocks/code/__init__.py +6 -2
  34. notionary/blocks/code/code_element.py +96 -181
  35. notionary/blocks/code/code_markdown_node.py +64 -13
  36. notionary/blocks/code/code_models.py +94 -0
  37. notionary/blocks/column/__init__.py +25 -1
  38. notionary/blocks/column/column_element.py +44 -312
  39. notionary/blocks/column/column_list_element.py +52 -0
  40. notionary/blocks/column/column_list_markdown_node.py +50 -0
  41. notionary/blocks/column/column_markdown_node.py +59 -0
  42. notionary/blocks/column/column_models.py +26 -0
  43. notionary/blocks/divider/__init__.py +9 -2
  44. notionary/blocks/divider/divider_element.py +18 -49
  45. notionary/blocks/divider/divider_markdown_node.py +2 -1
  46. notionary/blocks/divider/divider_models.py +12 -0
  47. notionary/blocks/embed/__init__.py +9 -2
  48. notionary/blocks/embed/embed_element.py +65 -111
  49. notionary/blocks/embed/embed_markdown_node.py +3 -1
  50. notionary/blocks/embed/embed_models.py +14 -0
  51. notionary/blocks/equation/__init__.py +14 -0
  52. notionary/blocks/equation/equation_element.py +133 -0
  53. notionary/blocks/equation/equation_element_markdown_node.py +35 -0
  54. notionary/blocks/equation/equation_models.py +11 -0
  55. notionary/blocks/file/__init__.py +25 -0
  56. notionary/blocks/file/file_element.py +112 -0
  57. notionary/blocks/file/file_element_markdown_node.py +37 -0
  58. notionary/blocks/file/file_element_models.py +39 -0
  59. notionary/blocks/guards.py +22 -0
  60. notionary/blocks/heading/__init__.py +16 -2
  61. notionary/blocks/heading/heading_element.py +83 -69
  62. notionary/blocks/heading/heading_markdown_node.py +2 -1
  63. notionary/blocks/heading/heading_models.py +29 -0
  64. notionary/blocks/image_block/__init__.py +13 -0
  65. notionary/blocks/image_block/image_element.py +89 -0
  66. notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
  67. notionary/blocks/image_block/image_models.py +10 -0
  68. notionary/blocks/mixins/captions/__init__.py +4 -0
  69. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  70. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  71. notionary/blocks/models.py +174 -0
  72. notionary/blocks/numbered_list/__init__.py +12 -2
  73. notionary/blocks/numbered_list/numbered_list_element.py +48 -56
  74. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  75. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  76. notionary/blocks/paragraph/__init__.py +12 -2
  77. notionary/blocks/paragraph/paragraph_element.py +40 -66
  78. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  79. notionary/blocks/paragraph/paragraph_models.py +16 -0
  80. notionary/blocks/pdf/__init__.py +13 -0
  81. notionary/blocks/pdf/pdf_element.py +97 -0
  82. notionary/blocks/pdf/pdf_markdown_node.py +37 -0
  83. notionary/blocks/pdf/pdf_models.py +11 -0
  84. notionary/blocks/quote/__init__.py +11 -2
  85. notionary/blocks/quote/quote_element.py +45 -62
  86. notionary/blocks/quote/quote_markdown_node.py +6 -3
  87. notionary/blocks/quote/quote_models.py +18 -0
  88. notionary/blocks/registry/__init__.py +4 -0
  89. notionary/blocks/registry/block_registry.py +60 -121
  90. notionary/blocks/registry/block_registry_builder.py +115 -59
  91. notionary/blocks/rich_text/__init__.py +33 -0
  92. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  93. notionary/blocks/rich_text/rich_text_models.py +221 -0
  94. notionary/blocks/rich_text/text_inline_formatter.py +456 -0
  95. notionary/blocks/syntax_prompt_builder.py +137 -0
  96. notionary/blocks/table/__init__.py +16 -2
  97. notionary/blocks/table/table_element.py +136 -228
  98. notionary/blocks/table/table_markdown_node.py +2 -1
  99. notionary/blocks/table/table_models.py +28 -0
  100. notionary/blocks/table_of_contents/__init__.py +19 -0
  101. notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
  102. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  103. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  104. notionary/blocks/todo/__init__.py +9 -2
  105. notionary/blocks/todo/todo_element.py +52 -92
  106. notionary/blocks/todo/todo_markdown_node.py +2 -1
  107. notionary/blocks/todo/todo_models.py +19 -0
  108. notionary/blocks/toggle/__init__.py +13 -3
  109. notionary/blocks/toggle/toggle_element.py +69 -260
  110. notionary/blocks/toggle/toggle_markdown_node.py +25 -15
  111. notionary/blocks/toggle/toggle_models.py +17 -0
  112. notionary/blocks/toggleable_heading/__init__.py +6 -2
  113. notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
  114. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  115. notionary/blocks/types.py +130 -0
  116. notionary/blocks/video/__init__.py +8 -2
  117. notionary/blocks/video/video_element.py +70 -141
  118. notionary/blocks/video/video_element_models.py +10 -0
  119. notionary/blocks/video/video_markdown_node.py +13 -6
  120. notionary/database/client.py +26 -8
  121. notionary/database/database.py +13 -14
  122. notionary/database/database_filter_builder.py +2 -2
  123. notionary/database/database_provider.py +5 -4
  124. notionary/database/models.py +337 -0
  125. notionary/database/notion_database.py +6 -7
  126. notionary/file_upload/client.py +5 -7
  127. notionary/file_upload/models.py +3 -2
  128. notionary/file_upload/notion_file_upload.py +2 -3
  129. notionary/markdown/markdown_builder.py +729 -0
  130. notionary/markdown/markdown_document_model.py +228 -0
  131. notionary/{blocks → markdown}/markdown_node.py +1 -0
  132. notionary/models/notion_database_response.py +0 -338
  133. notionary/page/client.py +34 -15
  134. notionary/page/models.py +327 -0
  135. notionary/page/notion_page.py +136 -58
  136. notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
  137. notionary/page/page_content_writer.py +177 -0
  138. notionary/page/page_context.py +65 -0
  139. notionary/page/reader/handler/__init__.py +19 -0
  140. notionary/page/reader/handler/base_block_renderer.py +44 -0
  141. notionary/page/reader/handler/block_processing_context.py +35 -0
  142. notionary/page/reader/handler/block_rendering_context.py +48 -0
  143. notionary/page/reader/handler/column_list_renderer.py +51 -0
  144. notionary/page/reader/handler/column_renderer.py +60 -0
  145. notionary/page/reader/handler/line_renderer.py +73 -0
  146. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  147. notionary/page/reader/handler/toggle_renderer.py +69 -0
  148. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  149. notionary/page/reader/page_content_retriever.py +81 -0
  150. notionary/page/search_filter_builder.py +2 -1
  151. notionary/page/writer/handler/__init__.py +24 -0
  152. notionary/page/writer/handler/code_handler.py +72 -0
  153. notionary/page/writer/handler/column_handler.py +141 -0
  154. notionary/page/writer/handler/column_list_handler.py +139 -0
  155. notionary/page/writer/handler/equation_handler.py +74 -0
  156. notionary/page/writer/handler/line_handler.py +35 -0
  157. notionary/page/writer/handler/line_processing_context.py +54 -0
  158. notionary/page/writer/handler/regular_line_handler.py +86 -0
  159. notionary/page/writer/handler/table_handler.py +66 -0
  160. notionary/page/writer/handler/toggle_handler.py +155 -0
  161. notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
  162. notionary/page/writer/markdown_to_notion_converter.py +95 -0
  163. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  164. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  165. notionary/page/writer/notion_text_length_processor.py +150 -0
  166. notionary/telemetry/__init__.py +2 -2
  167. notionary/telemetry/service.py +3 -3
  168. notionary/user/__init__.py +2 -2
  169. notionary/user/base_notion_user.py +2 -1
  170. notionary/user/client.py +2 -3
  171. notionary/user/models.py +1 -0
  172. notionary/user/notion_bot_user.py +4 -5
  173. notionary/user/notion_user.py +3 -4
  174. notionary/user/notion_user_manager.py +23 -95
  175. notionary/util/__init__.py +3 -2
  176. notionary/util/fuzzy.py +2 -1
  177. notionary/util/logging_mixin.py +2 -2
  178. notionary/util/singleton_metaclass.py +1 -1
  179. notionary/workspace.py +6 -5
  180. notionary-0.2.22.dist-info/METADATA +237 -0
  181. notionary-0.2.22.dist-info/RECORD +200 -0
  182. notionary/blocks/document/__init__.py +0 -7
  183. notionary/blocks/document/document_element.py +0 -102
  184. notionary/blocks/document/document_markdown_node.py +0 -31
  185. notionary/blocks/image/__init__.py +0 -7
  186. notionary/blocks/image/image_element.py +0 -151
  187. notionary/blocks/markdown_builder.py +0 -356
  188. notionary/blocks/mention/__init__.py +0 -7
  189. notionary/blocks/mention/mention_element.py +0 -229
  190. notionary/blocks/mention/mention_markdown_node.py +0 -38
  191. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  192. notionary/blocks/prompts/element_prompt_content.py +0 -41
  193. notionary/blocks/shared/models.py +0 -713
  194. notionary/blocks/shared/notion_block_element.py +0 -37
  195. notionary/blocks/shared/text_inline_formatter.py +0 -262
  196. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  197. notionary/database/models/page_result.py +0 -10
  198. notionary/models/notion_block_response.py +0 -264
  199. notionary/models/notion_page_response.py +0 -78
  200. notionary/models/search_response.py +0 -0
  201. notionary/page/__init__.py +0 -0
  202. notionary/page/content/markdown_whitespace_processor.py +0 -80
  203. notionary/page/content/notion_text_length_utils.py +0 -87
  204. notionary/page/content/page_content_retriever.py +0 -60
  205. notionary/page/formatting/line_processor.py +0 -153
  206. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  207. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  208. notionary/page/notion_to_markdown_converter.py +0 -179
  209. notionary/page/properites/property_value_extractor.py +0 -0
  210. notionary/user/notion_user_provider.py +0 -1
  211. notionary-0.2.19.dist-info/METADATA +0 -225
  212. notionary-0.2.19.dist-info/RECORD +0 -150
  213. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  214. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  215. /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
  216. /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
  217. /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
  218. /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
  219. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  220. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from notionary.blocks.column.column_list_element import ColumnListElement
6
+ from notionary.page.writer.handler.line_handler import (
7
+ LineHandler,
8
+ LineProcessingContext,
9
+ )
10
+ from notionary.page.writer.handler.line_processing_context import ParentBlockContext
11
+
12
+
13
+ class ColumnListHandler(LineHandler):
14
+ """Handles column list elements - both start and end.
15
+ Syntax:
16
+ ::: columns # Start column list
17
+ ::: column # Individual column
18
+ Content here
19
+ ::: # End column
20
+ ::: column # Another column
21
+ More content
22
+ ::: # End column
23
+ ::: # End column list
24
+ """
25
+
26
+ def __init__(self):
27
+ super().__init__()
28
+ self._start_pattern = re.compile(r"^:::\s*columns?\s*$", re.IGNORECASE)
29
+ self._end_pattern = re.compile(r"^:::\s*$")
30
+
31
+ def _can_handle(self, context: LineProcessingContext) -> bool:
32
+ return self._is_column_list_start(context) or self._is_column_list_end(context)
33
+
34
+ async def _process(self, context: LineProcessingContext) -> None:
35
+ if self._is_column_list_start(context):
36
+ await self._start_column_list(context)
37
+ context.was_processed = True
38
+ context.should_continue = True
39
+ return
40
+
41
+ if self._is_column_list_end(context):
42
+ await self._finalize_column_list(context)
43
+ context.was_processed = True
44
+ context.should_continue = True
45
+
46
+ def _is_column_list_start(self, context: LineProcessingContext) -> bool:
47
+ """Check if line starts a column list (::: columns)."""
48
+ return self._start_pattern.match(context.line.strip()) is not None
49
+
50
+ def _is_column_list_end(self, context: LineProcessingContext) -> bool:
51
+ """Check if we need to end a column list (:::)."""
52
+ if not self._end_pattern.match(context.line.strip()):
53
+ return False
54
+
55
+ if not context.parent_stack:
56
+ return False
57
+
58
+ # Check if top of stack is a ColumnList
59
+ current_parent = context.parent_stack[-1]
60
+ return issubclass(current_parent.element_type, ColumnListElement)
61
+
62
+ async def _start_column_list(self, context: LineProcessingContext) -> None:
63
+ """Start a new column list."""
64
+ # Create ColumnList block using the element from registry
65
+ column_list_element = None
66
+ for element in context.block_registry.get_elements():
67
+ if issubclass(element, ColumnListElement):
68
+ column_list_element = element
69
+ break
70
+
71
+ if not column_list_element:
72
+ return
73
+
74
+ # Create the block
75
+ result = await column_list_element.markdown_to_notion(context.line)
76
+ if not result:
77
+ return
78
+
79
+ block = result
80
+
81
+ # Push to parent stack
82
+ parent_context = ParentBlockContext(
83
+ block=block,
84
+ element_type=column_list_element,
85
+ child_lines=[],
86
+ )
87
+ context.parent_stack.append(parent_context)
88
+
89
+ async def _finalize_column_list(self, context: LineProcessingContext) -> None:
90
+ """Finalize a column list and add it to result_blocks."""
91
+ column_list_context = context.parent_stack.pop()
92
+ await self._assign_column_list_children_if_any(column_list_context, context)
93
+
94
+ # Check if we have a parent context to add this column_list to
95
+ if context.parent_stack:
96
+ # Add this column_list as a child block to the parent (like Toggle)
97
+ parent_context = context.parent_stack[-1]
98
+ parent_context.add_child_block(column_list_context.block)
99
+
100
+ else:
101
+ # No parent, add to top level
102
+ context.result_blocks.append(column_list_context.block)
103
+
104
+ async def _assign_column_list_children_if_any(
105
+ self, column_list_context: ParentBlockContext, context: LineProcessingContext
106
+ ) -> None:
107
+ """Collect and assign any column children blocks inside this column list."""
108
+ all_children = []
109
+
110
+ # Process text lines
111
+ if column_list_context.child_lines:
112
+ children_text = "\n".join(column_list_context.child_lines)
113
+ children_blocks = await self._convert_children_text(
114
+ children_text, context.block_registry
115
+ )
116
+ all_children.extend(children_blocks)
117
+
118
+ if column_list_context.child_blocks:
119
+ all_children.extend(column_list_context.child_blocks)
120
+
121
+ # Filter only column blocks
122
+ column_children = [
123
+ block
124
+ for block in all_children
125
+ if hasattr(block, "column") and getattr(block, "type", None) == "column"
126
+ ]
127
+ column_list_context.block.column_list.children = column_children
128
+
129
+ async def _convert_children_text(self, text: str, block_registry) -> list:
130
+ """Convert children text to blocks."""
131
+ from notionary.page.writer.markdown_to_notion_converter import (
132
+ MarkdownToNotionConverter,
133
+ )
134
+
135
+ if not text.strip():
136
+ return []
137
+
138
+ child_converter = MarkdownToNotionConverter(block_registry)
139
+ return await child_converter.process_lines(text)
@@ -0,0 +1,74 @@
1
+ import re
2
+
3
+ from notionary.blocks.equation.equation_element import EquationElement
4
+ from notionary.page.writer.handler.line_handler import (
5
+ LineHandler,
6
+ LineProcessingContext,
7
+ )
8
+
9
+
10
+ class EquationHandler(LineHandler):
11
+ """Handles equation block specific logic with batching.
12
+
13
+ Markdown syntax:
14
+ $$
15
+ \sum_{i=1}^n i = \frac{n(n+1)}{2} \\
16
+ \sum_{i=1}^n i^2 = \frac{n(n+1)(2n+1)}{6} \\
17
+ \sum_{i=1}^n i^3 = \left(\frac{n(n+1)}{2}\right)^2
18
+ $$
19
+ """
20
+
21
+ def __init__(self):
22
+ super().__init__()
23
+ self._equation_start_pattern = re.compile(r"^\$\$\s*$")
24
+ self._equation_end_pattern = re.compile(r"^\$\$\s*$")
25
+
26
+ def _can_handle(self, context: LineProcessingContext) -> bool:
27
+ if self._is_inside_parent_context(context):
28
+ return False
29
+ return self._is_equation_start(context)
30
+
31
+ async def _process(self, context: LineProcessingContext) -> None:
32
+ if self._is_equation_start(context):
33
+ await self._process_complete_equation_block(context)
34
+ self._mark_processed(context)
35
+
36
+ def _is_equation_start(self, context: LineProcessingContext) -> bool:
37
+ """Check if this line starts an equation block."""
38
+ return self._equation_start_pattern.match(context.line.strip()) is not None
39
+
40
+ def _is_inside_parent_context(self, context: LineProcessingContext) -> bool:
41
+ """Check if we're currently inside any parent context (toggle, heading, etc.)."""
42
+ return len(context.parent_stack) > 0
43
+
44
+ async def _process_complete_equation_block(
45
+ self, context: LineProcessingContext
46
+ ) -> None:
47
+ """Process the entire equation block in one go using EquationElement."""
48
+ equation_lines, lines_to_consume = self._collect_equation_lines(context)
49
+
50
+ block = EquationElement.create_from_markdown_block(
51
+ opening_line=context.line, equation_lines=equation_lines
52
+ )
53
+
54
+ if block:
55
+ context.lines_consumed = lines_to_consume
56
+ context.result_blocks.append(block)
57
+
58
+ def _collect_equation_lines(
59
+ self, context: LineProcessingContext
60
+ ) -> tuple[list[str], int]:
61
+ """Collect lines until closing $$ fence and return (lines, count_to_consume)."""
62
+ lines = []
63
+ for idx, ln in enumerate(context.get_remaining_lines()):
64
+ if self._equation_end_pattern.match(ln.strip()):
65
+ return lines, idx + 1
66
+ lines.append(ln)
67
+ # No closing fence: consume all remaining
68
+ rem = context.get_remaining_lines()
69
+ return rem, len(rem)
70
+
71
+ def _mark_processed(self, context: LineProcessingContext) -> None:
72
+ """Mark context as processed and continue."""
73
+ context.was_processed = True
74
+ context.should_continue = True
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from notionary.page.writer.handler.line_processing_context import LineProcessingContext
7
+
8
+
9
+ class LineHandler(ABC):
10
+ """Abstract base class for line handlers."""
11
+
12
+ def __init__(self):
13
+ self._next_handler: Optional[LineHandler] = None
14
+
15
+ def set_next(self, handler: LineHandler) -> LineHandler:
16
+ """Set the next handler in the chain."""
17
+ self._next_handler = handler
18
+ return handler
19
+
20
+ async def handle(self, context: LineProcessingContext) -> None:
21
+ """Handle the line or pass to next handler."""
22
+ if self._can_handle(context):
23
+ await self._process(context)
24
+ elif self._next_handler:
25
+ await self._next_handler.handle(context)
26
+
27
+ @abstractmethod
28
+ def _can_handle(self, context: LineProcessingContext) -> bool:
29
+ """Check if this handler can process the current line."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ async def _process(self, context: LineProcessingContext) -> None:
34
+ """Process the line and update context."""
35
+ pass
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.models import BlockCreateRequest
8
+ from notionary.blocks.registry.block_registry import BlockRegistry
9
+
10
+
11
+ @dataclass
12
+ class ParentBlockContext:
13
+ """Context for a block that expects children."""
14
+
15
+ block: BlockCreateRequest
16
+ element_type: BaseBlockElement
17
+ child_lines: list[str]
18
+ child_blocks: list[BlockCreateRequest] = field(default_factory=list)
19
+
20
+ def add_child_line(self, content: str):
21
+ """Adds a child line."""
22
+ self.child_lines.append(content)
23
+
24
+ def add_child_block(self, block: BlockCreateRequest):
25
+ """Adds a processed child block."""
26
+ self.child_blocks.append(block)
27
+
28
+ def has_children(self) -> bool:
29
+ """Checks if children have been collected."""
30
+ return len(self.child_lines) > 0 or len(self.child_blocks) > 0
31
+
32
+
33
+ @dataclass
34
+ class LineProcessingContext:
35
+ """Context that gets passed through the handler chain."""
36
+
37
+ line: str
38
+ result_blocks: list[BlockCreateRequest]
39
+ parent_stack: list[ParentBlockContext]
40
+ block_registry: BlockRegistry
41
+
42
+ all_lines: Optional[list[str]] = None
43
+ current_line_index: Optional[int] = None
44
+ lines_consumed: int = 0
45
+
46
+ # Result indicators
47
+ was_processed: bool = False
48
+ should_continue: bool = False
49
+
50
+ def get_remaining_lines(self) -> list[str]:
51
+ """Get all remaining lines from current position."""
52
+ if self.all_lines is None or self.current_line_index is None:
53
+ return []
54
+ return self.all_lines[self.current_line_index + 1 :]
@@ -0,0 +1,86 @@
1
+ from notionary.blocks.column.column_element import ColumnElement
2
+ from notionary.blocks.column.column_list_element import ColumnListElement
3
+ from notionary.page.writer.handler import LineHandler, LineProcessingContext
4
+
5
+
6
+ class RegularLineHandler(LineHandler):
7
+ """Handles regular lines - respects parent contexts like columns."""
8
+
9
+ def _can_handle(self, context: LineProcessingContext) -> bool:
10
+ return context.line.strip()
11
+
12
+ async def _process(self, context: LineProcessingContext) -> None:
13
+ if self._is_in_column_context(context):
14
+ self._add_to_column_context(context)
15
+ context.was_processed = True
16
+ context.should_continue = True
17
+ return
18
+
19
+ block_created = await self._process_single_line_content(context)
20
+ if not block_created:
21
+ await self._process_as_paragraph(context)
22
+
23
+ context.was_processed = True
24
+
25
+ def _is_in_column_context(self, context: LineProcessingContext) -> bool:
26
+ """Check if we're inside a Column/ColumnList context."""
27
+ if not context.parent_stack:
28
+ return False
29
+
30
+ current_parent = context.parent_stack[-1]
31
+ return issubclass(
32
+ current_parent.element_type, (ColumnListElement, ColumnElement)
33
+ )
34
+
35
+ def _add_to_column_context(self, context: LineProcessingContext) -> None:
36
+ """Add line as child to the current Column context."""
37
+ context.parent_stack[-1].add_child_line(context.line)
38
+
39
+ async def _process_single_line_content(
40
+ self, context: LineProcessingContext
41
+ ) -> bool:
42
+ """Process a regular line for simple elements (lists, etc.)."""
43
+ specialized_elements = self._get_specialized_elements()
44
+
45
+ for element in context.block_registry.get_elements():
46
+
47
+ if issubclass(element, specialized_elements):
48
+ continue
49
+
50
+ result = await element.markdown_to_notion(context.line)
51
+ if not result:
52
+ continue
53
+
54
+ context.result_blocks.append(result)
55
+
56
+ return True
57
+
58
+ return False
59
+
60
+ async def _process_as_paragraph(self, context: LineProcessingContext) -> None:
61
+ """Process a line as a paragraph."""
62
+ from notionary.blocks.paragraph.paragraph_element import ParagraphElement
63
+
64
+ paragraph_element = ParagraphElement()
65
+ result = await paragraph_element.markdown_to_notion(context.line)
66
+
67
+ if result:
68
+ context.result_blocks.append(result)
69
+
70
+ def _get_specialized_elements(self):
71
+ """Get tuple of elements that have specialized handlers."""
72
+ from notionary.blocks.code import CodeElement
73
+ from notionary.blocks.paragraph import ParagraphElement
74
+ from notionary.blocks.table import TableElement
75
+ from notionary.blocks.toggle import ToggleElement
76
+ from notionary.blocks.toggleable_heading import ToggleableHeadingElement
77
+
78
+ return (
79
+ ColumnListElement,
80
+ ColumnElement,
81
+ ToggleElement,
82
+ ToggleableHeadingElement,
83
+ TableElement,
84
+ CodeElement,
85
+ ParagraphElement,
86
+ )
@@ -0,0 +1,66 @@
1
+ import re
2
+
3
+ from notionary.blocks.table.table_element import TableElement
4
+ from notionary.page.writer.handler import LineHandler, LineProcessingContext
5
+
6
+
7
+ class TableHandler(LineHandler):
8
+ """Handles table specific logic with batching."""
9
+
10
+ def __init__(self):
11
+ super().__init__()
12
+ self._table_row_pattern = re.compile(r"^\s*\|(.+)\|\s*$")
13
+ self._separator_pattern = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
14
+
15
+ def _can_handle(self, context: LineProcessingContext) -> bool:
16
+ if self._is_inside_parent_context(context):
17
+ return False
18
+ return self._is_table_start(context)
19
+
20
+ async def _process(self, context: LineProcessingContext) -> None:
21
+ if not self._is_table_start(context):
22
+ return
23
+
24
+ await self._process_complete_table(context)
25
+ context.was_processed = True
26
+ context.should_continue = True
27
+
28
+ def _is_inside_parent_context(self, context: LineProcessingContext) -> bool:
29
+ """Check if we're currently inside any parent context (toggle, heading, etc.)."""
30
+ return len(context.parent_stack) > 0
31
+
32
+ def _is_table_start(self, context: LineProcessingContext) -> bool:
33
+ """Check if this line starts a table."""
34
+ return self._table_row_pattern.match(context.line.strip()) is not None
35
+
36
+ async def _process_complete_table(self, context: LineProcessingContext) -> None:
37
+ """Process the entire table in one go using TableElement."""
38
+ # Collect all table lines (including the current one)
39
+ table_lines = [context.line]
40
+ remaining_lines = context.get_remaining_lines()
41
+ lines_to_consume = 0
42
+
43
+ # Find all consecutive table rows
44
+ for i, line in enumerate(remaining_lines):
45
+ line_stripped = line.strip()
46
+ if not line_stripped:
47
+ # Empty line - continue to allow for spacing in tables
48
+ table_lines.append(line)
49
+ continue
50
+
51
+ if self._table_row_pattern.match(
52
+ line_stripped
53
+ ) or self._separator_pattern.match(line_stripped):
54
+ table_lines.append(line)
55
+ else:
56
+ # Not a table line - stop here
57
+ lines_to_consume = i
58
+ break
59
+ else:
60
+ lines_to_consume = len(remaining_lines)
61
+
62
+ block = await TableElement.create_from_markdown_table(table_lines)
63
+
64
+ if block:
65
+ context.lines_consumed = lines_to_consume
66
+ context.result_blocks.append(block)
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from notionary.blocks.toggle.toggle_element import ToggleElement
6
+ from notionary.page.writer.handler import (
7
+ LineHandler,
8
+ LineProcessingContext,
9
+ ParentBlockContext,
10
+ )
11
+
12
+
13
+ class ToggleHandler(LineHandler):
14
+ """Handles regular toggle blocks with ultra-simplified +++ syntax."""
15
+
16
+ def __init__(self):
17
+ super().__init__()
18
+ self._start_pattern = re.compile(r"^[+]{3}\s+(.+)$", re.IGNORECASE)
19
+ self._end_pattern = re.compile(r"^[+]{3}\s*$")
20
+
21
+ def _can_handle(self, context: LineProcessingContext) -> bool:
22
+ return (
23
+ self._is_toggle_start(context)
24
+ or self._is_toggle_end(context)
25
+ or self._is_toggle_content(context)
26
+ )
27
+
28
+ async def _process(self, context: LineProcessingContext) -> None:
29
+ # Explicit, readable branches (small duplication is acceptable)
30
+ if self._is_toggle_start(context):
31
+ await self._start_toggle(context)
32
+ context.was_processed = True
33
+ context.should_continue = True
34
+
35
+ if self._is_toggle_end(context):
36
+ await self._finalize_toggle(context)
37
+ context.was_processed = True
38
+ context.should_continue = True
39
+
40
+ if self._is_toggle_content(context):
41
+ self._add_toggle_content(context)
42
+ context.was_processed = True
43
+ context.should_continue = True
44
+
45
+ def _is_toggle_start(self, context: LineProcessingContext) -> bool:
46
+ """Check if line starts a toggle (+++ Title)."""
47
+ line = context.line.strip()
48
+
49
+ # Must match our pattern
50
+ if not self._start_pattern.match(line):
51
+ return False
52
+
53
+ # But NOT match toggleable heading pattern (has # after +++)
54
+ toggleable_heading_pattern = re.compile(r"^[+]{3}#{1,3}\s+.+$", re.IGNORECASE)
55
+ if toggleable_heading_pattern.match(line):
56
+ return False
57
+
58
+ return True
59
+
60
+ def _is_toggle_end(self, context: LineProcessingContext) -> bool:
61
+ """Check if we need to end a toggle (+++)."""
62
+ if not self._end_pattern.match(context.line.strip()):
63
+ return False
64
+
65
+ if not context.parent_stack:
66
+ return False
67
+
68
+ # Check if top of stack is a Toggle
69
+ current_parent = context.parent_stack[-1]
70
+ return issubclass(current_parent.element_type, ToggleElement)
71
+
72
+ async def _start_toggle(self, context: LineProcessingContext) -> None:
73
+ """Start a new toggle block."""
74
+ toggle_element = ToggleElement()
75
+
76
+ # Create the block
77
+ result = await toggle_element.markdown_to_notion(context.line)
78
+ if not result:
79
+ return
80
+
81
+ block = result
82
+
83
+ # Push to parent stack
84
+ parent_context = ParentBlockContext(
85
+ block=block,
86
+ element_type=ToggleElement,
87
+ child_lines=[],
88
+ )
89
+ context.parent_stack.append(parent_context)
90
+
91
+ async def _finalize_toggle(self, context: LineProcessingContext) -> None:
92
+ """Finalize a toggle block and add it to result_blocks."""
93
+ toggle_context = context.parent_stack.pop()
94
+
95
+ if toggle_context.has_children():
96
+ all_children = await self._get_all_children(
97
+ toggle_context, context.block_registry
98
+ )
99
+ toggle_context.block.toggle.children = all_children
100
+
101
+ # Check if we have a parent context to add this toggle to
102
+ if context.parent_stack:
103
+ # Add this toggle as a child block to the parent
104
+ parent_context = context.parent_stack[-1]
105
+ parent_context.add_child_block(toggle_context.block)
106
+ else:
107
+ # No parent, add to top level
108
+ context.result_blocks.append(toggle_context.block)
109
+
110
+ def _is_toggle_content(self, context: LineProcessingContext) -> bool:
111
+ """Check if we're inside a toggle context and should handle content."""
112
+ if not context.parent_stack:
113
+ return False
114
+
115
+ current_parent = context.parent_stack[-1]
116
+ if not issubclass(current_parent.element_type, ToggleElement):
117
+ return False
118
+
119
+ # Handle all content inside toggle (not start/end patterns)
120
+ line = context.line.strip()
121
+ return not (self._start_pattern.match(line) or self._end_pattern.match(line))
122
+
123
+ def _add_toggle_content(self, context: LineProcessingContext) -> None:
124
+ """Add content to the current toggle context."""
125
+ context.parent_stack[-1].add_child_line(context.line)
126
+
127
+ async def _convert_children_text(self, text: str, block_registry) -> list:
128
+ """Convert children text to blocks."""
129
+ from notionary.page.writer.markdown_to_notion_converter import (
130
+ MarkdownToNotionConverter,
131
+ )
132
+
133
+ if not text.strip():
134
+ return []
135
+
136
+ child_converter = MarkdownToNotionConverter(block_registry)
137
+ return await child_converter.process_lines(text)
138
+
139
+ async def _get_all_children(self, parent_context, block_registry) -> list:
140
+ """Helper method to combine text-based and direct block children."""
141
+ children_blocks = []
142
+
143
+ # Process text lines
144
+ if parent_context.child_lines:
145
+ children_text = "\n".join(parent_context.child_lines)
146
+ text_blocks = await self._convert_children_text(
147
+ children_text, block_registry
148
+ )
149
+ children_blocks.extend(text_blocks)
150
+
151
+ # Add direct blocks (like processed columns)
152
+ if hasattr(parent_context, "child_blocks") and parent_context.child_blocks:
153
+ children_blocks.extend(parent_context.child_blocks)
154
+
155
+ return children_blocks