notionary 0.1.2__tar.gz → 0.1.4__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.
Files changed (52) hide show
  1. {notionary-0.1.2 → notionary-0.1.4}/PKG-INFO +1 -1
  2. notionary-0.1.4/notionary/core/converters/__init__.py +50 -0
  3. notionary-0.1.4/notionary/core/converters/elements/bookmark_element.py +224 -0
  4. notionary-0.1.4/notionary/core/converters/elements/callout_element.py +179 -0
  5. notionary-0.1.4/notionary/core/converters/elements/code_block_element.py +153 -0
  6. notionary-0.1.4/notionary/core/converters/elements/column_element.py +294 -0
  7. notionary-0.1.4/notionary/core/converters/elements/divider_element.py +73 -0
  8. notionary-0.1.4/notionary/core/converters/elements/heading_element.py +84 -0
  9. notionary-0.1.4/notionary/core/converters/elements/image_element.py +130 -0
  10. notionary-0.1.4/notionary/core/converters/elements/list_element.py +130 -0
  11. notionary-0.1.4/notionary/core/converters/elements/notion_block_element.py +51 -0
  12. notionary-0.1.4/notionary/core/converters/elements/paragraph_element.py +73 -0
  13. notionary-0.1.4/notionary/core/converters/elements/qoute_element.py +242 -0
  14. notionary-0.1.4/notionary/core/converters/elements/table_element.py +306 -0
  15. notionary-0.1.4/notionary/core/converters/elements/text_inline_formatter.py +294 -0
  16. notionary-0.1.4/notionary/core/converters/elements/todo_lists.py +114 -0
  17. notionary-0.1.4/notionary/core/converters/elements/toggle_element.py +205 -0
  18. notionary-0.1.4/notionary/core/converters/elements/video_element.py +159 -0
  19. notionary-0.1.4/notionary/core/converters/markdown_to_notion_converter.py +482 -0
  20. notionary-0.1.4/notionary/core/converters/notion_to_markdown_converter.py +45 -0
  21. notionary-0.1.4/notionary/core/converters/registry/block_element_registry.py +234 -0
  22. notionary-0.1.4/notionary/core/converters/registry/block_element_registry_builder.py +280 -0
  23. notionary-0.1.4/notionary/core/database/database_info_service.py +43 -0
  24. notionary-0.1.4/notionary/core/database/database_query_service.py +73 -0
  25. notionary-0.1.4/notionary/core/database/database_schema_service.py +57 -0
  26. notionary-0.1.4/notionary/core/database/models/page_result.py +10 -0
  27. notionary-0.1.4/notionary/core/database/notion_database_manager.py +332 -0
  28. notionary-0.1.4/notionary/core/database/notion_database_manager_factory.py +233 -0
  29. notionary-0.1.4/notionary/core/database/notion_database_schema.py +415 -0
  30. notionary-0.1.4/notionary/core/database/notion_database_writer.py +390 -0
  31. notionary-0.1.4/notionary/core/database/page_service.py +161 -0
  32. notionary-0.1.4/notionary/core/notion_client.py +137 -0
  33. notionary-0.1.4/notionary/core/page/meta_data/metadata_editor.py +37 -0
  34. notionary-0.1.4/notionary/core/page/notion_page_manager.py +129 -0
  35. notionary-0.1.4/notionary/core/page/page_content_manager.py +85 -0
  36. notionary-0.1.4/notionary/core/page/property_formatter.py +97 -0
  37. notionary-0.1.4/notionary/exceptions/database_exceptions.py +76 -0
  38. notionary-0.1.4/notionary/exceptions/page_creation_exception.py +9 -0
  39. notionary-0.1.4/notionary/util/logging_mixin.py +47 -0
  40. notionary-0.1.4/notionary/util/singleton_decorator.py +20 -0
  41. notionary-0.1.4/notionary/util/uuid_utils.py +24 -0
  42. {notionary-0.1.2 → notionary-0.1.4}/notionary.egg-info/PKG-INFO +1 -1
  43. notionary-0.1.4/notionary.egg-info/SOURCES.txt +49 -0
  44. {notionary-0.1.2 → notionary-0.1.4}/setup.py +3 -3
  45. notionary-0.1.2/notionary.egg-info/SOURCES.txt +0 -9
  46. {notionary-0.1.2 → notionary-0.1.4}/LICENSE +0 -0
  47. {notionary-0.1.2 → notionary-0.1.4}/README.md +0 -0
  48. {notionary-0.1.2 → notionary-0.1.4}/notionary/__init__.py +0 -0
  49. {notionary-0.1.2 → notionary-0.1.4}/notionary.egg-info/dependency_links.txt +0 -0
  50. {notionary-0.1.2 → notionary-0.1.4}/notionary.egg-info/requires.txt +0 -0
  51. {notionary-0.1.2 → notionary-0.1.4}/notionary.egg-info/top_level.txt +0 -0
  52. {notionary-0.1.2 → notionary-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A toolkit to convert between Markdown and Notion blocks
5
5
  Home-page: https://github.com/mathisarends/notionary
6
6
  Author: Mathis Arends
@@ -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
+ ]
@@ -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
+ }