notionary 0.1.1__tar.gz → 0.1.3__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.1 → notionary-0.1.3}/PKG-INFO +1 -1
- notionary-0.1.3/notionary/__init__.py +9 -0
- notionary-0.1.3/notionary/core/__init__.py +0 -0
- notionary-0.1.3/notionary/core/converters/__init__.py +50 -0
- notionary-0.1.3/notionary/core/converters/elements/__init__.py +0 -0
- notionary-0.1.3/notionary/core/converters/elements/bookmark_element.py +224 -0
- notionary-0.1.3/notionary/core/converters/elements/callout_element.py +179 -0
- notionary-0.1.3/notionary/core/converters/elements/code_block_element.py +153 -0
- notionary-0.1.3/notionary/core/converters/elements/column_element.py +294 -0
- notionary-0.1.3/notionary/core/converters/elements/divider_element.py +73 -0
- notionary-0.1.3/notionary/core/converters/elements/heading_element.py +84 -0
- notionary-0.1.3/notionary/core/converters/elements/image_element.py +130 -0
- notionary-0.1.3/notionary/core/converters/elements/list_element.py +130 -0
- notionary-0.1.3/notionary/core/converters/elements/notion_block_element.py +51 -0
- notionary-0.1.3/notionary/core/converters/elements/paragraph_element.py +73 -0
- notionary-0.1.3/notionary/core/converters/elements/qoute_element.py +242 -0
- notionary-0.1.3/notionary/core/converters/elements/table_element.py +306 -0
- notionary-0.1.3/notionary/core/converters/elements/text_inline_formatter.py +294 -0
- notionary-0.1.3/notionary/core/converters/elements/todo_lists.py +114 -0
- notionary-0.1.3/notionary/core/converters/elements/toggle_element.py +205 -0
- notionary-0.1.3/notionary/core/converters/elements/video_element.py +159 -0
- notionary-0.1.3/notionary/core/converters/markdown_to_notion_converter.py +482 -0
- notionary-0.1.3/notionary/core/converters/notion_to_markdown_converter.py +45 -0
- notionary-0.1.3/notionary/core/converters/registry/__init__.py +0 -0
- notionary-0.1.3/notionary/core/converters/registry/block_element_registry.py +234 -0
- notionary-0.1.3/notionary/core/converters/registry/block_element_registry_builder.py +280 -0
- notionary-0.1.3/notionary/core/database/database_info_service.py +43 -0
- notionary-0.1.3/notionary/core/database/database_query_service.py +73 -0
- notionary-0.1.3/notionary/core/database/database_schema_service.py +57 -0
- notionary-0.1.3/notionary/core/database/models/page_result.py +10 -0
- notionary-0.1.3/notionary/core/database/notion_database_manager.py +332 -0
- notionary-0.1.3/notionary/core/database/notion_database_manager_factory.py +233 -0
- notionary-0.1.3/notionary/core/database/notion_database_schema.py +415 -0
- notionary-0.1.3/notionary/core/database/notion_database_writer.py +390 -0
- notionary-0.1.3/notionary/core/database/page_service.py +161 -0
- notionary-0.1.3/notionary/core/notion_client.py +134 -0
- notionary-0.1.3/notionary/core/page/meta_data/metadata_editor.py +37 -0
- notionary-0.1.3/notionary/core/page/notion_page_manager.py +110 -0
- notionary-0.1.3/notionary/core/page/page_content_manager.py +85 -0
- notionary-0.1.3/notionary/core/page/property_formatter.py +97 -0
- notionary-0.1.3/notionary/exceptions/database_exceptions.py +76 -0
- notionary-0.1.3/notionary/exceptions/page_creation_exception.py +9 -0
- notionary-0.1.3/notionary/util/logging_mixin.py +47 -0
- notionary-0.1.3/notionary/util/singleton_decorator.py +20 -0
- notionary-0.1.3/notionary/util/uuid_utils.py +24 -0
- {notionary-0.1.1 → notionary-0.1.3}/notionary.egg-info/PKG-INFO +1 -1
- notionary-0.1.3/notionary.egg-info/SOURCES.txt +52 -0
- notionary-0.1.3/notionary.egg-info/top_level.txt +1 -0
- {notionary-0.1.1 → notionary-0.1.3}/setup.py +3 -3
- notionary-0.1.1/notionary.egg-info/SOURCES.txt +0 -8
- notionary-0.1.1/notionary.egg-info/top_level.txt +0 -1
- {notionary-0.1.1 → notionary-0.1.3}/LICENSE +0 -0
- {notionary-0.1.1 → notionary-0.1.3}/README.md +0 -0
- {notionary-0.1.1 → notionary-0.1.3}/notionary.egg-info/dependency_links.txt +0 -0
- {notionary-0.1.1 → notionary-0.1.3}/notionary.egg-info/requires.txt +0 -0
- {notionary-0.1.1 → notionary-0.1.3}/setup.cfg +0 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
from .core.page.notion_page_manager import NotionPageManager
|
2
|
+
from .core.database.notion_database_manager import NotionDatabaseManager
|
3
|
+
from .core.database.notion_database_manager_factory import NotionDatabaseFactory
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
"NotionPageManager",
|
7
|
+
"NotionDatabaseManager",
|
8
|
+
"NotionDatabaseFactory",
|
9
|
+
]
|
File without changes
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Import converters
|
2
|
+
from .markdown_to_notion_converter import MarkdownToNotionConverter
|
3
|
+
from .notion_to_markdown_converter import NotionToMarkdownConverter
|
4
|
+
|
5
|
+
# Import registry classes
|
6
|
+
from .registry.block_element_registry import BlockElementRegistry
|
7
|
+
from .registry.block_element_registry_builder import BlockElementRegistryBuilder
|
8
|
+
|
9
|
+
# Import elements for type hints and direct use
|
10
|
+
from .elements.paragraph_element import ParagraphElement
|
11
|
+
from .elements.heading_element import HeadingElement
|
12
|
+
from .elements.callout_element import CalloutElement
|
13
|
+
from .elements.code_block_element import CodeBlockElement
|
14
|
+
from .elements.divider_element import DividerElement
|
15
|
+
from .elements.table_element import TableElement
|
16
|
+
from .elements.todo_lists import TodoElement
|
17
|
+
from .elements.list_element import BulletedListElement, NumberedListElement
|
18
|
+
from .elements.qoute_element import QuoteElement
|
19
|
+
from .elements.image_element import ImageElement
|
20
|
+
from .elements.video_element import VideoElement
|
21
|
+
from .elements.toggle_element import ToggleElement
|
22
|
+
from .elements.bookmark_element import BookmarkElement
|
23
|
+
from .elements.column_element import ColumnElement
|
24
|
+
|
25
|
+
default_registry = BlockElementRegistryBuilder.create_standard_registry()
|
26
|
+
|
27
|
+
# Define what to export
|
28
|
+
__all__ = [
|
29
|
+
"BlockElementRegistry",
|
30
|
+
"BlockElementRegistryBuilder",
|
31
|
+
"MarkdownToNotionConverter",
|
32
|
+
"NotionToMarkdownConverter",
|
33
|
+
"default_registry",
|
34
|
+
# Element classes
|
35
|
+
"ParagraphElement",
|
36
|
+
"HeadingElement",
|
37
|
+
"CalloutElement",
|
38
|
+
"CodeBlockElement",
|
39
|
+
"DividerElement",
|
40
|
+
"TableElement",
|
41
|
+
"TodoElement",
|
42
|
+
"QuoteElement",
|
43
|
+
"BulletedListElement",
|
44
|
+
"NumberedListElement",
|
45
|
+
"ImageElement",
|
46
|
+
"VideoElement",
|
47
|
+
"ToggleElement",
|
48
|
+
"BookmarkElement",
|
49
|
+
"ColumnElement",
|
50
|
+
]
|
File without changes
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple
|
3
|
+
from typing_extensions import override
|
4
|
+
|
5
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
6
|
+
|
7
|
+
|
8
|
+
class BookmarkElement(NotionBlockElement):
|
9
|
+
"""
|
10
|
+
Handles conversion between Markdown bookmarks and Notion bookmark blocks.
|
11
|
+
|
12
|
+
Markdown bookmark syntax:
|
13
|
+
- [bookmark](https://example.com) - Simple bookmark with URL only
|
14
|
+
- [bookmark](https://example.com "Title") - Bookmark with URL and title
|
15
|
+
- [bookmark](https://example.com "Title" "Description") - Bookmark with URL, title, and description
|
16
|
+
|
17
|
+
Where:
|
18
|
+
- URL is the required bookmark URL
|
19
|
+
- Title is an optional title (enclosed in quotes)
|
20
|
+
- Description is an optional description (enclosed in quotes)
|
21
|
+
"""
|
22
|
+
|
23
|
+
# Regex pattern for bookmark syntax with optional title and description
|
24
|
+
PATTERN = re.compile(
|
25
|
+
r"^\[bookmark\]\(" # [bookmark]( prefix
|
26
|
+
+ r'(https?://[^\s"]+)' # URL (required)
|
27
|
+
+ r'(?:\s+"([^"]+)")?' # Optional title in quotes
|
28
|
+
+ r'(?:\s+"([^"]+)")?' # Optional description in quotes
|
29
|
+
+ r"\)$" # closing parenthesis
|
30
|
+
)
|
31
|
+
|
32
|
+
@override
|
33
|
+
@staticmethod
|
34
|
+
def match_markdown(text: str) -> bool:
|
35
|
+
"""Check if text is a markdown bookmark."""
|
36
|
+
return text.strip().startswith("[bookmark]") and bool(
|
37
|
+
BookmarkElement.PATTERN.match(text.strip())
|
38
|
+
)
|
39
|
+
|
40
|
+
@override
|
41
|
+
@staticmethod
|
42
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
43
|
+
"""Check if block is a Notion bookmark."""
|
44
|
+
# Handle both standard "bookmark" type and "external-bookmark" type
|
45
|
+
return block.get("type") in ["bookmark", "external-bookmark"]
|
46
|
+
|
47
|
+
@override
|
48
|
+
@staticmethod
|
49
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
50
|
+
"""Convert markdown bookmark to Notion bookmark block."""
|
51
|
+
bookmark_match = BookmarkElement.PATTERN.match(text.strip())
|
52
|
+
if not bookmark_match:
|
53
|
+
return None
|
54
|
+
|
55
|
+
url = bookmark_match.group(1)
|
56
|
+
title = bookmark_match.group(2)
|
57
|
+
description = bookmark_match.group(3)
|
58
|
+
|
59
|
+
bookmark_data = {"url": url}
|
60
|
+
|
61
|
+
# Add caption if title or description is provided
|
62
|
+
if title or description:
|
63
|
+
caption = []
|
64
|
+
|
65
|
+
if title:
|
66
|
+
caption.append(
|
67
|
+
{
|
68
|
+
"type": "text",
|
69
|
+
"text": {"content": title, "link": None},
|
70
|
+
"annotations": {
|
71
|
+
"bold": False,
|
72
|
+
"italic": False,
|
73
|
+
"strikethrough": False,
|
74
|
+
"underline": False,
|
75
|
+
"code": False,
|
76
|
+
"color": "default",
|
77
|
+
},
|
78
|
+
"plain_text": title,
|
79
|
+
"href": None,
|
80
|
+
}
|
81
|
+
)
|
82
|
+
|
83
|
+
# Add a separator if both title and description are provided
|
84
|
+
if description:
|
85
|
+
caption.append(
|
86
|
+
{
|
87
|
+
"type": "text",
|
88
|
+
"text": {"content": " - ", "link": None},
|
89
|
+
"annotations": {
|
90
|
+
"bold": False,
|
91
|
+
"italic": False,
|
92
|
+
"strikethrough": False,
|
93
|
+
"underline": False,
|
94
|
+
"code": False,
|
95
|
+
"color": "default",
|
96
|
+
},
|
97
|
+
"plain_text": " - ",
|
98
|
+
"href": None,
|
99
|
+
}
|
100
|
+
)
|
101
|
+
|
102
|
+
if description:
|
103
|
+
caption.append(
|
104
|
+
{
|
105
|
+
"type": "text",
|
106
|
+
"text": {"content": description, "link": None},
|
107
|
+
"annotations": {
|
108
|
+
"bold": False,
|
109
|
+
"italic": False,
|
110
|
+
"strikethrough": False,
|
111
|
+
"underline": False,
|
112
|
+
"code": False,
|
113
|
+
"color": "default",
|
114
|
+
},
|
115
|
+
"plain_text": description,
|
116
|
+
"href": None,
|
117
|
+
}
|
118
|
+
)
|
119
|
+
|
120
|
+
bookmark_data["caption"] = caption
|
121
|
+
else:
|
122
|
+
# Empty caption list to match Notion's format for bookmarks without titles
|
123
|
+
bookmark_data["caption"] = []
|
124
|
+
|
125
|
+
return {"type": "bookmark", "bookmark": bookmark_data}
|
126
|
+
|
127
|
+
@override
|
128
|
+
@staticmethod
|
129
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
130
|
+
"""Convert Notion bookmark block to markdown bookmark."""
|
131
|
+
block_type = block.get("type", "")
|
132
|
+
|
133
|
+
if block_type == "bookmark":
|
134
|
+
bookmark_data = block.get("bookmark", {})
|
135
|
+
elif block_type == "external-bookmark":
|
136
|
+
# Handle external-bookmark type
|
137
|
+
# Extract URL from the external-bookmark structure
|
138
|
+
url = block.get("url", "")
|
139
|
+
if not url:
|
140
|
+
return None
|
141
|
+
|
142
|
+
# For external bookmarks, create a simple bookmark format
|
143
|
+
return f"[bookmark]({url})"
|
144
|
+
else:
|
145
|
+
return None
|
146
|
+
|
147
|
+
url = bookmark_data.get("url", "")
|
148
|
+
|
149
|
+
if not url:
|
150
|
+
return None
|
151
|
+
|
152
|
+
caption = bookmark_data.get("caption", [])
|
153
|
+
|
154
|
+
if not caption:
|
155
|
+
# Simple bookmark with URL only
|
156
|
+
return f"[bookmark]({url})"
|
157
|
+
|
158
|
+
# Extract title and description from caption
|
159
|
+
title, description = BookmarkElement._parse_caption(caption)
|
160
|
+
|
161
|
+
if title and description:
|
162
|
+
return f'[bookmark]({url} "{title}" "{description}")'
|
163
|
+
elif title:
|
164
|
+
return f'[bookmark]({url} "{title}")'
|
165
|
+
else:
|
166
|
+
return f"[bookmark]({url})"
|
167
|
+
|
168
|
+
@override
|
169
|
+
@staticmethod
|
170
|
+
def is_multiline() -> bool:
|
171
|
+
"""Bookmarks are single-line elements."""
|
172
|
+
return False
|
173
|
+
|
174
|
+
@staticmethod
|
175
|
+
def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
|
176
|
+
"""Extract plain text content from Notion rich_text elements."""
|
177
|
+
result = ""
|
178
|
+
for text_obj in rich_text:
|
179
|
+
if text_obj.get("type") == "text":
|
180
|
+
result += text_obj.get("text", {}).get("content", "")
|
181
|
+
elif "plain_text" in text_obj:
|
182
|
+
result += text_obj.get("plain_text", "")
|
183
|
+
return result
|
184
|
+
|
185
|
+
@staticmethod
|
186
|
+
def _parse_caption(caption: List[Dict[str, Any]]) -> Tuple[str, str]:
|
187
|
+
"""
|
188
|
+
Parse Notion caption into title and description components.
|
189
|
+
Returns a tuple of (title, description).
|
190
|
+
"""
|
191
|
+
if not caption:
|
192
|
+
return "", ""
|
193
|
+
|
194
|
+
# Extract the full text content from caption
|
195
|
+
full_text = BookmarkElement._extract_text_content(caption)
|
196
|
+
|
197
|
+
# Check if the text contains a separator
|
198
|
+
if " - " in full_text:
|
199
|
+
parts = full_text.split(" - ", 1)
|
200
|
+
return parts[0].strip(), parts[1].strip()
|
201
|
+
else:
|
202
|
+
# If no separator, assume the whole content is the title
|
203
|
+
return full_text.strip(), ""
|
204
|
+
|
205
|
+
@override
|
206
|
+
@classmethod
|
207
|
+
def get_llm_prompt_content(cls) -> dict:
|
208
|
+
"""
|
209
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
210
|
+
Includes description, usage guidance, syntax options, and examples.
|
211
|
+
"""
|
212
|
+
return {
|
213
|
+
"description": "Creates a bookmark that links to an external website.",
|
214
|
+
"when_to_use": "Use bookmarks when you want to reference external content while keeping the page clean and organized. Bookmarks display a preview card for the linked content.",
|
215
|
+
"syntax": [
|
216
|
+
"[bookmark](https://example.com) - Simple bookmark with URL only",
|
217
|
+
'[bookmark](https://example.com "Title") - Bookmark with URL and title',
|
218
|
+
'[bookmark](https://example.com "Title" "Description") - Bookmark with URL, title, and description',
|
219
|
+
],
|
220
|
+
"examples": [
|
221
|
+
'[bookmark](https://notion.so "Notion Homepage" "Your connected workspace")',
|
222
|
+
'[bookmark](https://github.com "GitHub" "Where the world builds software")',
|
223
|
+
],
|
224
|
+
}
|
@@ -0,0 +1,179 @@
|
|
1
|
+
from typing import Dict, Any, Optional
|
2
|
+
from typing_extensions import override
|
3
|
+
import re
|
4
|
+
|
5
|
+
from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
|
6
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
7
|
+
|
8
|
+
|
9
|
+
class CalloutElement(NotionBlockElement):
|
10
|
+
"""
|
11
|
+
Handles conversion between Markdown callouts and Notion callout blocks.
|
12
|
+
|
13
|
+
Markdown callout syntax:
|
14
|
+
- !> [emoji] Text - Callout with custom emoji
|
15
|
+
- !> {color} [emoji] Text - Callout with custom color and emoji
|
16
|
+
- !> Text - Simple callout with default emoji and color
|
17
|
+
|
18
|
+
Where:
|
19
|
+
- {color} can be one of Notion's color options (e.g., "blue_background")
|
20
|
+
- [emoji] is any emoji character
|
21
|
+
- Text is the callout content with optional inline formatting
|
22
|
+
"""
|
23
|
+
|
24
|
+
COLOR_PATTERN = r"(?:(?:{([a-z_]+)})?\s*)?"
|
25
|
+
EMOJI_PATTERN = r"(?:\[([^\]]+)\])?\s*"
|
26
|
+
TEXT_PATTERN = r"(.+)"
|
27
|
+
|
28
|
+
# Combine the patterns
|
29
|
+
PATTERN = re.compile(
|
30
|
+
r"^!>\s+" # Callout prefix
|
31
|
+
+ COLOR_PATTERN
|
32
|
+
+ EMOJI_PATTERN
|
33
|
+
+ TEXT_PATTERN
|
34
|
+
+ r"$" # End of line
|
35
|
+
)
|
36
|
+
|
37
|
+
DEFAULT_EMOJI = "💡"
|
38
|
+
DEFAULT_COLOR = "gray_background"
|
39
|
+
|
40
|
+
VALID_COLORS = [
|
41
|
+
"default",
|
42
|
+
"gray",
|
43
|
+
"brown",
|
44
|
+
"orange",
|
45
|
+
"yellow",
|
46
|
+
"green",
|
47
|
+
"blue",
|
48
|
+
"purple",
|
49
|
+
"pink",
|
50
|
+
"red",
|
51
|
+
"gray_background",
|
52
|
+
"brown_background",
|
53
|
+
"orange_background",
|
54
|
+
"yellow_background",
|
55
|
+
"green_background",
|
56
|
+
"blue_background",
|
57
|
+
"purple_background",
|
58
|
+
"pink_background",
|
59
|
+
"red_background",
|
60
|
+
]
|
61
|
+
|
62
|
+
@override
|
63
|
+
@staticmethod
|
64
|
+
def match_markdown(text: str) -> bool:
|
65
|
+
"""Check if text is a markdown callout."""
|
66
|
+
return text.strip().startswith("!>") and bool(
|
67
|
+
CalloutElement.PATTERN.match(text)
|
68
|
+
)
|
69
|
+
|
70
|
+
@override
|
71
|
+
@staticmethod
|
72
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
73
|
+
"""Check if block is a Notion callout."""
|
74
|
+
return block.get("type") == "callout"
|
75
|
+
|
76
|
+
@override
|
77
|
+
@staticmethod
|
78
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
79
|
+
"""Convert markdown callout to Notion callout block."""
|
80
|
+
callout_match = CalloutElement.PATTERN.match(text)
|
81
|
+
if not callout_match:
|
82
|
+
return None
|
83
|
+
|
84
|
+
color = callout_match.group(1)
|
85
|
+
emoji = callout_match.group(2)
|
86
|
+
content = callout_match.group(3)
|
87
|
+
|
88
|
+
if not emoji:
|
89
|
+
emoji = CalloutElement.DEFAULT_EMOJI
|
90
|
+
|
91
|
+
if not color or color not in CalloutElement.VALID_COLORS:
|
92
|
+
color = CalloutElement.DEFAULT_COLOR
|
93
|
+
|
94
|
+
return {
|
95
|
+
"type": "callout",
|
96
|
+
"callout": {
|
97
|
+
"rich_text": TextInlineFormatter.parse_inline_formatting(content),
|
98
|
+
"icon": {"type": "emoji", "emoji": emoji},
|
99
|
+
"color": color,
|
100
|
+
},
|
101
|
+
}
|
102
|
+
|
103
|
+
@override
|
104
|
+
@staticmethod
|
105
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
106
|
+
"""Convert Notion callout block to markdown callout."""
|
107
|
+
if block.get("type") != "callout":
|
108
|
+
return None
|
109
|
+
|
110
|
+
callout_data = block.get("callout", {})
|
111
|
+
rich_text = callout_data.get("rich_text", [])
|
112
|
+
icon = callout_data.get("icon", {})
|
113
|
+
color = callout_data.get("color", CalloutElement.DEFAULT_COLOR)
|
114
|
+
|
115
|
+
text = TextInlineFormatter.extract_text_with_formatting(rich_text)
|
116
|
+
if not text:
|
117
|
+
return None
|
118
|
+
|
119
|
+
emoji = ""
|
120
|
+
if icon and icon.get("type") == "emoji":
|
121
|
+
emoji = icon.get("emoji", "")
|
122
|
+
|
123
|
+
color_str = ""
|
124
|
+
if color and color != CalloutElement.DEFAULT_COLOR:
|
125
|
+
color_str = f"{{{color}}} "
|
126
|
+
|
127
|
+
emoji_str = ""
|
128
|
+
if emoji:
|
129
|
+
emoji_str = f"[{emoji}] "
|
130
|
+
|
131
|
+
return f"!> {color_str}{emoji_str}{text}"
|
132
|
+
|
133
|
+
@override
|
134
|
+
@staticmethod
|
135
|
+
def is_multiline() -> bool:
|
136
|
+
return False
|
137
|
+
|
138
|
+
@classmethod
|
139
|
+
def get_llm_prompt_content(cls) -> dict:
|
140
|
+
"""
|
141
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
142
|
+
Includes description, usage guidance, syntax options, and examples.
|
143
|
+
"""
|
144
|
+
return {
|
145
|
+
"description": "Creates a callout block to highlight important information with an icon and background color.",
|
146
|
+
"when_to_use": "Use callouts when you want to draw attention to important information, tips, warnings, or notes that stand out from the main content.",
|
147
|
+
"syntax": [
|
148
|
+
"!> Text - Simple callout with default emoji (💡) and color (gray background)",
|
149
|
+
"!> [emoji] Text - Callout with custom emoji",
|
150
|
+
"!> {color} [emoji] Text - Callout with custom color and emoji",
|
151
|
+
],
|
152
|
+
"color_options": [
|
153
|
+
"default",
|
154
|
+
"gray",
|
155
|
+
"brown",
|
156
|
+
"orange",
|
157
|
+
"yellow",
|
158
|
+
"green",
|
159
|
+
"blue",
|
160
|
+
"purple",
|
161
|
+
"pink",
|
162
|
+
"red",
|
163
|
+
"gray_background",
|
164
|
+
"brown_background",
|
165
|
+
"orange_background",
|
166
|
+
"yellow_background",
|
167
|
+
"green_background",
|
168
|
+
"blue_background",
|
169
|
+
"purple_background",
|
170
|
+
"pink_background",
|
171
|
+
"red_background",
|
172
|
+
],
|
173
|
+
"examples": [
|
174
|
+
"!> This is a default callout with the light bulb emoji",
|
175
|
+
"!> [🔔] This is a callout with a bell emoji",
|
176
|
+
"!> {blue_background} [💧] This is a blue callout with a water drop emoji",
|
177
|
+
"!> {yellow_background} [⚠️] Warning: This is an important note to pay attention to",
|
178
|
+
],
|
179
|
+
}
|
@@ -0,0 +1,153 @@
|
|
1
|
+
from typing import Dict, Any, Optional, List, Tuple
|
2
|
+
from typing_extensions import override
|
3
|
+
import re
|
4
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
5
|
+
|
6
|
+
|
7
|
+
class CodeBlockElement(NotionBlockElement):
|
8
|
+
"""
|
9
|
+
Handles conversion between Markdown code blocks and Notion code blocks.
|
10
|
+
|
11
|
+
Markdown code block syntax:
|
12
|
+
```language
|
13
|
+
code content
|
14
|
+
```
|
15
|
+
|
16
|
+
Where:
|
17
|
+
- language is optional and specifies the programming language
|
18
|
+
- code content is the code to be displayed
|
19
|
+
"""
|
20
|
+
|
21
|
+
PATTERN = re.compile(r"```(\w*)\n([\s\S]+?)```", re.MULTILINE)
|
22
|
+
|
23
|
+
@override
|
24
|
+
@staticmethod
|
25
|
+
def match_markdown(text: str) -> bool:
|
26
|
+
"""Check if text contains a markdown code block."""
|
27
|
+
return bool(CodeBlockElement.PATTERN.search(text))
|
28
|
+
|
29
|
+
@override
|
30
|
+
@staticmethod
|
31
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
32
|
+
"""Check if block is a Notion code block."""
|
33
|
+
return block.get("type") == "code"
|
34
|
+
|
35
|
+
@override
|
36
|
+
@staticmethod
|
37
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
38
|
+
"""Convert markdown code block to Notion code block."""
|
39
|
+
match = CodeBlockElement.PATTERN.search(text)
|
40
|
+
if not match:
|
41
|
+
return None
|
42
|
+
|
43
|
+
language = match.group(1) or "plain text"
|
44
|
+
content = match.group(2)
|
45
|
+
|
46
|
+
if content.endswith("\n"):
|
47
|
+
content = content[:-1]
|
48
|
+
|
49
|
+
return {
|
50
|
+
"type": "code",
|
51
|
+
"code": {
|
52
|
+
"rich_text": [
|
53
|
+
{
|
54
|
+
"type": "text",
|
55
|
+
"text": {"content": content},
|
56
|
+
"annotations": {
|
57
|
+
"bold": False,
|
58
|
+
"italic": False,
|
59
|
+
"strikethrough": False,
|
60
|
+
"underline": False,
|
61
|
+
"code": False,
|
62
|
+
"color": "default",
|
63
|
+
},
|
64
|
+
"plain_text": content,
|
65
|
+
}
|
66
|
+
],
|
67
|
+
"language": language,
|
68
|
+
},
|
69
|
+
}
|
70
|
+
|
71
|
+
@override
|
72
|
+
@staticmethod
|
73
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
74
|
+
"""Convert Notion code block to markdown code block."""
|
75
|
+
if block.get("type") != "code":
|
76
|
+
return None
|
77
|
+
|
78
|
+
code_data = block.get("code", {})
|
79
|
+
rich_text = code_data.get("rich_text", [])
|
80
|
+
|
81
|
+
# Extract the code content
|
82
|
+
content = ""
|
83
|
+
for text_block in rich_text:
|
84
|
+
content += text_block.get("plain_text", "")
|
85
|
+
|
86
|
+
language = code_data.get("language", "")
|
87
|
+
|
88
|
+
# Format as a markdown code block
|
89
|
+
return f"```{language}\n{content}\n```"
|
90
|
+
|
91
|
+
@staticmethod
|
92
|
+
def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
93
|
+
"""
|
94
|
+
Find all code block matches in the text and return their positions.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
text: The text to search in
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
List of tuples with (start_pos, end_pos, block)
|
101
|
+
"""
|
102
|
+
matches = []
|
103
|
+
for match in CodeBlockElement.PATTERN.finditer(text):
|
104
|
+
language = match.group(1) or "plain text"
|
105
|
+
content = match.group(2)
|
106
|
+
|
107
|
+
# Remove trailing newline if present
|
108
|
+
if content.endswith("\n"):
|
109
|
+
content = content[:-1]
|
110
|
+
|
111
|
+
block = {
|
112
|
+
"type": "code",
|
113
|
+
"code": {
|
114
|
+
"rich_text": [
|
115
|
+
{
|
116
|
+
"type": "text",
|
117
|
+
"text": {"content": content},
|
118
|
+
"annotations": {
|
119
|
+
"bold": False,
|
120
|
+
"italic": False,
|
121
|
+
"strikethrough": False,
|
122
|
+
"underline": False,
|
123
|
+
"code": False,
|
124
|
+
"color": "default",
|
125
|
+
},
|
126
|
+
"plain_text": content,
|
127
|
+
}
|
128
|
+
],
|
129
|
+
"language": language,
|
130
|
+
},
|
131
|
+
}
|
132
|
+
|
133
|
+
matches.append((match.start(), match.end(), block))
|
134
|
+
|
135
|
+
return matches
|
136
|
+
|
137
|
+
@override
|
138
|
+
@staticmethod
|
139
|
+
def is_multiline() -> bool:
|
140
|
+
return True
|
141
|
+
|
142
|
+
@classmethod
|
143
|
+
def get_llm_prompt_content(cls) -> dict:
|
144
|
+
"""
|
145
|
+
Returns a dictionary with all information needed for LLM prompts about this element.
|
146
|
+
"""
|
147
|
+
return {
|
148
|
+
"description": "Use fenced code blocks to format content as code. Supports language annotations like 'python', 'json', or 'mermaid'. Use when you want to display code, configurations, command-line examples, or diagram syntax. Also useful when breaking down or visualizing a system or architecture for complex problems (e.g. using mermaid).",
|
149
|
+
"examples": [
|
150
|
+
"```python\nprint('Hello, world!')\n```",
|
151
|
+
"```mermaid\nflowchart TD\n A --> B\n```",
|
152
|
+
],
|
153
|
+
}
|