notionary 0.2.17__py3-none-any.whl → 0.2.19__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 +3 -2
- notionary/blocks/__init__.py +54 -25
- notionary/blocks/audio/__init__.py +7 -0
- notionary/blocks/audio/audio_element.py +152 -0
- notionary/blocks/audio/audio_markdown_node.py +29 -0
- notionary/blocks/audio/audio_models.py +59 -0
- notionary/blocks/bookmark/__init__.py +7 -0
- notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
- notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
- notionary/blocks/bookmark/bookmark_models.py +0 -0
- notionary/blocks/bulleted_list/__init__.py +7 -0
- notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
- notionary/blocks/callout/__init__.py +7 -0
- notionary/blocks/callout/callout_element.py +132 -0
- notionary/blocks/callout/callout_markdown_node.py +31 -0
- notionary/blocks/callout/callout_models.py +0 -0
- notionary/blocks/code/__init__.py +7 -0
- notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
- notionary/blocks/code/code_markdown_node.py +43 -0
- notionary/blocks/code/code_models.py +0 -0
- notionary/blocks/column/__init__.py +5 -0
- notionary/blocks/{column_element.py → column/column_element.py} +24 -55
- notionary/blocks/column/column_models.py +0 -0
- notionary/blocks/divider/__init__.py +7 -0
- notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
- notionary/blocks/divider/divider_markdown_node.py +24 -0
- notionary/blocks/divider/divider_models.py +0 -0
- notionary/blocks/document/__init__.py +7 -0
- notionary/blocks/document/document_element.py +102 -0
- notionary/blocks/document/document_markdown_node.py +31 -0
- notionary/blocks/document/document_models.py +0 -0
- notionary/blocks/embed/__init__.py +7 -0
- notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
- notionary/blocks/embed/embed_markdown_node.py +30 -0
- notionary/blocks/embed/embed_models.py +0 -0
- notionary/blocks/heading/__init__.py +7 -0
- notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
- notionary/blocks/heading/heading_markdown_node.py +29 -0
- notionary/blocks/heading/heading_models.py +0 -0
- notionary/blocks/image/__init__.py +7 -0
- notionary/blocks/{image_element.py → image/image_element.py} +62 -42
- notionary/blocks/image/image_markdown_node.py +33 -0
- notionary/blocks/image/image_models.py +0 -0
- notionary/blocks/markdown_builder.py +356 -0
- notionary/blocks/markdown_node.py +29 -0
- notionary/blocks/mention/__init__.py +7 -0
- notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
- notionary/blocks/mention/mention_markdown_node.py +38 -0
- notionary/blocks/mention/mention_models.py +0 -0
- notionary/blocks/numbered_list/__init__.py +7 -0
- notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
- notionary/blocks/numbered_list/numbered_list_models.py +0 -0
- notionary/blocks/paragraph/__init__.py +7 -0
- notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
- notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
- notionary/blocks/paragraph/paragraph_models.py +0 -0
- notionary/blocks/quote/__init__.py +7 -0
- notionary/blocks/quote/quote_element.py +92 -0
- notionary/blocks/quote/quote_markdown_node.py +23 -0
- notionary/blocks/quote/quote_models.py +0 -0
- notionary/blocks/registry/block_registry.py +17 -3
- notionary/blocks/registry/block_registry_builder.py +90 -178
- notionary/blocks/shared/__init__.py +0 -0
- notionary/blocks/shared/block_client.py +256 -0
- notionary/blocks/shared/models.py +713 -0
- notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
- notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
- notionary/blocks/shared/text_inline_formatter_new.py +139 -0
- notionary/blocks/table/__init__.py +7 -0
- notionary/blocks/{table_element.py → table/table_element.py} +23 -11
- notionary/blocks/table/table_markdown_node.py +40 -0
- notionary/blocks/table/table_models.py +0 -0
- notionary/blocks/todo/__init__.py +7 -0
- notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
- notionary/blocks/todo/todo_markdown_node.py +31 -0
- notionary/blocks/todo/todo_models.py +0 -0
- notionary/blocks/toggle/__init__.py +4 -0
- notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
- notionary/blocks/toggle/toggle_markdown_node.py +35 -0
- notionary/blocks/toggle/toggle_models.py +0 -0
- notionary/blocks/toggleable_heading/__init__.py +9 -0
- notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
- notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/blocks/video/__init__.py +7 -0
- notionary/blocks/{video_element.py → video/video_element.py} +82 -57
- notionary/blocks/video/video_markdown_node.py +30 -0
- notionary/file_upload/notion_file_upload.py +1 -1
- notionary/page/content/markdown_whitespace_processor.py +80 -0
- notionary/page/content/notion_text_length_utils.py +87 -0
- notionary/page/content/page_content_retriever.py +18 -10
- notionary/page/content/page_content_writer.py +97 -148
- notionary/page/formatting/line_processor.py +153 -0
- notionary/page/formatting/markdown_to_notion_converter.py +104 -425
- notionary/page/notion_page.py +9 -11
- notionary/page/notion_to_markdown_converter.py +9 -13
- notionary/util/factory_decorator.py +0 -0
- notionary/workspace.py +0 -1
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/METADATA +1 -1
- notionary-0.2.19.dist-info/RECORD +150 -0
- notionary/blocks/audio_element.py +0 -144
- notionary/blocks/callout_element.py +0 -122
- notionary/blocks/document_element.py +0 -194
- notionary/blocks/notion_block_client.py +0 -26
- notionary/blocks/qoute_element.py +0 -169
- notionary/page/content/notion_page_content_chunker.py +0 -84
- notionary/page/formatting/spacer_rules.py +0 -483
- notionary-0.2.17.dist-info/RECORD +0 -85
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/LICENSE +0 -0
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/WHEEL +0 -0
@@ -1,49 +1,31 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Optional
|
2
2
|
|
3
|
-
from notionary.blocks import
|
4
|
-
|
5
|
-
from notionary.
|
3
|
+
from notionary.blocks import BlockRegistry
|
4
|
+
from notionary.blocks.shared.block_client import NotionBlockClient
|
5
|
+
from notionary.models.notion_block_response import Block
|
6
|
+
from notionary.page.content.markdown_whitespace_processor import (
|
7
|
+
MarkdownWhitespaceProcessor,
|
8
|
+
)
|
9
|
+
from notionary.page.content.notion_text_length_utils import fix_blocks_content_length
|
6
10
|
from notionary.page.formatting.markdown_to_notion_converter import (
|
7
11
|
MarkdownToNotionConverter,
|
8
12
|
)
|
9
|
-
|
10
|
-
NotionToMarkdownConverter,
|
11
|
-
)
|
12
|
-
from notionary.page.content.notion_page_content_chunker import (
|
13
|
-
NotionPageContentChunker,
|
14
|
-
)
|
13
|
+
|
15
14
|
from notionary.util import LoggingMixin
|
16
15
|
|
17
16
|
|
18
17
|
class PageContentWriter(LoggingMixin):
|
19
|
-
def __init__(
|
20
|
-
self,
|
21
|
-
page_id: str,
|
22
|
-
client: NotionPageClient,
|
23
|
-
block_registry: BlockRegistry,
|
24
|
-
):
|
18
|
+
def __init__(self, page_id: str, block_registry: BlockRegistry):
|
25
19
|
self.page_id = page_id
|
26
|
-
self._client = client
|
27
20
|
self.block_registry = block_registry
|
21
|
+
self._block_client = NotionBlockClient()
|
22
|
+
|
28
23
|
self._markdown_to_notion_converter = MarkdownToNotionConverter(
|
29
24
|
block_registry=block_registry
|
30
25
|
)
|
31
|
-
self._notion_to_markdown_converter = NotionToMarkdownConverter(
|
32
|
-
block_registry=block_registry
|
33
|
-
)
|
34
|
-
self._chunker = NotionPageContentChunker()
|
35
|
-
|
36
|
-
async def append_markdown(self, markdown_text: str, append_divider=False) -> bool:
|
37
|
-
"""
|
38
|
-
Append markdown text to a Notion page, automatically handling content length limits.
|
39
|
-
"""
|
40
|
-
if append_divider and not self.block_registry.contains(DividerElement):
|
41
|
-
self.logger.warning(
|
42
|
-
"DividerElement not registered. Appending divider skipped."
|
43
|
-
)
|
44
|
-
append_divider = False
|
45
26
|
|
46
|
-
|
27
|
+
async def append_markdown(self, markdown_text: str, append_divider=True) -> bool:
|
28
|
+
"""Append markdown text to a Notion page, automatically handling content length limits."""
|
47
29
|
if append_divider:
|
48
30
|
markdown_text = markdown_text + "---\n"
|
49
31
|
|
@@ -51,29 +33,36 @@ class PageContentWriter(LoggingMixin):
|
|
51
33
|
|
52
34
|
try:
|
53
35
|
blocks = self._markdown_to_notion_converter.convert(markdown_text)
|
54
|
-
fixed_blocks = self._chunker.fix_blocks_content_length(blocks)
|
55
36
|
|
56
|
-
|
57
|
-
|
37
|
+
fixed_blocks = fix_blocks_content_length(blocks)
|
38
|
+
|
39
|
+
result = await self._block_client.append_block_children(
|
40
|
+
block_id=self.page_id, children=fixed_blocks
|
58
41
|
)
|
42
|
+
self.logger.debug("Append block children result: %r", result)
|
59
43
|
return bool(result)
|
60
44
|
except Exception as e:
|
61
|
-
|
45
|
+
import traceback
|
46
|
+
|
47
|
+
self.logger.error(
|
48
|
+
"Error appending markdown: %s\nTraceback:\n%s",
|
49
|
+
str(e),
|
50
|
+
traceback.format_exc(),
|
51
|
+
)
|
62
52
|
return False
|
63
53
|
|
64
54
|
async def clear_page_content(self) -> bool:
|
65
|
-
"""
|
66
|
-
Clear all content of the page.
|
67
|
-
"""
|
55
|
+
"""Clear all content of the page."""
|
68
56
|
try:
|
69
|
-
|
70
|
-
|
57
|
+
children_response = await self._block_client.get_block_children(
|
58
|
+
block_id=self.page_id
|
59
|
+
)
|
71
60
|
|
72
|
-
if not results:
|
61
|
+
if not children_response or not children_response.results:
|
73
62
|
return True
|
74
63
|
|
75
64
|
success = True
|
76
|
-
for block in results:
|
65
|
+
for block in children_response.results:
|
77
66
|
block_success = await self._delete_block_with_children(block)
|
78
67
|
if not block_success:
|
79
68
|
success = False
|
@@ -83,120 +72,80 @@ class PageContentWriter(LoggingMixin):
|
|
83
72
|
self.logger.error("Error clearing page content: %s", str(e))
|
84
73
|
return False
|
85
74
|
|
86
|
-
async def _delete_block_with_children(self, block:
|
87
|
-
"""
|
88
|
-
|
89
|
-
|
75
|
+
async def _delete_block_with_children(self, block: Block) -> bool:
|
76
|
+
"""Delete a block and all its children recursively."""
|
77
|
+
if not block.id:
|
78
|
+
self.logger.error("Block has no valid ID")
|
79
|
+
return False
|
80
|
+
|
81
|
+
self.logger.debug("Deleting block: %s (type: %s)", block.id, block.type)
|
82
|
+
|
90
83
|
try:
|
91
|
-
if block.
|
92
|
-
|
93
|
-
child_results = children_resp.get("results", [])
|
84
|
+
if block.has_children and not await self._delete_block_children(block):
|
85
|
+
return False
|
94
86
|
|
95
|
-
|
96
|
-
child_success = await self._delete_block_with_children(child)
|
97
|
-
if not child_success:
|
98
|
-
return False
|
87
|
+
return await self._delete_single_block(block)
|
99
88
|
|
100
|
-
return await self._client.delete(f"blocks/{block['id']}")
|
101
89
|
except Exception as e:
|
102
|
-
self.logger.error("Failed to delete block: %s", str(e))
|
90
|
+
self.logger.error("Failed to delete block %s: %s", block.id, str(e))
|
103
91
|
return False
|
104
92
|
|
105
|
-
def
|
106
|
-
"""
|
107
|
-
|
108
|
-
Strips all leading whitespace from regular lines, but preserves relative indentation
|
109
|
-
within code blocks.
|
93
|
+
async def _delete_block_children(self, block: Block) -> bool:
|
94
|
+
"""Delete all children of a block."""
|
95
|
+
self.logger.debug("Block %s has children, deleting children first", block.id)
|
110
96
|
|
111
|
-
|
112
|
-
|
97
|
+
try:
|
98
|
+
children_blocks = await self._block_client.get_all_block_children(block.id)
|
113
99
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
lines = markdown_text.split("\n")
|
118
|
-
if not lines:
|
119
|
-
return ""
|
100
|
+
if not children_blocks:
|
101
|
+
self.logger.debug("No children found for block: %s", block.id)
|
102
|
+
return True
|
120
103
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
# Ending a code block
|
136
|
-
processed_lines.extend(
|
137
|
-
self._process_code_block_content(current_code_block)
|
138
|
-
)
|
139
|
-
processed_lines.append("```")
|
140
|
-
in_code_block = False
|
141
|
-
continue
|
142
|
-
|
143
|
-
# Handle code block content
|
144
|
-
if in_code_block:
|
145
|
-
current_code_block.append(line)
|
146
|
-
continue
|
147
|
-
|
148
|
-
# Handle regular text
|
149
|
-
processed_lines.append(line.lstrip())
|
150
|
-
|
151
|
-
# Handle unclosed code block
|
152
|
-
if in_code_block and current_code_block:
|
153
|
-
processed_lines.extend(self._process_code_block_content(current_code_block))
|
154
|
-
processed_lines.append("```")
|
155
|
-
|
156
|
-
return "\n".join(processed_lines)
|
157
|
-
|
158
|
-
def _is_code_block_marker(self, line: str) -> bool:
|
159
|
-
"""Check if a line is a code block marker."""
|
160
|
-
return line.lstrip().startswith("```")
|
161
|
-
|
162
|
-
def _process_code_block_start(self, line: str) -> str:
|
163
|
-
"""Extract and normalize the code block opening marker."""
|
164
|
-
language = line.lstrip().replace("```", "", 1).strip()
|
165
|
-
return "```" + language
|
166
|
-
|
167
|
-
def _process_code_block_content(self, code_lines: list) -> list:
|
168
|
-
"""
|
169
|
-
Normalize code block indentation by removing the minimum common indentation.
|
170
|
-
|
171
|
-
Args:
|
172
|
-
code_lines: List of code block content lines
|
173
|
-
|
174
|
-
Returns:
|
175
|
-
List of processed code lines with normalized indentation
|
176
|
-
"""
|
177
|
-
if not code_lines:
|
178
|
-
return []
|
179
|
-
|
180
|
-
# Find non-empty lines to determine minimum indentation
|
181
|
-
non_empty_code_lines = [line for line in code_lines if line.strip()]
|
182
|
-
if not non_empty_code_lines:
|
183
|
-
return [""] * len(code_lines) # All empty lines stay empty
|
184
|
-
|
185
|
-
# Calculate minimum indentation
|
186
|
-
min_indent = min(
|
187
|
-
len(line) - len(line.lstrip()) for line in non_empty_code_lines
|
188
|
-
)
|
189
|
-
if min_indent == 0:
|
190
|
-
return code_lines # No common indentation to remove
|
104
|
+
self.logger.debug(
|
105
|
+
"Found %d children to delete for block: %s",
|
106
|
+
len(children_blocks),
|
107
|
+
block.id,
|
108
|
+
)
|
109
|
+
|
110
|
+
# Delete all children recursively
|
111
|
+
for child_block in children_blocks:
|
112
|
+
if not await self._delete_block_with_children(child_block):
|
113
|
+
self.logger.error(
|
114
|
+
"Failed to delete child block: %s", child_block.id
|
115
|
+
)
|
116
|
+
return False
|
191
117
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
118
|
+
self.logger.debug(
|
119
|
+
"Successfully deleted all children of block: %s", block.id
|
120
|
+
)
|
121
|
+
return True
|
122
|
+
|
123
|
+
except Exception as e:
|
124
|
+
self.logger.error(
|
125
|
+
"Failed to delete children of block %s: %s", block.id, str(e)
|
126
|
+
)
|
127
|
+
return False
|
198
128
|
|
199
|
-
|
200
|
-
|
129
|
+
async def _delete_single_block(self, block: Block) -> bool:
|
130
|
+
"""Delete a single block."""
|
131
|
+
deleted_block: Optional[Block] = await self._block_client.delete_block(block.id)
|
132
|
+
|
133
|
+
if deleted_block is None:
|
134
|
+
self.logger.error("Failed to delete block: %s", block.id)
|
135
|
+
return False
|
136
|
+
|
137
|
+
if deleted_block.archived or deleted_block.in_trash:
|
138
|
+
self.logger.debug("Successfully deleted/archived block: %s", block.id)
|
139
|
+
return True
|
140
|
+
else:
|
141
|
+
self.logger.warning("Block %s was not properly archived/deleted", block.id)
|
142
|
+
return False
|
143
|
+
|
144
|
+
def _process_markdown_whitespace(self, markdown_text: str) -> str:
|
145
|
+
"""Process markdown text to normalize whitespace while preserving code blocks."""
|
146
|
+
lines = markdown_text.split("\n")
|
147
|
+
if not lines:
|
148
|
+
return ""
|
201
149
|
|
202
|
-
|
150
|
+
processor = MarkdownWhitespaceProcessor()
|
151
|
+
return processor.process_lines(lines)
|
@@ -0,0 +1,153 @@
|
|
1
|
+
import re
|
2
|
+
from notionary.blocks.shared.notion_block_element import NotionBlock
|
3
|
+
from notionary.blocks.registry.block_registry import BlockRegistry
|
4
|
+
|
5
|
+
|
6
|
+
class LineProcessingState:
|
7
|
+
"""Tracks state during line-by-line processing"""
|
8
|
+
|
9
|
+
def __init__(self):
|
10
|
+
self.paragraph_lines: list[str] = []
|
11
|
+
self.paragraph_start: int = 0
|
12
|
+
|
13
|
+
def add_to_paragraph(self, line: str, current_pos: int):
|
14
|
+
"""Add line to current paragraph"""
|
15
|
+
if not self.paragraph_lines:
|
16
|
+
self.paragraph_start = current_pos
|
17
|
+
self.paragraph_lines.append(line)
|
18
|
+
|
19
|
+
def reset_paragraph(self):
|
20
|
+
"""Reset paragraph state"""
|
21
|
+
self.paragraph_lines = []
|
22
|
+
self.paragraph_start = 0
|
23
|
+
|
24
|
+
def has_paragraph(self) -> bool:
|
25
|
+
"""Check if there are paragraph lines to process"""
|
26
|
+
return len(self.paragraph_lines) > 0
|
27
|
+
|
28
|
+
|
29
|
+
class LineProcessor:
|
30
|
+
"""Handles line-by-line processing of markdown text"""
|
31
|
+
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
block_registry: BlockRegistry,
|
35
|
+
excluded_ranges: set[int],
|
36
|
+
pipe_pattern: str,
|
37
|
+
):
|
38
|
+
self._block_registry = block_registry
|
39
|
+
self._excluded_ranges = excluded_ranges
|
40
|
+
self._pipe_pattern = pipe_pattern
|
41
|
+
|
42
|
+
@staticmethod
|
43
|
+
def _normalize_to_list(result) -> list[dict[str, any]]:
|
44
|
+
"""Normalize Union[list[dict], dict] to list[dict]"""
|
45
|
+
if result is None:
|
46
|
+
return []
|
47
|
+
return result if isinstance(result, list) else [result]
|
48
|
+
|
49
|
+
def process_lines(self, text: str) -> list[tuple[int, int, dict[str, any]]]:
|
50
|
+
"""Process all lines and return blocks with positions"""
|
51
|
+
lines = text.split("\n")
|
52
|
+
line_blocks = []
|
53
|
+
|
54
|
+
state = LineProcessingState()
|
55
|
+
current_pos = 0
|
56
|
+
|
57
|
+
for line in lines:
|
58
|
+
line_length = len(line) + 1 # +1 for newline
|
59
|
+
line_end = current_pos + line_length - 1
|
60
|
+
|
61
|
+
if self._should_skip_line(line, current_pos, line_end):
|
62
|
+
current_pos += line_length
|
63
|
+
continue
|
64
|
+
|
65
|
+
self._process_single_line(line, current_pos, line_end, line_blocks, state)
|
66
|
+
current_pos += line_length
|
67
|
+
|
68
|
+
# Process any remaining paragraph
|
69
|
+
self._finalize_paragraph(state, current_pos, line_blocks)
|
70
|
+
|
71
|
+
return line_blocks
|
72
|
+
|
73
|
+
def _should_skip_line(self, line: str, current_pos: int, line_end: int) -> bool:
|
74
|
+
"""Check if line should be skipped (excluded or pipe syntax)"""
|
75
|
+
return self._overlaps_with_excluded(
|
76
|
+
current_pos, line_end
|
77
|
+
) or self._is_pipe_syntax_line(line)
|
78
|
+
|
79
|
+
def _overlaps_with_excluded(self, start_pos: int, end_pos: int) -> bool:
|
80
|
+
"""Check if position range overlaps with excluded ranges"""
|
81
|
+
return any(
|
82
|
+
pos in self._excluded_ranges for pos in range(start_pos, end_pos + 1)
|
83
|
+
)
|
84
|
+
|
85
|
+
def _is_pipe_syntax_line(self, line: str) -> bool:
|
86
|
+
"""Check if line uses pipe syntax for nested content"""
|
87
|
+
return bool(re.match(self._pipe_pattern, line))
|
88
|
+
|
89
|
+
def _process_single_line(
|
90
|
+
self,
|
91
|
+
line: str,
|
92
|
+
current_pos: int,
|
93
|
+
line_end: int,
|
94
|
+
line_blocks: list[tuple[int, int, dict[str, any]]],
|
95
|
+
state: LineProcessingState,
|
96
|
+
):
|
97
|
+
"""Process a single line of text"""
|
98
|
+
# Handle empty lines
|
99
|
+
if not line.strip():
|
100
|
+
self._finalize_paragraph(state, current_pos, line_blocks)
|
101
|
+
state.reset_paragraph()
|
102
|
+
return
|
103
|
+
|
104
|
+
# Handle special blocks (headings, todos, dividers, etc.)
|
105
|
+
special_blocks = self._extract_special_block(line)
|
106
|
+
if special_blocks:
|
107
|
+
self._finalize_paragraph(state, current_pos, line_blocks)
|
108
|
+
# Mehrere Blöcke hinzufügen
|
109
|
+
for block in special_blocks:
|
110
|
+
line_blocks.append((current_pos, line_end, block))
|
111
|
+
state.reset_paragraph()
|
112
|
+
return
|
113
|
+
|
114
|
+
# Add to current paragraph
|
115
|
+
state.add_to_paragraph(line, current_pos)
|
116
|
+
|
117
|
+
def _extract_special_block(self, line: str) -> list[NotionBlock]:
|
118
|
+
"""Extract special block (non-paragraph) from line"""
|
119
|
+
for element in (
|
120
|
+
element
|
121
|
+
for element in self._block_registry.get_elements()
|
122
|
+
if not element.is_multiline()
|
123
|
+
):
|
124
|
+
if not element.match_markdown(line):
|
125
|
+
continue
|
126
|
+
|
127
|
+
result = element.markdown_to_notion(line)
|
128
|
+
blocks = self._normalize_to_list(result)
|
129
|
+
if not blocks:
|
130
|
+
continue
|
131
|
+
|
132
|
+
# Gibt nur zurück, wenn mindestens ein Nicht-Paragraph-Block dabei ist
|
133
|
+
if any(block.get("type") != "paragraph" for block in blocks):
|
134
|
+
return blocks
|
135
|
+
|
136
|
+
return []
|
137
|
+
|
138
|
+
def _finalize_paragraph(
|
139
|
+
self,
|
140
|
+
state: LineProcessingState,
|
141
|
+
end_pos: int,
|
142
|
+
line_blocks: list[tuple[int, int, dict[str, any]]],
|
143
|
+
):
|
144
|
+
"""Convert current paragraph lines to paragraph block"""
|
145
|
+
if not state.has_paragraph():
|
146
|
+
return
|
147
|
+
|
148
|
+
paragraph_text = "\n".join(state.paragraph_lines)
|
149
|
+
result = self._block_registry.markdown_to_notion(paragraph_text)
|
150
|
+
blocks = self._normalize_to_list(result)
|
151
|
+
|
152
|
+
for block in blocks:
|
153
|
+
line_blocks.append((state.paragraph_start, end_pos, block))
|