notionary 0.1.10__tar.gz → 0.1.12__tar.gz
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-0.1.10 → notionary-0.1.12}/PKG-INFO +1 -1
- notionary-0.1.12/notionary/__init__.py +20 -0
- notionary-0.1.12/notionary/core/converters/elements/audio_element.py +143 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/embed_element.py +2 -4
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/toggle_element.py +28 -20
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/markdown_to_notion_converter.py +70 -109
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry.py +2 -6
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry_builder.py +2 -0
- notionary-0.1.12/notionary/core/database/database_discovery.py +140 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/database/notion_database_manager.py +26 -49
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/database/notion_database_manager_factory.py +10 -4
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/notion_client.py +4 -2
- notionary-0.1.12/notionary/core/page/content/notion_page_content_chunker.py +84 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/content/page_content_manager.py +26 -8
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/metadata/metadata_editor.py +57 -44
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/metadata/notion_icon_manager.py +9 -11
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/metadata/notion_page_cover_manager.py +15 -20
- notionary-0.1.12/notionary/core/page/notion_page_manager.py +312 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/properites/database_property_service.py +114 -98
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/properites/page_property_manager.py +78 -49
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/properites/property_formatter.py +1 -1
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/properites/property_operation_result.py +43 -30
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/properites/property_value_extractor.py +26 -8
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/relations/notion_page_relation_manager.py +71 -52
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/relations/notion_page_title_resolver.py +11 -11
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/relations/page_database_relation.py +14 -14
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/page/relations/relation_operation_result.py +50 -41
- {notionary-0.1.10 → notionary-0.1.12}/notionary/util/page_id_utils.py +11 -7
- {notionary-0.1.10 → notionary-0.1.12}/notionary.egg-info/PKG-INFO +1 -1
- {notionary-0.1.10 → notionary-0.1.12}/notionary.egg-info/SOURCES.txt +3 -1
- {notionary-0.1.10 → notionary-0.1.12}/setup.py +2 -2
- notionary-0.1.10/notionary/__init__.py +0 -9
- notionary-0.1.10/notionary/core/database/notion_database_schema.py +0 -104
- notionary-0.1.10/notionary/core/page/notion_page_manager.py +0 -331
- {notionary-0.1.10 → notionary-0.1.12}/LICENSE +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/README.md +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/__init__.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/bookmark_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/callout_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/code_block_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/column_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/divider_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/heading_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/image_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/list_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/notion_block_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/paragraph_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/qoute_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/table_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/text_inline_formatter.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/todo_lists.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/elements/video_element.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/notion_to_markdown_converter.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/database/database_info_service.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/core/database/models/page_result.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/exceptions/database_exceptions.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/exceptions/page_creation_exception.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/util/logging_mixin.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary/util/singleton_decorator.py +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary.egg-info/dependency_links.txt +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary.egg-info/requires.txt +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/notionary.egg-info/top_level.txt +0 -0
- {notionary-0.1.10 → notionary-0.1.12}/setup.cfg +0 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
from .core.notion_client import NotionClient
|
2
|
+
|
3
|
+
from .core.database.notion_database_manager import NotionDatabaseManager
|
4
|
+
from .core.database.notion_database_manager_factory import NotionDatabaseFactory
|
5
|
+
from .core.database.database_discovery import DatabaseDiscovery
|
6
|
+
|
7
|
+
from .core.page.notion_page_manager import NotionPageManager
|
8
|
+
|
9
|
+
from .core.converters.registry.block_element_registry import BlockElementRegistry
|
10
|
+
from .core.converters.registry.block_element_registry_builder import BlockElementRegistryBuilder
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"NotionClient",
|
14
|
+
"NotionDatabaseManager",
|
15
|
+
"NotionDatabaseFactory",
|
16
|
+
"DatabaseDiscovery",
|
17
|
+
"NotionPageManager",
|
18
|
+
"BlockElementRegistry",
|
19
|
+
"BlockElementRegistryBuilder",
|
20
|
+
]
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List
|
3
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
4
|
+
|
5
|
+
|
6
|
+
class AudioElement(NotionBlockElement):
|
7
|
+
"""
|
8
|
+
Handles conversion between Markdown audio embeds and Notion audio blocks.
|
9
|
+
|
10
|
+
Markdown audio syntax (custom format since standard Markdown doesn't support audio):
|
11
|
+
- $[Caption](https://example.com/audio.mp3) - Basic audio with caption
|
12
|
+
- $[](https://example.com/audio.mp3) - Audio without caption
|
13
|
+
- $[Caption](https://storage.googleapis.com/audio_summaries/example.mp3) - CDN hosted audio
|
14
|
+
|
15
|
+
Supports various audio URLs including direct audio file links from CDNs and other sources.
|
16
|
+
"""
|
17
|
+
|
18
|
+
# Regex pattern for audio syntax
|
19
|
+
PATTERN = re.compile(
|
20
|
+
r"^\$\[(.*?)\]" # $[Caption] part
|
21
|
+
+ r'\((https?://[^\s"]+)' # (URL part
|
22
|
+
+ r"\)$" # closing parenthesis
|
23
|
+
)
|
24
|
+
|
25
|
+
# Audio file extensions
|
26
|
+
AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"]
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def match_markdown(text: str) -> bool:
|
30
|
+
"""Check if text is a markdown audio embed."""
|
31
|
+
text = text.strip()
|
32
|
+
return text.startswith("$[") and bool(AudioElement.PATTERN.match(text))
|
33
|
+
|
34
|
+
@staticmethod
|
35
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
36
|
+
"""Check if block is a Notion audio."""
|
37
|
+
return block.get("type") == "audio"
|
38
|
+
|
39
|
+
@staticmethod
|
40
|
+
def is_audio_url(url: str) -> bool:
|
41
|
+
"""Check if URL points to an audio file."""
|
42
|
+
return (
|
43
|
+
any(url.lower().endswith(ext) for ext in AudioElement.AUDIO_EXTENSIONS)
|
44
|
+
or "audio" in url.lower()
|
45
|
+
or "storage.googleapis.com/audio_summaries" in url.lower()
|
46
|
+
)
|
47
|
+
|
48
|
+
@staticmethod
|
49
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
50
|
+
"""Convert markdown audio embed to Notion audio block."""
|
51
|
+
audio_match = AudioElement.PATTERN.match(text.strip())
|
52
|
+
if not audio_match:
|
53
|
+
return None
|
54
|
+
|
55
|
+
caption = audio_match.group(1)
|
56
|
+
url = audio_match.group(2)
|
57
|
+
|
58
|
+
if not url:
|
59
|
+
return None
|
60
|
+
|
61
|
+
# Make sure this is an audio URL
|
62
|
+
if not AudioElement.is_audio_url(url):
|
63
|
+
# If not obviously an audio URL, we'll still accept it as the user might know better
|
64
|
+
pass
|
65
|
+
|
66
|
+
# Prepare the audio block
|
67
|
+
audio_block = {
|
68
|
+
"type": "audio",
|
69
|
+
"audio": {"type": "external", "external": {"url": url}},
|
70
|
+
}
|
71
|
+
|
72
|
+
# Add caption if provided
|
73
|
+
if caption:
|
74
|
+
audio_block["audio"]["caption"] = [
|
75
|
+
{"type": "text", "text": {"content": caption}}
|
76
|
+
]
|
77
|
+
|
78
|
+
return audio_block
|
79
|
+
|
80
|
+
@staticmethod
|
81
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
82
|
+
"""Convert Notion audio block to markdown audio embed."""
|
83
|
+
if block.get("type") != "audio":
|
84
|
+
return None
|
85
|
+
|
86
|
+
audio_data = block.get("audio", {})
|
87
|
+
|
88
|
+
# Handle both external and file (uploaded) audios
|
89
|
+
if audio_data.get("type") == "external":
|
90
|
+
url = audio_data.get("external", {}).get("url", "")
|
91
|
+
elif audio_data.get("type") == "file":
|
92
|
+
url = audio_data.get("file", {}).get("url", "")
|
93
|
+
else:
|
94
|
+
return None
|
95
|
+
|
96
|
+
if not url:
|
97
|
+
return None
|
98
|
+
|
99
|
+
# Extract caption if available
|
100
|
+
caption = ""
|
101
|
+
caption_rich_text = audio_data.get("caption", [])
|
102
|
+
if caption_rich_text:
|
103
|
+
caption = AudioElement._extract_text_content(caption_rich_text)
|
104
|
+
|
105
|
+
return f"$[{caption}]({url})"
|
106
|
+
|
107
|
+
@staticmethod
|
108
|
+
def is_multiline() -> bool:
|
109
|
+
"""Audio embeds are single-line elements."""
|
110
|
+
return False
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
|
114
|
+
"""Extract plain text content from Notion rich_text elements."""
|
115
|
+
result = ""
|
116
|
+
for text_obj in rich_text:
|
117
|
+
if text_obj.get("type") == "text":
|
118
|
+
result += text_obj.get("text", {}).get("content", "")
|
119
|
+
elif "plain_text" in text_obj:
|
120
|
+
result += text_obj.get("plain_text", "")
|
121
|
+
return result
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
def get_llm_prompt_content(cls) -> dict:
|
125
|
+
"""Returns information for LLM prompts about this element."""
|
126
|
+
return {
|
127
|
+
"description": "Embeds audio content from external sources like CDNs or direct audio URLs.",
|
128
|
+
"when_to_use": "Use audio embeds when you want to include audio content directly in your document. Audio embeds are useful for podcasts, music, voice recordings, or any content that benefits from audio explanation.",
|
129
|
+
"syntax": [
|
130
|
+
"$[](https://example.com/audio.mp3) - Audio without caption",
|
131
|
+
"$[Caption text](https://example.com/audio.mp3) - Audio with caption",
|
132
|
+
],
|
133
|
+
"supported_sources": [
|
134
|
+
"Direct links to audio files (.mp3, .wav, .ogg, etc.)",
|
135
|
+
"Google Cloud Storage links (storage.googleapis.com)",
|
136
|
+
"Other audio hosting platforms supported by Notion",
|
137
|
+
],
|
138
|
+
"examples": [
|
139
|
+
"$[Podcast Episode](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)",
|
140
|
+
"$[Voice recording](https://example.com/audio/recording.mp3)",
|
141
|
+
"$[](https://storage.googleapis.com/audio_summaries/example.mp3)",
|
142
|
+
],
|
143
|
+
}
|
@@ -16,9 +16,7 @@ class EmbedElement(NotionBlockElement):
|
|
16
16
|
"""
|
17
17
|
|
18
18
|
PATTERN = re.compile(
|
19
|
-
r"^<embed(?:\:(.*?))?>(?:\s*)"
|
20
|
-
+ r'\((https?://[^\s"]+)'
|
21
|
-
+ r"\)$"
|
19
|
+
r"^<embed(?:\:(.*?))?>(?:\s*)" + r'\((https?://[^\s"]+)' + r"\)$"
|
22
20
|
)
|
23
21
|
|
24
22
|
@staticmethod
|
@@ -126,4 +124,4 @@ class EmbedElement(NotionBlockElement):
|
|
126
124
|
"<embed:Project documentation>(https://github.com/username/repo)",
|
127
125
|
"<embed>(https://example.com/important-reference.pdf)",
|
128
126
|
],
|
129
|
-
}
|
127
|
+
}
|
@@ -6,20 +6,15 @@ from notionary.core.converters.elements.notion_block_element import NotionBlockE
|
|
6
6
|
|
7
7
|
class ToggleElement(NotionBlockElement):
|
8
8
|
"""
|
9
|
-
|
10
|
-
|
11
|
-
Markdown toggle syntax:
|
12
|
-
+++ Toggle title
|
13
|
-
Indented content that belongs to the toggle
|
14
|
-
More indented content
|
15
|
-
|
16
|
-
Non-indented content marks the end of the toggle block.
|
9
|
+
Verbesserte ToggleElement-Klasse, die Kontext berücksichtigt.
|
17
10
|
"""
|
18
11
|
|
19
12
|
TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+(.+)$")
|
20
|
-
|
21
13
|
INDENT_PATTERN = re.compile(r"^(\s{2,}|\t+)(.+)$")
|
22
14
|
|
15
|
+
# Ein neues Pattern, um spezifisch nach der "Transcript" Überschrift zu suchen
|
16
|
+
TRANSCRIPT_TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+Transcript$")
|
17
|
+
|
23
18
|
@staticmethod
|
24
19
|
def match_markdown(text: str) -> bool:
|
25
20
|
"""Check if text is a markdown toggle."""
|
@@ -32,11 +27,7 @@ class ToggleElement(NotionBlockElement):
|
|
32
27
|
|
33
28
|
@staticmethod
|
34
29
|
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
35
|
-
"""Convert markdown toggle to Notion toggle block.
|
36
|
-
|
37
|
-
Note: This method only converts the toggle title line.
|
38
|
-
The nested content needs to be processed separately.
|
39
|
-
"""
|
30
|
+
"""Convert markdown toggle to Notion toggle block."""
|
40
31
|
toggle_match = ToggleElement.TOGGLE_PATTERN.match(text.strip())
|
41
32
|
if not toggle_match:
|
42
33
|
return None
|
@@ -149,18 +140,21 @@ class ToggleElement(NotionBlockElement):
|
|
149
140
|
|
150
141
|
@classmethod
|
151
142
|
def find_matches(
|
152
|
-
cls,
|
143
|
+
cls,
|
144
|
+
text: str,
|
145
|
+
process_nested_content: Callable = None,
|
146
|
+
context_aware: bool = True,
|
153
147
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
154
148
|
"""
|
155
|
-
|
149
|
+
Verbesserte find_matches-Methode, die Kontext beim Finden von Toggles berücksichtigt.
|
156
150
|
|
157
151
|
Args:
|
158
|
-
text:
|
159
|
-
process_nested_content:
|
160
|
-
|
152
|
+
text: Der zu durchsuchende Text
|
153
|
+
process_nested_content: Optionale Callback-Funktion zur Verarbeitung verschachtelter Inhalte
|
154
|
+
context_aware: Ob der Kontext (vorhergehende Zeilen) beim Finden von Toggles berücksichtigt werden soll
|
161
155
|
|
162
156
|
Returns:
|
163
|
-
|
157
|
+
Liste von (start_pos, end_pos, block) Tupeln
|
164
158
|
"""
|
165
159
|
if not text:
|
166
160
|
return []
|
@@ -177,6 +171,20 @@ class ToggleElement(NotionBlockElement):
|
|
177
171
|
i += 1
|
178
172
|
continue
|
179
173
|
|
174
|
+
# Wenn context_aware aktiviert ist, prüfen wir für "Transcript"-Toggles
|
175
|
+
# ob sie direkt nach einem Bullet Point kommen
|
176
|
+
is_transcript_toggle = cls.TRANSCRIPT_TOGGLE_PATTERN.match(line.strip())
|
177
|
+
|
178
|
+
if context_aware and is_transcript_toggle:
|
179
|
+
# Prüfen, ob der Toggle in einem gültigen Kontext ist (nach Bullet Point)
|
180
|
+
if i > 0 and lines[i - 1].strip().startswith("- "):
|
181
|
+
# Gültiger Kontext, fahre fort
|
182
|
+
pass
|
183
|
+
else:
|
184
|
+
# Ungültiger Kontext für Transcript-Toggle, überspringe ihn
|
185
|
+
i += 1
|
186
|
+
continue
|
187
|
+
|
180
188
|
start_pos = 0
|
181
189
|
for j in range(i):
|
182
190
|
start_pos += len(lines[j]) + 1
|
{notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/markdown_to_notion_converter.py
RENAMED
@@ -12,6 +12,8 @@ class MarkdownToNotionConverter:
|
|
12
12
|
SPACER_MARKER = "<!-- spacer -->"
|
13
13
|
MULTILINE_CONTENT_MARKER = "<!-- REMOVED_MULTILINE_CONTENT -->"
|
14
14
|
TOGGLE_MARKER = "<!-- toggle_content -->"
|
15
|
+
TOGGLE_MARKER_PREFIX = "<!-- toggle_"
|
16
|
+
TOGGLE_MARKER_SUFFIX = " -->"
|
15
17
|
|
16
18
|
def __init__(self, block_registry: Optional[BlockElementRegistry] = None):
|
17
19
|
"""
|
@@ -46,38 +48,48 @@ class MarkdownToNotionConverter:
|
|
46
48
|
if not markdown_text:
|
47
49
|
return []
|
48
50
|
|
49
|
-
#
|
50
|
-
|
51
|
+
# We'll process all blocks in order, preserving their original positions
|
52
|
+
all_blocks = []
|
53
|
+
|
54
|
+
# First, identify all toggle blocks
|
55
|
+
toggle_blocks = self._identify_toggle_blocks(markdown_text)
|
56
|
+
|
57
|
+
# If we have toggles, process them and extract positions
|
58
|
+
if toggle_blocks:
|
59
|
+
all_blocks.extend(toggle_blocks)
|
51
60
|
|
52
61
|
# Process other multiline elements
|
53
|
-
|
54
|
-
|
55
|
-
|
62
|
+
multiline_blocks = self._identify_multiline_blocks(markdown_text, toggle_blocks)
|
63
|
+
if multiline_blocks:
|
64
|
+
all_blocks.extend(multiline_blocks)
|
56
65
|
|
57
66
|
# Process remaining text line by line
|
58
|
-
line_blocks = self._process_text_lines(
|
67
|
+
line_blocks = self._process_text_lines(
|
68
|
+
markdown_text, toggle_blocks + multiline_blocks
|
69
|
+
)
|
70
|
+
if line_blocks:
|
71
|
+
all_blocks.extend(line_blocks)
|
59
72
|
|
60
|
-
#
|
61
|
-
all_blocks = toggle_blocks + multiline_blocks + line_blocks
|
73
|
+
# Sort all blocks by their position in the text
|
62
74
|
all_blocks.sort(key=lambda x: x[0])
|
63
75
|
|
64
|
-
# Extract just the blocks
|
76
|
+
# Extract just the blocks without position information
|
65
77
|
blocks = [block for _, _, block in all_blocks]
|
66
78
|
|
67
79
|
# Process spacing between blocks
|
68
80
|
return self._process_block_spacing(blocks)
|
69
81
|
|
70
|
-
def
|
82
|
+
def _identify_toggle_blocks(
|
71
83
|
self, text: str
|
72
|
-
) ->
|
84
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
73
85
|
"""
|
74
|
-
|
86
|
+
Identify all toggle blocks in the text without replacing them.
|
75
87
|
|
76
88
|
Args:
|
77
89
|
text: The text to process
|
78
90
|
|
79
91
|
Returns:
|
80
|
-
|
92
|
+
List of (start_pos, end_pos, block) tuples
|
81
93
|
"""
|
82
94
|
# Find toggle element in registry
|
83
95
|
toggle_element = None
|
@@ -91,67 +103,28 @@ class MarkdownToNotionConverter:
|
|
91
103
|
break
|
92
104
|
|
93
105
|
if not toggle_element:
|
94
|
-
|
95
|
-
return text, []
|
106
|
+
return []
|
96
107
|
|
97
|
-
# Use the find_matches method
|
108
|
+
# Use the find_matches method with context awareness
|
98
109
|
# Pass the converter's convert method as a callback to process nested content
|
99
|
-
toggle_blocks = toggle_element.find_matches(
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
110
|
+
toggle_blocks = toggle_element.find_matches(
|
111
|
+
text, self.convert, context_aware=True
|
112
|
+
)
|
113
|
+
return toggle_blocks
|
136
114
|
|
137
|
-
def
|
138
|
-
self, text: str
|
139
|
-
) ->
|
115
|
+
def _identify_multiline_blocks(
|
116
|
+
self, text: str, exclude_blocks: List[Tuple[int, int, Dict[str, Any]]]
|
117
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
140
118
|
"""
|
141
|
-
|
119
|
+
Identify all multiline blocks (except toggle blocks) without altering the text.
|
142
120
|
|
143
121
|
Args:
|
144
122
|
text: The text to process
|
123
|
+
exclude_blocks: Blocks to exclude (e.g., already identified toggle blocks)
|
145
124
|
|
146
125
|
Returns:
|
147
|
-
|
126
|
+
List of (start_pos, end_pos, block) tuples
|
148
127
|
"""
|
149
|
-
if not text:
|
150
|
-
return text, []
|
151
|
-
|
152
|
-
multiline_blocks = []
|
153
|
-
processed_text = text
|
154
|
-
|
155
128
|
# Get all multiline elements except ToggleElement
|
156
129
|
multiline_elements = [
|
157
130
|
element
|
@@ -160,51 +133,45 @@ class MarkdownToNotionConverter:
|
|
160
133
|
]
|
161
134
|
|
162
135
|
if not multiline_elements:
|
163
|
-
return
|
136
|
+
return []
|
164
137
|
|
138
|
+
# Create a set of ranges to exclude
|
139
|
+
exclude_ranges = set()
|
140
|
+
for start, end, _ in exclude_blocks:
|
141
|
+
exclude_ranges.update(range(start, end + 1))
|
142
|
+
|
143
|
+
multiline_blocks = []
|
165
144
|
for element in multiline_elements:
|
166
145
|
if not hasattr(element, "find_matches"):
|
167
146
|
continue
|
168
147
|
|
169
|
-
# Find all matches for this element
|
148
|
+
# Find all matches for this element
|
170
149
|
if hasattr(element, "set_converter_callback"):
|
171
|
-
matches = element.find_matches(
|
150
|
+
matches = element.find_matches(text, self.convert)
|
172
151
|
else:
|
173
|
-
matches = element.find_matches(
|
152
|
+
matches = element.find_matches(text)
|
174
153
|
|
175
154
|
if not matches:
|
176
155
|
continue
|
177
156
|
|
178
|
-
|
157
|
+
# Add only blocks that don't overlap with excluded ranges
|
158
|
+
for start, end, block in matches:
|
159
|
+
# Check if this block overlaps with any excluded range
|
160
|
+
if any(start <= i <= end for i in exclude_ranges):
|
161
|
+
continue
|
162
|
+
multiline_blocks.append((start, end, block))
|
179
163
|
|
180
|
-
|
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
|
164
|
+
return multiline_blocks
|
201
165
|
|
202
|
-
def _process_text_lines(
|
166
|
+
def _process_text_lines(
|
167
|
+
self, text: str, exclude_blocks: List[Tuple[int, int, Dict[str, Any]]]
|
168
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
203
169
|
"""
|
204
|
-
Process text line by line
|
170
|
+
Process text line by line, excluding ranges already processed.
|
205
171
|
|
206
172
|
Args:
|
207
173
|
text: The text to process
|
174
|
+
exclude_blocks: Blocks to exclude (e.g., already identified toggle and multiline blocks)
|
208
175
|
|
209
176
|
Returns:
|
210
177
|
List of (start_pos, end_pos, block) tuples
|
@@ -212,6 +179,11 @@ class MarkdownToNotionConverter:
|
|
212
179
|
if not text:
|
213
180
|
return []
|
214
181
|
|
182
|
+
# Create a set of excluded positions
|
183
|
+
exclude_positions = set()
|
184
|
+
for start, end, _ in exclude_blocks:
|
185
|
+
exclude_positions.update(range(start, end + 1))
|
186
|
+
|
215
187
|
line_blocks = []
|
216
188
|
lines = text.split("\n")
|
217
189
|
|
@@ -222,9 +194,10 @@ class MarkdownToNotionConverter:
|
|
222
194
|
|
223
195
|
for line in lines:
|
224
196
|
line_length = len(line) + 1 # +1 for newline
|
197
|
+
line_end = current_pos + line_length - 1
|
225
198
|
|
226
|
-
# Skip
|
227
|
-
if
|
199
|
+
# Skip lines that are part of excluded blocks
|
200
|
+
if any(current_pos <= pos <= line_end for pos in exclude_positions):
|
228
201
|
current_pos += line_length
|
229
202
|
continue
|
230
203
|
|
@@ -233,7 +206,7 @@ class MarkdownToNotionConverter:
|
|
233
206
|
line_blocks.append(
|
234
207
|
(
|
235
208
|
current_pos,
|
236
|
-
current_pos + line_length,
|
209
|
+
current_pos + line_length - 1,
|
237
210
|
self._create_empty_paragraph(),
|
238
211
|
)
|
239
212
|
)
|
@@ -273,7 +246,7 @@ class MarkdownToNotionConverter:
|
|
273
246
|
current_paragraph, paragraph_start, current_pos, line_blocks
|
274
247
|
)
|
275
248
|
line_blocks.append(
|
276
|
-
(current_pos, current_pos + line_length, special_block)
|
249
|
+
(current_pos, current_pos + line_length - 1, special_block)
|
277
250
|
)
|
278
251
|
current_paragraph = []
|
279
252
|
current_pos += line_length
|
@@ -292,18 +265,6 @@ class MarkdownToNotionConverter:
|
|
292
265
|
|
293
266
|
return line_blocks
|
294
267
|
|
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
268
|
def _is_spacer_marker(self, line: str) -> bool:
|
308
269
|
"""Check if a line is a spacer marker."""
|
309
270
|
return line.strip() == self.SPACER_MARKER
|
@@ -343,7 +304,7 @@ class MarkdownToNotionConverter:
|
|
343
304
|
)
|
344
305
|
current_paragraph.clear()
|
345
306
|
|
346
|
-
line_blocks.append((current_pos, current_pos + line_length, todo_block))
|
307
|
+
line_blocks.append((current_pos, current_pos + line_length - 1, todo_block))
|
347
308
|
|
348
309
|
def _extract_special_block(self, line: str) -> Optional[Dict[str, Any]]:
|
349
310
|
"""
|
{notionary-0.1.10 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry.py
RENAMED
@@ -95,8 +95,8 @@ class BlockElementRegistry:
|
|
95
95
|
"""
|
96
96
|
# Create a copy of registered elements
|
97
97
|
element_classes = self._elements.copy()
|
98
|
-
|
99
|
-
# TODO: Das hier besser formattieren und über debug level lösen . )
|
98
|
+
|
99
|
+
# TODO: Das hier besser formattieren und über debug level lösen . )
|
100
100
|
print("Elements in registry:", element_classes)
|
101
101
|
|
102
102
|
formatter_names = [e.__name__ for e in element_classes]
|
@@ -234,7 +234,3 @@ paragraphs, lists, quotes, etc.
|
|
234
234
|
element_docs = cls.generate_element_docs(element_classes)
|
235
235
|
|
236
236
|
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
# TODO: Testen ob der Inline Formatter hier überhaupt funktionieren tut:
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import List, Type
|
2
2
|
from collections import OrderedDict
|
3
3
|
|
4
|
+
from notionary.core.converters.elements.audio_element import AudioElement
|
4
5
|
from notionary.core.converters.elements.embed_element import EmbedElement
|
5
6
|
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
6
7
|
from notionary.core.converters.registry.block_element_registry import (
|
@@ -92,6 +93,7 @@ class BlockElementRegistryBuilder:
|
|
92
93
|
.add_element(ImageElement)
|
93
94
|
.add_element(VideoElement)
|
94
95
|
.add_element(EmbedElement)
|
96
|
+
.add_element(AudioElement)
|
95
97
|
.add_element(ParagraphElement)
|
96
98
|
) # Add paragraph last as fallback
|
97
99
|
|