notionary 0.1.1__py3-none-any.whl → 0.1.3__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 +9 -0
- notionary/core/__init__.py +0 -0
- notionary/core/converters/__init__.py +50 -0
- notionary/core/converters/elements/__init__.py +0 -0
- notionary/core/converters/elements/bookmark_element.py +224 -0
- notionary/core/converters/elements/callout_element.py +179 -0
- notionary/core/converters/elements/code_block_element.py +153 -0
- notionary/core/converters/elements/column_element.py +294 -0
- notionary/core/converters/elements/divider_element.py +73 -0
- notionary/core/converters/elements/heading_element.py +84 -0
- notionary/core/converters/elements/image_element.py +130 -0
- notionary/core/converters/elements/list_element.py +130 -0
- notionary/core/converters/elements/notion_block_element.py +51 -0
- notionary/core/converters/elements/paragraph_element.py +73 -0
- notionary/core/converters/elements/qoute_element.py +242 -0
- notionary/core/converters/elements/table_element.py +306 -0
- notionary/core/converters/elements/text_inline_formatter.py +294 -0
- notionary/core/converters/elements/todo_lists.py +114 -0
- notionary/core/converters/elements/toggle_element.py +205 -0
- notionary/core/converters/elements/video_element.py +159 -0
- notionary/core/converters/markdown_to_notion_converter.py +482 -0
- notionary/core/converters/notion_to_markdown_converter.py +45 -0
- notionary/core/converters/registry/__init__.py +0 -0
- notionary/core/converters/registry/block_element_registry.py +234 -0
- notionary/core/converters/registry/block_element_registry_builder.py +280 -0
- notionary/core/database/database_info_service.py +43 -0
- notionary/core/database/database_query_service.py +73 -0
- notionary/core/database/database_schema_service.py +57 -0
- notionary/core/database/models/page_result.py +10 -0
- notionary/core/database/notion_database_manager.py +332 -0
- notionary/core/database/notion_database_manager_factory.py +233 -0
- notionary/core/database/notion_database_schema.py +415 -0
- notionary/core/database/notion_database_writer.py +390 -0
- notionary/core/database/page_service.py +161 -0
- notionary/core/notion_client.py +134 -0
- notionary/core/page/meta_data/metadata_editor.py +37 -0
- notionary/core/page/notion_page_manager.py +110 -0
- notionary/core/page/page_content_manager.py +85 -0
- notionary/core/page/property_formatter.py +97 -0
- notionary/exceptions/database_exceptions.py +76 -0
- notionary/exceptions/page_creation_exception.py +9 -0
- notionary/util/logging_mixin.py +47 -0
- notionary/util/singleton_decorator.py +20 -0
- notionary/util/uuid_utils.py +24 -0
- {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
- notionary-0.1.3.dist-info/RECORD +49 -0
- notionary-0.1.3.dist-info/top_level.txt +1 -0
- notionary-0.1.1.dist-info/RECORD +0 -5
- notionary-0.1.1.dist-info/top_level.txt +0 -1
- {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
- {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,482 @@
|
|
1
|
+
from typing import Dict, Any, List, Optional, Tuple
|
2
|
+
|
3
|
+
from notionary.core.converters.registry.block_element_registry import (
|
4
|
+
BlockElementRegistry,
|
5
|
+
)
|
6
|
+
from notionary.core.converters.registry.block_element_registry_builder import (
|
7
|
+
BlockElementRegistryBuilder,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class MarkdownToNotionConverter:
|
12
|
+
SPACER_MARKER = "<!-- spacer -->"
|
13
|
+
MULTILINE_CONTENT_MARKER = "<!-- REMOVED_MULTILINE_CONTENT -->"
|
14
|
+
TOGGLE_MARKER = "<!-- toggle_content -->"
|
15
|
+
|
16
|
+
def __init__(self, block_registry: Optional[BlockElementRegistry] = None):
|
17
|
+
"""
|
18
|
+
Initialize the MarkdownToNotionConverter.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
block_registry: Optional registry of Notion block elements
|
22
|
+
"""
|
23
|
+
self._block_registry = (
|
24
|
+
block_registry or BlockElementRegistryBuilder().create_standard_registry()
|
25
|
+
)
|
26
|
+
|
27
|
+
self._setup_element_callbacks()
|
28
|
+
|
29
|
+
def _setup_element_callbacks(self) -> None:
|
30
|
+
"""Registriert den Converter als Callback für Elemente, die ihn benötigen."""
|
31
|
+
|
32
|
+
for element in self._block_registry.get_elements():
|
33
|
+
if hasattr(element, "set_converter_callback"):
|
34
|
+
element.set_converter_callback(self.convert)
|
35
|
+
|
36
|
+
def convert(self, markdown_text: str) -> List[Dict[str, Any]]:
|
37
|
+
"""
|
38
|
+
Convert markdown text to Notion API block format.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
markdown_text: The markdown text to convert
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
List of Notion blocks
|
45
|
+
"""
|
46
|
+
if not markdown_text:
|
47
|
+
return []
|
48
|
+
|
49
|
+
# Process toggles first
|
50
|
+
processed_text, toggle_blocks = self._extract_toggle_elements(markdown_text)
|
51
|
+
|
52
|
+
# Process other multiline elements
|
53
|
+
processed_text, multiline_blocks = self._extract_multiline_elements(
|
54
|
+
processed_text
|
55
|
+
)
|
56
|
+
|
57
|
+
# Process remaining text line by line
|
58
|
+
line_blocks = self._process_text_lines(processed_text)
|
59
|
+
|
60
|
+
# Combine and sort all blocks
|
61
|
+
all_blocks = toggle_blocks + multiline_blocks + line_blocks
|
62
|
+
all_blocks.sort(key=lambda x: x[0])
|
63
|
+
|
64
|
+
# Extract just the blocks from position tuples
|
65
|
+
blocks = [block for _, _, block in all_blocks]
|
66
|
+
|
67
|
+
# Process spacing between blocks
|
68
|
+
return self._process_block_spacing(blocks)
|
69
|
+
|
70
|
+
def _extract_toggle_elements(
|
71
|
+
self, text: str
|
72
|
+
) -> Tuple[str, List[Tuple[int, int, Dict[str, Any]]]]:
|
73
|
+
"""
|
74
|
+
Extract toggle elements and their nested content using the ToggleElement class.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
text: The text to process
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Tuple of (processed text, list of (start_pos, end_pos, block) tuples)
|
81
|
+
"""
|
82
|
+
# Find toggle element in registry
|
83
|
+
toggle_element = None
|
84
|
+
for element in self._block_registry.get_elements():
|
85
|
+
if (
|
86
|
+
element.is_multiline()
|
87
|
+
and hasattr(element, "match_markdown")
|
88
|
+
and element.__name__ == "ToggleElement"
|
89
|
+
):
|
90
|
+
toggle_element = element
|
91
|
+
break
|
92
|
+
|
93
|
+
if not toggle_element:
|
94
|
+
# No toggle element found, return text as is
|
95
|
+
return text, []
|
96
|
+
|
97
|
+
# Use the find_matches method of ToggleElement to find and process all toggles
|
98
|
+
# Pass the converter's convert method as a callback to process nested content
|
99
|
+
toggle_blocks = toggle_element.find_matches(text, self.convert)
|
100
|
+
|
101
|
+
if not toggle_blocks:
|
102
|
+
return text, []
|
103
|
+
|
104
|
+
# Create a processed text with toggle markers
|
105
|
+
lines = text.split("\n")
|
106
|
+
processed_lines = lines.copy()
|
107
|
+
|
108
|
+
# Replace toggle content with markers
|
109
|
+
for start_pos, end_pos, _ in reversed(toggle_blocks):
|
110
|
+
# Calculate line indices for this toggle
|
111
|
+
start_line_index = 0
|
112
|
+
current_pos = 0
|
113
|
+
for i, line in enumerate(lines):
|
114
|
+
line_length = len(line) + 1 # +1 for newline
|
115
|
+
if current_pos <= start_pos < current_pos + line_length:
|
116
|
+
start_line_index = i
|
117
|
+
break
|
118
|
+
current_pos += line_length
|
119
|
+
|
120
|
+
end_line_index = start_line_index
|
121
|
+
current_pos = 0
|
122
|
+
for i, line in enumerate(lines):
|
123
|
+
line_length = len(line) + 1 # +1 for newline
|
124
|
+
if current_pos <= end_pos < current_pos + line_length:
|
125
|
+
end_line_index = i
|
126
|
+
break
|
127
|
+
current_pos += line_length
|
128
|
+
|
129
|
+
# Replace toggle content with markers
|
130
|
+
num_lines = end_line_index - start_line_index + 1
|
131
|
+
for i in range(start_line_index, start_line_index + num_lines):
|
132
|
+
processed_lines[i] = self.TOGGLE_MARKER
|
133
|
+
|
134
|
+
processed_text = "\n".join(processed_lines)
|
135
|
+
return processed_text, toggle_blocks
|
136
|
+
|
137
|
+
def _extract_multiline_elements(
|
138
|
+
self, text: str
|
139
|
+
) -> Tuple[str, List[Tuple[int, int, Dict[str, Any]]]]:
|
140
|
+
"""
|
141
|
+
Extract multiline elements and remove them from the text.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
text: The text to process
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
Tuple of (processed text, list of (start_pos, end_pos, block) tuples)
|
148
|
+
"""
|
149
|
+
if not text:
|
150
|
+
return text, []
|
151
|
+
|
152
|
+
multiline_blocks = []
|
153
|
+
processed_text = text
|
154
|
+
|
155
|
+
# Get all multiline elements except ToggleElement
|
156
|
+
multiline_elements = [
|
157
|
+
element
|
158
|
+
for element in self._block_registry.get_multiline_elements()
|
159
|
+
if element.__name__ != "ToggleElement"
|
160
|
+
]
|
161
|
+
|
162
|
+
if not multiline_elements:
|
163
|
+
return text, []
|
164
|
+
|
165
|
+
for element in multiline_elements:
|
166
|
+
if not hasattr(element, "find_matches"):
|
167
|
+
continue
|
168
|
+
|
169
|
+
# Find all matches for this element (pass the convert method as callback if needed)
|
170
|
+
if hasattr(element, "set_converter_callback"):
|
171
|
+
matches = element.find_matches(processed_text, self.convert)
|
172
|
+
else:
|
173
|
+
matches = element.find_matches(processed_text)
|
174
|
+
|
175
|
+
if not matches:
|
176
|
+
continue
|
177
|
+
|
178
|
+
multiline_blocks.extend(matches)
|
179
|
+
|
180
|
+
# Remove matched content from the text to avoid processing it again
|
181
|
+
processed_text = self._replace_matched_content_with_markers(
|
182
|
+
processed_text, matches
|
183
|
+
)
|
184
|
+
|
185
|
+
return processed_text, multiline_blocks
|
186
|
+
|
187
|
+
def _replace_matched_content_with_markers(
|
188
|
+
self, text: str, matches: List[Tuple[int, int, Dict[str, Any]]]
|
189
|
+
) -> str:
|
190
|
+
"""Replace matched content with marker placeholders to preserve line structure."""
|
191
|
+
for start, end, _ in reversed(matches):
|
192
|
+
num_newlines = text[start:end].count("\n")
|
193
|
+
text = (
|
194
|
+
text[:start]
|
195
|
+
+ "\n"
|
196
|
+
+ self.MULTILINE_CONTENT_MARKER
|
197
|
+
+ "\n" * num_newlines
|
198
|
+
+ text[end:]
|
199
|
+
)
|
200
|
+
return text
|
201
|
+
|
202
|
+
def _process_text_lines(self, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
203
|
+
"""
|
204
|
+
Process text line by line for single-line elements.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
text: The text to process
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
List of (start_pos, end_pos, block) tuples
|
211
|
+
"""
|
212
|
+
if not text:
|
213
|
+
return []
|
214
|
+
|
215
|
+
line_blocks = []
|
216
|
+
lines = text.split("\n")
|
217
|
+
|
218
|
+
current_pos = 0
|
219
|
+
current_paragraph = []
|
220
|
+
paragraph_start = 0
|
221
|
+
in_todo_sequence = False
|
222
|
+
|
223
|
+
for line in lines:
|
224
|
+
line_length = len(line) + 1 # +1 for newline
|
225
|
+
|
226
|
+
# Skip marker lines
|
227
|
+
if self._is_marker_line(line):
|
228
|
+
current_pos += line_length
|
229
|
+
continue
|
230
|
+
|
231
|
+
# Check for spacer marker
|
232
|
+
if self._is_spacer_marker(line):
|
233
|
+
line_blocks.append(
|
234
|
+
(
|
235
|
+
current_pos,
|
236
|
+
current_pos + line_length,
|
237
|
+
self._create_empty_paragraph(),
|
238
|
+
)
|
239
|
+
)
|
240
|
+
current_pos += line_length
|
241
|
+
continue
|
242
|
+
|
243
|
+
# Process todos first to keep them grouped
|
244
|
+
todo_block = self._extract_todo_item(line)
|
245
|
+
if todo_block:
|
246
|
+
self._handle_todo_item(
|
247
|
+
todo_block,
|
248
|
+
line_length,
|
249
|
+
current_pos,
|
250
|
+
current_paragraph,
|
251
|
+
paragraph_start,
|
252
|
+
line_blocks,
|
253
|
+
in_todo_sequence,
|
254
|
+
)
|
255
|
+
in_todo_sequence = True
|
256
|
+
current_pos += line_length
|
257
|
+
continue
|
258
|
+
|
259
|
+
if in_todo_sequence:
|
260
|
+
in_todo_sequence = False
|
261
|
+
|
262
|
+
if not line.strip():
|
263
|
+
self._process_paragraph_if_present(
|
264
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
265
|
+
)
|
266
|
+
current_paragraph = []
|
267
|
+
current_pos += line_length
|
268
|
+
continue
|
269
|
+
|
270
|
+
special_block = self._extract_special_block(line)
|
271
|
+
if special_block:
|
272
|
+
self._process_paragraph_if_present(
|
273
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
274
|
+
)
|
275
|
+
line_blocks.append(
|
276
|
+
(current_pos, current_pos + line_length, special_block)
|
277
|
+
)
|
278
|
+
current_paragraph = []
|
279
|
+
current_pos += line_length
|
280
|
+
continue
|
281
|
+
|
282
|
+
# Handle as part of paragraph
|
283
|
+
if not current_paragraph:
|
284
|
+
paragraph_start = current_pos
|
285
|
+
current_paragraph.append(line)
|
286
|
+
current_pos += line_length
|
287
|
+
|
288
|
+
# Process any remaining paragraph content
|
289
|
+
self._process_paragraph_if_present(
|
290
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
291
|
+
)
|
292
|
+
|
293
|
+
return line_blocks
|
294
|
+
|
295
|
+
def _is_marker_line(self, line: str) -> bool:
|
296
|
+
"""Check if a line is any kind of marker line that should be skipped."""
|
297
|
+
return self._is_multiline_marker(line) or self._is_toggle_marker(line)
|
298
|
+
|
299
|
+
def _is_multiline_marker(self, line: str) -> bool:
|
300
|
+
"""Check if a line is a multiline content marker."""
|
301
|
+
return line.strip() == self.MULTILINE_CONTENT_MARKER
|
302
|
+
|
303
|
+
def _is_toggle_marker(self, line: str) -> bool:
|
304
|
+
"""Check if a line is a toggle content marker."""
|
305
|
+
return line.strip() == self.TOGGLE_MARKER
|
306
|
+
|
307
|
+
def _is_spacer_marker(self, line: str) -> bool:
|
308
|
+
"""Check if a line is a spacer marker."""
|
309
|
+
return line.strip() == self.SPACER_MARKER
|
310
|
+
|
311
|
+
def _extract_todo_item(self, line: str) -> Optional[Dict[str, Any]]:
|
312
|
+
"""
|
313
|
+
Try to extract a todo item from a line.
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
Todo block if line is a todo item, None otherwise
|
317
|
+
"""
|
318
|
+
for element in self._block_registry.get_elements():
|
319
|
+
if (
|
320
|
+
not element.is_multiline()
|
321
|
+
and hasattr(element, "match_markdown")
|
322
|
+
and element.__name__ == "TodoElement"
|
323
|
+
and element.match_markdown(line)
|
324
|
+
):
|
325
|
+
return element.markdown_to_notion(line)
|
326
|
+
return None
|
327
|
+
|
328
|
+
def _handle_todo_item(
|
329
|
+
self,
|
330
|
+
todo_block: Dict[str, Any],
|
331
|
+
line_length: int,
|
332
|
+
current_pos: int,
|
333
|
+
current_paragraph: List[str],
|
334
|
+
paragraph_start: int,
|
335
|
+
line_blocks: List[Tuple[int, int, Dict[str, Any]]],
|
336
|
+
in_todo_sequence: bool,
|
337
|
+
) -> None:
|
338
|
+
"""Handle a todo item line."""
|
339
|
+
# If we were building a paragraph, finish it before starting todos
|
340
|
+
if not in_todo_sequence and current_paragraph:
|
341
|
+
self._process_paragraph_if_present(
|
342
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
343
|
+
)
|
344
|
+
current_paragraph.clear()
|
345
|
+
|
346
|
+
line_blocks.append((current_pos, current_pos + line_length, todo_block))
|
347
|
+
|
348
|
+
def _extract_special_block(self, line: str) -> Optional[Dict[str, Any]]:
|
349
|
+
"""
|
350
|
+
Try to extract a special block (not paragraph) from a line.
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
Block if line is a special block, None otherwise
|
354
|
+
"""
|
355
|
+
for element in self._block_registry.get_elements():
|
356
|
+
if (
|
357
|
+
not element.is_multiline()
|
358
|
+
and hasattr(element, "match_markdown")
|
359
|
+
and element.match_markdown(line)
|
360
|
+
):
|
361
|
+
block = element.markdown_to_notion(line)
|
362
|
+
if block and block.get("type") != "paragraph":
|
363
|
+
return block
|
364
|
+
return None
|
365
|
+
|
366
|
+
def _process_paragraph_if_present(
|
367
|
+
self,
|
368
|
+
paragraph_lines: List[str],
|
369
|
+
start_pos: int,
|
370
|
+
end_pos: int,
|
371
|
+
blocks: List[Tuple[int, int, Dict[str, Any]]],
|
372
|
+
) -> None:
|
373
|
+
"""
|
374
|
+
Process a paragraph and add it to the blocks list if valid.
|
375
|
+
|
376
|
+
Args:
|
377
|
+
paragraph_lines: Lines that make up the paragraph
|
378
|
+
start_pos: Starting position of the paragraph
|
379
|
+
end_pos: Ending position of the paragraph
|
380
|
+
blocks: List to add the processed paragraph block to
|
381
|
+
"""
|
382
|
+
if not paragraph_lines:
|
383
|
+
return
|
384
|
+
|
385
|
+
paragraph_text = "\n".join(paragraph_lines)
|
386
|
+
block = self._block_registry.markdown_to_notion(paragraph_text)
|
387
|
+
|
388
|
+
if not block:
|
389
|
+
return
|
390
|
+
|
391
|
+
blocks.append((start_pos, end_pos, block))
|
392
|
+
|
393
|
+
def _process_block_spacing(
|
394
|
+
self, blocks: List[Dict[str, Any]]
|
395
|
+
) -> List[Dict[str, Any]]:
|
396
|
+
"""
|
397
|
+
Process blocks and add spacing only where no explicit spacer is present.
|
398
|
+
|
399
|
+
Args:
|
400
|
+
blocks: List of Notion blocks
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
List of Notion blocks with processed spacing
|
404
|
+
"""
|
405
|
+
if not blocks:
|
406
|
+
return blocks
|
407
|
+
|
408
|
+
final_blocks = []
|
409
|
+
i = 0
|
410
|
+
|
411
|
+
while i < len(blocks):
|
412
|
+
current_block = blocks[i]
|
413
|
+
final_blocks.append(current_block)
|
414
|
+
|
415
|
+
# Check if this is a multiline element that needs spacing
|
416
|
+
if not self._is_multiline_block_type(current_block.get("type")):
|
417
|
+
i += 1
|
418
|
+
continue
|
419
|
+
|
420
|
+
# Check if the next block is already a spacer
|
421
|
+
if i + 1 < len(blocks) and self._is_empty_paragraph(blocks[i + 1]):
|
422
|
+
# Next block is already a spacer, don't add another
|
423
|
+
pass
|
424
|
+
else:
|
425
|
+
# No explicit spacer found, add one automatically
|
426
|
+
final_blocks.append(self._create_empty_paragraph())
|
427
|
+
|
428
|
+
i += 1
|
429
|
+
|
430
|
+
return final_blocks
|
431
|
+
|
432
|
+
def _is_multiline_block_type(self, block_type: str) -> bool:
|
433
|
+
"""
|
434
|
+
Check if a block type corresponds to a multiline element.
|
435
|
+
|
436
|
+
Args:
|
437
|
+
block_type: The type of block to check
|
438
|
+
|
439
|
+
Returns:
|
440
|
+
True if the block type is a multiline element, False otherwise
|
441
|
+
"""
|
442
|
+
if not block_type:
|
443
|
+
return False
|
444
|
+
|
445
|
+
multiline_elements = self._block_registry.get_multiline_elements()
|
446
|
+
|
447
|
+
for element in multiline_elements:
|
448
|
+
element_name = element.__name__.lower()
|
449
|
+
if block_type in element_name:
|
450
|
+
return True
|
451
|
+
|
452
|
+
if hasattr(element, "match_notion"):
|
453
|
+
dummy_block = {"type": block_type}
|
454
|
+
if element.match_notion(dummy_block):
|
455
|
+
return True
|
456
|
+
|
457
|
+
return False
|
458
|
+
|
459
|
+
def _is_empty_paragraph(self, block: Dict[str, Any]) -> bool:
|
460
|
+
"""
|
461
|
+
Check if a block is an empty paragraph.
|
462
|
+
|
463
|
+
Args:
|
464
|
+
block: The block to check
|
465
|
+
|
466
|
+
Returns:
|
467
|
+
True if it's an empty paragraph, False otherwise
|
468
|
+
"""
|
469
|
+
if block.get("type") != "paragraph":
|
470
|
+
return False
|
471
|
+
|
472
|
+
rich_text = block.get("paragraph", {}).get("rich_text", [])
|
473
|
+
return not rich_text or len(rich_text) == 0
|
474
|
+
|
475
|
+
def _create_empty_paragraph(self) -> Dict[str, Any]:
|
476
|
+
"""
|
477
|
+
Create an empty paragraph block.
|
478
|
+
|
479
|
+
Returns:
|
480
|
+
Empty paragraph block dictionary
|
481
|
+
"""
|
482
|
+
return {"type": "paragraph", "paragraph": {"rich_text": []}}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
2
|
+
|
3
|
+
from notionary.core.converters.registry.block_element_registry import (
|
4
|
+
BlockElementRegistry,
|
5
|
+
)
|
6
|
+
from notionary.core.converters.registry.block_element_registry_builder import (
|
7
|
+
BlockElementRegistryBuilder,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class NotionToMarkdownConverter:
|
12
|
+
"""Converts Notion blocks to Markdown text."""
|
13
|
+
|
14
|
+
def __init__(self, block_registry: Optional[BlockElementRegistry] = None):
|
15
|
+
"""
|
16
|
+
Initialize the MarkdownToNotionConverter.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
block_registry: Optional registry of Notion block elements
|
20
|
+
"""
|
21
|
+
self._block_registry = (
|
22
|
+
block_registry or BlockElementRegistryBuilder().create_standard_registry()
|
23
|
+
)
|
24
|
+
|
25
|
+
def convert(self, blocks: List[Dict[str, Any]]) -> str:
|
26
|
+
"""
|
27
|
+
Convert Notion blocks to Markdown text.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
blocks: List of Notion blocks
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Markdown text
|
34
|
+
"""
|
35
|
+
if not blocks:
|
36
|
+
return ""
|
37
|
+
|
38
|
+
markdown_parts = []
|
39
|
+
|
40
|
+
for block in blocks:
|
41
|
+
markdown = self._block_registry.notion_to_markdown(block)
|
42
|
+
if markdown:
|
43
|
+
markdown_parts.append(markdown)
|
44
|
+
|
45
|
+
return "\n\n".join(markdown_parts)
|
File without changes
|