notionary 0.2.16__py3-none-any.whl → 0.2.18__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.
Files changed (137) hide show
  1. notionary/__init__.py +10 -5
  2. notionary/base_notion_client.py +18 -7
  3. notionary/blocks/__init__.py +55 -24
  4. notionary/blocks/audio/__init__.py +7 -0
  5. notionary/blocks/audio/audio_element.py +152 -0
  6. notionary/blocks/audio/audio_markdown_node.py +29 -0
  7. notionary/blocks/audio/audio_models.py +59 -0
  8. notionary/blocks/bookmark/__init__.py +7 -0
  9. notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
  10. notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  11. notionary/blocks/bulleted_list/__init__.py +7 -0
  12. notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
  13. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  14. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  15. notionary/blocks/callout/__init__.py +7 -0
  16. notionary/blocks/callout/callout_element.py +132 -0
  17. notionary/blocks/callout/callout_markdown_node.py +31 -0
  18. notionary/blocks/callout/callout_models.py +0 -0
  19. notionary/blocks/code/__init__.py +7 -0
  20. notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
  21. notionary/blocks/code/code_markdown_node.py +43 -0
  22. notionary/blocks/code/code_models.py +0 -0
  23. notionary/blocks/column/__init__.py +5 -0
  24. notionary/blocks/{column_element.py → column/column_element.py} +24 -55
  25. notionary/blocks/column/column_models.py +0 -0
  26. notionary/blocks/divider/__init__.py +7 -0
  27. notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
  28. notionary/blocks/divider/divider_markdown_node.py +24 -0
  29. notionary/blocks/divider/divider_models.py +0 -0
  30. notionary/blocks/document/__init__.py +7 -0
  31. notionary/blocks/document/document_element.py +102 -0
  32. notionary/blocks/document/document_markdown_node.py +31 -0
  33. notionary/blocks/document/document_models.py +0 -0
  34. notionary/blocks/embed/__init__.py +7 -0
  35. notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
  36. notionary/blocks/embed/embed_markdown_node.py +30 -0
  37. notionary/blocks/embed/embed_models.py +0 -0
  38. notionary/blocks/heading/__init__.py +7 -0
  39. notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
  40. notionary/blocks/heading/heading_markdown_node.py +29 -0
  41. notionary/blocks/heading/heading_models.py +0 -0
  42. notionary/blocks/image/__init__.py +7 -0
  43. notionary/blocks/{image_element.py → image/image_element.py} +62 -42
  44. notionary/blocks/image/image_markdown_node.py +33 -0
  45. notionary/blocks/image/image_models.py +0 -0
  46. notionary/blocks/markdown_builder.py +356 -0
  47. notionary/blocks/markdown_node.py +29 -0
  48. notionary/blocks/mention/__init__.py +7 -0
  49. notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
  50. notionary/blocks/mention/mention_markdown_node.py +38 -0
  51. notionary/blocks/mention/mention_models.py +0 -0
  52. notionary/blocks/numbered_list/__init__.py +7 -0
  53. notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
  54. notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  55. notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  56. notionary/blocks/paragraph/__init__.py +7 -0
  57. notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
  58. notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  59. notionary/blocks/paragraph/paragraph_models.py +0 -0
  60. notionary/blocks/quote/__init__.py +7 -0
  61. notionary/blocks/quote/quote_element.py +92 -0
  62. notionary/blocks/quote/quote_markdown_node.py +23 -0
  63. notionary/blocks/quote/quote_models.py +0 -0
  64. notionary/blocks/registry/block_registry.py +17 -3
  65. notionary/blocks/registry/block_registry_builder.py +90 -178
  66. notionary/blocks/shared/__init__.py +0 -0
  67. notionary/blocks/shared/block_client.py +256 -0
  68. notionary/blocks/shared/models.py +710 -0
  69. notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
  70. notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
  71. notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  72. notionary/blocks/table/__init__.py +7 -0
  73. notionary/blocks/{table_element.py → table/table_element.py} +23 -11
  74. notionary/blocks/table/table_markdown_node.py +40 -0
  75. notionary/blocks/table/table_models.py +0 -0
  76. notionary/blocks/todo/__init__.py +7 -0
  77. notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
  78. notionary/blocks/todo/todo_markdown_node.py +31 -0
  79. notionary/blocks/todo/todo_models.py +0 -0
  80. notionary/blocks/toggle/__init__.py +4 -0
  81. notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
  82. notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  83. notionary/blocks/toggle/toggle_models.py +0 -0
  84. notionary/blocks/toggleable_heading/__init__.py +9 -0
  85. notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
  86. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  87. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  88. notionary/blocks/video/__init__.py +7 -0
  89. notionary/blocks/{video_element.py → video/video_element.py} +82 -57
  90. notionary/blocks/video/video_markdown_node.py +30 -0
  91. notionary/database/__init__.py +4 -0
  92. notionary/database/database.py +481 -0
  93. notionary/database/{filter_builder.py → database_filter_builder.py} +27 -29
  94. notionary/database/{notion_database_provider.py → database_provider.py} +4 -4
  95. notionary/database/notion_database.py +45 -18
  96. notionary/file_upload/__init__.py +7 -0
  97. notionary/file_upload/client.py +254 -0
  98. notionary/file_upload/models.py +60 -0
  99. notionary/file_upload/notion_file_upload.py +387 -0
  100. notionary/page/content/markdown_whitespace_processor.py +80 -0
  101. notionary/page/content/notion_text_length_utils.py +87 -0
  102. notionary/page/content/page_content_retriever.py +2 -2
  103. notionary/page/content/page_content_writer.py +97 -148
  104. notionary/page/formatting/line_processor.py +153 -0
  105. notionary/page/formatting/markdown_to_notion_converter.py +103 -424
  106. notionary/page/notion_page.py +13 -14
  107. notionary/page/notion_to_markdown_converter.py +9 -13
  108. notionary/telemetry/views.py +15 -6
  109. notionary/user/__init__.py +11 -0
  110. notionary/user/base_notion_user.py +52 -0
  111. notionary/user/client.py +129 -0
  112. notionary/user/models.py +83 -0
  113. notionary/user/notion_bot_user.py +227 -0
  114. notionary/user/notion_user.py +256 -0
  115. notionary/user/notion_user_manager.py +173 -0
  116. notionary/user/notion_user_provider.py +1 -0
  117. notionary/util/__init__.py +3 -5
  118. notionary/util/factory_decorator.py +0 -33
  119. notionary/util/factory_only.py +37 -0
  120. notionary/util/fuzzy.py +74 -0
  121. notionary/util/logging_mixin.py +12 -12
  122. notionary/workspace.py +38 -3
  123. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/METADATA +2 -1
  124. notionary-0.2.18.dist-info/RECORD +149 -0
  125. notionary/blocks/audio_element.py +0 -144
  126. notionary/blocks/callout_element.py +0 -122
  127. notionary/blocks/notion_block_client.py +0 -26
  128. notionary/blocks/qoute_element.py +0 -169
  129. notionary/page/content/notion_page_content_chunker.py +0 -84
  130. notionary/page/formatting/spacer_rules.py +0 -483
  131. notionary/util/fuzzy_matcher.py +0 -82
  132. notionary-0.2.16.dist-info/RECORD +0 -71
  133. /notionary/{elements/__init__.py → blocks/bookmark/bookmark_models.py} +0 -0
  134. /notionary/database/{database_exceptions.py → exceptions.py} +0 -0
  135. /notionary/util/{singleton_decorator.py → singleton.py} +0 -0
  136. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/LICENSE +0 -0
  137. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/WHEEL +0 -0
@@ -1,8 +1,12 @@
1
1
  import re
2
-
3
2
  from typing import Dict, Any, Optional, List
3
+
4
4
  from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
5
+ from notionary.blocks import (
6
+ ElementPromptContent,
7
+ ElementPromptBuilder,
8
+ NotionBlockResult,
9
+ )
6
10
 
7
11
 
8
12
  class ImageElement(NotionBlockElement):
@@ -10,23 +14,26 @@ class ImageElement(NotionBlockElement):
10
14
  Handles conversion between Markdown images and Notion image blocks.
11
15
 
12
16
  Markdown image syntax:
13
- - ![Caption](https://example.com/image.jpg) - Basic image with caption
14
- - ![](https://example.com/image.jpg) - Image without caption
15
- - ![Caption](https://example.com/image.jpg "alt text") - Image with caption and alt text
17
+ - [image](https://example.com/image.jpg) - Simple image with URL only
18
+ - [image](https://example.com/image.jpg "Caption") - Image with URL and caption
19
+
20
+ Where:
21
+ - URL is the required image URL
22
+ - Caption is an optional descriptive text (enclosed in quotes)
16
23
  """
17
24
 
18
- # Regex pattern for image syntax with optional alt text
25
+ # Regex pattern for image syntax with optional caption
19
26
  PATTERN = re.compile(
20
- r"^\!\[(.*?)\]" # ![Caption] part
21
- + r'\((https?://[^\s"]+)' # (URL part
22
- + r'(?:\s+"([^"]+)")?' # Optional alt text in quotes
27
+ r"^\[image\]\(" # [image]( prefix
28
+ + r'(https?://[^\s"]+)' # URL (required)
29
+ + r'(?:\s+"([^"]+)")?' # Optional caption in quotes
23
30
  + r"\)$" # closing parenthesis
24
31
  )
25
32
 
26
33
  @classmethod
27
34
  def match_markdown(cls, text: str) -> bool:
28
35
  """Check if text is a markdown image."""
29
- return text.strip().startswith("![") and bool(
36
+ return text.strip().startswith("[image]") and bool(
30
37
  ImageElement.PATTERN.match(text.strip())
31
38
  )
32
39
 
@@ -36,31 +43,33 @@ class ImageElement(NotionBlockElement):
36
43
  return block.get("type") == "image"
37
44
 
38
45
  @classmethod
39
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
46
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
40
47
  """Convert markdown image to Notion image block."""
41
48
  image_match = ImageElement.PATTERN.match(text.strip())
42
49
  if not image_match:
43
50
  return None
44
51
 
45
- caption = image_match.group(1)
46
- url = image_match.group(2)
52
+ url = image_match.group(1)
53
+ caption = image_match.group(2)
47
54
 
48
55
  if not url:
49
56
  return None
50
57
 
51
- # Prepare the image block
52
- image_block = {
53
- "type": "image",
54
- "image": {"type": "external", "external": {"url": url}},
55
- }
58
+ image_data = {"type": "external", "external": {"url": url}}
56
59
 
57
60
  # Add caption if provided
58
61
  if caption:
59
- image_block["image"]["caption"] = [
60
- {"type": "text", "text": {"content": caption}}
61
- ]
62
+ image_data["caption"] = [{"type": "text", "text": {"content": caption}}]
63
+ else:
64
+ image_data["caption"] = []
65
+
66
+ # Prepare the image block
67
+ image_block = {"type": "image", "image": image_data}
68
+
69
+ # Add empty paragraph after image
70
+ empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
62
71
 
63
- return image_block
72
+ return [image_block, empty_paragraph]
64
73
 
65
74
  @classmethod
66
75
  def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
@@ -71,23 +80,37 @@ class ImageElement(NotionBlockElement):
71
80
  image_data = block.get("image", {})
72
81
 
73
82
  # Handle both external and file (uploaded) images
74
- if image_data.get("type") == "external":
75
- url = image_data.get("external", {}).get("url", "")
76
- elif image_data.get("type") == "file":
77
- url = image_data.get("file", {}).get("url", "")
78
- else:
79
- return None
80
-
83
+ url = ImageElement._extract_image_url(image_data)
81
84
  if not url:
82
85
  return None
83
86
 
84
- # Extract caption if available
85
- caption = ""
86
87
  caption_rich_text = image_data.get("caption", [])
87
- if caption_rich_text:
88
- caption = ImageElement._extract_text_content(caption_rich_text)
89
88
 
90
- return f"![{caption}]({url})"
89
+ if not caption_rich_text:
90
+ # Simple image with URL only
91
+ return f"[image]({url})"
92
+
93
+ # Extract caption text
94
+ caption = ImageElement._extract_text_content(caption_rich_text)
95
+
96
+ if caption:
97
+ return f'[image]({url} "{caption}")'
98
+
99
+ return f"[image]({url})"
100
+
101
+ @classmethod
102
+ def is_multiline(cls) -> bool:
103
+ """Images are single-line elements."""
104
+ return False
105
+
106
+ @classmethod
107
+ def _extract_image_url(cls, image_data: Dict[str, Any]) -> str:
108
+ """Extract URL from image data, handling both external and uploaded images."""
109
+ if image_data.get("type") == "external":
110
+ return image_data.get("external", {}).get("url", "")
111
+ elif image_data.get("type") == "file":
112
+ return image_data.get("file", {}).get("url", "")
113
+ return ""
91
114
 
92
115
  @classmethod
93
116
  def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
@@ -100,10 +123,6 @@ class ImageElement(NotionBlockElement):
100
123
  result += text_obj.get("plain_text", "")
101
124
  return result
102
125
 
103
- @classmethod
104
- def is_multiline(cls) -> bool:
105
- return False
106
-
107
126
  @classmethod
108
127
  def get_llm_prompt_content(cls) -> ElementPromptContent:
109
128
  """
@@ -119,12 +138,13 @@ class ImageElement(NotionBlockElement):
119
138
  "that enhance your document. Images can make complex information easier to understand, create visual interest, "
120
139
  "or provide evidence for your points."
121
140
  )
122
- .with_syntax("![Caption](https://example.com/image.jpg)")
141
+ .with_syntax('[image](https://example.com/image.jpg "Optional caption")')
123
142
  .with_examples(
124
143
  [
125
- "![Data visualization showing monthly trends](https://example.com/chart.png)",
126
- "![](https://example.com/screenshot.jpg)",
127
- '![Company logo](https://company.com/logo.png "Company Inc. logo")',
144
+ "[image](https://example.com/chart.png)",
145
+ '[image](https://example.com/screenshot.jpg "Data visualization showing monthly trends")',
146
+ '[image](https://company.com/logo.png "Company Inc. logo")',
147
+ '[image](https://example.com/diagram.jpg "System architecture overview")',
128
148
  ]
129
149
  )
130
150
  .build()
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+ from pydantic import BaseModel
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class ImageMarkdownBlockParams(BaseModel):
9
+ url: str
10
+ caption: Optional[str] = None
11
+
12
+
13
+ class ImageMarkdownNode(MarkdownNode):
14
+ """
15
+ Programmatic interface for creating Notion-style image blocks.
16
+ Example: [image](https://example.com/image.jpg "Optional caption")
17
+ """
18
+
19
+ def __init__(
20
+ self, url: str, caption: Optional[str] = None, alt: Optional[str] = None
21
+ ):
22
+ self.url = url
23
+ self.caption = caption
24
+ # Note: 'alt' is kept for API compatibility but not used in Notion syntax
25
+
26
+ @classmethod
27
+ def from_params(cls, params: ImageMarkdownBlockParams) -> ImageMarkdownNode:
28
+ return cls(url=params.url, caption=params.caption)
29
+
30
+ def to_markdown(self) -> str:
31
+ if self.caption:
32
+ return f'[image]({self.url} "{self.caption}")'
33
+ return f"[image]({self.url})"
File without changes
@@ -0,0 +1,356 @@
1
+ """
2
+ Clean Fluent Markdown Builder
3
+ ============================
4
+
5
+ A direct, chainable builder for all MarkdownNode types without overengineering.
6
+ Maps 1:1 to the available blocks with clear, expressive method names.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from typing import Optional, Self, Union
11
+
12
+ from notionary.blocks import (
13
+ HeadingMarkdownNode,
14
+ ImageMarkdownNode,
15
+ ParagraphMarkdownNode,
16
+ AudioMarkdownNode,
17
+ BookmarkMarkdownNode,
18
+ CalloutMarkdownNode,
19
+ CodeMarkdownNode,
20
+ DividerMarkdownNode,
21
+ DocumentMarkdownNode,
22
+ EmbedMarkdownNode,
23
+ MentionMarkdownNode,
24
+ NumberedListMarkdownNode,
25
+ BulletedListMarkdownNode,
26
+ QuoteMarkdownNode,
27
+ TableMarkdownNode,
28
+ TodoMarkdownNode,
29
+ ToggleMarkdownNode,
30
+ ToggleableHeadingMarkdownNode,
31
+ VideoMarkdownNode,
32
+ MarkdownNode,
33
+ )
34
+
35
+
36
+ class MarkdownBuilder:
37
+ """
38
+ Fluent interface builder for creating Notion content with clean, direct methods.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ self.children: list[MarkdownNode] = []
43
+
44
+ def h1(self, text: str) -> Self:
45
+ """
46
+ Add an H1 heading.
47
+
48
+ Args:
49
+ text: The heading text content
50
+ """
51
+ self.children.append(HeadingMarkdownNode(text=text, level=1))
52
+ return self
53
+
54
+ def h2(self, text: str) -> Self:
55
+ """
56
+ Add an H2 heading.
57
+
58
+ Args:
59
+ text: The heading text content
60
+ """
61
+ self.children.append(HeadingMarkdownNode(text=text, level=2))
62
+ return self
63
+
64
+ def h3(self, text: str) -> Self:
65
+ """
66
+ Add an H3 heading.
67
+
68
+ Args:
69
+ text: The heading text content
70
+ """
71
+ self.children.append(HeadingMarkdownNode(text=text, level=3))
72
+ return self
73
+
74
+ def heading(self, text: str, level: int = 2) -> Self:
75
+ """
76
+ Add a heading with specified level.
77
+
78
+ Args:
79
+ text: The heading text content
80
+ level: Heading level (1-3), defaults to 2
81
+ """
82
+ self.children.append(HeadingMarkdownNode(text=text, level=level))
83
+ return self
84
+
85
+ def paragraph(self, text: str) -> Self:
86
+ """
87
+ Add a paragraph block.
88
+
89
+ Args:
90
+ text: The paragraph text content
91
+ """
92
+ self.children.append(ParagraphMarkdownNode(text=text))
93
+ return self
94
+
95
+ def text(self, content: str) -> Self:
96
+ """
97
+ Add a text paragraph (alias for paragraph).
98
+
99
+ Args:
100
+ content: The text content
101
+ """
102
+ return self.paragraph(content)
103
+
104
+ def quote(self, text: str, author: Optional[str] = None) -> Self:
105
+ """
106
+ Add a blockquote.
107
+
108
+ Args:
109
+ text: Quote text content
110
+ author: Optional quote author/attribution
111
+ """
112
+ self.children.append(QuoteMarkdownNode(text=text, author=author))
113
+ return self
114
+
115
+ def divider(self) -> Self:
116
+ """Add a horizontal divider."""
117
+ self.children.append(DividerMarkdownNode())
118
+ return self
119
+
120
+ def numbered_list(self, items: list[str]) -> Self:
121
+ """
122
+ Add a numbered list.
123
+
124
+ Args:
125
+ items: List of text items for the numbered list
126
+ """
127
+ self.children.append(NumberedListMarkdownNode(texts=items))
128
+ return self
129
+
130
+ def bulleted_list(self, items: list[str]) -> Self:
131
+ """
132
+ Add a bulleted list.
133
+
134
+ Args:
135
+ items: List of text items for the bulleted list
136
+ """
137
+ self.children.append(BulletedListMarkdownNode(texts=items))
138
+ return self
139
+
140
+ def todo(self, text: str, checked: bool = False) -> Self:
141
+ """
142
+ Add a single todo item.
143
+
144
+ Args:
145
+ text: The todo item text
146
+ checked: Whether the todo item is completed, defaults to False
147
+ """
148
+ self.children.append(TodoMarkdownNode(text=text, checked=checked))
149
+ return self
150
+
151
+ def todo_list(
152
+ self, items: list[str], completed: Optional[list[bool]] = None
153
+ ) -> Self:
154
+ """
155
+ Add multiple todo items.
156
+
157
+ Args:
158
+ items: List of todo item texts
159
+ completed: List of completion states for each item, defaults to all False
160
+ """
161
+ if completed is None:
162
+ completed = [False] * len(items)
163
+
164
+ for i, item in enumerate(items):
165
+ is_done = completed[i] if i < len(completed) else False
166
+ self.children.append(TodoMarkdownNode(text=item, checked=is_done))
167
+ return self
168
+
169
+ def callout(self, text: str, emoji: Optional[str] = None) -> Self:
170
+ """
171
+ Add a callout block.
172
+
173
+ Args:
174
+ text: The callout text content
175
+ emoji: Optional emoji for the callout icon
176
+ """
177
+ self.children.append(CalloutMarkdownNode(text=text, emoji=emoji))
178
+ return self
179
+
180
+ def toggle(self, title: str, content: Optional[list[str]] = None) -> Self:
181
+ """
182
+ Add a toggle block.
183
+
184
+ Args:
185
+ title: The toggle title/header text
186
+ content: Optional list of content items inside the toggle
187
+ """
188
+ self.children.append(ToggleMarkdownNode(title=title, content=content))
189
+ return self
190
+
191
+ def toggleable_heading(
192
+ self, text: str, level: int = 2, content: Optional[list[str]] = None
193
+ ) -> Self:
194
+ """
195
+ Add a toggleable heading.
196
+
197
+ Args:
198
+ text: The heading text content
199
+ level: Heading level (1-3), defaults to 2
200
+ content: Optional list of content items inside the toggleable heading
201
+ """
202
+ self.children.append(
203
+ ToggleableHeadingMarkdownNode(text=text, level=level, content=content)
204
+ )
205
+ return self
206
+
207
+ def image(
208
+ self, url: str, caption: Optional[str] = None, alt: Optional[str] = None
209
+ ) -> Self:
210
+ """
211
+ Add an image.
212
+
213
+ Args:
214
+ url: Image URL or file path
215
+ caption: Optional image caption text
216
+ alt: Optional alternative text for accessibility
217
+ """
218
+ self.children.append(ImageMarkdownNode(url=url, caption=caption, alt=alt))
219
+ return self
220
+
221
+ def video(self, url: str, caption: Optional[str] = None) -> Self:
222
+ """
223
+ Add a video.
224
+
225
+ Args:
226
+ url: Video URL or file path
227
+ caption: Optional video caption text
228
+ """
229
+ self.children.append(VideoMarkdownNode(url=url, caption=caption))
230
+ return self
231
+
232
+ def audio(self, url: str, caption: Optional[str] = None) -> Self:
233
+ """
234
+ Add audio content.
235
+
236
+ Args:
237
+ url: Audio file URL or path
238
+ caption: Optional audio caption text
239
+ """
240
+ self.children.append(AudioMarkdownNode(url=url, caption=caption))
241
+ return self
242
+
243
+ def document(self, url: str, caption: Optional[str] = None) -> Self:
244
+ """
245
+ Add a document file.
246
+
247
+ Args:
248
+ url: Document file URL or path
249
+ caption: Optional document caption text
250
+ """
251
+ self.children.append(DocumentMarkdownNode(url=url, caption=caption))
252
+ return self
253
+
254
+ def bookmark(
255
+ self, url: str, title: Optional[str] = None, description: Optional[str] = None
256
+ ) -> Self:
257
+ """
258
+ Add a bookmark.
259
+
260
+ Args:
261
+ url: Bookmark URL
262
+ title: Optional bookmark title
263
+ description: Optional bookmark description text
264
+ """
265
+ self.children.append(
266
+ BookmarkMarkdownNode(url=url, title=title, description=description)
267
+ )
268
+ return self
269
+
270
+ def embed(self, url: str, caption: Optional[str] = None) -> Self:
271
+ """
272
+ Add an embed.
273
+
274
+ Args:
275
+ url: URL to embed (e.g., YouTube, Twitter, etc.)
276
+ caption: Optional embed caption text
277
+ """
278
+ self.children.append(EmbedMarkdownNode(url=url, caption=caption))
279
+ return self
280
+
281
+ def code(
282
+ self, code: str, language: Optional[str] = None, caption: Optional[str] = None
283
+ ) -> Self:
284
+ """
285
+ Add a code block.
286
+
287
+ Args:
288
+ code: The source code content
289
+ language: Optional programming language for syntax highlighting
290
+ caption: Optional code block caption text
291
+ """
292
+ self.children.append(
293
+ CodeMarkdownNode(code=code, language=language, caption=caption)
294
+ )
295
+ return self
296
+
297
+ def table(self, headers: list[str], rows: list[list[str]]) -> Self:
298
+ """
299
+ Add a table.
300
+
301
+ Args:
302
+ headers: List of column header texts
303
+ rows: List of rows, where each row is a list of cell texts
304
+ """
305
+ self.children.append(TableMarkdownNode(headers=headers, rows=rows))
306
+ return self
307
+
308
+ def mention_page(self, page_id: str) -> Self:
309
+ """
310
+ Add a page mention.
311
+
312
+ Args:
313
+ page_id: The ID of the page to mention
314
+ """
315
+ self.children.append(MentionMarkdownNode("page", page_id))
316
+ return self
317
+
318
+ def mention_database(self, database_id: str) -> Self:
319
+ """
320
+ Add a database mention.
321
+
322
+ Args:
323
+ database_id: The ID of the database to mention
324
+ """
325
+ self.children.append(MentionMarkdownNode("database", database_id))
326
+ return self
327
+
328
+ def mention_date(self, date: str) -> Self:
329
+ """
330
+ Add a date mention.
331
+
332
+ Args:
333
+ date: Date in YYYY-MM-DD format
334
+ """
335
+ self.children.append(MentionMarkdownNode("date", date))
336
+ return self
337
+
338
+ def add_custom(self, node: MarkdownNode) -> Self:
339
+ """
340
+ Add a custom MarkdownNode.
341
+
342
+ Args:
343
+ node: A custom MarkdownNode instance
344
+ """
345
+ self.children.append(node)
346
+ return self
347
+
348
+ def space(self) -> Self:
349
+ """Add vertical spacing."""
350
+ return self.paragraph("")
351
+
352
+ def build(self) -> str:
353
+ """Build and return the final markdown string."""
354
+ return "\n\n".join(
355
+ child.to_markdown() for child in self.children if child is not None
356
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class MarkdownNode(ABC):
6
+ """
7
+ Abstract base class for all Markdown block elements.
8
+ Enforces implementation of to_markdown().
9
+ """
10
+
11
+ @abstractmethod
12
+ def to_markdown(self) -> str:
13
+ """
14
+ Returns the Markdown representation of the block.
15
+ Must be implemented by subclasses.
16
+ """
17
+ pass
18
+
19
+ @classmethod
20
+ @abstractmethod
21
+ def from_params(cls, params) -> MarkdownNode:
22
+ """
23
+ Creates an instance from a params object.
24
+ Must be implemented by subclasses.
25
+ """
26
+ pass
27
+
28
+ def __str__(self):
29
+ return self.to_markdown()
@@ -0,0 +1,7 @@
1
+ from .mention_element import MentionElement
2
+ from .mention_markdown_node import MentionMarkdownNode
3
+
4
+ __all__ = [
5
+ "MentionElement",
6
+ "MentionMarkdownNode",
7
+ ]
@@ -2,7 +2,11 @@ import re
2
2
  from typing import Dict, Any, Optional, List
3
3
 
4
4
  from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
5
+ from notionary.blocks import (
6
+ ElementPromptContent,
7
+ ElementPromptBuilder,
8
+ NotionBlockResult,
9
+ )
6
10
 
7
11
 
8
12
  class MentionElement(NotionBlockElement):
@@ -74,7 +78,7 @@ class MentionElement(NotionBlockElement):
74
78
  return any(text_item.get("type") == "mention" for text_item in rich_text)
75
79
 
76
80
  @classmethod
77
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
81
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
78
82
  """Convert markdown text with mentions to a Notion paragraph block."""
79
83
  if not MentionElement.match_markdown(text):
80
84
  return None
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+ from typing import Literal
3
+ from pydantic import BaseModel
4
+ from notionary.blocks.markdown_node import MarkdownNode
5
+
6
+
7
+ class MentionMarkdownBlockParams(BaseModel):
8
+ mention_type: Literal["page", "database", "date"]
9
+ value: str
10
+
11
+
12
+ class MentionMarkdownNode(MarkdownNode):
13
+ """
14
+ Programmatic interface for creating Notion-style Markdown mentions.
15
+ Supports: page, database, date.
16
+ Examples: @[page-id], @db[database-id], @date[YYYY-MM-DD]
17
+ """
18
+
19
+ def __init__(self, mention_type: str, value: str):
20
+ allowed = {"page", "database", "date"}
21
+ if mention_type not in allowed:
22
+ raise ValueError(f"mention_type must be one of {allowed}")
23
+ self.mention_type = mention_type
24
+ self.value = value
25
+
26
+ @classmethod
27
+ def from_params(cls, params: MentionMarkdownBlockParams) -> MentionMarkdownNode:
28
+ return cls(mention_type=params.mention_type, value=params.value)
29
+
30
+ def to_markdown(self) -> str:
31
+ if self.mention_type == "page":
32
+ return f"@[{self.value}]"
33
+ elif self.mention_type == "database":
34
+ return f"@db[{self.value}]"
35
+ elif self.mention_type == "date":
36
+ return f"@date[{self.value}]"
37
+ else:
38
+ return f"@[{self.value}]"
File without changes
@@ -0,0 +1,7 @@
1
+ from .numbered_list_element import NumberedListElement
2
+ from .numbered_list_markdown_node import NumberedListMarkdownNode
3
+
4
+ __all__ = [
5
+ "NumberedListElement",
6
+ "NumberedListMarkdownNode",
7
+ ]