notionary 0.2.22__py3-none-any.whl → 0.2.24__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 (105) hide show
  1. notionary/__init__.py +1 -1
  2. notionary/blocks/__init__.py +3 -1
  3. notionary/blocks/audio/__init__.py +0 -2
  4. notionary/blocks/audio/audio_element.py +92 -49
  5. notionary/blocks/audio/audio_markdown_node.py +4 -17
  6. notionary/blocks/bookmark/__init__.py +0 -2
  7. notionary/blocks/bookmark/bookmark_markdown_node.py +5 -21
  8. notionary/blocks/breadcrumbs/__init__.py +0 -2
  9. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +2 -21
  10. notionary/blocks/bulleted_list/__init__.py +0 -2
  11. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +3 -17
  12. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -1
  13. notionary/blocks/callout/__init__.py +0 -2
  14. notionary/blocks/callout/callout_markdown_node.py +4 -18
  15. notionary/blocks/callout/callout_models.py +3 -4
  16. notionary/blocks/child_database/child_database_element.py +2 -4
  17. notionary/blocks/code/code_markdown_node.py +5 -19
  18. notionary/blocks/column/__init__.py +0 -4
  19. notionary/blocks/column/column_list_markdown_node.py +3 -19
  20. notionary/blocks/column/column_markdown_node.py +4 -21
  21. notionary/blocks/divider/__init__.py +0 -2
  22. notionary/blocks/divider/divider_markdown_node.py +2 -16
  23. notionary/blocks/embed/__init__.py +0 -2
  24. notionary/blocks/embed/embed_markdown_node.py +4 -17
  25. notionary/blocks/equation/__init__.py +0 -1
  26. notionary/blocks/equation/equation_element_markdown_node.py +3 -15
  27. notionary/blocks/file/__init__.py +0 -2
  28. notionary/blocks/file/file_element.py +67 -46
  29. notionary/blocks/file/file_element_markdown_node.py +4 -17
  30. notionary/blocks/heading/__init__.py +0 -2
  31. notionary/blocks/heading/heading_markdown_node.py +5 -19
  32. notionary/blocks/heading/heading_models.py +3 -3
  33. notionary/blocks/image_block/__init__.py +0 -2
  34. notionary/blocks/image_block/image_element.py +66 -25
  35. notionary/blocks/image_block/image_markdown_node.py +5 -20
  36. notionary/{markdown → blocks/markdown}/markdown_builder.py +29 -233
  37. notionary/blocks/markdown/markdown_node.py +25 -0
  38. notionary/blocks/mixins/file_upload/__init__.py +3 -0
  39. notionary/blocks/mixins/file_upload/file_upload_mixin.py +320 -0
  40. notionary/blocks/numbered_list/__init__.py +0 -1
  41. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -17
  42. notionary/blocks/numbered_list/numbered_list_models.py +3 -3
  43. notionary/blocks/paragraph/__init__.py +0 -2
  44. notionary/blocks/paragraph/paragraph_markdown_node.py +3 -13
  45. notionary/blocks/pdf/__init__.py +0 -2
  46. notionary/blocks/pdf/pdf_element.py +81 -32
  47. notionary/blocks/pdf/pdf_markdown_node.py +5 -18
  48. notionary/blocks/quote/__init__.py +0 -2
  49. notionary/blocks/quote/quote_markdown_node.py +3 -13
  50. notionary/blocks/registry/__init__.py +1 -2
  51. notionary/blocks/registry/block_registry.py +116 -61
  52. notionary/blocks/rich_text/text_inline_formatter.py +1 -1
  53. notionary/blocks/table/__init__.py +0 -2
  54. notionary/blocks/table/table_markdown_node.py +17 -16
  55. notionary/blocks/table_of_contents/__init__.py +0 -2
  56. notionary/blocks/table_of_contents/table_of_contents_element.py +27 -15
  57. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +3 -17
  58. notionary/blocks/table_of_contents/table_of_contents_models.py +2 -2
  59. notionary/blocks/todo/__init__.py +0 -2
  60. notionary/blocks/todo/todo_markdown_node.py +9 -20
  61. notionary/blocks/todo/todo_models.py +2 -3
  62. notionary/blocks/toggle/__init__.py +0 -2
  63. notionary/blocks/toggle/toggle_markdown_node.py +5 -19
  64. notionary/blocks/toggleable_heading/__init__.py +0 -2
  65. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +6 -23
  66. notionary/blocks/video/__init__.py +0 -2
  67. notionary/blocks/video/video_element.py +110 -34
  68. notionary/blocks/video/video_markdown_node.py +4 -15
  69. notionary/comments/__init__.py +26 -0
  70. notionary/comments/client.py +211 -0
  71. notionary/comments/models.py +129 -0
  72. notionary/file_upload/client.py +3 -2
  73. notionary/file_upload/models.py +10 -1
  74. notionary/file_upload/notion_file_upload.py +5 -5
  75. notionary/page/client.py +1 -6
  76. notionary/page/markdown_whitespace_processor.py +129 -0
  77. notionary/page/notion_page.py +87 -48
  78. notionary/page/page_content_deleting_service.py +1 -1
  79. notionary/page/page_content_writer.py +32 -129
  80. notionary/page/page_context.py +0 -6
  81. notionary/page/reader/handler/column_list_renderer.py +2 -2
  82. notionary/page/reader/handler/column_renderer.py +2 -2
  83. notionary/page/reader/handler/line_renderer.py +2 -2
  84. notionary/page/reader/handler/toggle_renderer.py +2 -2
  85. notionary/page/reader/handler/toggleable_heading_renderer.py +2 -2
  86. notionary/page/writer/handler/toggle_handler.py +8 -4
  87. notionary/page/writer/handler/toggleable_heading_handler.py +3 -2
  88. notionary/page/writer/markdown_to_notion_converter.py +74 -30
  89. notionary/schemas/__init__.py +3 -0
  90. notionary/schemas/base.py +73 -0
  91. notionary/shared/__init__.py +3 -0
  92. notionary/{blocks/rich_text → shared}/name_to_id_resolver.py +0 -2
  93. {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/METADATA +15 -2
  94. {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/RECORD +97 -95
  95. notionary/blocks/guards.py +0 -22
  96. notionary/blocks/registry/block_registry_builder.py +0 -264
  97. notionary/markdown/makdown_document_model.py +0 -0
  98. notionary/markdown/markdown_document_model.py +0 -228
  99. notionary/markdown/markdown_node.py +0 -30
  100. notionary/models/notion_database_response.py +0 -0
  101. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +0 -73
  102. notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
  103. /notionary/{markdown/___init__.py → blocks/markdown/markdown_document_model.py} +0 -0
  104. {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/LICENSE +0 -0
  105. {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/WHEEL +0 -0
@@ -5,16 +5,20 @@ import random
5
5
  from typing import TYPE_CHECKING, Any, Callable, Optional, Union
6
6
 
7
7
  from notionary.blocks.client import NotionBlockClient
8
+ from notionary.comments import CommentClient, Comment
8
9
  from notionary.blocks.syntax_prompt_builder import SyntaxPromptBuilder
9
10
  from notionary.blocks.models import DatabaseParent
10
11
  from notionary.blocks.registry.block_registry import BlockRegistry
11
- from notionary.blocks.registry.block_registry_builder import BlockRegistryBuilder
12
12
  from notionary.database.client import NotionDatabaseClient
13
- from notionary.markdown.markdown_builder import MarkdownBuilder
13
+ from notionary.file_upload.client import NotionFileUploadClient
14
+ from notionary.blocks.markdown.markdown_builder import MarkdownBuilder
15
+ from notionary.schemas import NotionContentSchema
16
+ from notionary.page import page_context
14
17
  from notionary.page.client import NotionPageClient
15
18
  from notionary.page.models import NotionPageResponse
16
19
  from notionary.page.page_content_deleting_service import PageContentDeletingService
17
20
  from notionary.page.page_content_writer import PageContentWriter
21
+ from notionary.page.page_context import PageContextProvider, page_context
18
22
  from notionary.page.property_formatter import NotionPropertyFormatter
19
23
  from notionary.page.reader.page_content_retriever import PageContentRetriever
20
24
  from notionary.page.utils import extract_property_value
@@ -24,6 +28,7 @@ from notionary.util.fuzzy import find_best_match
24
28
  if TYPE_CHECKING:
25
29
  from notionary import NotionDatabase
26
30
 
31
+
27
32
  class NotionPage(LoggingMixin):
28
33
  """
29
34
  Managing content and metadata of a Notion page.
@@ -56,8 +61,9 @@ class NotionPage(LoggingMixin):
56
61
 
57
62
  self._client = NotionPageClient(token=token)
58
63
  self._block_client = NotionBlockClient(token=token)
64
+ self._comment_client = CommentClient(token=token)
59
65
  self._page_data = None
60
-
66
+
61
67
  self.block_element_registry = BlockRegistry.create_registry()
62
68
 
63
69
  self._page_content_writer = PageContentWriter(
@@ -74,6 +80,8 @@ class NotionPage(LoggingMixin):
74
80
  block_registry=self.block_element_registry,
75
81
  )
76
82
 
83
+ self.page_context_provider = self._setup_page_context_provider()
84
+
77
85
  @classmethod
78
86
  async def from_page_id(
79
87
  cls, page_id: str, token: Optional[str] = None
@@ -202,17 +210,49 @@ class NotionPage(LoggingMixin):
202
210
  def is_in_trash(self) -> bool:
203
211
  return self._is_in_trash
204
212
 
205
- @property
206
- def block_registry_builder(self) -> BlockRegistryBuilder:
207
- """
208
- Returns the block registry builder for this page.
209
- """
210
- return self.block_element_registry.builder
211
-
212
213
  def get_prompt_information(self) -> str:
213
214
  markdown_syntax_builder = SyntaxPromptBuilder()
214
215
  return markdown_syntax_builder.build_concise_reference()
215
216
 
217
+ async def get_comments(self) -> list[Comment]:
218
+ return await self._comment_client.list_all_comments_for_page(
219
+ page_id=self._page_id
220
+ )
221
+
222
+ async def post_comment(
223
+ self,
224
+ content: str,
225
+ *,
226
+ discussion_id: Optional[str] = None,
227
+ rich_text: Optional[list[dict[str, Any]]] = None,
228
+ ) -> Optional[Comment]:
229
+ """
230
+ Post a comment on this page.
231
+
232
+ Args:
233
+ content: The plain text content of the comment
234
+ discussion_id: Optional discussion ID to reply to an existing discussion
235
+ rich_text: Optional rich text formatting for the comment content
236
+
237
+ Returns:
238
+ Comment: The created comment object, or None if creation failed
239
+ """
240
+ try:
241
+ # Use the comment client to create the comment
242
+ comment = await self._comment_client.create_comment(
243
+ page_id=self._page_id,
244
+ content=content,
245
+ discussion_id=discussion_id,
246
+ rich_text=rich_text,
247
+ )
248
+ self.logger.info(f"Successfully posted comment on page '{self._title}'")
249
+ return comment
250
+ except Exception as e:
251
+ self.logger.error(
252
+ f"Failed to post comment on page '{self._title}': {str(e)}"
253
+ )
254
+ return None
255
+
216
256
  async def set_title(self, title: str) -> str:
217
257
  """
218
258
  Set the title of the page.
@@ -234,43 +274,31 @@ class NotionPage(LoggingMixin):
234
274
 
235
275
  async def append_markdown(
236
276
  self,
237
- content: Union[str, Callable[[MarkdownBuilder], MarkdownBuilder]],
238
- *,
239
- prepend_table_of_contents: bool = False,
240
- append_divider: bool = False,
277
+ content: Union[
278
+ str, Callable[[MarkdownBuilder], MarkdownBuilder], NotionContentSchema
279
+ ],
241
280
  ) -> bool:
242
281
  """
243
- Append markdown content to the page.
244
-
245
- Args:
246
- content: Either raw markdown text OR a callback function that receives a MarkdownBuilder
247
- prepend_table_of_contents: Whether to prepend table of contents
248
- append_divider: Whether to append a divider
249
-
250
- Returns:
251
- bool: True if successful, False otherwise
282
+ Append markdown content to the page using text, builder callback, MarkdownDocumentModel, or NotionContentSchema.
252
283
  """
253
- result = await self._page_content_writer.append_markdown(
254
- content=content,
255
- append_divider=append_divider,
256
- prepend_table_of_contents=prepend_table_of_contents,
257
- )
258
- return result is not None
284
+ async with page_context(self.page_context_provider):
285
+ result = await self._page_content_writer.append_markdown(
286
+ content=content,
287
+ )
288
+ return result is not None
259
289
 
260
290
  async def replace_content(
261
291
  self,
262
- content: Union[str, Callable[[MarkdownBuilder], MarkdownBuilder]],
263
- *,
264
- prepend_table_of_contents: bool = False,
265
- append_divider: bool = False,
292
+ content: Union[
293
+ str, Callable[[MarkdownBuilder], MarkdownBuilder], NotionContentSchema
294
+ ],
266
295
  ) -> bool:
267
296
  """
268
297
  Replace the entire page content with new markdown content.
269
298
 
270
299
  Args:
271
- content: Either raw markdown text OR a callback function that receives a MarkdownBuilder
272
- prepend_table_of_contents: Whether to prepend table of contents
273
- append_divider: Whether to append a divider
300
+ content: Either raw markdown text, a callback function that receives a MarkdownBuilder,
301
+ a MarkdownDocumentModel, or a NotionContentSchema
274
302
 
275
303
  Returns:
276
304
  bool: True if successful, False otherwise
@@ -281,8 +309,6 @@ class NotionPage(LoggingMixin):
281
309
 
282
310
  result = await self._page_content_writer.append_markdown(
283
311
  content=content,
284
- prepend_table_of_contents=prepend_table_of_contents,
285
- append_divider=append_divider,
286
312
  )
287
313
  return result is not None
288
314
 
@@ -320,27 +346,33 @@ class NotionPage(LoggingMixin):
320
346
 
321
347
  self.logger.error(f"Error updating page emoji: {str(e)}")
322
348
  return None
323
-
349
+
324
350
  async def create_child_database(self, title: str) -> NotionDatabase:
325
351
  from notionary import NotionDatabase
352
+
326
353
  database_client = NotionDatabaseClient(token=self._client.token)
327
-
328
- create_database_response = await database_client.create_database(
354
+
355
+ create_database_response = await database_client.create_database(
329
356
  title=title,
330
357
  parent_page_id=self._page_id,
331
358
  )
332
-
333
- return await NotionDatabase.from_database_id(id=create_database_response.id, token=self._client.token)
334
-
359
+
360
+ return await NotionDatabase.from_database_id(
361
+ id=create_database_response.id, token=self._client.token
362
+ )
363
+
335
364
  async def create_child_page(self, title: str) -> NotionPage:
336
365
  from notionary import NotionPage
366
+
337
367
  child_page_response = await self._client.create_page(
338
368
  parent_page_id=self._page_id,
339
369
  title=title,
340
370
  )
341
-
342
- return await NotionPage.from_page_id(page_id=child_page_response.id, token=self._client.token)
343
-
371
+
372
+ return await NotionPage.from_page_id(
373
+ page_id=child_page_response.id, token=self._client.token
374
+ )
375
+
344
376
  async def set_external_icon(self, url: str) -> Optional[str]:
345
377
  """
346
378
  Sets the page icon to an external image.
@@ -636,4 +668,11 @@ class NotionPage(LoggingMixin):
636
668
  """Extract parent database ID from page response."""
637
669
  parent = page_response.parent
638
670
  if isinstance(parent, DatabaseParent):
639
- return parent.database_id
671
+ return parent.database_id
672
+
673
+ def _setup_page_context_provider(self) -> PageContextProvider:
674
+ return PageContextProvider(
675
+ page_id=self._page_id,
676
+ database_client=NotionDatabaseClient(token=self._client.token),
677
+ file_upload_client=NotionFileUploadClient(),
678
+ )
@@ -27,7 +27,7 @@ class PageContentDeletingService(LoggingMixin):
27
27
  return None
28
28
 
29
29
  # Use PageContentRetriever for sophisticated markdown conversion
30
- deleted_content = self._content_retriever._convert_blocks_to_markdown(
30
+ deleted_content = await self._content_retriever._convert_blocks_to_markdown(
31
31
  children_response.results, indent_level=0
32
32
  )
33
33
 
@@ -1,10 +1,10 @@
1
1
  from typing import Callable, Optional, Union
2
2
 
3
3
  from notionary.blocks.client import NotionBlockClient
4
- from notionary.blocks.divider import DividerElement
5
4
  from notionary.blocks.registry.block_registry import BlockRegistry
6
- from notionary.blocks.table_of_contents import TableOfContentsElement
7
- from notionary.markdown.markdown_builder import MarkdownBuilder
5
+ from notionary.blocks.markdown.markdown_builder import MarkdownBuilder
6
+ from notionary.schemas.base import NotionContentSchema
7
+ from notionary.page.markdown_whitespace_processor import MarkdownWhitespaceProcessor
8
8
  from notionary.page.writer.markdown_to_notion_converter import MarkdownToNotionConverter
9
9
  from notionary.util import LoggingMixin
10
10
 
@@ -21,36 +21,18 @@ class PageContentWriter(LoggingMixin):
21
21
 
22
22
  async def append_markdown(
23
23
  self,
24
- content: Union[str, Callable[[MarkdownBuilder], MarkdownBuilder]],
25
- *,
26
- append_divider: bool = True,
27
- prepend_table_of_contents: bool = False,
24
+ content: Union[
25
+ str, Callable[[MarkdownBuilder], MarkdownBuilder], NotionContentSchema
26
+ ],
28
27
  ) -> Optional[str]:
29
28
  """
30
- Append markdown content to a Notion page using either text or builder callback.
29
+ Append markdown content to a Notion page using text, builder callback, MarkdownDocumentModel, or NotionContentSchema.
31
30
  """
31
+ markdown = self._extract_markdown_from_param(content)
32
32
 
33
- if isinstance(content, str):
34
- final_markdown = content
35
- elif callable(content):
36
- builder = MarkdownBuilder()
37
- content(builder)
38
- final_markdown = builder.build()
39
- else:
40
- raise ValueError(
41
- "content must be either a string or a callable that takes a MarkdownBuilder"
42
- )
43
-
44
- # Add optional components
45
- if prepend_table_of_contents:
46
- self._ensure_table_of_contents_exists_in_registry()
47
- final_markdown = "[toc]\n\n" + final_markdown
48
-
49
- if append_divider:
50
- self._ensure_divider_exists_in_registry()
51
- final_markdown = final_markdown + "\n\n---\n"
52
-
53
- processed_markdown = self._process_markdown_whitespace(final_markdown)
33
+ processed_markdown = MarkdownWhitespaceProcessor.process_markdown_whitespace(
34
+ markdown
35
+ )
54
36
 
55
37
  try:
56
38
  blocks = await self._markdown_to_notion_converter.convert(
@@ -72,106 +54,27 @@ class PageContentWriter(LoggingMixin):
72
54
  self.logger.error("Error appending markdown: %s", str(e), exc_info=True)
73
55
  return None
74
56
 
75
- def _process_markdown_whitespace(self, markdown_text: str) -> str:
76
- """Process markdown text to normalize whitespace while preserving code blocks."""
77
- lines = markdown_text.split("\n")
78
- if not lines:
79
- return ""
80
-
81
- return self._process_whitespace_lines(lines)
82
-
83
- def _process_whitespace_lines(self, lines: list[str]) -> str:
84
- """Process all lines and return the processed markdown."""
85
- processed_lines = []
86
- in_code_block = False
87
- current_code_block = []
88
-
89
- for line in lines:
90
- processed_lines, in_code_block, current_code_block = (
91
- self._process_single_line(
92
- line, processed_lines, in_code_block, current_code_block
93
- )
94
- )
95
-
96
- return "\n".join(processed_lines)
97
-
98
- def _process_single_line(
57
+ def _extract_markdown_from_param(
99
58
  self,
100
- line: str,
101
- processed_lines: list[str],
102
- in_code_block: bool,
103
- current_code_block: list[str],
104
- ) -> tuple[list[str], bool, list[str]]:
105
- """Process a single line and return updated state."""
106
- if self._is_code_block_marker(line):
107
- return self._handle_code_block_marker(
108
- line, processed_lines, in_code_block, current_code_block
109
- )
110
- if in_code_block:
111
- current_code_block.append(line)
112
- return processed_lines, in_code_block, current_code_block
113
- else:
114
- processed_lines.append(line.lstrip())
115
- return processed_lines, in_code_block, current_code_block
59
+ content: Union[
60
+ str, Callable[[MarkdownBuilder], MarkdownBuilder], NotionContentSchema
61
+ ],
62
+ ) -> str:
63
+ """
64
+ Prepare markdown content from string, builder callback, MarkdownDocumentModel, or NotionContentSchema.
65
+ """
66
+ if isinstance(content, str):
67
+ return content
68
+ elif isinstance(content, NotionContentSchema):
69
+ # Use new injection-based API
70
+ builder = MarkdownBuilder()
71
+ return content.to_notion_content(builder)
116
72
 
117
- def _handle_code_block_marker(
118
- self,
119
- line: str,
120
- processed_lines: list[str],
121
- in_code_block: bool,
122
- current_code_block: list[str],
123
- ) -> tuple[list[str], bool, list[str]]:
124
- """Handle code block start/end markers."""
125
- if not in_code_block:
126
- return self._start_code_block(line, processed_lines)
73
+ elif callable(content):
74
+ builder = MarkdownBuilder()
75
+ content(builder)
76
+ return builder.build()
127
77
  else:
128
- return self._end_code_block(processed_lines, current_code_block)
129
-
130
- def _start_code_block(
131
- self, line: str, processed_lines: list[str]
132
- ) -> tuple[list[str], bool, list[str]]:
133
- """Start a new code block."""
134
- processed_lines.append(self._normalize_code_block_start(line))
135
- return processed_lines, True, []
136
-
137
- def _end_code_block(
138
- self, processed_lines: list[str], current_code_block: list[str]
139
- ) -> tuple[list[str], bool, list[str]]:
140
- """End the current code block."""
141
- processed_lines.extend(self._normalize_code_block_content(current_code_block))
142
- processed_lines.append("```")
143
- return processed_lines, False, []
144
-
145
- def _is_code_block_marker(self, line: str) -> bool:
146
- """Check if line is a code block marker."""
147
- return line.lstrip().startswith("```")
148
-
149
- def _normalize_code_block_start(self, line: str) -> str:
150
- """Normalize code block opening marker."""
151
- language = line.lstrip().replace("```", "", 1).strip()
152
- return "```" + language
153
-
154
- def _normalize_code_block_content(self, code_lines: list[str]) -> list[str]:
155
- """Normalize code block indentation."""
156
- if not code_lines:
157
- return []
158
-
159
- # Find minimum indentation from non-empty lines
160
- non_empty_lines = [line for line in code_lines if line.strip()]
161
- if not non_empty_lines:
162
- return [""] * len(code_lines)
163
-
164
- min_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
165
- if min_indent == 0:
166
- return code_lines
167
-
168
- # Remove common indentation
169
- return ["" if not line.strip() else line[min_indent:] for line in code_lines]
170
-
171
- def _ensure_table_of_contents_exists_in_registry(self) -> None:
172
- """Ensure TableOfContents is registered in the block registry."""
173
- self.block_registry.register(TableOfContentsElement)
174
-
175
- def _ensure_divider_exists_in_registry(self) -> None:
176
- """Ensure DividerBlock is registered in the block registry."""
177
- self.block_registry.register(DividerElement)
78
+ raise ValueError(
79
+ "content must be either a string, a NotionContentSchema, a MarkdownDocumentModel, or a callable that takes a MarkdownBuilder"
80
+ )
@@ -1,4 +1,3 @@
1
- # notionary/blocks/context/page_context.py
2
1
  from __future__ import annotations
3
2
 
4
3
  from typing import TYPE_CHECKING, Optional
@@ -35,11 +34,6 @@ def get_page_context() -> PageContextProvider:
35
34
  return context
36
35
 
37
36
 
38
- def get_page_context_optional() -> Optional[PageContextProvider]:
39
- """Get current page context or None if not available."""
40
- return _page_context.get()
41
-
42
-
43
37
  class page_context:
44
38
  """Async-only context manager for page operations."""
45
39
 
@@ -8,7 +8,7 @@ class ColumnListRenderer(BlockHandler):
8
8
  def _can_handle(self, context: BlockRenderingContext) -> bool:
9
9
  return ColumnListElement.match_notion(context.block)
10
10
 
11
- def _process(self, context: BlockRenderingContext) -> None:
11
+ async def _process(self, context: BlockRenderingContext) -> None:
12
12
  # Create column list start line
13
13
  column_list_start = "::: columns"
14
14
 
@@ -28,7 +28,7 @@ class ColumnListRenderer(BlockHandler):
28
28
 
29
29
  # Create a temporary retriever to process children
30
30
  retriever = PageContentRetriever(context.block_registry)
31
- children_markdown = retriever._convert_blocks_to_markdown(
31
+ children_markdown = await retriever._convert_blocks_to_markdown(
32
32
  context.get_children_blocks(),
33
33
  indent_level=0, # No indentation for content inside column lists
34
34
  )
@@ -8,7 +8,7 @@ class ColumnRenderer(BlockHandler):
8
8
  def _can_handle(self, context: BlockRenderingContext) -> bool:
9
9
  return ColumnElement.match_notion(context.block)
10
10
 
11
- def _process(self, context: BlockRenderingContext) -> None:
11
+ async def _process(self, context: BlockRenderingContext) -> None:
12
12
  # Get the column start line with potential width ratio
13
13
  column_start = self._extract_column_start(context.block)
14
14
 
@@ -28,7 +28,7 @@ class ColumnRenderer(BlockHandler):
28
28
 
29
29
  # Create a temporary retriever to process children
30
30
  retriever = PageContentRetriever(context.block_registry)
31
- children_markdown = retriever._convert_blocks_to_markdown(
31
+ children_markdown = await retriever._convert_blocks_to_markdown(
32
32
  context.get_children_blocks(),
33
33
  indent_level=0, # No indentation for content inside columns
34
34
  )
@@ -29,7 +29,7 @@ class LineRenderer(BlockHandler):
29
29
  )
30
30
 
31
31
  retriever = PageContentRetriever(context.block_registry)
32
- children_markdown = retriever._convert_blocks_to_markdown(
32
+ children_markdown = await retriever._convert_blocks_to_markdown(
33
33
  context.get_children_blocks(), indent_level=context.indent_level + 1
34
34
  )
35
35
  context.markdown_result = children_markdown
@@ -52,7 +52,7 @@ class LineRenderer(BlockHandler):
52
52
  from notionary.page.reader.page_content_retriever import PageContentRetriever
53
53
 
54
54
  retriever = PageContentRetriever(context.block_registry)
55
- children_markdown = retriever._convert_blocks_to_markdown(
55
+ children_markdown = await retriever._convert_blocks_to_markdown(
56
56
  context.get_children_blocks(), indent_level=context.indent_level + 1
57
57
  )
58
58
 
@@ -8,7 +8,7 @@ class ToggleRenderer(BlockHandler):
8
8
  def _can_handle(self, context: BlockRenderingContext) -> bool:
9
9
  return ToggleElement.match_notion(context.block)
10
10
 
11
- def _process(self, context: BlockRenderingContext) -> None:
11
+ async def _process(self, context: BlockRenderingContext) -> None:
12
12
  # Get the toggle title from the block
13
13
  toggle_title = self._extract_toggle_title(context.block)
14
14
 
@@ -34,7 +34,7 @@ class ToggleRenderer(BlockHandler):
34
34
 
35
35
  # Create a temporary retriever to process children
36
36
  retriever = PageContentRetriever(context.block_registry)
37
- children_markdown = retriever._convert_blocks_to_markdown(
37
+ children_markdown = await retriever._convert_blocks_to_markdown(
38
38
  context.get_children_blocks(),
39
39
  indent_level=0, # No indentation for content inside toggles
40
40
  )
@@ -11,7 +11,7 @@ class ToggleableHeadingRenderer(BlockHandler):
11
11
  def _can_handle(self, context: BlockRenderingContext) -> bool:
12
12
  return ToggleableHeadingElement.match_notion(context.block)
13
13
 
14
- def _process(self, context: BlockRenderingContext) -> None:
14
+ async def _process(self, context: BlockRenderingContext) -> None:
15
15
  # Get the heading level and title
16
16
  level, title = self._extract_heading_info(context.block)
17
17
 
@@ -38,7 +38,7 @@ class ToggleableHeadingRenderer(BlockHandler):
38
38
 
39
39
  # Create a temporary retriever to process children
40
40
  retriever = PageContentRetriever(context.block_registry)
41
- children_markdown = retriever._convert_blocks_to_markdown(
41
+ children_markdown = await retriever._convert_blocks_to_markdown(
42
42
  context.get_children_blocks(),
43
43
  indent_level=0, # No indentation for content inside toggleable headings
44
44
  )
@@ -15,7 +15,8 @@ class ToggleHandler(LineHandler):
15
15
 
16
16
  def __init__(self):
17
17
  super().__init__()
18
- self._start_pattern = re.compile(r"^[+]{3}\s+(.+)$", re.IGNORECASE)
18
+ # Updated: Support both "+++title" and "+++ title"
19
+ self._start_pattern = re.compile(r"^[+]{3}\s*(.+)$", re.IGNORECASE)
19
20
  self._end_pattern = re.compile(r"^[+]{3}\s*$")
20
21
 
21
22
  def _can_handle(self, context: LineProcessingContext) -> bool:
@@ -43,15 +44,18 @@ class ToggleHandler(LineHandler):
43
44
  context.should_continue = True
44
45
 
45
46
  def _is_toggle_start(self, context: LineProcessingContext) -> bool:
46
- """Check if line starts a toggle (+++ Title)."""
47
+ """Check if line starts a toggle (+++ Title or +++Title)."""
47
48
  line = context.line.strip()
48
49
 
49
- # Must match our pattern
50
+ # Must match our pattern (now allows optional space)
50
51
  if not self._start_pattern.match(line):
51
52
  return False
52
53
 
53
54
  # But NOT match toggleable heading pattern (has # after +++)
54
- toggleable_heading_pattern = re.compile(r"^[+]{3}#{1,3}\s+.+$", re.IGNORECASE)
55
+ # Updated: Support both "+++#title" and "+++ # title"
56
+ toggleable_heading_pattern = re.compile(
57
+ r"^[+]{3}\s*#{1,3}\s+.+$", re.IGNORECASE
58
+ )
55
59
  if toggleable_heading_pattern.match(line):
56
60
  return False
57
61
 
@@ -19,8 +19,9 @@ class ToggleableHeadingHandler(LineHandler):
19
19
 
20
20
  def __init__(self):
21
21
  super().__init__()
22
+ # Updated: Support both "+++# title" and "+++#title"
22
23
  self._start_pattern = re.compile(
23
- r"^[+]{3}(?P<level>#{1,3})\s+(.+)$", re.IGNORECASE
24
+ r"^[+]{3}\s*(?P<level>#{1,3})\s*(.+)$", re.IGNORECASE
24
25
  )
25
26
  # +++
26
27
  self._end_pattern = re.compile(r"^[+]{3}\s*$")
@@ -49,7 +50,7 @@ class ToggleableHeadingHandler(LineHandler):
49
50
  return await _handle(self._add_toggleable_heading_content)
50
51
 
51
52
  def _is_toggleable_heading_start(self, context: LineProcessingContext) -> bool:
52
- """Check if line starts a toggleable heading (+++# "Title")."""
53
+ """Check if line starts a toggleable heading (+++# "Title" or +++#"Title")."""
53
54
  return self._start_pattern.match(context.line.strip()) is not None
54
55
 
55
56
  def _is_toggleable_heading_end(self, context: LineProcessingContext) -> bool: