notionary 0.2.17__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 (113) hide show
  1. notionary/__init__.py +3 -2
  2. notionary/blocks/__init__.py +54 -25
  3. notionary/blocks/audio/__init__.py +7 -0
  4. notionary/blocks/audio/audio_element.py +152 -0
  5. notionary/blocks/audio/audio_markdown_node.py +29 -0
  6. notionary/blocks/audio/audio_models.py +59 -0
  7. notionary/blocks/bookmark/__init__.py +7 -0
  8. notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
  9. notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  10. notionary/blocks/bulleted_list/__init__.py +7 -0
  11. notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
  12. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  13. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  14. notionary/blocks/callout/__init__.py +7 -0
  15. notionary/blocks/callout/callout_element.py +132 -0
  16. notionary/blocks/callout/callout_markdown_node.py +31 -0
  17. notionary/blocks/callout/callout_models.py +0 -0
  18. notionary/blocks/code/__init__.py +7 -0
  19. notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
  20. notionary/blocks/code/code_markdown_node.py +43 -0
  21. notionary/blocks/code/code_models.py +0 -0
  22. notionary/blocks/column/__init__.py +5 -0
  23. notionary/blocks/{column_element.py → column/column_element.py} +24 -55
  24. notionary/blocks/column/column_models.py +0 -0
  25. notionary/blocks/divider/__init__.py +7 -0
  26. notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
  27. notionary/blocks/divider/divider_markdown_node.py +24 -0
  28. notionary/blocks/divider/divider_models.py +0 -0
  29. notionary/blocks/document/__init__.py +7 -0
  30. notionary/blocks/document/document_element.py +102 -0
  31. notionary/blocks/document/document_markdown_node.py +31 -0
  32. notionary/blocks/document/document_models.py +0 -0
  33. notionary/blocks/embed/__init__.py +7 -0
  34. notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
  35. notionary/blocks/embed/embed_markdown_node.py +30 -0
  36. notionary/blocks/embed/embed_models.py +0 -0
  37. notionary/blocks/heading/__init__.py +7 -0
  38. notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
  39. notionary/blocks/heading/heading_markdown_node.py +29 -0
  40. notionary/blocks/heading/heading_models.py +0 -0
  41. notionary/blocks/image/__init__.py +7 -0
  42. notionary/blocks/{image_element.py → image/image_element.py} +62 -42
  43. notionary/blocks/image/image_markdown_node.py +33 -0
  44. notionary/blocks/image/image_models.py +0 -0
  45. notionary/blocks/markdown_builder.py +356 -0
  46. notionary/blocks/markdown_node.py +29 -0
  47. notionary/blocks/mention/__init__.py +7 -0
  48. notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
  49. notionary/blocks/mention/mention_markdown_node.py +38 -0
  50. notionary/blocks/mention/mention_models.py +0 -0
  51. notionary/blocks/numbered_list/__init__.py +7 -0
  52. notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
  53. notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  54. notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  55. notionary/blocks/paragraph/__init__.py +7 -0
  56. notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
  57. notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  58. notionary/blocks/paragraph/paragraph_models.py +0 -0
  59. notionary/blocks/quote/__init__.py +7 -0
  60. notionary/blocks/quote/quote_element.py +92 -0
  61. notionary/blocks/quote/quote_markdown_node.py +23 -0
  62. notionary/blocks/quote/quote_models.py +0 -0
  63. notionary/blocks/registry/block_registry.py +17 -3
  64. notionary/blocks/registry/block_registry_builder.py +90 -178
  65. notionary/blocks/shared/__init__.py +0 -0
  66. notionary/blocks/shared/block_client.py +256 -0
  67. notionary/blocks/shared/models.py +710 -0
  68. notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
  69. notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
  70. notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  71. notionary/blocks/table/__init__.py +7 -0
  72. notionary/blocks/{table_element.py → table/table_element.py} +23 -11
  73. notionary/blocks/table/table_markdown_node.py +40 -0
  74. notionary/blocks/table/table_models.py +0 -0
  75. notionary/blocks/todo/__init__.py +7 -0
  76. notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
  77. notionary/blocks/todo/todo_markdown_node.py +31 -0
  78. notionary/blocks/todo/todo_models.py +0 -0
  79. notionary/blocks/toggle/__init__.py +4 -0
  80. notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
  81. notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  82. notionary/blocks/toggle/toggle_models.py +0 -0
  83. notionary/blocks/toggleable_heading/__init__.py +9 -0
  84. notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
  85. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  86. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  87. notionary/blocks/video/__init__.py +7 -0
  88. notionary/blocks/{video_element.py → video/video_element.py} +82 -57
  89. notionary/blocks/video/video_markdown_node.py +30 -0
  90. notionary/file_upload/notion_file_upload.py +1 -1
  91. notionary/page/content/markdown_whitespace_processor.py +80 -0
  92. notionary/page/content/notion_text_length_utils.py +87 -0
  93. notionary/page/content/page_content_retriever.py +2 -2
  94. notionary/page/content/page_content_writer.py +97 -148
  95. notionary/page/formatting/line_processor.py +153 -0
  96. notionary/page/formatting/markdown_to_notion_converter.py +103 -424
  97. notionary/page/notion_page.py +9 -11
  98. notionary/page/notion_to_markdown_converter.py +9 -13
  99. notionary/util/factory_decorator.py +0 -0
  100. notionary/workspace.py +0 -1
  101. {notionary-0.2.17.dist-info → notionary-0.2.18.dist-info}/METADATA +1 -1
  102. notionary-0.2.18.dist-info/RECORD +149 -0
  103. notionary/blocks/audio_element.py +0 -144
  104. notionary/blocks/callout_element.py +0 -122
  105. notionary/blocks/document_element.py +0 -194
  106. notionary/blocks/notion_block_client.py +0 -26
  107. notionary/blocks/qoute_element.py +0 -169
  108. notionary/page/content/notion_page_content_chunker.py +0 -84
  109. notionary/page/formatting/spacer_rules.py +0 -483
  110. notionary-0.2.17.dist-info/RECORD +0 -85
  111. /notionary/{elements/__init__.py → blocks/bookmark/bookmark_models.py} +0 -0
  112. {notionary-0.2.17.dist-info → notionary-0.2.18.dist-info}/LICENSE +0 -0
  113. {notionary-0.2.17.dist-info → notionary-0.2.18.dist-info}/WHEEL +0 -0
notionary/__init__.py CHANGED
@@ -2,7 +2,8 @@ from .database import NotionDatabase, DatabaseFilterBuilder
2
2
  from .page.notion_page import NotionPage
3
3
  from .workspace import NotionWorkspace
4
4
  from .user import NotionUser, NotionUserManager, NotionBotUser
5
- from .file_upload import NotionFileUpload, NotionFileUploadClient
5
+ from .file_upload import NotionFileUpload
6
+ from .blocks.markdown_builder import MarkdownBuilder
6
7
 
7
8
  __all__ = [
8
9
  "NotionDatabase",
@@ -13,5 +14,5 @@ __all__ = [
13
14
  "NotionUserManager",
14
15
  "NotionBotUser",
15
16
  "NotionFileUpload",
16
- "NotionFileUploadClient",
17
+ "MarkdownBuilder",
17
18
  ]
@@ -2,62 +2,91 @@
2
2
  from .prompts.element_prompt_content import ElementPromptContent
3
3
  from .prompts.element_prompt_builder import ElementPromptBuilder
4
4
 
5
- from .notion_block_element import NotionBlockElement
5
+ from .shared.notion_block_element import (
6
+ NotionBlockElement,
7
+ NotionBlockResult,
8
+ NotionBlock,
9
+ )
6
10
 
11
+ from .audio import AudioElement, AudioMarkdownNode
12
+ from .bulleted_list import BulletedListElement, BulletedListMarkdownNode
13
+ from .callout import CalloutElement, CalloutMarkdownNode
14
+ from .code import CodeElement, CodeMarkdownNode
15
+ from .column.column_element import ColumnElement
16
+ from .divider import DividerElement, DividerMarkdownNode
17
+ from .embed import EmbedElement, EmbedMarkdownNode
18
+ from .heading import HeadingElement, HeadingMarkdownNode
19
+ from .image import ImageElement, ImageMarkdownNode
20
+ from .numbered_list import NumberedListElement, NumberedListMarkdownNode
21
+ from .paragraph import ParagraphElement, ParagraphMarkdownNode
22
+ from .table import TableElement, TableMarkdownNode
23
+ from .toggle import ToggleElement, ToggleMarkdownNode
24
+ from .todo import TodoElement, TodoMarkdownNode
25
+ from .video import VideoElement, VideoMarkdownNode
26
+ from .toggleable_heading import ToggleableHeadingElement, ToggleableHeadingMarkdownNode
27
+ from .bookmark import BookmarkElement, BookmarkMarkdownNode
28
+ from .divider import DividerElement, DividerMarkdownNode
29
+ from .heading import HeadingElement, HeadingMarkdownNode
30
+ from .mention import MentionElement, MentionMarkdownNode
31
+ from .quote import QuoteElement, QuoteMarkdownNode
32
+ from .document import DocumentElement, DocumentMarkdownNode
33
+ from .shared.text_inline_formatter import TextInlineFormatter
7
34
 
8
- from .audio_element import AudioElement
9
- from .bulleted_list_element import BulletedListElement
10
- from .callout_element import CalloutElement
11
- from .code_block_element import CodeBlockElement
12
- from .column_element import ColumnElement
13
- from .divider_element import DividerElement
14
- from .embed_element import EmbedElement
15
- from .heading_element import HeadingElement
16
- from .image_element import ImageElement
17
- from .numbered_list_element import NumberedListElement
18
- from .paragraph_element import ParagraphElement
19
- from .table_element import TableElement
20
- from .toggle_element import ToggleElement
21
- from .todo_element import TodoElement
22
- from .video_element import VideoElement
23
- from .toggleable_heading_element import ToggleableHeadingElement
24
- from .bookmark_element import BookmarkElement
25
- from .divider_element import DividerElement
26
- from .heading_element import HeadingElement
27
- from .mention_element import MentionElement
28
- from .qoute_element import QuoteElement
29
- from .document_element import DocumentElement
35
+ from .markdown_node import MarkdownNode
30
36
 
31
37
  from .registry.block_registry import BlockRegistry
32
38
  from .registry.block_registry_builder import BlockRegistryBuilder
33
- from .notion_block_client import NotionBlockClient
34
39
 
40
+ from .shared.block_client import NotionBlockClient
35
41
 
36
42
  __all__ = [
43
+ "MarkdownNode",
37
44
  "ElementPromptContent",
38
45
  "ElementPromptBuilder",
39
46
  "NotionBlockElement",
40
47
  "AudioElement",
48
+ "AudioMarkdownNode",
41
49
  "BulletedListElement",
50
+ "BulletedListMarkdownNode",
42
51
  "CalloutElement",
43
- "CodeBlockElement",
52
+ "CalloutMarkdownNode",
53
+ "CodeElement",
54
+ "CodeMarkdownNode",
44
55
  "ColumnElement",
45
56
  "DividerElement",
57
+ "DividerMarkdownNode",
46
58
  "EmbedElement",
59
+ "EmbedMarkdownNode",
47
60
  "HeadingElement",
61
+ "HeadingMarkdownNode",
48
62
  "ImageElement",
63
+ "ImageMarkdownNode",
49
64
  "NumberedListElement",
65
+ "NumberedListMarkdownNode",
50
66
  "ParagraphElement",
67
+ "ParagraphMarkdownNode",
51
68
  "TableElement",
69
+ "TableMarkdownNode",
52
70
  "ToggleElement",
71
+ "ToggleMarkdownNode",
53
72
  "TodoElement",
73
+ "TodoMarkdownNode",
54
74
  "VideoElement",
75
+ "VideoMarkdownNode",
55
76
  "ToggleableHeadingElement",
77
+ "ToggleableHeadingMarkdownNode",
56
78
  "BookmarkElement",
79
+ "BookmarkMarkdownNode",
57
80
  "MentionElement",
81
+ "MentionMarkdownNode",
58
82
  "QuoteElement",
83
+ "QuoteMarkdownNode",
59
84
  "DocumentElement",
85
+ "DocumentMarkdownNode",
60
86
  "BlockRegistry",
61
87
  "BlockRegistryBuilder",
88
+ "TextInlineFormatter",
89
+ "NotionBlockResult",
90
+ "NotionBlock",
62
91
  "NotionBlockClient",
63
92
  ]
@@ -0,0 +1,7 @@
1
+ from .audio_element import AudioElement
2
+ from .audio_markdown_node import AudioMarkdownNode
3
+
4
+ __all__ = [
5
+ "AudioElement",
6
+ "AudioMarkdownNode",
7
+ ]
@@ -0,0 +1,152 @@
1
+ import re
2
+ from typing import Any, Optional, List
3
+
4
+ from notionary.blocks import (
5
+ NotionBlockElement,
6
+ ElementPromptContent,
7
+ ElementPromptBuilder,
8
+ NotionBlockResult,
9
+ )
10
+ from notionary.blocks.shared.models import RichTextObject
11
+
12
+
13
+ class AudioElement(NotionBlockElement):
14
+ """
15
+ Handles conversion between Markdown audio embeds and Notion audio blocks.
16
+
17
+ Markdown audio syntax:
18
+ - [audio](https://example.com/audio.mp3) - Simple audio embed
19
+ - [audio](https://example.com/audio.mp3 "Caption text") - Audio with caption
20
+
21
+ Where:
22
+ - URL is the required audio file URL
23
+ - Caption is optional descriptive text (enclosed in quotes)
24
+ """
25
+
26
+ # Regex patterns
27
+ URL_PATTERN = r"(https?://[^\s\"]+)"
28
+ CAPTION_PATTERN = r'(?:\s+"([^"]+)")?'
29
+
30
+ PATTERN = re.compile(r"^\[audio\]\(" + URL_PATTERN + CAPTION_PATTERN + r"\)$")
31
+
32
+ # Supported audio extensions
33
+ SUPPORTED_EXTENSIONS = {".mp3", ".wav", ".ogg", ".oga", ".m4a"}
34
+
35
+ @classmethod
36
+ def match_markdown(cls, text: str) -> bool:
37
+ m = cls.PATTERN.match(text.strip())
38
+ if not m:
39
+ return False
40
+ url = m.group(1)
41
+ return cls._is_likely_audio_url(url)
42
+
43
+ @classmethod
44
+ def match_notion(cls, block: dict[str, Any]) -> bool:
45
+ """Check if block is a Notion audio block."""
46
+ return block.get("type") == "audio"
47
+
48
+ @classmethod
49
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
50
+ """Convert markdown audio embed to Notion audio block."""
51
+ audio_match = cls.PATTERN.match(text.strip())
52
+ if not audio_match:
53
+ return None
54
+
55
+ url = audio_match.group(1)
56
+ caption_text = audio_match.group(2)
57
+
58
+ if not url:
59
+ return None
60
+
61
+ # Validate URL if possible
62
+ if not cls._is_likely_audio_url(url):
63
+ # Still proceed - user might know better
64
+ pass
65
+
66
+ audio_data = {"type": "external", "external": {"url": url}}
67
+
68
+ # Add caption if provided
69
+ if caption_text:
70
+ caption_rich_text = RichTextObject.from_plain_text(caption_text)
71
+ audio_data["caption"] = [caption_rich_text.model_dump()]
72
+ else:
73
+ audio_data["caption"] = []
74
+
75
+ return {"type": "audio", "audio": audio_data}
76
+
77
+ @classmethod
78
+ def notion_to_markdown(cls, block: dict[str, Any]) -> Optional[str]:
79
+ """Convert Notion audio block to markdown audio embed."""
80
+ if block.get("type") != "audio":
81
+ return None
82
+
83
+ audio_data = block.get("audio", {})
84
+
85
+ # Get URL from external source
86
+ if audio_data.get("type") == "external":
87
+ url = audio_data.get("external", {}).get("url", "")
88
+ else:
89
+ # Handle file or file_upload types if needed
90
+ return None
91
+
92
+ if not url:
93
+ return None
94
+
95
+ # Extract caption
96
+ caption = audio_data.get("caption", [])
97
+ if caption:
98
+ caption_text = cls._extract_text_content(caption)
99
+ return f'[audio]({url} "{caption_text}")'
100
+
101
+ return f"[audio]({url})"
102
+
103
+ @classmethod
104
+ def is_multiline(cls) -> bool:
105
+ """Audio embeds are single-line elements."""
106
+ return False
107
+
108
+ @classmethod
109
+ def _is_likely_audio_url(cls, url: str) -> bool:
110
+ """Check if URL likely points to an audio file."""
111
+ return any(url.lower().endswith(ext) for ext in cls.SUPPORTED_EXTENSIONS)
112
+
113
+ @classmethod
114
+ def _extract_text_content(cls, rich_text: List[dict[str, Any]]) -> str:
115
+ """Extract plain text content from Notion rich_text elements."""
116
+ result = ""
117
+ for text_obj in rich_text:
118
+ if text_obj.get("type") == "text":
119
+ result += text_obj.get("text", {}).get("content", "")
120
+ elif "plain_text" in text_obj:
121
+ result += text_obj.get("plain_text", "")
122
+ return result
123
+
124
+ @classmethod
125
+ def get_llm_prompt_content(cls) -> ElementPromptContent:
126
+ """
127
+ Returns structured LLM prompt metadata for the audio element.
128
+ """
129
+ return (
130
+ ElementPromptBuilder()
131
+ .with_description(
132
+ "Embeds an audio file that can be played directly in the page."
133
+ )
134
+ .with_usage_guidelines(
135
+ "Use audio embeds when you want to include sound files, music, podcasts, "
136
+ "or voice recordings. Supports common audio formats like MP3, WAV, OGG, and M4A."
137
+ )
138
+ .with_syntax('![audio](https://example.com/audio.mp3 "Optional caption")')
139
+ .with_examples(
140
+ [
141
+ "[audio](https://example.com/song.mp3)",
142
+ '[audio](https://example.com/podcast.mp3 "Episode 1: Introduction")',
143
+ '[audio](https://example.com/sound.wav "Sound effect for presentation")',
144
+ '[audio](https://example.com/recording.m4a "Voice memo from meeting")',
145
+ ]
146
+ )
147
+ .with_avoidance_guidelines(
148
+ "Ensure the URL points to a valid audio file. "
149
+ "Some audio formats may not be supported by all browsers."
150
+ )
151
+ .build()
152
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class AudioMarkdownBlockParams(BaseModel):
9
+ url: str
10
+ caption: Optional[str] = None
11
+
12
+
13
+ class AudioMarkdownNode(MarkdownNode):
14
+ """
15
+ Programmatic interface for creating Notion-style audio blocks.
16
+ """
17
+
18
+ def __init__(self, url: str, caption: Optional[str] = None):
19
+ self.url = url
20
+ self.caption = caption
21
+
22
+ @classmethod
23
+ def from_params(cls, params: AudioMarkdownBlockParams) -> AudioMarkdownNode:
24
+ return cls(url=params.url, caption=params.caption)
25
+
26
+ def to_markdown(self) -> str:
27
+ if self.caption:
28
+ return f'[audio]({self.url} "{self.caption}")'
29
+ return f"[audio]({self.url})"
@@ -0,0 +1,59 @@
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] = []
20
+
21
+
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
47
+
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
+
54
+ # Create typed result
55
+ return NotionAudioBlock(
56
+ audio=NotionAudioData(
57
+ external=ExternalAudioSource(url=url), caption=caption_list
58
+ )
59
+ )
@@ -0,0 +1,7 @@
1
+ from .bookmark_element import BookmarkElement
2
+ from .bookmark_markdown_node import BookmarkMarkdownNode
3
+
4
+ __all__ = [
5
+ "BookmarkElement",
6
+ "BookmarkMarkdownNode",
7
+ ]
@@ -2,7 +2,12 @@ import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
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
+ )
10
+ from notionary.blocks.shared.models import RichTextObject
6
11
 
7
12
 
8
13
  class BookmarkElement(NotionBlockElement):
@@ -33,7 +38,7 @@ class BookmarkElement(NotionBlockElement):
33
38
  def match_markdown(cls, text: str) -> bool:
34
39
  """Check if text is a markdown bookmark."""
35
40
  return text.strip().startswith("[bookmark]") and bool(
36
- BookmarkElement.PATTERN.match(text.strip())
41
+ cls.PATTERN.match(text.strip())
37
42
  )
38
43
 
39
44
  @classmethod
@@ -42,7 +47,7 @@ class BookmarkElement(NotionBlockElement):
42
47
  return block.get("type") in ["bookmark", "external-bookmark"]
43
48
 
44
49
  @classmethod
45
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
50
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
46
51
  """Convert markdown bookmark to Notion bookmark block."""
47
52
  bookmark_match = BookmarkElement.PATTERN.match(text.strip())
48
53
  if not bookmark_match:
@@ -54,71 +59,21 @@ class BookmarkElement(NotionBlockElement):
54
59
 
55
60
  bookmark_data = {"url": url}
56
61
 
57
- # Add caption if title or description is provided
58
- if title or description:
59
- caption = []
60
-
61
- if title:
62
- caption.append(
63
- {
64
- "type": "text",
65
- "text": {"content": title, "link": None},
66
- "annotations": {
67
- "bold": False,
68
- "italic": False,
69
- "strikethrough": False,
70
- "underline": False,
71
- "code": False,
72
- "color": "default",
73
- },
74
- "plain_text": title,
75
- "href": None,
76
- }
77
- )
78
-
79
- # Add a separator if both title and description are provided
80
- if description:
81
- caption.append(
82
- {
83
- "type": "text",
84
- "text": {"content": " - ", "link": None},
85
- "annotations": {
86
- "bold": False,
87
- "italic": False,
88
- "strikethrough": False,
89
- "underline": False,
90
- "code": False,
91
- "color": "default",
92
- },
93
- "plain_text": " - ",
94
- "href": None,
95
- }
96
- )
97
-
98
- if description:
99
- caption.append(
100
- {
101
- "type": "text",
102
- "text": {"content": description, "link": None},
103
- "annotations": {
104
- "bold": False,
105
- "italic": False,
106
- "strikethrough": False,
107
- "underline": False,
108
- "code": False,
109
- "color": "default",
110
- },
111
- "plain_text": description,
112
- "href": None,
113
- }
114
- )
115
-
116
- bookmark_data["caption"] = caption
62
+ # Build caption string
63
+ caption_parts = []
64
+ if title:
65
+ caption_parts.append(title)
66
+ if description:
67
+ caption_parts.append(description)
68
+
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()]
117
73
  else:
118
- # Empty caption list to match Notion's format for bookmarks without titles
119
74
  bookmark_data["caption"] = []
120
75
 
121
- return {"type": "bookmark", "bookmark": bookmark_data}
76
+ return [{"type": "bookmark", "bookmark": bookmark_data}]
122
77
 
123
78
  @classmethod
124
79
  def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class BookmarkMarkdownBlockParams(BaseModel):
9
+ url: str
10
+ title: Optional[str] = None
11
+ description: Optional[str] = None
12
+
13
+
14
+ class BookmarkMarkdownNode(MarkdownNode):
15
+ """
16
+ Programmatic interface for creating Notion-style bookmark Markdown blocks.
17
+ """
18
+
19
+ def __init__(
20
+ self, url: str, title: Optional[str] = None, description: Optional[str] = None
21
+ ):
22
+ self.url = url
23
+ self.title = title
24
+ self.description = description
25
+
26
+ @classmethod
27
+ def from_params(cls, params: BookmarkMarkdownBlockParams) -> BookmarkMarkdownNode:
28
+ return cls(url=params.url, title=params.title, description=params.description)
29
+
30
+ def to_markdown(self) -> str:
31
+ """
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) + ")"
@@ -0,0 +1,7 @@
1
+ from .bulleted_list_element import BulletedListElement
2
+ from .bulleted_list_markdown_node import BulletedListMarkdownNode
3
+
4
+ __all__ = [
5
+ "BulletedListElement",
6
+ "BulletedListMarkdownNode",
7
+ ]
@@ -1,16 +1,20 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
3
  from notionary.blocks import NotionBlockElement
4
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
4
+ from notionary.blocks import (
5
+ ElementPromptContent,
6
+ ElementPromptBuilder,
7
+ NotionBlockResult,
8
+ )
5
9
 
6
- from notionary.blocks.text_inline_formatter import TextInlineFormatter
10
+ from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
7
11
 
8
12
 
9
13
  class BulletedListElement(NotionBlockElement):
10
14
  """Class for converting between Markdown bullet lists and Notion bulleted list items."""
11
15
 
12
16
  @classmethod
13
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
17
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
14
18
  """Convert markdown bulleted list item to Notion block."""
15
19
  pattern = re.compile(
16
20
  r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$"
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+ from notionary.blocks.markdown_node import MarkdownNode
5
+
6
+
7
+ class BulletedListMarkdownBlockParams(BaseModel):
8
+ texts: list[str]
9
+
10
+
11
+ class BulletedListMarkdownNode(MarkdownNode):
12
+ """
13
+ Programmatic interface for creating Markdown bulleted list items.
14
+ Example:
15
+ - First item
16
+ - Second item
17
+ - Third item
18
+ """
19
+
20
+ def __init__(self, texts: list[str]):
21
+ self.texts = texts
22
+
23
+ @classmethod
24
+ def from_params(
25
+ cls, params: BulletedListMarkdownBlockParams
26
+ ) -> BulletedListMarkdownNode:
27
+ return cls(texts=params.texts)
28
+
29
+ def to_markdown(self) -> str:
30
+ result = []
31
+ for text in self.texts:
32
+ result.append(f"- {text}")
33
+ return "\n".join(result)
File without changes
@@ -0,0 +1,7 @@
1
+ from .callout_element import CalloutElement
2
+ from .callout_markdown_node import CalloutMarkdownNode
3
+
4
+ __all__ = [
5
+ "CalloutElement",
6
+ "CalloutMarkdownNode",
7
+ ]