notionary 0.1.24__tar.gz → 0.1.26__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 (70) hide show
  1. {notionary-0.1.24 → notionary-0.1.26}/PKG-INFO +1 -1
  2. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/audio_element.py +41 -38
  3. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/bookmark_element.py +36 -27
  4. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/bulleted_list_element.py +28 -21
  5. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/callout_element.py +39 -31
  6. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/code_block_element.py +38 -26
  7. notionary-0.1.26/notionary/elements/divider_element.py +67 -0
  8. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/embed_element.py +37 -28
  9. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/heading_element.py +39 -24
  10. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/image_element.py +33 -24
  11. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/mention_element.py +40 -29
  12. notionary-0.1.26/notionary/elements/notion_block_element.py +34 -0
  13. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/numbered_list_element.py +29 -20
  14. notionary-0.1.26/notionary/elements/paragraph_element.py +83 -0
  15. notionary-0.1.26/notionary/elements/prompts/element_prompt_content.py +107 -0
  16. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/prompts/synthax_prompt_builder.py +64 -17
  17. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/qoute_element.py +72 -74
  18. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/registry/block_element_registry.py +1 -1
  19. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/registry/block_element_registry_builder.py +6 -9
  20. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/table_element.py +49 -36
  21. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/text_inline_formatter.py +23 -15
  22. notionary-0.1.24/notionary/elements/todo_lists.py → notionary-0.1.26/notionary/elements/todo_element.py +34 -25
  23. notionary-0.1.26/notionary/elements/toggle_element.py +302 -0
  24. notionary-0.1.26/notionary/elements/toggleable_heading_element.py +269 -0
  25. {notionary-0.1.24 → notionary-0.1.26}/notionary/elements/video_element.py +37 -28
  26. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/content/page_content_manager.py +5 -8
  27. notionary-0.1.26/notionary/page/markdown_to_notion_converter.py +436 -0
  28. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/notion_page.py +1 -1
  29. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/notion_to_markdown_converter.py +20 -95
  30. {notionary-0.1.24 → notionary-0.1.26}/notionary.egg-info/PKG-INFO +1 -1
  31. {notionary-0.1.24 → notionary-0.1.26}/notionary.egg-info/SOURCES.txt +2 -2
  32. {notionary-0.1.24 → notionary-0.1.26}/setup.py +1 -1
  33. notionary-0.1.24/notionary/elements/column_element.py +0 -307
  34. notionary-0.1.24/notionary/elements/divider_element.py +0 -56
  35. notionary-0.1.24/notionary/elements/notion_block_element.py +0 -52
  36. notionary-0.1.24/notionary/elements/paragraph_element.py +0 -77
  37. notionary-0.1.24/notionary/elements/prompts/element_prompt_content.py +0 -24
  38. notionary-0.1.24/notionary/elements/toggle_element.py +0 -226
  39. notionary-0.1.24/notionary/page/markdown_to_notion_converter.py +0 -441
  40. {notionary-0.1.24 → notionary-0.1.26}/LICENSE +0 -0
  41. {notionary-0.1.24 → notionary-0.1.26}/README.md +0 -0
  42. {notionary-0.1.24 → notionary-0.1.26}/notionary/__init__.py +0 -0
  43. {notionary-0.1.24 → notionary-0.1.26}/notionary/database/database_discovery.py +0 -0
  44. {notionary-0.1.24 → notionary-0.1.26}/notionary/database/database_info_service.py +0 -0
  45. {notionary-0.1.24 → notionary-0.1.26}/notionary/database/models/page_result.py +0 -0
  46. {notionary-0.1.24 → notionary-0.1.26}/notionary/database/notion_database.py +0 -0
  47. {notionary-0.1.24 → notionary-0.1.26}/notionary/database/notion_database_factory.py +0 -0
  48. {notionary-0.1.24 → notionary-0.1.26}/notionary/exceptions/database_exceptions.py +0 -0
  49. {notionary-0.1.24 → notionary-0.1.26}/notionary/exceptions/page_creation_exception.py +0 -0
  50. {notionary-0.1.24 → notionary-0.1.26}/notionary/notion_client.py +0 -0
  51. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/content/notion_page_content_chunker.py +0 -0
  52. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/metadata/metadata_editor.py +0 -0
  53. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/metadata/notion_icon_manager.py +0 -0
  54. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/metadata/notion_page_cover_manager.py +0 -0
  55. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/notion_page_factory.py +0 -0
  56. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/properites/database_property_service.py +0 -0
  57. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/properites/page_property_manager.py +0 -0
  58. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/properites/property_formatter.py +0 -0
  59. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/properites/property_operation_result.py +0 -0
  60. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/properites/property_value_extractor.py +0 -0
  61. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/relations/notion_page_relation_manager.py +0 -0
  62. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/relations/notion_page_title_resolver.py +0 -0
  63. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/relations/page_database_relation.py +0 -0
  64. {notionary-0.1.24 → notionary-0.1.26}/notionary/page/relations/relation_operation_result.py +0 -0
  65. {notionary-0.1.24 → notionary-0.1.26}/notionary/util/logging_mixin.py +0 -0
  66. {notionary-0.1.24 → notionary-0.1.26}/notionary/util/page_id_utils.py +0 -0
  67. {notionary-0.1.24 → notionary-0.1.26}/notionary.egg-info/dependency_links.txt +0 -0
  68. {notionary-0.1.24 → notionary-0.1.26}/notionary.egg-info/requires.txt +0 -0
  69. {notionary-0.1.24 → notionary-0.1.26}/notionary.egg-info/top_level.txt +0 -0
  70. {notionary-0.1.24 → notionary-0.1.26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.24
3
+ Version: 0.1.26
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
@@ -1,7 +1,10 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
3
  from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
4
+ from notionary.elements.prompts.element_prompt_content import (
5
+ ElementPromptBuilder,
6
+ ElementPromptContent,
7
+ )
5
8
 
6
9
 
7
10
  class AudioElement(NotionBlockElement):
@@ -16,40 +19,34 @@ class AudioElement(NotionBlockElement):
16
19
  Supports various audio URLs including direct audio file links from CDNs and other sources.
17
20
  """
18
21
 
19
- # Regex pattern for audio syntax
20
- PATTERN = re.compile(
21
- r"^\$\[(.*?)\]" # $[Caption] part
22
- + r'\((https?://[^\s"]+)' # (URL part
23
- + r"\)$" # closing parenthesis
24
- )
22
+ PATTERN = re.compile(r"^\$\[(.*?)\]" + r'\((https?://[^\s"]+)' + r"\)$")
25
23
 
26
- # Audio file extensions
27
24
  AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"]
28
25
 
29
- @staticmethod
30
- def match_markdown(text: str) -> bool:
26
+ @classmethod
27
+ def match_markdown(cls, text: str) -> bool:
31
28
  """Check if text is a markdown audio embed."""
32
29
  text = text.strip()
33
- return text.startswith("$[") and bool(AudioElement.PATTERN.match(text))
30
+ return text.startswith("$[") and bool(cls.PATTERN.match(text))
34
31
 
35
- @staticmethod
36
- def match_notion(block: Dict[str, Any]) -> bool:
32
+ @classmethod
33
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
37
34
  """Check if block is a Notion audio."""
38
35
  return block.get("type") == "audio"
39
36
 
40
- @staticmethod
41
- def is_audio_url(url: str) -> bool:
37
+ @classmethod
38
+ def is_audio_url(cls, url: str) -> bool:
42
39
  """Check if URL points to an audio file."""
43
40
  return (
44
- any(url.lower().endswith(ext) for ext in AudioElement.AUDIO_EXTENSIONS)
41
+ any(url.lower().endswith(ext) for ext in cls.AUDIO_EXTENSIONS)
45
42
  or "audio" in url.lower()
46
43
  or "storage.googleapis.com/audio_summaries" in url.lower()
47
44
  )
48
45
 
49
- @staticmethod
50
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
46
+ @classmethod
47
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
51
48
  """Convert markdown audio embed to Notion audio block."""
52
- audio_match = AudioElement.PATTERN.match(text.strip())
49
+ audio_match = cls.PATTERN.match(text.strip())
53
50
  if not audio_match:
54
51
  return None
55
52
 
@@ -60,7 +57,7 @@ class AudioElement(NotionBlockElement):
60
57
  return None
61
58
 
62
59
  # Make sure this is an audio URL
63
- if not AudioElement.is_audio_url(url):
60
+ if not cls.is_audio_url(url):
64
61
  # If not obviously an audio URL, we'll still accept it as the user might know better
65
62
  pass
66
63
 
@@ -78,8 +75,8 @@ class AudioElement(NotionBlockElement):
78
75
 
79
76
  return audio_block
80
77
 
81
- @staticmethod
82
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
78
+ @classmethod
79
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
83
80
  """Convert Notion audio block to markdown audio embed."""
84
81
  if block.get("type") != "audio":
85
82
  return None
@@ -101,17 +98,17 @@ class AudioElement(NotionBlockElement):
101
98
  caption = ""
102
99
  caption_rich_text = audio_data.get("caption", [])
103
100
  if caption_rich_text:
104
- caption = AudioElement._extract_text_content(caption_rich_text)
101
+ caption = cls._extract_text_content(caption_rich_text)
105
102
 
106
103
  return f"$[{caption}]({url})"
107
104
 
108
- @staticmethod
109
- def is_multiline() -> bool:
105
+ @classmethod
106
+ def is_multiline(cls) -> bool:
110
107
  """Audio embeds are single-line elements."""
111
108
  return False
112
109
 
113
- @staticmethod
114
- def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
110
+ @classmethod
111
+ def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
115
112
  """Extract plain text content from Notion rich_text elements."""
116
113
  result = ""
117
114
  for text_obj in rich_text:
@@ -124,16 +121,22 @@ class AudioElement(NotionBlockElement):
124
121
  @classmethod
125
122
  def get_llm_prompt_content(cls) -> ElementPromptContent:
126
123
  """Returns information for LLM prompts about this element."""
127
- return {
128
- "description": "Embeds audio content from external sources like CDNs or direct audio URLs.",
129
- "syntax": "$[Caption](https://example.com/audio.mp3)",
130
- "examples": [
131
- "$[Podcast Episode](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)",
132
- "$[Voice recording](https://example.com/audio/recording.mp3)",
133
- "$[](https://storage.googleapis.com/audio_summaries/example.mp3)",
134
- ],
135
- "when_to_use": (
124
+ return (
125
+ ElementPromptBuilder()
126
+ .with_description(
127
+ "Embeds audio content from external sources like CDNs or direct audio URLs."
128
+ )
129
+ .with_syntax("$[Caption](https://example.com/audio.mp3)")
130
+ .with_examples(
131
+ [
132
+ "$[Podcast Episode](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)",
133
+ "$[Voice recording](https://example.com/audio/recording.mp3)",
134
+ "$[](https://storage.googleapis.com/audio_summaries/example.mp3)",
135
+ ]
136
+ )
137
+ .with_usage_guidelines(
136
138
  "Use audio embeds when you want to include audio content directly in your document. "
137
139
  "Audio embeds are useful for podcasts, music, voice recordings, or any content that benefits from audio explanation."
138
- ),
139
- }
140
+ )
141
+ .build()
142
+ )
@@ -2,7 +2,10 @@ import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
3
 
4
4
  from notionary.elements.notion_block_element import NotionBlockElement
5
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
5
+ from notionary.elements.prompts.element_prompt_content import (
6
+ ElementPromptBuilder,
7
+ ElementPromptContent,
8
+ )
6
9
 
7
10
 
8
11
  class BookmarkElement(NotionBlockElement):
@@ -29,20 +32,20 @@ class BookmarkElement(NotionBlockElement):
29
32
  + r"\)$" # closing parenthesis
30
33
  )
31
34
 
32
- @staticmethod
33
- def match_markdown(text: str) -> bool:
35
+ @classmethod
36
+ def match_markdown(cls, text: str) -> bool:
34
37
  """Check if text is a markdown bookmark."""
35
38
  return text.strip().startswith("[bookmark]") and bool(
36
39
  BookmarkElement.PATTERN.match(text.strip())
37
40
  )
38
41
 
39
- @staticmethod
40
- def match_notion(block: Dict[str, Any]) -> bool:
42
+ @classmethod
43
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
41
44
  """Check if block is a Notion bookmark."""
42
45
  return block.get("type") in ["bookmark", "external-bookmark"]
43
46
 
44
- @staticmethod
45
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
47
+ @classmethod
48
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
46
49
  """Convert markdown bookmark to Notion bookmark block."""
47
50
  bookmark_match = BookmarkElement.PATTERN.match(text.strip())
48
51
  if not bookmark_match:
@@ -120,8 +123,8 @@ class BookmarkElement(NotionBlockElement):
120
123
 
121
124
  return {"type": "bookmark", "bookmark": bookmark_data}
122
125
 
123
- @staticmethod
124
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
126
+ @classmethod
127
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
125
128
  """Convert Notion bookmark block to markdown bookmark."""
126
129
  block_type = block.get("type", "")
127
130
 
@@ -158,13 +161,13 @@ class BookmarkElement(NotionBlockElement):
158
161
 
159
162
  return f"[bookmark]({url})"
160
163
 
161
- @staticmethod
162
- def is_multiline() -> bool:
164
+ @classmethod
165
+ def is_multiline(cls) -> bool:
163
166
  """Bookmarks are single-line elements."""
164
167
  return False
165
168
 
166
- @staticmethod
167
- def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
169
+ @classmethod
170
+ def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
168
171
  """Extract plain text content from Notion rich_text elements."""
169
172
  result = ""
170
173
  for text_obj in rich_text:
@@ -174,8 +177,8 @@ class BookmarkElement(NotionBlockElement):
174
177
  result += text_obj.get("plain_text", "")
175
178
  return result
176
179
 
177
- @staticmethod
178
- def _parse_caption(caption: List[Dict[str, Any]]) -> Tuple[str, str]:
180
+ @classmethod
181
+ def _parse_caption(cls, caption: List[Dict[str, Any]]) -> Tuple[str, str]:
179
182
  """
180
183
  Parse Notion caption into title and description components.
181
184
  Returns a tuple of (title, description).
@@ -196,17 +199,23 @@ class BookmarkElement(NotionBlockElement):
196
199
  """
197
200
  Returns structured LLM prompt metadata for the bookmark element.
198
201
  """
199
- return {
200
- "description": "Creates a bookmark that links to an external website.",
201
- "when_to_use": (
202
+ return (
203
+ ElementPromptBuilder()
204
+ .with_description("Creates a bookmark that links to an external website.")
205
+ .with_usage_guidelines(
202
206
  "Use bookmarks when you want to reference external content while keeping the page clean and organized. "
203
207
  "Bookmarks display a preview card for the linked content."
204
- ),
205
- "syntax": '[bookmark](https://example.com "Optional Title" "Optional Description")',
206
- "examples": [
207
- "[bookmark](https://example.com)",
208
- '[bookmark](https://example.com "Example Title")',
209
- '[bookmark](https://example.com "Example Title" "Example description of the site")',
210
- '[bookmark](https://github.com "GitHub" "Where the world builds software")',
211
- ],
212
- }
208
+ )
209
+ .with_syntax(
210
+ '[bookmark](https://example.com "Optional Title" "Optional Description")'
211
+ )
212
+ .with_examples(
213
+ [
214
+ "[bookmark](https://example.com)",
215
+ '[bookmark](https://example.com "Example Title")',
216
+ '[bookmark](https://example.com "Example Title" "Example description of the site")',
217
+ '[bookmark](https://github.com "GitHub" "Where the world builds software")',
218
+ ]
219
+ )
220
+ .build()
221
+ )
@@ -1,17 +1,18 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from typing_extensions import override
4
3
  from notionary.elements.notion_block_element import NotionBlockElement
5
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
4
+ from notionary.elements.prompts.element_prompt_content import (
5
+ ElementPromptBuilder,
6
+ ElementPromptContent,
7
+ )
6
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
7
9
 
8
10
 
9
11
  class BulletedListElement(NotionBlockElement):
10
12
  """Class for converting between Markdown bullet lists and Notion bulleted list items."""
11
13
 
12
- @override
13
- @staticmethod
14
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
14
+ @classmethod
15
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
15
16
  """Convert markdown bulleted list item to Notion block."""
16
17
  pattern = re.compile(
17
18
  r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$"
@@ -30,8 +31,8 @@ class BulletedListElement(NotionBlockElement):
30
31
  "bulleted_list_item": {"rich_text": rich_text, "color": "default"},
31
32
  }
32
33
 
33
- @staticmethod
34
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
34
+ @classmethod
35
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
35
36
  """Convert Notion bulleted list item block to markdown."""
36
37
  if block.get("type") != "bulleted_list_item":
37
38
  return None
@@ -41,14 +42,14 @@ class BulletedListElement(NotionBlockElement):
41
42
 
42
43
  return f"- {content}"
43
44
 
44
- @staticmethod
45
- def match_markdown(text: str) -> bool:
45
+ @classmethod
46
+ def match_markdown(cls, text: str) -> bool:
46
47
  """Check if this element can handle the given markdown text."""
47
48
  pattern = re.compile(r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$")
48
49
  return bool(pattern.match(text))
49
50
 
50
- @staticmethod
51
- def match_notion(block: Dict[str, Any]) -> bool:
51
+ @classmethod
52
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
52
53
  """Check if this element can handle the given Notion block."""
53
54
  return block.get("type") == "bulleted_list_item"
54
55
 
@@ -57,13 +58,19 @@ class BulletedListElement(NotionBlockElement):
57
58
  """
58
59
  Returns structured LLM prompt metadata for the bulleted list element.
59
60
  """
60
- return {
61
- "description": "Creates bulleted list items for unordered lists.",
62
- "when_to_use": "Use for lists where order doesn't matter, such as features, options, or items without hierarchy.",
63
- "syntax": "- Item text",
64
- "examples": [
65
- "- First item\n- Second item\n- Third item",
66
- "* Apple\n* Banana\n* Cherry",
67
- "+ Task A\n+ Task B",
68
- ],
69
- }
61
+ return (
62
+ ElementPromptBuilder()
63
+ .with_description("Creates bulleted list items for unordered lists.")
64
+ .with_usage_guidelines(
65
+ "Use for lists where order doesn't matter, such as features, options, or items without hierarchy."
66
+ )
67
+ .with_syntax("- Item text")
68
+ .with_examples(
69
+ [
70
+ "- First item\n- Second item\n- Third item",
71
+ "* Apple\n* Banana\n* Cherry",
72
+ "+ Task A\n+ Task B",
73
+ ]
74
+ )
75
+ .build()
76
+ )
@@ -1,8 +1,10 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from typing_extensions import override
4
3
 
5
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
4
+ from notionary.elements.prompts.element_prompt_content import (
5
+ ElementPromptBuilder,
6
+ ElementPromptContent,
7
+ )
6
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
7
9
  from notionary.elements.notion_block_element import NotionBlockElement
8
10
 
@@ -28,23 +30,20 @@ class CalloutElement(NotionBlockElement):
28
30
  DEFAULT_EMOJI = "💡"
29
31
  DEFAULT_COLOR = "gray_background"
30
32
 
31
- @override
32
- @staticmethod
33
- def match_markdown(text: str) -> bool:
33
+ @classmethod
34
+ def match_markdown(cls, text: str) -> bool:
34
35
  """Check if text is a markdown callout."""
35
36
  return text.strip().startswith("!>") and bool(
36
37
  CalloutElement.PATTERN.match(text)
37
38
  )
38
39
 
39
- @override
40
- @staticmethod
41
- def match_notion(block: Dict[str, Any]) -> bool:
40
+ @classmethod
41
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
42
42
  """Check if block is a Notion callout."""
43
43
  return block.get("type") == "callout"
44
44
 
45
- @override
46
- @staticmethod
47
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
45
+ @classmethod
46
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
48
47
  """Convert markdown callout to Notion callout block."""
49
48
  callout_match = CalloutElement.PATTERN.match(text)
50
49
  if not callout_match:
@@ -65,9 +64,8 @@ class CalloutElement(NotionBlockElement):
65
64
  },
66
65
  }
67
66
 
68
- @override
69
- @staticmethod
70
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
67
+ @classmethod
68
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
71
69
  """Convert Notion callout block to markdown callout."""
72
70
  if block.get("type") != "callout":
73
71
  return None
@@ -90,28 +88,38 @@ class CalloutElement(NotionBlockElement):
90
88
 
91
89
  return f"!> {emoji_str}{text}"
92
90
 
93
- @override
94
- @staticmethod
95
- def is_multiline() -> bool:
91
+ @classmethod
92
+ def is_multiline(cls) -> bool:
96
93
  return False
97
94
 
98
95
  @classmethod
99
96
  def get_llm_prompt_content(cls) -> ElementPromptContent:
100
97
  """
101
- Returns a dictionary with all information needed for LLM prompts about this element.
98
+ Returns structured LLM prompt metadata for the callout element.
102
99
  Includes description, usage guidance, syntax options, and examples.
103
100
  """
104
- return {
105
- "description": "Creates a callout block to highlight important information with an icon.",
106
- "when_to_use": (
101
+ return (
102
+ ElementPromptBuilder()
103
+ .with_description(
104
+ "Creates a callout block to highlight important information with an icon."
105
+ )
106
+ .with_usage_guidelines(
107
107
  "Use callouts when you want to draw attention to important information, "
108
- "tips, warnings, or notes that stand out from the main content."
109
- ),
110
- "syntax": "!> [emoji] Text",
111
- "examples": [
112
- "!> This is a default callout with the light bulb emoji",
113
- "!> [🔔] This is a callout with a bell emoji",
114
- "!> [⚠️] Warning: This is an important note to pay attention to",
115
- "!> [💡] Tip: Add emoji that matches your content's purpose",
116
- ],
117
- }
108
+ "tips, warnings, or notes that stand out from the main content. "
109
+ "The emoji MUST be enclosed in square brackets to properly display."
110
+ )
111
+ .with_syntax("!> [emoji] Text")
112
+ .with_examples(
113
+ [
114
+ "!> [💡] This is a default callout with the light bulb emoji",
115
+ "!> [🔔] This is a callout with a bell emoji",
116
+ "!> [⚠️] Warning: This is an important note to pay attention to",
117
+ "!> [💡] Tip: Add emoji that matches your content's purpose",
118
+ ]
119
+ )
120
+ .with_avoidance_guidelines(
121
+ "NEVER omit the square brackets around the emoji. The format MUST be !> [emoji] and not !> emoji. "
122
+ "Without the square brackets, Notion will not properly render the callout with the specified emoji."
123
+ )
124
+ .build()
125
+ )
@@ -1,7 +1,10 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
3
  from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
4
+ from notionary.elements.prompts.element_prompt_content import (
5
+ ElementPromptBuilder,
6
+ ElementPromptContent,
7
+ )
5
8
 
6
9
 
7
10
  class CodeBlockElement(NotionBlockElement):
@@ -20,18 +23,18 @@ class CodeBlockElement(NotionBlockElement):
20
23
 
21
24
  PATTERN = re.compile(r"```(\w*)\n([\s\S]+?)```", re.MULTILINE)
22
25
 
23
- @staticmethod
24
- def match_markdown(text: str) -> bool:
26
+ @classmethod
27
+ def match_markdown(cls, text: str) -> bool:
25
28
  """Check if text contains a markdown code block."""
26
29
  return bool(CodeBlockElement.PATTERN.search(text))
27
30
 
28
- @staticmethod
29
- def match_notion(block: Dict[str, Any]) -> bool:
31
+ @classmethod
32
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
30
33
  """Check if block is a Notion code block."""
31
34
  return block.get("type") == "code"
32
35
 
33
- @staticmethod
34
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
36
+ @classmethod
37
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
35
38
  """Convert markdown code block to Notion code block."""
36
39
  match = CodeBlockElement.PATTERN.search(text)
37
40
  if not match:
@@ -65,8 +68,8 @@ class CodeBlockElement(NotionBlockElement):
65
68
  },
66
69
  }
67
70
 
68
- @staticmethod
69
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
71
+ @classmethod
72
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
70
73
  """Convert Notion code block to markdown code block."""
71
74
  if block.get("type") != "code":
72
75
  return None
@@ -84,8 +87,8 @@ class CodeBlockElement(NotionBlockElement):
84
87
  # Format as a markdown code block
85
88
  return f"```{language}\n{content}\n```"
86
89
 
87
- @staticmethod
88
- def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
90
+ @classmethod
91
+ def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
89
92
  """
90
93
  Find all code block matches in the text and return their positions.
91
94
 
@@ -130,8 +133,8 @@ class CodeBlockElement(NotionBlockElement):
130
133
 
131
134
  return matches
132
135
 
133
- @staticmethod
134
- def is_multiline() -> bool:
136
+ @classmethod
137
+ def is_multiline(cls) -> bool:
135
138
  return True
136
139
 
137
140
  @classmethod
@@ -139,21 +142,30 @@ class CodeBlockElement(NotionBlockElement):
139
142
  """
140
143
  Returns structured LLM prompt metadata for the code block element.
141
144
  """
142
- return {
143
- "description": (
145
+ return (
146
+ ElementPromptBuilder()
147
+ .with_description(
144
148
  "Use fenced code blocks to format content as code. Supports language annotations like "
145
149
  "'python', 'json', or 'mermaid'. Useful for displaying code, configurations, command-line "
146
150
  "examples, or diagram syntax. Also suitable for explaining or visualizing systems with diagram languages."
147
- ),
148
- "when_to_use": (
151
+ )
152
+ .with_usage_guidelines(
149
153
  "Use code blocks when you want to present technical content like code snippets, terminal commands, "
150
154
  "JSON structures, or system diagrams. Especially helpful when structure and formatting are essential."
151
- ),
152
- "syntax": "```language\ncode content\n```",
153
- "examples": [
154
- "```python\nprint('Hello, world!')\n```",
155
- '```json\n{"name": "Alice", "age": 30}\n```',
156
- "```mermaid\nflowchart TD\n A --> B\n```",
157
- ],
158
- "avoid": "NEVER EVER wrap markdown content with ```markdown. Markdown should be written directly without code block formatting. NEVER use ```markdown under any circumstances."
159
- }
155
+ )
156
+ .with_syntax("```language\ncode content\n```")
157
+ .with_examples(
158
+ [
159
+ "```python\nprint('Hello, world!')\n```",
160
+ '```json\n{"name": "Alice", "age": 30}\n```',
161
+ "```mermaid\nflowchart TD\n A --> B\n```",
162
+ ]
163
+ )
164
+ .with_avoidance_guidelines(
165
+ "NEVER EVER wrap markdown content with ```markdown. Markdown should be written directly without code block formatting. "
166
+ "NEVER use ```markdown under any circumstances. "
167
+ "For Mermaid diagrams, use ONLY the default styling without colors, backgrounds, or custom styling attributes. "
168
+ "Keep Mermaid diagrams simple and minimal without any styling or color modifications."
169
+ )
170
+ .build()
171
+ )
@@ -0,0 +1,67 @@
1
+ import re
2
+ from typing import Dict, Any, Optional
3
+
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
+ from notionary.elements.prompts.element_prompt_content import (
6
+ ElementPromptBuilder,
7
+ ElementPromptContent,
8
+ )
9
+
10
+
11
+ class DividerElement(NotionBlockElement):
12
+ """
13
+ Handles conversion between Markdown horizontal dividers and Notion divider blocks.
14
+
15
+ Markdown divider syntax:
16
+ - Three or more hyphens (---) on a line by themselves
17
+ """
18
+
19
+ PATTERN = re.compile(r"^\s*-{3,}\s*$")
20
+
21
+ @classmethod
22
+ def match_markdown(cls, text: str) -> bool:
23
+ """Check if text is a markdown divider."""
24
+ return bool(DividerElement.PATTERN.match(text))
25
+
26
+ @classmethod
27
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
28
+ """Check if block is a Notion divider."""
29
+ return block.get("type") == "divider"
30
+
31
+ @classmethod
32
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
33
+ """Convert markdown divider to Notion divider block."""
34
+ if not DividerElement.match_markdown(text):
35
+ return None
36
+
37
+ return {"type": "divider", "divider": {}}
38
+
39
+ @classmethod
40
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
41
+ """Convert Notion divider block to markdown divider."""
42
+ if block.get("type") != "divider":
43
+ return None
44
+
45
+ return "---"
46
+
47
+ @classmethod
48
+ def is_multiline(cls) -> bool:
49
+ return False
50
+
51
+ @classmethod
52
+ def get_llm_prompt_content(cls) -> ElementPromptContent:
53
+ """Returns structured LLM prompt metadata for the divider element."""
54
+ return (
55
+ ElementPromptBuilder()
56
+ .with_description(
57
+ "Creates a horizontal divider line to visually separate sections of content."
58
+ )
59
+ .with_usage_guidelines(
60
+ "Use to create clear visual breaks between different sections without requiring headings."
61
+ )
62
+ .with_syntax("---")
63
+ .with_examples(
64
+ ["## Section 1\nContent\n\n---\n\n## Section 2\nMore content"]
65
+ )
66
+ .build()
67
+ )