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,294 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
|
+
from typing_extensions import override
|
4
|
+
|
5
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
6
|
+
|
7
|
+
|
8
|
+
class ColumnElement(NotionBlockElement):
|
9
|
+
"""
|
10
|
+
Handles conversion between custom Markdown column syntax and Notion column blocks.
|
11
|
+
|
12
|
+
Markdown column syntax:
|
13
|
+
::: columns
|
14
|
+
::: column
|
15
|
+
Content for first column
|
16
|
+
:::
|
17
|
+
::: column
|
18
|
+
Content for second column
|
19
|
+
:::
|
20
|
+
:::
|
21
|
+
|
22
|
+
This creates a column layout in Notion with the specified content in each column.
|
23
|
+
"""
|
24
|
+
|
25
|
+
COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
|
26
|
+
COLUMN_START = re.compile(r"^:::\s*column\s*$")
|
27
|
+
BLOCK_END = re.compile(r"^:::\s*$")
|
28
|
+
|
29
|
+
_converter_callback = None
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def set_converter_callback(
|
33
|
+
cls, callback: Callable[[str], List[Dict[str, Any]]]
|
34
|
+
) -> None:
|
35
|
+
"""
|
36
|
+
Setze die Callback-Funktion, die zum Konvertieren von Markdown zu Notion-Blöcken verwendet wird.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
callback: Funktion, die Markdown-Text annimmt und eine Liste von Notion-Blöcken zurückgibt
|
40
|
+
"""
|
41
|
+
cls._converter_callback = callback
|
42
|
+
|
43
|
+
@override
|
44
|
+
@staticmethod
|
45
|
+
def match_markdown(text: str) -> bool:
|
46
|
+
"""Check if text starts a columns block."""
|
47
|
+
return bool(ColumnElement.COLUMNS_START.match(text.strip()))
|
48
|
+
|
49
|
+
@override
|
50
|
+
@staticmethod
|
51
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
52
|
+
"""Check if block is a Notion column_list."""
|
53
|
+
return block.get("type") == "column_list"
|
54
|
+
|
55
|
+
@override
|
56
|
+
@staticmethod
|
57
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
58
|
+
"""
|
59
|
+
Convert markdown column syntax to Notion column blocks.
|
60
|
+
|
61
|
+
Note: This only processes the first line (columns start).
|
62
|
+
The full column content needs to be processed separately.
|
63
|
+
"""
|
64
|
+
if not ColumnElement.COLUMNS_START.match(text.strip()):
|
65
|
+
return None
|
66
|
+
|
67
|
+
# Create an empty column_list block
|
68
|
+
# Child columns will be added by the column processor
|
69
|
+
return {"type": "column_list", "column_list": {"children": []}}
|
70
|
+
|
71
|
+
@override
|
72
|
+
@staticmethod
|
73
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
74
|
+
"""Convert Notion column_list block to markdown column syntax."""
|
75
|
+
if block.get("type") != "column_list":
|
76
|
+
return None
|
77
|
+
|
78
|
+
column_children = block.get("column_list", {}).get("children", [])
|
79
|
+
|
80
|
+
# Start the columns block
|
81
|
+
result = ["::: columns"]
|
82
|
+
|
83
|
+
# Process each column
|
84
|
+
for column_block in column_children:
|
85
|
+
if column_block.get("type") == "column":
|
86
|
+
result.append("::: column")
|
87
|
+
|
88
|
+
for _ in column_block.get("column", {}).get("children", []):
|
89
|
+
result.append(" [Column content]") # Placeholder
|
90
|
+
|
91
|
+
result.append(":::")
|
92
|
+
|
93
|
+
# End the columns block
|
94
|
+
result.append(":::")
|
95
|
+
|
96
|
+
return "\n".join(result)
|
97
|
+
|
98
|
+
@override
|
99
|
+
@staticmethod
|
100
|
+
def is_multiline() -> bool:
|
101
|
+
"""Column blocks span multiple lines."""
|
102
|
+
return True
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def find_matches(
|
106
|
+
cls, text: str, converter_callback: Optional[Callable] = None
|
107
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
108
|
+
"""
|
109
|
+
Find all column block matches in the text and return their positions and blocks.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
text: The input markdown text
|
113
|
+
converter_callback: Optional callback to convert nested content
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
List of tuples (start_pos, end_pos, block)
|
117
|
+
"""
|
118
|
+
# Wenn ein Callback übergeben wurde, nutze diesen, sonst die gespeicherte Referenz
|
119
|
+
converter = converter_callback or cls._converter_callback
|
120
|
+
if not converter:
|
121
|
+
raise ValueError(
|
122
|
+
"No converter callback provided for ColumnElement. Call set_converter_callback first or provide converter_callback parameter."
|
123
|
+
)
|
124
|
+
|
125
|
+
matches = []
|
126
|
+
lines = text.split("\n")
|
127
|
+
i = 0
|
128
|
+
|
129
|
+
while i < len(lines):
|
130
|
+
# Skip non-column lines
|
131
|
+
if not ColumnElement.COLUMNS_START.match(lines[i].strip()):
|
132
|
+
i += 1
|
133
|
+
continue
|
134
|
+
|
135
|
+
# Process a column block and add to matches
|
136
|
+
column_block_info = cls._process_column_block(lines, i, converter)
|
137
|
+
matches.append(column_block_info)
|
138
|
+
|
139
|
+
# Skip to the end of the processed column block
|
140
|
+
i = column_block_info[3] # i is returned as the 4th element in the tuple
|
141
|
+
|
142
|
+
return [(start, end, block) for start, end, block, _ in matches]
|
143
|
+
|
144
|
+
@classmethod
|
145
|
+
def _process_column_block(
|
146
|
+
cls, lines: List[str], start_index: int, converter_callback: Callable
|
147
|
+
) -> Tuple[int, int, Dict[str, Any], int]:
|
148
|
+
"""
|
149
|
+
Process a complete column block structure from the given starting line.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
lines: All lines of the text
|
153
|
+
start_index: Index of the column block start line
|
154
|
+
converter_callback: Callback function to convert markdown to notion blocks
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
Tuple of (start_pos, end_pos, block, next_line_index)
|
158
|
+
"""
|
159
|
+
columns_start = start_index
|
160
|
+
columns_block = cls.markdown_to_notion(lines[start_index].strip())
|
161
|
+
columns_children = []
|
162
|
+
|
163
|
+
next_index = cls._collect_columns(
|
164
|
+
lines, start_index + 1, columns_children, converter_callback
|
165
|
+
)
|
166
|
+
|
167
|
+
# Add columns to the main block
|
168
|
+
if columns_children:
|
169
|
+
columns_block["column_list"]["children"] = columns_children
|
170
|
+
|
171
|
+
# Calculate positions
|
172
|
+
start_pos = sum(len(lines[j]) + 1 for j in range(columns_start))
|
173
|
+
end_pos = sum(len(lines[j]) + 1 for j in range(next_index))
|
174
|
+
|
175
|
+
return (start_pos, end_pos, columns_block, next_index)
|
176
|
+
|
177
|
+
@classmethod
|
178
|
+
def _collect_columns(
|
179
|
+
cls,
|
180
|
+
lines: List[str],
|
181
|
+
start_index: int,
|
182
|
+
columns_children: List[Dict[str, Any]],
|
183
|
+
converter_callback: Callable,
|
184
|
+
) -> int:
|
185
|
+
"""
|
186
|
+
Collect all columns within a column block structure.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
lines: All lines of the text
|
190
|
+
start_index: Index to start collecting from
|
191
|
+
columns_children: List to append collected columns to
|
192
|
+
converter_callback: Callback function to convert column content
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
Next line index after all columns have been processed
|
196
|
+
"""
|
197
|
+
i = start_index
|
198
|
+
in_column = False
|
199
|
+
column_content = []
|
200
|
+
|
201
|
+
while i < len(lines):
|
202
|
+
current_line = lines[i].strip()
|
203
|
+
|
204
|
+
if cls.COLUMNS_START.match(current_line):
|
205
|
+
break
|
206
|
+
|
207
|
+
if cls.COLUMN_START.match(current_line):
|
208
|
+
cls._finalize_column(
|
209
|
+
column_content, columns_children, in_column, converter_callback
|
210
|
+
)
|
211
|
+
column_content = []
|
212
|
+
in_column = True
|
213
|
+
i += 1
|
214
|
+
continue
|
215
|
+
|
216
|
+
if cls.BLOCK_END.match(current_line) and in_column:
|
217
|
+
cls._finalize_column(
|
218
|
+
column_content, columns_children, in_column, converter_callback
|
219
|
+
)
|
220
|
+
column_content = []
|
221
|
+
in_column = False
|
222
|
+
i += 1
|
223
|
+
continue
|
224
|
+
|
225
|
+
if cls.BLOCK_END.match(current_line) and not in_column:
|
226
|
+
i += 1
|
227
|
+
break
|
228
|
+
|
229
|
+
if in_column:
|
230
|
+
column_content.append(lines[i])
|
231
|
+
|
232
|
+
i += 1
|
233
|
+
|
234
|
+
cls._finalize_column(
|
235
|
+
column_content, columns_children, in_column, converter_callback
|
236
|
+
)
|
237
|
+
|
238
|
+
return i
|
239
|
+
|
240
|
+
@staticmethod
|
241
|
+
def _finalize_column(
|
242
|
+
column_content: List[str],
|
243
|
+
columns_children: List[Dict[str, Any]],
|
244
|
+
in_column: bool,
|
245
|
+
converter_callback: Callable,
|
246
|
+
) -> None:
|
247
|
+
"""
|
248
|
+
Finalize a column by processing its content and adding it to the columns_children list.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
column_content: Content lines of the column
|
252
|
+
columns_children: List to append the column block to
|
253
|
+
in_column: Whether we're currently in a column (if False, does nothing)
|
254
|
+
converter_callback: Callback function to convert column content
|
255
|
+
"""
|
256
|
+
if not (in_column and column_content):
|
257
|
+
return
|
258
|
+
|
259
|
+
# Process column content using the provided callback
|
260
|
+
column_blocks = converter_callback("\n".join(column_content))
|
261
|
+
|
262
|
+
# Create column block
|
263
|
+
column_block = {"type": "column", "column": {"children": column_blocks}}
|
264
|
+
columns_children.append(column_block)
|
265
|
+
|
266
|
+
@classmethod
|
267
|
+
def get_llm_prompt_content(cls) -> dict:
|
268
|
+
"""
|
269
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
270
|
+
Includes description, usage guidance, syntax options, and examples.
|
271
|
+
"""
|
272
|
+
return {
|
273
|
+
"description": "Creates a multi-column layout that displays content side by side.",
|
274
|
+
"when_to_use": "Use columns sparingly, only for direct comparisons or when parallel presentation significantly improves readability. Best for pros/cons lists, feature comparisons, or pairing images with descriptions. Avoid overusing as it can complicate document structure.",
|
275
|
+
"syntax": [
|
276
|
+
"::: columns",
|
277
|
+
"::: column",
|
278
|
+
"Content for first column",
|
279
|
+
":::",
|
280
|
+
"::: column",
|
281
|
+
"Content for second column",
|
282
|
+
":::",
|
283
|
+
":::",
|
284
|
+
],
|
285
|
+
"notes": [
|
286
|
+
"Any Notion block can be placed within columns",
|
287
|
+
"Add more columns with additional '::: column' sections",
|
288
|
+
"Each column must close with ':::' and the entire columns section with another ':::'",
|
289
|
+
],
|
290
|
+
"examples": [
|
291
|
+
"::: columns\n::: column\n## Features\n- Fast response time\n- Intuitive interface\n- Regular updates\n:::\n::: column\n## Benefits\n- Increased productivity\n- Better collaboration\n- Simplified workflows\n:::\n:::",
|
292
|
+
"::: columns\n::: column\n\n:::\n::: column\nThis text appears next to the image, creating a media-with-caption style layout that's perfect for documentation or articles.\n:::\n:::",
|
293
|
+
],
|
294
|
+
}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# File: elements/dividers.py
|
2
|
+
|
3
|
+
from typing import Dict, Any, Optional
|
4
|
+
from typing_extensions import override
|
5
|
+
import re
|
6
|
+
|
7
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
8
|
+
|
9
|
+
|
10
|
+
class DividerElement(NotionBlockElement):
|
11
|
+
"""
|
12
|
+
Handles conversion between Markdown horizontal dividers and Notion divider blocks.
|
13
|
+
|
14
|
+
Markdown divider syntax:
|
15
|
+
- Three or more hyphens (---) on a line by themselves
|
16
|
+
"""
|
17
|
+
|
18
|
+
PATTERN = re.compile(r"^\s*-{3,}\s*$")
|
19
|
+
|
20
|
+
@override
|
21
|
+
@staticmethod
|
22
|
+
def match_markdown(text: str) -> bool:
|
23
|
+
"""Check if text is a markdown divider."""
|
24
|
+
return bool(DividerElement.PATTERN.match(text))
|
25
|
+
|
26
|
+
@override
|
27
|
+
@staticmethod
|
28
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
29
|
+
"""Check if block is a Notion divider."""
|
30
|
+
return block.get("type") == "divider"
|
31
|
+
|
32
|
+
@override
|
33
|
+
@staticmethod
|
34
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
35
|
+
"""Convert markdown divider to Notion divider block."""
|
36
|
+
if not DividerElement.match_markdown(text):
|
37
|
+
return None
|
38
|
+
|
39
|
+
return {"type": "divider", "divider": {}}
|
40
|
+
|
41
|
+
@override
|
42
|
+
@staticmethod
|
43
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
44
|
+
"""Convert Notion divider block to markdown divider."""
|
45
|
+
if block.get("type") != "divider":
|
46
|
+
return None
|
47
|
+
|
48
|
+
return "---"
|
49
|
+
|
50
|
+
@override
|
51
|
+
@staticmethod
|
52
|
+
def is_multiline() -> bool:
|
53
|
+
return False
|
54
|
+
|
55
|
+
@classmethod
|
56
|
+
def get_llm_prompt_content(cls) -> dict:
|
57
|
+
"""
|
58
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
59
|
+
Includes description, usage guidance, syntax options, and examples.
|
60
|
+
"""
|
61
|
+
return {
|
62
|
+
"description": "Creates a horizontal divider line that visually separates sections of content.",
|
63
|
+
"when_to_use": "Use dividers when you want to create clear visual breaks between different sections or topics in your document. Dividers help improve readability by organizing content into distinct sections without requiring headings.",
|
64
|
+
"syntax": ["---"],
|
65
|
+
"notes": [
|
66
|
+
"Dividers must be on their own line with no other content",
|
67
|
+
"Dividers work well when combined with headings to clearly separate major document sections",
|
68
|
+
],
|
69
|
+
"examples": [
|
70
|
+
"## Introduction\nThis is the introduction section of the document.\n\n---\n\n## Main Content\nThis is the main content section.",
|
71
|
+
"Task List:\n- Complete project proposal\n- Review feedback\n\n---\n\nMeeting Notes:\n- Discussed timeline\n- Assigned responsibilities",
|
72
|
+
],
|
73
|
+
}
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from typing import Dict, Any, Optional
|
2
|
+
from typing_extensions import override
|
3
|
+
import re
|
4
|
+
|
5
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
6
|
+
from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
|
7
|
+
|
8
|
+
|
9
|
+
class HeadingElement(NotionBlockElement):
|
10
|
+
"""Handles conversion between Markdown headings and Notion heading blocks."""
|
11
|
+
|
12
|
+
PATTERN = re.compile(r"^(#{1,6})\s(.+)$")
|
13
|
+
|
14
|
+
@override
|
15
|
+
@staticmethod
|
16
|
+
def match_markdown(text: str) -> bool:
|
17
|
+
"""Check if text is a markdown heading."""
|
18
|
+
return bool(HeadingElement.PATTERN.match(text))
|
19
|
+
|
20
|
+
@override
|
21
|
+
@staticmethod
|
22
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
23
|
+
"""Check if block is a Notion heading."""
|
24
|
+
block_type: str = block.get("type", "")
|
25
|
+
return block_type.startswith("heading_") and block_type[-1] in "123456"
|
26
|
+
|
27
|
+
@override
|
28
|
+
@staticmethod
|
29
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
30
|
+
"""Convert markdown heading to Notion heading block."""
|
31
|
+
header_match = HeadingElement.PATTERN.match(text)
|
32
|
+
if not header_match:
|
33
|
+
return None
|
34
|
+
|
35
|
+
level = len(header_match.group(1))
|
36
|
+
content = header_match.group(2)
|
37
|
+
|
38
|
+
# Import here to avoid circular imports
|
39
|
+
|
40
|
+
return {
|
41
|
+
"type": f"heading_{level}",
|
42
|
+
f"heading_{level}": {
|
43
|
+
"rich_text": TextInlineFormatter.parse_inline_formatting(content)
|
44
|
+
},
|
45
|
+
}
|
46
|
+
|
47
|
+
@override
|
48
|
+
@staticmethod
|
49
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
50
|
+
"""Convert Notion heading block to markdown heading."""
|
51
|
+
block_type = block.get("type", "")
|
52
|
+
|
53
|
+
if not block_type.startswith("heading_"):
|
54
|
+
return None
|
55
|
+
|
56
|
+
try:
|
57
|
+
level = int(block_type[-1])
|
58
|
+
if not 1 <= level <= 6:
|
59
|
+
return None
|
60
|
+
except ValueError:
|
61
|
+
return None
|
62
|
+
|
63
|
+
heading_data = block.get(block_type, {})
|
64
|
+
rich_text = heading_data.get("rich_text", [])
|
65
|
+
|
66
|
+
text = TextInlineFormatter.extract_text_with_formatting(rich_text)
|
67
|
+
prefix = "#" * level
|
68
|
+
return f"{prefix} {text or ''}"
|
69
|
+
|
70
|
+
@override
|
71
|
+
@staticmethod
|
72
|
+
def is_multiline() -> bool:
|
73
|
+
return False
|
74
|
+
|
75
|
+
@override
|
76
|
+
@classmethod
|
77
|
+
def get_llm_prompt_content(cls) -> dict:
|
78
|
+
"""
|
79
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
80
|
+
"""
|
81
|
+
return {
|
82
|
+
"description": "Use Markdown headings (#, ##, ###, etc.) to structure content hierarchically.",
|
83
|
+
"when_to_use": "Use to group content into sections and define a visual hierarchy.",
|
84
|
+
}
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List
|
3
|
+
from typing_extensions import override
|
4
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
5
|
+
|
6
|
+
|
7
|
+
class ImageElement(NotionBlockElement):
|
8
|
+
"""
|
9
|
+
Handles conversion between Markdown images and Notion image blocks.
|
10
|
+
|
11
|
+
Markdown image syntax:
|
12
|
+
-  - Basic image with caption
|
13
|
+
-  - Image without caption
|
14
|
+
-  - Image with caption and alt text
|
15
|
+
"""
|
16
|
+
|
17
|
+
# Regex pattern for image syntax with optional alt text
|
18
|
+
PATTERN = re.compile(
|
19
|
+
r"^\!\[(.*?)\]" # ![Caption] part
|
20
|
+
+ r'\((https?://[^\s"]+)' # (URL part
|
21
|
+
+ r'(?:\s+"([^"]+)")?' # Optional alt text in quotes
|
22
|
+
+ r"\)$" # closing parenthesis
|
23
|
+
)
|
24
|
+
|
25
|
+
@override
|
26
|
+
@staticmethod
|
27
|
+
def match_markdown(text: str) -> bool:
|
28
|
+
"""Check if text is a markdown image."""
|
29
|
+
return text.strip().startswith("![") and bool(
|
30
|
+
ImageElement.PATTERN.match(text.strip())
|
31
|
+
)
|
32
|
+
|
33
|
+
@override
|
34
|
+
@staticmethod
|
35
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
36
|
+
"""Check if block is a Notion image."""
|
37
|
+
return block.get("type") == "image"
|
38
|
+
|
39
|
+
@override
|
40
|
+
@staticmethod
|
41
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
42
|
+
"""Convert markdown image to Notion image block."""
|
43
|
+
image_match = ImageElement.PATTERN.match(text.strip())
|
44
|
+
if not image_match:
|
45
|
+
return None
|
46
|
+
|
47
|
+
caption = image_match.group(1)
|
48
|
+
url = image_match.group(2)
|
49
|
+
|
50
|
+
if not url:
|
51
|
+
return None
|
52
|
+
|
53
|
+
# Prepare the image block
|
54
|
+
image_block = {
|
55
|
+
"type": "image",
|
56
|
+
"image": {"type": "external", "external": {"url": url}},
|
57
|
+
}
|
58
|
+
|
59
|
+
# Add caption if provided
|
60
|
+
if caption:
|
61
|
+
image_block["image"]["caption"] = [
|
62
|
+
{"type": "text", "text": {"content": caption}}
|
63
|
+
]
|
64
|
+
|
65
|
+
return image_block
|
66
|
+
|
67
|
+
@override
|
68
|
+
@staticmethod
|
69
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
70
|
+
"""Convert Notion image block to markdown image."""
|
71
|
+
if block.get("type") != "image":
|
72
|
+
return None
|
73
|
+
|
74
|
+
image_data = block.get("image", {})
|
75
|
+
|
76
|
+
# Handle both external and file (uploaded) images
|
77
|
+
if image_data.get("type") == "external":
|
78
|
+
url = image_data.get("external", {}).get("url", "")
|
79
|
+
elif image_data.get("type") == "file":
|
80
|
+
url = image_data.get("file", {}).get("url", "")
|
81
|
+
else:
|
82
|
+
return None
|
83
|
+
|
84
|
+
if not url:
|
85
|
+
return None
|
86
|
+
|
87
|
+
# Extract caption if available
|
88
|
+
caption = ""
|
89
|
+
caption_rich_text = image_data.get("caption", [])
|
90
|
+
if caption_rich_text:
|
91
|
+
caption = ImageElement._extract_text_content(caption_rich_text)
|
92
|
+
|
93
|
+
return f""
|
94
|
+
|
95
|
+
@staticmethod
|
96
|
+
def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
|
97
|
+
"""Extract plain text content from Notion rich_text elements."""
|
98
|
+
result = ""
|
99
|
+
for text_obj in rich_text:
|
100
|
+
if text_obj.get("type") == "text":
|
101
|
+
result += text_obj.get("text", {}).get("content", "")
|
102
|
+
elif "plain_text" in text_obj:
|
103
|
+
result += text_obj.get("plain_text", "")
|
104
|
+
return result
|
105
|
+
|
106
|
+
@override
|
107
|
+
@staticmethod
|
108
|
+
def is_multiline() -> bool:
|
109
|
+
return False
|
110
|
+
|
111
|
+
@classmethod
|
112
|
+
def get_llm_prompt_content(cls) -> dict:
|
113
|
+
"""
|
114
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
115
|
+
Includes description, usage guidance, syntax options, and examples.
|
116
|
+
"""
|
117
|
+
return {
|
118
|
+
"description": "Embeds an image from an external URL into your document.",
|
119
|
+
"when_to_use": "Use images to include visual content such as diagrams, screenshots, charts, photos, or illustrations that enhance your document. Images can make complex information easier to understand, create visual interest, or provide evidence for your points.",
|
120
|
+
"syntax": [
|
121
|
+
" - Image without caption",
|
122
|
+
" - Image with caption",
|
123
|
+
' - Image with caption and alt text',
|
124
|
+
],
|
125
|
+
"examples": [
|
126
|
+
"",
|
127
|
+
"",
|
128
|
+
'',
|
129
|
+
],
|
130
|
+
}
|