notionary 0.2.19__py3-none-any.whl → 0.2.22__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 (220) hide show
  1. notionary/__init__.py +8 -4
  2. notionary/base_notion_client.py +3 -1
  3. notionary/blocks/__init__.py +2 -91
  4. notionary/blocks/_bootstrap.py +271 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +69 -106
  7. notionary/blocks/audio/audio_markdown_node.py +13 -5
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +42 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +49 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
  13. notionary/blocks/bookmark/bookmark_models.py +15 -0
  14. notionary/blocks/breadcrumbs/__init__.py +17 -0
  15. notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
  16. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
  17. notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
  18. notionary/blocks/bulleted_list/__init__.py +12 -2
  19. notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
  20. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
  21. notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
  22. notionary/blocks/callout/__init__.py +9 -2
  23. notionary/blocks/callout/callout_element.py +53 -86
  24. notionary/blocks/callout/callout_markdown_node.py +3 -1
  25. notionary/blocks/callout/callout_models.py +33 -0
  26. notionary/blocks/child_database/__init__.py +14 -0
  27. notionary/blocks/child_database/child_database_element.py +61 -0
  28. notionary/blocks/child_database/child_database_models.py +12 -0
  29. notionary/blocks/child_page/__init__.py +9 -0
  30. notionary/blocks/child_page/child_page_element.py +94 -0
  31. notionary/blocks/child_page/child_page_models.py +12 -0
  32. notionary/blocks/{shared/block_client.py → client.py} +54 -54
  33. notionary/blocks/code/__init__.py +6 -2
  34. notionary/blocks/code/code_element.py +96 -181
  35. notionary/blocks/code/code_markdown_node.py +64 -13
  36. notionary/blocks/code/code_models.py +94 -0
  37. notionary/blocks/column/__init__.py +25 -1
  38. notionary/blocks/column/column_element.py +44 -312
  39. notionary/blocks/column/column_list_element.py +52 -0
  40. notionary/blocks/column/column_list_markdown_node.py +50 -0
  41. notionary/blocks/column/column_markdown_node.py +59 -0
  42. notionary/blocks/column/column_models.py +26 -0
  43. notionary/blocks/divider/__init__.py +9 -2
  44. notionary/blocks/divider/divider_element.py +18 -49
  45. notionary/blocks/divider/divider_markdown_node.py +2 -1
  46. notionary/blocks/divider/divider_models.py +12 -0
  47. notionary/blocks/embed/__init__.py +9 -2
  48. notionary/blocks/embed/embed_element.py +65 -111
  49. notionary/blocks/embed/embed_markdown_node.py +3 -1
  50. notionary/blocks/embed/embed_models.py +14 -0
  51. notionary/blocks/equation/__init__.py +14 -0
  52. notionary/blocks/equation/equation_element.py +133 -0
  53. notionary/blocks/equation/equation_element_markdown_node.py +35 -0
  54. notionary/blocks/equation/equation_models.py +11 -0
  55. notionary/blocks/file/__init__.py +25 -0
  56. notionary/blocks/file/file_element.py +112 -0
  57. notionary/blocks/file/file_element_markdown_node.py +37 -0
  58. notionary/blocks/file/file_element_models.py +39 -0
  59. notionary/blocks/guards.py +22 -0
  60. notionary/blocks/heading/__init__.py +16 -2
  61. notionary/blocks/heading/heading_element.py +83 -69
  62. notionary/blocks/heading/heading_markdown_node.py +2 -1
  63. notionary/blocks/heading/heading_models.py +29 -0
  64. notionary/blocks/image_block/__init__.py +13 -0
  65. notionary/blocks/image_block/image_element.py +89 -0
  66. notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
  67. notionary/blocks/image_block/image_models.py +10 -0
  68. notionary/blocks/mixins/captions/__init__.py +4 -0
  69. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  70. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  71. notionary/blocks/models.py +174 -0
  72. notionary/blocks/numbered_list/__init__.py +12 -2
  73. notionary/blocks/numbered_list/numbered_list_element.py +48 -56
  74. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  75. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  76. notionary/blocks/paragraph/__init__.py +12 -2
  77. notionary/blocks/paragraph/paragraph_element.py +40 -66
  78. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  79. notionary/blocks/paragraph/paragraph_models.py +16 -0
  80. notionary/blocks/pdf/__init__.py +13 -0
  81. notionary/blocks/pdf/pdf_element.py +97 -0
  82. notionary/blocks/pdf/pdf_markdown_node.py +37 -0
  83. notionary/blocks/pdf/pdf_models.py +11 -0
  84. notionary/blocks/quote/__init__.py +11 -2
  85. notionary/blocks/quote/quote_element.py +45 -62
  86. notionary/blocks/quote/quote_markdown_node.py +6 -3
  87. notionary/blocks/quote/quote_models.py +18 -0
  88. notionary/blocks/registry/__init__.py +4 -0
  89. notionary/blocks/registry/block_registry.py +60 -121
  90. notionary/blocks/registry/block_registry_builder.py +115 -59
  91. notionary/blocks/rich_text/__init__.py +33 -0
  92. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  93. notionary/blocks/rich_text/rich_text_models.py +221 -0
  94. notionary/blocks/rich_text/text_inline_formatter.py +456 -0
  95. notionary/blocks/syntax_prompt_builder.py +137 -0
  96. notionary/blocks/table/__init__.py +16 -2
  97. notionary/blocks/table/table_element.py +136 -228
  98. notionary/blocks/table/table_markdown_node.py +2 -1
  99. notionary/blocks/table/table_models.py +28 -0
  100. notionary/blocks/table_of_contents/__init__.py +19 -0
  101. notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
  102. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  103. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  104. notionary/blocks/todo/__init__.py +9 -2
  105. notionary/blocks/todo/todo_element.py +52 -92
  106. notionary/blocks/todo/todo_markdown_node.py +2 -1
  107. notionary/blocks/todo/todo_models.py +19 -0
  108. notionary/blocks/toggle/__init__.py +13 -3
  109. notionary/blocks/toggle/toggle_element.py +69 -260
  110. notionary/blocks/toggle/toggle_markdown_node.py +25 -15
  111. notionary/blocks/toggle/toggle_models.py +17 -0
  112. notionary/blocks/toggleable_heading/__init__.py +6 -2
  113. notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
  114. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  115. notionary/blocks/types.py +130 -0
  116. notionary/blocks/video/__init__.py +8 -2
  117. notionary/blocks/video/video_element.py +70 -141
  118. notionary/blocks/video/video_element_models.py +10 -0
  119. notionary/blocks/video/video_markdown_node.py +13 -6
  120. notionary/database/client.py +26 -8
  121. notionary/database/database.py +13 -14
  122. notionary/database/database_filter_builder.py +2 -2
  123. notionary/database/database_provider.py +5 -4
  124. notionary/database/models.py +337 -0
  125. notionary/database/notion_database.py +6 -7
  126. notionary/file_upload/client.py +5 -7
  127. notionary/file_upload/models.py +3 -2
  128. notionary/file_upload/notion_file_upload.py +2 -3
  129. notionary/markdown/markdown_builder.py +729 -0
  130. notionary/markdown/markdown_document_model.py +228 -0
  131. notionary/{blocks → markdown}/markdown_node.py +1 -0
  132. notionary/models/notion_database_response.py +0 -338
  133. notionary/page/client.py +34 -15
  134. notionary/page/models.py +327 -0
  135. notionary/page/notion_page.py +136 -58
  136. notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
  137. notionary/page/page_content_writer.py +177 -0
  138. notionary/page/page_context.py +65 -0
  139. notionary/page/reader/handler/__init__.py +19 -0
  140. notionary/page/reader/handler/base_block_renderer.py +44 -0
  141. notionary/page/reader/handler/block_processing_context.py +35 -0
  142. notionary/page/reader/handler/block_rendering_context.py +48 -0
  143. notionary/page/reader/handler/column_list_renderer.py +51 -0
  144. notionary/page/reader/handler/column_renderer.py +60 -0
  145. notionary/page/reader/handler/line_renderer.py +73 -0
  146. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  147. notionary/page/reader/handler/toggle_renderer.py +69 -0
  148. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  149. notionary/page/reader/page_content_retriever.py +81 -0
  150. notionary/page/search_filter_builder.py +2 -1
  151. notionary/page/writer/handler/__init__.py +24 -0
  152. notionary/page/writer/handler/code_handler.py +72 -0
  153. notionary/page/writer/handler/column_handler.py +141 -0
  154. notionary/page/writer/handler/column_list_handler.py +139 -0
  155. notionary/page/writer/handler/equation_handler.py +74 -0
  156. notionary/page/writer/handler/line_handler.py +35 -0
  157. notionary/page/writer/handler/line_processing_context.py +54 -0
  158. notionary/page/writer/handler/regular_line_handler.py +86 -0
  159. notionary/page/writer/handler/table_handler.py +66 -0
  160. notionary/page/writer/handler/toggle_handler.py +155 -0
  161. notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
  162. notionary/page/writer/markdown_to_notion_converter.py +95 -0
  163. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  164. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  165. notionary/page/writer/notion_text_length_processor.py +150 -0
  166. notionary/telemetry/__init__.py +2 -2
  167. notionary/telemetry/service.py +3 -3
  168. notionary/user/__init__.py +2 -2
  169. notionary/user/base_notion_user.py +2 -1
  170. notionary/user/client.py +2 -3
  171. notionary/user/models.py +1 -0
  172. notionary/user/notion_bot_user.py +4 -5
  173. notionary/user/notion_user.py +3 -4
  174. notionary/user/notion_user_manager.py +23 -95
  175. notionary/util/__init__.py +3 -2
  176. notionary/util/fuzzy.py +2 -1
  177. notionary/util/logging_mixin.py +2 -2
  178. notionary/util/singleton_metaclass.py +1 -1
  179. notionary/workspace.py +6 -5
  180. notionary-0.2.22.dist-info/METADATA +237 -0
  181. notionary-0.2.22.dist-info/RECORD +200 -0
  182. notionary/blocks/document/__init__.py +0 -7
  183. notionary/blocks/document/document_element.py +0 -102
  184. notionary/blocks/document/document_markdown_node.py +0 -31
  185. notionary/blocks/image/__init__.py +0 -7
  186. notionary/blocks/image/image_element.py +0 -151
  187. notionary/blocks/markdown_builder.py +0 -356
  188. notionary/blocks/mention/__init__.py +0 -7
  189. notionary/blocks/mention/mention_element.py +0 -229
  190. notionary/blocks/mention/mention_markdown_node.py +0 -38
  191. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  192. notionary/blocks/prompts/element_prompt_content.py +0 -41
  193. notionary/blocks/shared/models.py +0 -713
  194. notionary/blocks/shared/notion_block_element.py +0 -37
  195. notionary/blocks/shared/text_inline_formatter.py +0 -262
  196. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  197. notionary/database/models/page_result.py +0 -10
  198. notionary/models/notion_block_response.py +0 -264
  199. notionary/models/notion_page_response.py +0 -78
  200. notionary/models/search_response.py +0 -0
  201. notionary/page/__init__.py +0 -0
  202. notionary/page/content/markdown_whitespace_processor.py +0 -80
  203. notionary/page/content/notion_text_length_utils.py +0 -87
  204. notionary/page/content/page_content_retriever.py +0 -60
  205. notionary/page/formatting/line_processor.py +0 -153
  206. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  207. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  208. notionary/page/notion_to_markdown_converter.py +0 -179
  209. notionary/page/properites/property_value_extractor.py +0 -0
  210. notionary/user/notion_user_provider.py +0 -1
  211. notionary-0.2.19.dist-info/METADATA +0 -225
  212. notionary-0.2.19.dist-info/RECORD +0 -150
  213. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  214. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  215. /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
  216. /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
  217. /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
  218. /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
  219. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  220. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -1,59 +1,10 @@
1
- from typing import Optional
2
- from pydantic import BaseModel
3
-
4
- from notionary.blocks.shared.models import RichTextObject
5
-
6
-
7
- # TODO: Diesen Kram hier auch verwenden
8
- class ExternalAudioSource(BaseModel):
9
- """External audio source."""
10
-
11
- url: str
12
-
13
-
14
- class NotionAudioData(BaseModel):
15
- """Audio block data."""
16
-
17
- type: str = "external"
18
- external: ExternalAudioSource
19
- caption: list[dict] = []
1
+ from typing import Literal
20
2
 
3
+ from pydantic import BaseModel
21
4
 
22
- class NotionAudioBlock(BaseModel):
23
- """Audio block result."""
24
-
25
- type: str = "audio"
26
- audio: NotionAudioData
27
-
28
-
29
- # Updated method with typed return
30
- @classmethod
31
- def markdown_to_notion(cls, text: str) -> Optional[NotionAudioBlock]:
32
- """Convert markdown audio embed to Notion audio block."""
33
- audio_match = cls.PATTERN.match(text.strip())
34
- if not audio_match:
35
- return None
36
-
37
- url = audio_match.group(1)
38
- caption_text = audio_match.group(2)
39
-
40
- if not url:
41
- return None
42
-
43
- # Validate URL if possible
44
- if not cls._is_likely_audio_url(url):
45
- # Still proceed - user might know better
46
- pass
5
+ from notionary.blocks.file.file_element_models import FileBlock
47
6
 
48
- # Build caption list
49
- caption_list = []
50
- if caption_text:
51
- caption_rich_text = RichTextObject.from_plain_text(caption_text)
52
- caption_list = [caption_rich_text.model_dump()]
53
7
 
54
- # Create typed result
55
- return NotionAudioBlock(
56
- audio=NotionAudioData(
57
- external=ExternalAudioSource(url=url), caption=caption_list
58
- )
59
- )
8
+ class CreateAudioBlock(BaseModel):
9
+ type: Literal["audio"] = "audio"
10
+ audio: FileBlock
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
+ from notionary.blocks.models import Block, BlockCreateResult
8
+
9
+
10
+ class BaseBlockElement(ABC):
11
+ """Base class for elements that can be converted between Markdown and Notion."""
12
+
13
+ @classmethod
14
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
15
+ """
16
+ Convert markdown to Notion block content.
17
+
18
+ Returns:
19
+ - BlockContent: Single block content (e.g., ToDoBlock, ParagraphBlock)
20
+ - list[BlockContent]: Multiple block contents
21
+ - None: Cannot convert this markdown
22
+ """
23
+
24
+ @classmethod
25
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
26
+ """Convert Notion block to markdown."""
27
+
28
+ @classmethod
29
+ def match_notion(cls, block: Block) -> bool:
30
+ """Check if this element can handle the given Notion block."""
31
+ # Default implementation - subclasses should override this method
32
+ # Cannot call async notion_to_markdown here
33
+ return False
34
+
35
+ @classmethod
36
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
37
+ """Get system prompt information for this block element.
38
+
39
+ Subclasses should override this method to provide their specific information.
40
+ Return None if the element should not be included in documentation.
41
+ """
42
+ return None
@@ -1,7 +1,14 @@
1
- from .bookmark_element import BookmarkElement
2
- from .bookmark_markdown_node import BookmarkMarkdownNode
1
+ from notionary.blocks.bookmark.bookmark_element import BookmarkElement
2
+ from notionary.blocks.bookmark.bookmark_markdown_node import (
3
+ BookmarkMarkdownBlockParams,
4
+ BookmarkMarkdownNode,
5
+ )
6
+ from notionary.blocks.bookmark.bookmark_models import BookmarkBlock, CreateBookmarkBlock
3
7
 
4
8
  __all__ = [
5
9
  "BookmarkElement",
10
+ "BookmarkBlock",
11
+ "CreateBookmarkBlock",
6
12
  "BookmarkMarkdownNode",
13
+ "BookmarkMarkdownBlockParams",
7
14
  ]
@@ -1,173 +1,83 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional, List, Tuple
4
+ from typing import Optional
3
5
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import (
6
- ElementPromptContent,
7
- ElementPromptBuilder,
8
- NotionBlockResult,
9
- )
10
- from notionary.blocks.shared.models import RichTextObject
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.bookmark.bookmark_models import BookmarkBlock, CreateBookmarkBlock
8
+ from notionary.blocks.mixins.captions import CaptionMixin
9
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
10
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
11
11
 
12
12
 
13
- class BookmarkElement(NotionBlockElement):
13
+ class BookmarkElement(BaseBlockElement, CaptionMixin):
14
14
  """
15
15
  Handles conversion between Markdown bookmarks and Notion bookmark blocks.
16
16
 
17
17
  Markdown bookmark syntax:
18
- - [bookmark](https://example.com) - Simple bookmark with URL only
19
- - [bookmark](https://example.com "Title") - Bookmark with URL and title
20
- - [bookmark](https://example.com "Title" "Description") - Bookmark with URL, title, and description
21
-
22
- Where:
23
- - URL is the required bookmark URL
24
- - Title is an optional title (enclosed in quotes)
25
- - Description is an optional description (enclosed in quotes)
18
+ - [bookmark](https://example.com) - URL only
19
+ - [bookmark](https://example.com)(caption:This is a caption) - URL with caption
20
+ - (caption:This is a caption)[bookmark](https://example.com) - caption before URL
26
21
  """
27
22
 
28
- # Regex pattern for bookmark syntax with optional title and description
29
- PATTERN = re.compile(
30
- r"^\[bookmark\]\(" # [bookmark]( prefix
31
- + r'(https?://[^\s"]+)' # URL (required)
32
- + r'(?:\s+"([^"]+)")?' # Optional title in quotes
33
- + r'(?:\s+"([^"]+)")?' # Optional description in quotes
34
- + r"\)$" # closing parenthesis
35
- )
23
+ # Flexible pattern that can handle caption in any position
24
+ BOOKMARK_PATTERN = re.compile(r"\[bookmark\]\((https?://[^\s\"]+)\)")
36
25
 
37
26
  @classmethod
38
- def match_markdown(cls, text: str) -> bool:
39
- """Check if text is a markdown bookmark."""
40
- return text.strip().startswith("[bookmark]") and bool(
41
- cls.PATTERN.match(text.strip())
42
- )
27
+ def match_notion(cls, block: Block) -> bool:
28
+ return block.type == BlockType.BOOKMARK and block.bookmark
43
29
 
44
30
  @classmethod
45
- def match_notion(cls, block: Dict[str, Any]) -> bool:
46
- """Check if block is a Notion bookmark."""
47
- return block.get("type") in ["bookmark", "external-bookmark"]
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
+ """
33
+ Convert a markdown bookmark into a Notion BookmarkBlock.
34
+ """
35
+ # First remove captions to get clean text for URL extraction
36
+ clean_text = cls.remove_caption(text.strip())
48
37
 
49
- @classmethod
50
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
51
- """Convert markdown bookmark to Notion bookmark block."""
52
- bookmark_match = BookmarkElement.PATTERN.match(text.strip())
38
+ # Use our own regex to find the bookmark URL
39
+ bookmark_match = cls.BOOKMARK_PATTERN.search(clean_text)
53
40
  if not bookmark_match:
54
41
  return None
55
42
 
56
43
  url = bookmark_match.group(1)
57
- title = bookmark_match.group(2)
58
- description = bookmark_match.group(3)
59
-
60
- bookmark_data = {"url": url}
61
44
 
62
- # Build caption string
63
- caption_parts = []
64
- if title:
65
- caption_parts.append(title)
66
- if description:
67
- caption_parts.append(description)
45
+ caption_text = cls.extract_caption(text.strip())
46
+ caption_rich_text = cls.build_caption_rich_text(caption_text or "")
68
47
 
69
- if caption_parts:
70
- caption_text = " - ".join(caption_parts)
71
- caption_rich_text = RichTextObject.from_plain_text(caption_text)
72
- bookmark_data["caption"] = [caption_rich_text.model_dump()]
73
- else:
74
- bookmark_data["caption"] = []
75
-
76
- return [{"type": "bookmark", "bookmark": bookmark_data}]
48
+ bookmark_data = BookmarkBlock(url=url, caption=caption_rich_text)
49
+ return CreateBookmarkBlock(bookmark=bookmark_data)
77
50
 
78
51
  @classmethod
79
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
80
- """Convert Notion bookmark block to markdown bookmark."""
81
- block_type = block.get("type", "")
82
-
83
- if block_type == "bookmark":
84
- bookmark_data = block.get("bookmark", {})
85
- elif block_type == "external-bookmark":
86
- url = block.get("url", "")
87
- if not url:
88
- return None
89
-
90
- return f"[bookmark]({url})"
91
- else:
52
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
53
+ if block.type != BlockType.BOOKMARK or block.bookmark is None:
92
54
  return None
93
55
 
94
- url = bookmark_data.get("url", "")
95
-
56
+ bm = block.bookmark
57
+ url = bm.url
96
58
  if not url:
97
59
  return None
98
60
 
99
- caption = bookmark_data.get("caption", [])
100
-
101
- if not caption:
102
- # Simple bookmark with URL only
103
- return f"[bookmark]({url})"
104
-
105
- # Extract title and description from caption
106
- title, description = BookmarkElement._parse_caption(caption)
107
-
108
- if title and description:
109
- return f'[bookmark]({url} "{title}" "{description}")'
110
-
111
- if title:
112
- return f'[bookmark]({url} "{title}")'
61
+ result = f"[bookmark]({url})"
113
62
 
114
- return f"[bookmark]({url})"
63
+ # Add caption if present
64
+ caption_markdown = await cls.format_caption_for_markdown(bm.caption or [])
65
+ if caption_markdown:
66
+ result += caption_markdown
115
67
 
116
- @classmethod
117
- def is_multiline(cls) -> bool:
118
- """Bookmarks are single-line elements."""
119
- return False
120
-
121
- @classmethod
122
- def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
123
- """Extract plain text content from Notion rich_text elements."""
124
- result = ""
125
- for text_obj in rich_text:
126
- if text_obj.get("type") == "text":
127
- result += text_obj.get("text", {}).get("content", "")
128
- elif "plain_text" in text_obj:
129
- result += text_obj.get("plain_text", "")
130
68
  return result
131
69
 
132
70
  @classmethod
133
- def _parse_caption(cls, caption: List[Dict[str, Any]]) -> Tuple[str, str]:
134
- """
135
- Parse Notion caption into title and description components.
136
- Returns a tuple of (title, description).
137
- """
138
- if not caption:
139
- return "", ""
140
-
141
- full_text = BookmarkElement._extract_text_content(caption)
142
-
143
- if " - " in full_text:
144
- parts = full_text.split(" - ", 1)
145
- return parts[0].strip(), parts[1].strip()
146
-
147
- return full_text.strip(), ""
148
-
149
- @classmethod
150
- def get_llm_prompt_content(cls) -> ElementPromptContent:
151
- """
152
- Returns structured LLM prompt metadata for the bookmark element.
153
- """
154
- return (
155
- ElementPromptBuilder()
156
- .with_description("Creates a bookmark that links to an external website.")
157
- .with_usage_guidelines(
158
- "Use bookmarks when you want to reference external content while keeping the page clean and organized. "
159
- "Bookmarks display a preview card for the linked content."
160
- )
161
- .with_syntax(
162
- '[bookmark](https://example.com "Optional Title" "Optional Description")'
163
- )
164
- .with_examples(
165
- [
166
- "[bookmark](https://example.com)",
167
- '[bookmark](https://example.com "Example Title")',
168
- '[bookmark](https://example.com "Example Title" "Example description of the site")',
169
- '[bookmark](https://github.com "GitHub" "Where the world builds software")',
170
- ]
171
- )
172
- .build()
71
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
72
+ """Get system prompt information for bookmark blocks."""
73
+ return BlockElementMarkdownInformation(
74
+ block_type=cls.__name__,
75
+ description="Bookmark blocks create previews of web pages with optional captions",
76
+ syntax_examples=[
77
+ "[bookmark](https://example.com)",
78
+ "[bookmark](https://example.com)(caption:This is a caption)",
79
+ "(caption:Check out this repository)[bookmark](https://github.com/user/repo)",
80
+ "[bookmark](https://github.com/user/repo)(caption:Check out this awesome repository)",
81
+ ],
82
+ usage_guidelines="Use for linking to external websites with rich previews. URL is required. Caption supports rich text formatting and is optional.",
173
83
  )
@@ -1,43 +1,44 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Optional
4
+
3
5
  from pydantic import BaseModel
4
6
 
5
- from notionary.blocks.markdown_node import MarkdownNode
7
+ from notionary.markdown.markdown_node import MarkdownNode
8
+ from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
6
9
 
7
10
 
8
11
  class BookmarkMarkdownBlockParams(BaseModel):
9
12
  url: str
10
13
  title: Optional[str] = None
11
- description: Optional[str] = None
14
+ caption: Optional[str] = None
12
15
 
13
16
 
14
- class BookmarkMarkdownNode(MarkdownNode):
17
+ class BookmarkMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
15
18
  """
16
19
  Programmatic interface for creating Notion-style bookmark Markdown blocks.
17
20
  """
18
21
 
19
22
  def __init__(
20
- self, url: str, title: Optional[str] = None, description: Optional[str] = None
23
+ self, url: str, title: Optional[str] = None, caption: Optional[str] = None
21
24
  ):
22
25
  self.url = url
23
26
  self.title = title
24
- self.description = description
27
+ self.caption = caption
25
28
 
26
29
  @classmethod
27
30
  def from_params(cls, params: BookmarkMarkdownBlockParams) -> BookmarkMarkdownNode:
28
- return cls(url=params.url, title=params.title, description=params.description)
31
+ return cls(url=params.url, title=params.title, caption=params.caption)
29
32
 
30
33
  def to_markdown(self) -> str:
34
+ """Return the Markdown representation.
35
+
36
+ Examples:
37
+ - [bookmark](https://example.com)
38
+ - [bookmark](https://example.com)(caption:Some caption)
31
39
  """
32
- Returns the Markdown representation, e.g.:
33
- [bookmark](https://example.com "Title" "Description")
34
- """
35
- parts = [f"[bookmark]({self.url}"]
36
- if self.title is not None:
37
- parts.append(f'"{self.title}"')
38
- if self.description is not None:
39
- # Wenn title fehlt, aber description da ist, trotzdem Platzhalter für title:
40
- if self.title is None:
41
- parts.append('""')
42
- parts.append(f'"{self.description}"')
43
- return " ".join(parts) + ")"
40
+ # Use simple bookmark syntax like BookmarkElement
41
+ base_markdown = f"[bookmark]({self.url})"
42
+
43
+ # Append caption using mixin helper
44
+ return self.append_caption_to_markdown(base_markdown, self.caption)
@@ -0,0 +1,15 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
6
+
7
+
8
+ class BookmarkBlock(BaseModel):
9
+ caption: list[RichTextObject] = Field(default_factory=list)
10
+ url: str
11
+
12
+
13
+ class CreateBookmarkBlock(BaseModel):
14
+ type: Literal["bookmark"] = "bookmark"
15
+ bookmark: BookmarkBlock
@@ -0,0 +1,17 @@
1
+ from notionary.blocks.breadcrumbs.breadcrumb_element import BreadcrumbElement
2
+ from notionary.blocks.breadcrumbs.breadcrumb_markdown_node import (
3
+ BreadcrumbMarkdownBlockParams,
4
+ BreadcrumbMarkdownNode,
5
+ )
6
+ from notionary.blocks.breadcrumbs.breadcrumb_models import (
7
+ BreadcrumbBlock,
8
+ CreateBreadcrumbBlock,
9
+ )
10
+
11
+ __all__ = [
12
+ "BreadcrumbElement",
13
+ "BreadcrumbBlock",
14
+ "CreateBreadcrumbBlock",
15
+ "BreadcrumbMarkdownNode",
16
+ "BreadcrumbMarkdownBlockParams",
17
+ ]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.breadcrumbs.breadcrumb_models import (
8
+ BreadcrumbBlock,
9
+ CreateBreadcrumbBlock,
10
+ )
11
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
12
+
13
+
14
+ class BreadcrumbElement(BaseBlockElement):
15
+ """
16
+ Handles conversion between Markdown breadcrumb marker and Notion breadcrumb blocks.
17
+
18
+ Markdown syntax:
19
+ [breadcrumb]
20
+ """
21
+
22
+ BREADCRUMB_MARKER = "[breadcrumb]"
23
+ PATTERN = re.compile(r"^\[breadcrumb\]\s*$", re.IGNORECASE)
24
+
25
+ @classmethod
26
+ def match_notion(cls, block: Block) -> bool:
27
+ # Kein extra Payload – nur Typ prüfen
28
+ return block.type == BlockType.BREADCRUMB and block.breadcrumb
29
+
30
+ @classmethod
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
+ if not cls.PATTERN.match(text.strip()):
33
+ return None
34
+ return CreateBreadcrumbBlock(breadcrumb=BreadcrumbBlock())
35
+
36
+ @classmethod
37
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
38
+ if block.type == BlockType.BREADCRUMB and block.breadcrumb:
39
+ return cls.BREADCRUMB_MARKER
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
6
+
7
+
8
+ class BreadcrumbMarkdownBlockParams(BaseModel):
9
+ """Parameters for breadcrumb markdown block. No parameters needed."""
10
+
11
+ pass
12
+
13
+
14
+ class BreadcrumbMarkdownNode(MarkdownNode):
15
+ """
16
+ Programmatic interface for creating Markdown breadcrumb blocks.
17
+ Example:
18
+ [breadcrumb]
19
+ """
20
+
21
+ def __init__(self):
22
+ # No parameters needed for breadcrumb
23
+ pass
24
+
25
+ @classmethod
26
+ def from_params(
27
+ cls, params: BreadcrumbMarkdownBlockParams
28
+ ) -> BreadcrumbMarkdownNode:
29
+ return cls()
30
+
31
+ def to_markdown(self) -> str:
32
+ return "[breadcrumb]"
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class BreadcrumbBlock(BaseModel):
7
+ pass
8
+
9
+
10
+ class CreateBreadcrumbBlock(BaseModel):
11
+ type: Literal["breadcrumb"] = "breadcrumb"
12
+ breadcrumb: BreadcrumbBlock
@@ -1,7 +1,17 @@
1
- from .bulleted_list_element import BulletedListElement
2
- from .bulleted_list_markdown_node import BulletedListMarkdownNode
1
+ from notionary.blocks.bulleted_list.bulleted_list_element import BulletedListElement
2
+ from notionary.blocks.bulleted_list.bulleted_list_markdown_node import (
3
+ BulletedListMarkdownBlockParams,
4
+ BulletedListMarkdownNode,
5
+ )
6
+ from notionary.blocks.bulleted_list.bulleted_list_models import (
7
+ BulletedListItemBlock,
8
+ CreateBulletedListItemBlock,
9
+ )
3
10
 
4
11
  __all__ = [
5
12
  "BulletedListElement",
13
+ "BulletedListItemBlock",
14
+ "CreateBulletedListItemBlock",
6
15
  "BulletedListMarkdownNode",
16
+ "BulletedListMarkdownBlockParams",
7
17
  ]