notionary 0.2.23__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.
- notionary/__init__.py +1 -1
- notionary/blocks/__init__.py +3 -1
- notionary/blocks/audio/__init__.py +0 -2
- notionary/blocks/audio/audio_element.py +92 -49
- notionary/blocks/audio/audio_markdown_node.py +4 -17
- notionary/blocks/bookmark/__init__.py +0 -2
- notionary/blocks/bookmark/bookmark_markdown_node.py +5 -21
- notionary/blocks/breadcrumbs/__init__.py +0 -2
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +2 -21
- notionary/blocks/bulleted_list/__init__.py +0 -2
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +3 -17
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -1
- notionary/blocks/callout/__init__.py +0 -2
- notionary/blocks/callout/callout_markdown_node.py +4 -18
- notionary/blocks/callout/callout_models.py +3 -4
- notionary/blocks/code/code_markdown_node.py +5 -19
- notionary/blocks/column/__init__.py +0 -4
- notionary/blocks/column/column_list_markdown_node.py +3 -19
- notionary/blocks/column/column_markdown_node.py +4 -21
- notionary/blocks/divider/__init__.py +0 -2
- notionary/blocks/divider/divider_markdown_node.py +2 -16
- notionary/blocks/embed/__init__.py +0 -2
- notionary/blocks/embed/embed_markdown_node.py +4 -17
- notionary/blocks/equation/__init__.py +0 -1
- notionary/blocks/equation/equation_element_markdown_node.py +3 -15
- notionary/blocks/file/__init__.py +0 -2
- notionary/blocks/file/file_element.py +67 -46
- notionary/blocks/file/file_element_markdown_node.py +4 -17
- notionary/blocks/heading/__init__.py +0 -2
- notionary/blocks/heading/heading_markdown_node.py +5 -19
- notionary/blocks/heading/heading_models.py +3 -3
- notionary/blocks/image_block/__init__.py +0 -2
- notionary/blocks/image_block/image_element.py +66 -25
- notionary/blocks/image_block/image_markdown_node.py +5 -20
- notionary/{markdown → blocks/markdown}/markdown_builder.py +29 -233
- notionary/blocks/markdown/markdown_node.py +25 -0
- notionary/blocks/mixins/file_upload/__init__.py +3 -0
- notionary/blocks/mixins/file_upload/file_upload_mixin.py +320 -0
- notionary/blocks/numbered_list/__init__.py +0 -1
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -17
- notionary/blocks/numbered_list/numbered_list_models.py +3 -3
- notionary/blocks/paragraph/__init__.py +0 -2
- notionary/blocks/paragraph/paragraph_markdown_node.py +3 -13
- notionary/blocks/pdf/__init__.py +0 -2
- notionary/blocks/pdf/pdf_element.py +81 -32
- notionary/blocks/pdf/pdf_markdown_node.py +5 -18
- notionary/blocks/quote/__init__.py +0 -2
- notionary/blocks/quote/quote_markdown_node.py +3 -13
- notionary/blocks/registry/__init__.py +1 -2
- notionary/blocks/registry/block_registry.py +116 -61
- notionary/blocks/table/__init__.py +0 -2
- notionary/blocks/table/table_markdown_node.py +17 -16
- notionary/blocks/table_of_contents/__init__.py +0 -2
- notionary/blocks/table_of_contents/table_of_contents_element.py +27 -15
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +3 -17
- notionary/blocks/table_of_contents/table_of_contents_models.py +2 -2
- notionary/blocks/todo/__init__.py +0 -2
- notionary/blocks/todo/todo_markdown_node.py +9 -20
- notionary/blocks/todo/todo_models.py +2 -3
- notionary/blocks/toggle/__init__.py +0 -2
- notionary/blocks/toggle/toggle_markdown_node.py +5 -19
- notionary/blocks/toggleable_heading/__init__.py +0 -2
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +6 -23
- notionary/blocks/video/__init__.py +0 -2
- notionary/blocks/video/video_element.py +110 -34
- notionary/blocks/video/video_markdown_node.py +4 -15
- notionary/comments/client.py +1 -1
- notionary/file_upload/client.py +3 -2
- notionary/file_upload/models.py +10 -1
- notionary/file_upload/notion_file_upload.py +5 -5
- notionary/page/markdown_whitespace_processor.py +129 -0
- notionary/page/notion_page.py +35 -40
- notionary/page/page_content_deleting_service.py +1 -1
- notionary/page/page_content_writer.py +32 -129
- notionary/page/page_context.py +0 -5
- notionary/page/reader/handler/column_list_renderer.py +2 -2
- notionary/page/reader/handler/column_renderer.py +2 -2
- notionary/page/reader/handler/line_renderer.py +2 -2
- notionary/page/reader/handler/toggle_renderer.py +2 -2
- notionary/page/reader/handler/toggleable_heading_renderer.py +2 -2
- notionary/page/writer/handler/toggle_handler.py +8 -4
- notionary/page/writer/handler/toggleable_heading_handler.py +3 -2
- notionary/page/writer/markdown_to_notion_converter.py +74 -30
- notionary/schemas/__init__.py +3 -0
- notionary/schemas/base.py +73 -0
- notionary/shared/__init__.py +1 -3
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/METADATA +16 -1
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/RECORD +91 -93
- notionary/blocks/guards.py +0 -22
- notionary/blocks/registry/block_registry_builder.py +0 -264
- notionary/markdown/makdown_document_model.py +0 -0
- notionary/markdown/markdown_document_model.py +0 -228
- notionary/markdown/markdown_node.py +0 -30
- notionary/models/notion_database_response.py +0 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +0 -73
- notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
- /notionary/{markdown/___init__.py → blocks/markdown/markdown_document_model.py} +0 -0
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/LICENSE +0 -0
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/WHEEL +0 -0
@@ -5,7 +5,7 @@ from io import BytesIO
|
|
5
5
|
from pathlib import Path
|
6
6
|
from typing import Optional
|
7
7
|
|
8
|
-
from notionary.file_upload.models import FileUploadResponse
|
8
|
+
from notionary.file_upload.models import FileUploadResponse, UploadMode
|
9
9
|
from notionary.util import LoggingMixin
|
10
10
|
|
11
11
|
|
@@ -196,7 +196,7 @@ class NotionFileUpload(LoggingMixin):
|
|
196
196
|
filename=filename,
|
197
197
|
content_type=content_type,
|
198
198
|
content_length=file_size,
|
199
|
-
mode=
|
199
|
+
mode=UploadMode.SINGLE_PART,
|
200
200
|
)
|
201
201
|
|
202
202
|
if not file_upload:
|
@@ -228,7 +228,7 @@ class NotionFileUpload(LoggingMixin):
|
|
228
228
|
filename=filename,
|
229
229
|
content_type=content_type,
|
230
230
|
content_length=file_size,
|
231
|
-
mode=
|
231
|
+
mode=UploadMode.MULTI_PART,
|
232
232
|
)
|
233
233
|
|
234
234
|
if not file_upload:
|
@@ -269,7 +269,7 @@ class NotionFileUpload(LoggingMixin):
|
|
269
269
|
filename=filename,
|
270
270
|
content_type=content_type,
|
271
271
|
content_length=file_size,
|
272
|
-
mode=
|
272
|
+
mode=UploadMode.SINGLE_PART,
|
273
273
|
)
|
274
274
|
|
275
275
|
if not file_upload:
|
@@ -299,7 +299,7 @@ class NotionFileUpload(LoggingMixin):
|
|
299
299
|
filename=filename,
|
300
300
|
content_type=content_type,
|
301
301
|
content_length=file_size,
|
302
|
-
mode=
|
302
|
+
mode=UploadMode.MULTI_PART,
|
303
303
|
)
|
304
304
|
|
305
305
|
if not file_upload:
|
@@ -0,0 +1,129 @@
|
|
1
|
+
"""
|
2
|
+
Markdown whitespace processing utilities.
|
3
|
+
|
4
|
+
Handles normalization of markdown text while preserving code blocks and their indentation.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Tuple
|
8
|
+
|
9
|
+
|
10
|
+
class MarkdownWhitespaceProcessor:
|
11
|
+
"""
|
12
|
+
Processes markdown text to normalize whitespace while preserving code block formatting.
|
13
|
+
|
14
|
+
This processor handles:
|
15
|
+
- Removing leading whitespace from regular lines
|
16
|
+
- Preserving code block structure and indentation
|
17
|
+
- Normalizing code block markers
|
18
|
+
"""
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def process_markdown_whitespace(markdown_text: str) -> str:
|
22
|
+
"""Process markdown text to normalize whitespace while preserving code blocks."""
|
23
|
+
lines = markdown_text.split("\n")
|
24
|
+
if not lines:
|
25
|
+
return ""
|
26
|
+
|
27
|
+
return MarkdownWhitespaceProcessor._process_whitespace_lines(lines)
|
28
|
+
|
29
|
+
@staticmethod
|
30
|
+
def _process_whitespace_lines(lines: list[str]) -> str:
|
31
|
+
"""Process all lines and return the processed markdown."""
|
32
|
+
processed_lines = []
|
33
|
+
in_code_block = False
|
34
|
+
current_code_block = []
|
35
|
+
|
36
|
+
for line in lines:
|
37
|
+
processed_lines, in_code_block, current_code_block = (
|
38
|
+
MarkdownWhitespaceProcessor._process_single_line(
|
39
|
+
line, processed_lines, in_code_block, current_code_block
|
40
|
+
)
|
41
|
+
)
|
42
|
+
|
43
|
+
return "\n".join(processed_lines)
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def _process_single_line(
|
47
|
+
line: str,
|
48
|
+
processed_lines: list[str],
|
49
|
+
in_code_block: bool,
|
50
|
+
current_code_block: list[str],
|
51
|
+
) -> Tuple[list[str], bool, list[str]]:
|
52
|
+
"""Process a single line and return updated state."""
|
53
|
+
if MarkdownWhitespaceProcessor._is_code_block_marker(line):
|
54
|
+
return MarkdownWhitespaceProcessor._handle_code_block_marker(
|
55
|
+
line, processed_lines, in_code_block, current_code_block
|
56
|
+
)
|
57
|
+
if in_code_block:
|
58
|
+
current_code_block.append(line)
|
59
|
+
return processed_lines, in_code_block, current_code_block
|
60
|
+
else:
|
61
|
+
processed_lines.append(line.lstrip())
|
62
|
+
return processed_lines, in_code_block, current_code_block
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
def _handle_code_block_marker(
|
66
|
+
line: str,
|
67
|
+
processed_lines: list[str],
|
68
|
+
in_code_block: bool,
|
69
|
+
current_code_block: list[str],
|
70
|
+
) -> Tuple[list[str], bool, list[str]]:
|
71
|
+
"""Handle code block start/end markers."""
|
72
|
+
if not in_code_block:
|
73
|
+
return MarkdownWhitespaceProcessor._start_code_block(line, processed_lines)
|
74
|
+
else:
|
75
|
+
return MarkdownWhitespaceProcessor._end_code_block(
|
76
|
+
processed_lines, current_code_block
|
77
|
+
)
|
78
|
+
|
79
|
+
@staticmethod
|
80
|
+
def _start_code_block(
|
81
|
+
line: str, processed_lines: list[str]
|
82
|
+
) -> Tuple[list[str], bool, list[str]]:
|
83
|
+
"""Start a new code block."""
|
84
|
+
processed_lines.append(
|
85
|
+
MarkdownWhitespaceProcessor._normalize_code_block_start(line)
|
86
|
+
)
|
87
|
+
return processed_lines, True, []
|
88
|
+
|
89
|
+
@staticmethod
|
90
|
+
def _end_code_block(
|
91
|
+
processed_lines: list[str], current_code_block: list[str]
|
92
|
+
) -> Tuple[list[str], bool, list[str]]:
|
93
|
+
"""End the current code block."""
|
94
|
+
processed_lines.extend(
|
95
|
+
MarkdownWhitespaceProcessor._normalize_code_block_content(
|
96
|
+
current_code_block
|
97
|
+
)
|
98
|
+
)
|
99
|
+
processed_lines.append("```")
|
100
|
+
return processed_lines, False, []
|
101
|
+
|
102
|
+
@staticmethod
|
103
|
+
def _is_code_block_marker(line: str) -> bool:
|
104
|
+
"""Check if line is a code block marker."""
|
105
|
+
return line.lstrip().startswith("```")
|
106
|
+
|
107
|
+
@staticmethod
|
108
|
+
def _normalize_code_block_start(line: str) -> str:
|
109
|
+
"""Normalize code block opening marker."""
|
110
|
+
language = line.lstrip().replace("```", "", 1).strip()
|
111
|
+
return "```" + language
|
112
|
+
|
113
|
+
@staticmethod
|
114
|
+
def _normalize_code_block_content(code_lines: list[str]) -> list[str]:
|
115
|
+
"""Normalize code block indentation."""
|
116
|
+
if not code_lines:
|
117
|
+
return []
|
118
|
+
|
119
|
+
# Find minimum indentation from non-empty lines
|
120
|
+
non_empty_lines = [line for line in code_lines if line.strip()]
|
121
|
+
if not non_empty_lines:
|
122
|
+
return [""] * len(code_lines)
|
123
|
+
|
124
|
+
min_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
|
125
|
+
if min_indent == 0:
|
126
|
+
return code_lines
|
127
|
+
|
128
|
+
# Remove common indentation
|
129
|
+
return ["" if not line.strip() else line[min_indent:] for line in code_lines]
|
notionary/page/notion_page.py
CHANGED
@@ -9,13 +9,16 @@ from notionary.comments import CommentClient, Comment
|
|
9
9
|
from notionary.blocks.syntax_prompt_builder import SyntaxPromptBuilder
|
10
10
|
from notionary.blocks.models import DatabaseParent
|
11
11
|
from notionary.blocks.registry.block_registry import BlockRegistry
|
12
|
-
from notionary.blocks.registry.block_registry_builder import BlockRegistryBuilder
|
13
12
|
from notionary.database.client import NotionDatabaseClient
|
14
|
-
from notionary.
|
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
|
15
17
|
from notionary.page.client import NotionPageClient
|
16
18
|
from notionary.page.models import NotionPageResponse
|
17
19
|
from notionary.page.page_content_deleting_service import PageContentDeletingService
|
18
20
|
from notionary.page.page_content_writer import PageContentWriter
|
21
|
+
from notionary.page.page_context import PageContextProvider, page_context
|
19
22
|
from notionary.page.property_formatter import NotionPropertyFormatter
|
20
23
|
from notionary.page.reader.page_content_retriever import PageContentRetriever
|
21
24
|
from notionary.page.utils import extract_property_value
|
@@ -77,6 +80,8 @@ class NotionPage(LoggingMixin):
|
|
77
80
|
block_registry=self.block_element_registry,
|
78
81
|
)
|
79
82
|
|
83
|
+
self.page_context_provider = self._setup_page_context_provider()
|
84
|
+
|
80
85
|
@classmethod
|
81
86
|
async def from_page_id(
|
82
87
|
cls, page_id: str, token: Optional[str] = None
|
@@ -205,20 +210,15 @@ class NotionPage(LoggingMixin):
|
|
205
210
|
def is_in_trash(self) -> bool:
|
206
211
|
return self._is_in_trash
|
207
212
|
|
208
|
-
@property
|
209
|
-
def block_registry_builder(self) -> BlockRegistryBuilder:
|
210
|
-
"""
|
211
|
-
Returns the block registry builder for this page.
|
212
|
-
"""
|
213
|
-
return self.block_element_registry.builder
|
214
|
-
|
215
213
|
def get_prompt_information(self) -> str:
|
216
214
|
markdown_syntax_builder = SyntaxPromptBuilder()
|
217
215
|
return markdown_syntax_builder.build_concise_reference()
|
218
216
|
|
219
217
|
async def get_comments(self) -> list[Comment]:
|
220
|
-
return await self._comment_client.list_all_comments_for_page(
|
221
|
-
|
218
|
+
return await self._comment_client.list_all_comments_for_page(
|
219
|
+
page_id=self._page_id
|
220
|
+
)
|
221
|
+
|
222
222
|
async def post_comment(
|
223
223
|
self,
|
224
224
|
content: str,
|
@@ -248,7 +248,9 @@ class NotionPage(LoggingMixin):
|
|
248
248
|
self.logger.info(f"Successfully posted comment on page '{self._title}'")
|
249
249
|
return comment
|
250
250
|
except Exception as e:
|
251
|
-
self.logger.error(
|
251
|
+
self.logger.error(
|
252
|
+
f"Failed to post comment on page '{self._title}': {str(e)}"
|
253
|
+
)
|
252
254
|
return None
|
253
255
|
|
254
256
|
async def set_title(self, title: str) -> str:
|
@@ -272,43 +274,31 @@ class NotionPage(LoggingMixin):
|
|
272
274
|
|
273
275
|
async def append_markdown(
|
274
276
|
self,
|
275
|
-
content: Union[
|
276
|
-
|
277
|
-
|
278
|
-
append_divider: bool = False,
|
277
|
+
content: Union[
|
278
|
+
str, Callable[[MarkdownBuilder], MarkdownBuilder], NotionContentSchema
|
279
|
+
],
|
279
280
|
) -> bool:
|
280
281
|
"""
|
281
|
-
Append markdown content to the page.
|
282
|
-
|
283
|
-
Args:
|
284
|
-
content: Either raw markdown text OR a callback function that receives a MarkdownBuilder
|
285
|
-
prepend_table_of_contents: Whether to prepend table of contents
|
286
|
-
append_divider: Whether to append a divider
|
287
|
-
|
288
|
-
Returns:
|
289
|
-
bool: True if successful, False otherwise
|
282
|
+
Append markdown content to the page using text, builder callback, MarkdownDocumentModel, or NotionContentSchema.
|
290
283
|
"""
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
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
|
297
289
|
|
298
290
|
async def replace_content(
|
299
291
|
self,
|
300
|
-
content: Union[
|
301
|
-
|
302
|
-
|
303
|
-
append_divider: bool = False,
|
292
|
+
content: Union[
|
293
|
+
str, Callable[[MarkdownBuilder], MarkdownBuilder], NotionContentSchema
|
294
|
+
],
|
304
295
|
) -> bool:
|
305
296
|
"""
|
306
297
|
Replace the entire page content with new markdown content.
|
307
298
|
|
308
299
|
Args:
|
309
|
-
content: Either raw markdown text
|
310
|
-
|
311
|
-
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
|
312
302
|
|
313
303
|
Returns:
|
314
304
|
bool: True if successful, False otherwise
|
@@ -319,8 +309,6 @@ class NotionPage(LoggingMixin):
|
|
319
309
|
|
320
310
|
result = await self._page_content_writer.append_markdown(
|
321
311
|
content=content,
|
322
|
-
prepend_table_of_contents=prepend_table_of_contents,
|
323
|
-
append_divider=append_divider,
|
324
312
|
)
|
325
313
|
return result is not None
|
326
314
|
|
@@ -681,3 +669,10 @@ class NotionPage(LoggingMixin):
|
|
681
669
|
parent = page_response.parent
|
682
670
|
if isinstance(parent, DatabaseParent):
|
683
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.
|
7
|
-
from notionary.
|
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[
|
25
|
-
|
26
|
-
|
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
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
return
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
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
|
+
)
|
notionary/page/page_context.py
CHANGED
@@ -34,11 +34,6 @@ def get_page_context() -> PageContextProvider:
|
|
34
34
|
return context
|
35
35
|
|
36
36
|
|
37
|
-
def get_page_context_optional() -> Optional[PageContextProvider]:
|
38
|
-
"""Get current page context or None if not available."""
|
39
|
-
return _page_context.get()
|
40
|
-
|
41
|
-
|
42
37
|
class page_context:
|
43
38
|
"""Async-only context manager for page operations."""
|
44
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
|
-
|
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
|
-
|
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
|
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:
|