notionary 0.2.21__py3-none-any.whl → 0.2.23__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 (100) hide show
  1. notionary/blocks/_bootstrap.py +9 -1
  2. notionary/blocks/audio/audio_element.py +53 -28
  3. notionary/blocks/audio/audio_markdown_node.py +10 -4
  4. notionary/blocks/base_block_element.py +15 -3
  5. notionary/blocks/bookmark/bookmark_element.py +39 -36
  6. notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
  7. notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
  8. notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
  9. notionary/blocks/callout/callout_element.py +20 -4
  10. notionary/blocks/child_database/__init__.py +11 -4
  11. notionary/blocks/child_database/child_database_element.py +59 -0
  12. notionary/blocks/child_database/child_database_models.py +7 -14
  13. notionary/blocks/child_page/child_page_element.py +94 -0
  14. notionary/blocks/client.py +0 -1
  15. notionary/blocks/code/code_element.py +51 -2
  16. notionary/blocks/code/code_markdown_node.py +52 -1
  17. notionary/blocks/column/column_element.py +9 -3
  18. notionary/blocks/column/column_list_element.py +18 -3
  19. notionary/blocks/divider/divider_element.py +3 -11
  20. notionary/blocks/embed/embed_element.py +27 -6
  21. notionary/blocks/equation/equation_element.py +94 -41
  22. notionary/blocks/equation/equation_element_markdown_node.py +8 -9
  23. notionary/blocks/file/file_element.py +56 -37
  24. notionary/blocks/file/file_element_markdown_node.py +9 -7
  25. notionary/blocks/guards.py +22 -0
  26. notionary/blocks/heading/heading_element.py +23 -4
  27. notionary/blocks/image_block/image_element.py +43 -38
  28. notionary/blocks/image_block/image_markdown_node.py +10 -5
  29. notionary/blocks/mixins/captions/__init__.py +4 -0
  30. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  31. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  32. notionary/blocks/models.py +3 -1
  33. notionary/blocks/numbered_list/numbered_list_element.py +21 -4
  34. notionary/blocks/paragraph/paragraph_element.py +21 -5
  35. notionary/blocks/pdf/pdf_element.py +47 -41
  36. notionary/blocks/pdf/pdf_markdown_node.py +9 -7
  37. notionary/blocks/quote/quote_element.py +26 -9
  38. notionary/blocks/quote/quote_markdown_node.py +2 -2
  39. notionary/blocks/registry/block_registry.py +1 -46
  40. notionary/blocks/registry/block_registry_builder.py +8 -0
  41. notionary/blocks/rich_text/rich_text_models.py +62 -29
  42. notionary/blocks/rich_text/text_inline_formatter.py +432 -101
  43. notionary/blocks/syntax_prompt_builder.py +137 -0
  44. notionary/blocks/table/table_element.py +110 -9
  45. notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
  46. notionary/blocks/todo/todo_element.py +21 -4
  47. notionary/blocks/toggle/toggle_element.py +19 -3
  48. notionary/blocks/toggle/toggle_markdown_node.py +1 -1
  49. notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
  50. notionary/blocks/types.py +69 -0
  51. notionary/blocks/video/video_element.py +44 -39
  52. notionary/blocks/video/video_markdown_node.py +10 -5
  53. notionary/comments/__init__.py +26 -0
  54. notionary/comments/client.py +211 -0
  55. notionary/comments/models.py +129 -0
  56. notionary/database/client.py +23 -0
  57. notionary/file_upload/models.py +2 -2
  58. notionary/markdown/markdown_builder.py +34 -27
  59. notionary/page/client.py +21 -6
  60. notionary/page/notion_page.py +77 -2
  61. notionary/page/page_content_deleting_service.py +117 -0
  62. notionary/page/page_content_writer.py +89 -113
  63. notionary/page/page_context.py +64 -0
  64. notionary/page/reader/handler/__init__.py +2 -0
  65. notionary/page/reader/handler/base_block_renderer.py +4 -4
  66. notionary/page/reader/handler/block_rendering_context.py +5 -0
  67. notionary/page/reader/handler/line_renderer.py +16 -3
  68. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  69. notionary/page/reader/page_content_retriever.py +17 -5
  70. notionary/page/writer/handler/__init__.py +2 -0
  71. notionary/page/writer/handler/code_handler.py +12 -40
  72. notionary/page/writer/handler/column_handler.py +12 -12
  73. notionary/page/writer/handler/column_list_handler.py +13 -13
  74. notionary/page/writer/handler/equation_handler.py +74 -0
  75. notionary/page/writer/handler/line_handler.py +4 -4
  76. notionary/page/writer/handler/regular_line_handler.py +31 -37
  77. notionary/page/writer/handler/table_handler.py +8 -72
  78. notionary/page/writer/handler/toggle_handler.py +14 -12
  79. notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
  80. notionary/page/writer/markdown_to_notion_converter.py +28 -9
  81. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  82. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  83. notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
  84. notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
  85. notionary/page/writer/notion_text_length_processor.py +150 -0
  86. notionary/shared/__init__.py +5 -0
  87. notionary/shared/name_to_id_resolver.py +203 -0
  88. notionary/telemetry/service.py +0 -1
  89. notionary/user/notion_user_manager.py +22 -95
  90. notionary/util/concurrency_limiter.py +0 -0
  91. notionary/workspace.py +4 -4
  92. notionary-0.2.23.dist-info/METADATA +235 -0
  93. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
  94. notionary/page/markdown_whitespace_processor.py +0 -80
  95. notionary/page/notion_text_length_utils.py +0 -119
  96. notionary/user/notion_user_provider.py +0 -1
  97. notionary-0.2.21.dist-info/METADATA +0 -229
  98. /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
  99. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
  100. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
@@ -34,6 +34,7 @@ def bootstrap_blocks() -> None:
34
34
  toggle,
35
35
  toggleable_heading,
36
36
  video,
37
+ child_database,
37
38
  )
38
39
 
39
40
  # Collect all exports from modules
@@ -61,6 +62,7 @@ def bootstrap_blocks() -> None:
61
62
  video,
62
63
  toggleable_heading,
63
64
  table_of_contents,
65
+ child_database,
64
66
  ):
65
67
  ns.update(vars(m))
66
68
 
@@ -123,6 +125,10 @@ def bootstrap_blocks() -> None:
123
125
  from notionary.blocks.toggle.toggle_models import CreateToggleBlock, ToggleBlock
124
126
  from notionary.blocks.types import BlockType
125
127
  from notionary.blocks.video.video_element_models import CreateVideoBlock
128
+ from notionary.blocks.child_database.child_database_models import (
129
+ CreateChildDatabaseBlock,
130
+ ChildDatabaseBlock,
131
+ )
126
132
 
127
133
  # Define the Union types that are needed for model rebuilding
128
134
  BlockCreateRequest = Union[
@@ -150,9 +156,10 @@ def bootstrap_blocks() -> None:
150
156
  CreateVideoBlock,
151
157
  CreateTableOfContentsBlock,
152
158
  CreatePdfBlock,
159
+ CreateChildDatabaseBlock,
153
160
  ]
154
161
 
155
- BlockCreateResult = Optional[Union[list[BlockCreateRequest], BlockCreateRequest]]
162
+ BlockCreateResult = Optional[BlockCreateRequest]
156
163
 
157
164
  # Add all block types to namespace
158
165
  ns.update(
@@ -202,6 +209,7 @@ def bootstrap_blocks() -> None:
202
209
  "CreateVideoBlock": CreateVideoBlock,
203
210
  "TableOfContentsBlock": TableOfContentsBlock,
204
211
  "CreateTableOfContentsBlock": CreateTableOfContentsBlock,
212
+ "ChildDatabaseBlock": ChildDatabaseBlock,
205
213
  # Add the Union types
206
214
  "BlockCreateRequest": BlockCreateRequest,
207
215
  "BlockCreateResult": BlockCreateResult,
@@ -6,27 +6,40 @@ from typing import Optional
6
6
  from notionary.blocks.audio.audio_models import CreateAudioBlock
7
7
  from notionary.blocks.base_block_element import BaseBlockElement
8
8
  from notionary.blocks.file.file_element_models import ExternalFile, FileBlock, FileType
9
+ from notionary.blocks.mixins.captions import CaptionMixin
10
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
9
11
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
10
- from notionary.blocks.rich_text.rich_text_models import RichTextObject
11
- from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
12
12
 
13
13
 
14
- class AudioElement(BaseBlockElement):
14
+ class AudioElement(BaseBlockElement, CaptionMixin):
15
15
  """
16
16
  Handles conversion between Markdown audio embeds and Notion audio blocks.
17
17
 
18
18
  Markdown audio syntax:
19
19
  - [audio](https://example.com/audio.mp3) - Simple audio embed
20
- - [audio](https://example.com/audio.mp3 "Caption text") - Audio with optional caption
20
+ - [audio](https://example.com/audio.mp3)(caption:Episode 1) - Audio with caption
21
+ - (caption:Background music)[audio](https://example.com/song.mp3) - caption before URL
21
22
 
22
23
  Where:
23
24
  - URL is the required audio file URL
24
- - Caption is optional descriptive text (enclosed in quotes)
25
+ - Caption supports rich text formatting and is optional
25
26
  """
26
27
 
27
- URL_PATTERN = r"(https?://[^\s\"]+)"
28
- CAPTION_PATTERN = r'(?:\s+"([^"]+)")?'
29
- PATTERN = re.compile(r"^\[audio\]\(" + URL_PATTERN + CAPTION_PATTERN + r"\)$")
28
+ # Simple pattern that matches just the audio link, CaptionMixin handles caption separately
29
+ AUDIO_PATTERN = re.compile(r"\[audio\]\((https?://[^\s\"]+)\)")
30
+
31
+ @classmethod
32
+ def _extract_audio_url(cls, text: str) -> Optional[str]:
33
+ """Extract audio URL from text, handling caption patterns."""
34
+ # First remove any captions to get clean text for URL extraction
35
+ clean_text = cls.remove_caption(text)
36
+
37
+ # Now extract the URL from clean text
38
+ match = cls.AUDIO_PATTERN.search(clean_text)
39
+ if match:
40
+ return match.group(1)
41
+
42
+ return None
30
43
 
31
44
  SUPPORTED_EXTENSIONS = {".mp3", ".wav", ".ogg", ".oga", ".m4a"}
32
45
 
@@ -36,33 +49,30 @@ class AudioElement(BaseBlockElement):
36
49
  return block.type == BlockType.AUDIO
37
50
 
38
51
  @classmethod
39
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
40
- """Convert markdown audio embed to Notion audio block (or return None if not matching)."""
41
- match = cls.PATTERN.match(text.strip())
42
- if not match:
52
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
53
+ """Convert markdown audio embed to Notion audio block."""
54
+ # Use our helper method to extract the URL
55
+ url = cls._extract_audio_url(text.strip())
56
+ if not url:
43
57
  return None
44
- url = match.group(1)
45
58
 
46
59
  if not cls._is_likely_audio_url(url):
47
60
  return None
48
- caption_text = match.group(2)
49
61
 
50
- # Create caption rich text objects
51
- caption_objects = []
52
- if caption_text:
53
- caption_rt = RichTextObject.from_plain_text(caption_text)
54
- caption_objects = [caption_rt]
62
+ # Use mixin to extract caption (if present anywhere in text)
63
+ caption_text = cls.extract_caption(text.strip())
64
+ caption_rich_text = cls.build_caption_rich_text(caption_text or "")
55
65
 
56
66
  audio_content = FileBlock(
57
67
  type=FileType.EXTERNAL,
58
68
  external=ExternalFile(url=url),
59
- caption=caption_objects,
69
+ caption=caption_rich_text,
60
70
  )
61
71
 
62
72
  return CreateAudioBlock(audio=audio_content)
63
73
 
64
74
  @classmethod
65
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
75
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
66
76
  """Convert Notion audio block to markdown audio embed."""
67
77
  if block.type != BlockType.AUDIO or block.audio is None:
68
78
  return None
@@ -76,14 +86,29 @@ class AudioElement(BaseBlockElement):
76
86
  if not url:
77
87
  return None
78
88
 
79
- # Extract caption
80
- captions = audio.caption or []
81
- if captions:
82
- # use TextInlineFormatter instead of manual extraction
83
- caption_text = TextInlineFormatter.extract_text_with_formatting(captions)
84
- return f'[audio]({url} "{caption_text}")'
89
+ result = f"[audio]({url})"
85
90
 
86
- return f"[audio]({url})"
91
+ # Add caption if present
92
+ caption_markdown = await cls.format_caption_for_markdown(audio.caption or [])
93
+ if caption_markdown:
94
+ result += caption_markdown
95
+
96
+ return result
97
+
98
+ @classmethod
99
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
100
+ """Get system prompt information for audio blocks."""
101
+ return BlockElementMarkdownInformation(
102
+ block_type=cls.__name__,
103
+ description="Audio blocks embed audio files from external URLs with optional captions",
104
+ syntax_examples=[
105
+ "[audio](https://example.com/song.mp3)",
106
+ "[audio](https://example.com/podcast.wav)(caption:Episode 1)",
107
+ "(caption:Background music)[audio](https://soundcloud.com/track/123)",
108
+ "[audio](https://example.com/interview.mp3)(caption:**Live** interview)",
109
+ ],
110
+ usage_guidelines="Use for embedding audio files like music, podcasts, or sound effects. Supports common audio formats (mp3, wav, ogg, m4a). Caption supports rich text formatting and is optional.",
111
+ )
87
112
 
88
113
  @classmethod
89
114
  def _is_likely_audio_url(cls, url: str) -> bool:
@@ -5,6 +5,7 @@ from typing import Optional
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from notionary.markdown.markdown_node import MarkdownNode
8
+ from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
8
9
 
9
10
 
10
11
  class AudioMarkdownBlockParams(BaseModel):
@@ -12,7 +13,7 @@ class AudioMarkdownBlockParams(BaseModel):
12
13
  caption: Optional[str] = None
13
14
 
14
15
 
15
- class AudioMarkdownNode(MarkdownNode):
16
+ class AudioMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
16
17
  """
17
18
  Programmatic interface for creating Notion-style audio blocks.
18
19
  """
@@ -26,6 +27,11 @@ class AudioMarkdownNode(MarkdownNode):
26
27
  return cls(url=params.url, caption=params.caption)
27
28
 
28
29
  def to_markdown(self) -> str:
29
- if self.caption:
30
- return f'[audio]({self.url} "{self.caption}")'
31
- return f"[audio]({self.url})"
30
+ """Return the Markdown representation.
31
+
32
+ Examples:
33
+ - [audio](https://example.com/song.mp3)
34
+ - [audio](https://example.com/song.mp3)(caption:Background music)
35
+ """
36
+ base_markdown = f"[audio]({self.url})"
37
+ return self.append_caption_to_markdown(base_markdown, self.caption)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC
4
4
  from typing import Optional
5
5
 
6
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
6
7
  from notionary.blocks.models import Block, BlockCreateResult
7
8
 
8
9
 
@@ -10,7 +11,7 @@ class BaseBlockElement(ABC):
10
11
  """Base class for elements that can be converted between Markdown and Notion."""
11
12
 
12
13
  @classmethod
13
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
14
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
14
15
  """
15
16
  Convert markdown to Notion block content.
16
17
 
@@ -21,10 +22,21 @@ class BaseBlockElement(ABC):
21
22
  """
22
23
 
23
24
  @classmethod
24
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
25
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
25
26
  """Convert Notion block to markdown."""
26
27
 
27
28
  @classmethod
28
29
  def match_notion(cls, block: Block) -> bool:
29
30
  """Check if this element can handle the given Notion block."""
30
- return bool(cls.notion_to_markdown(block)) # Now calls the class's version
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
@@ -5,60 +5,51 @@ from typing import Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
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
8
10
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
9
- from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
10
11
 
11
12
 
12
- # BookmarkElement implementation using BlockType enum and TextInlineFormatter
13
- class BookmarkElement(BaseBlockElement):
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
18
  - [bookmark](https://example.com) - URL only
19
- - [bookmark](https://example.com "Title") - URL + title
20
- - [bookmark](https://example.com "Title" "Description") - URL + title + description
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
21
21
  """
22
22
 
23
- PATTERN = re.compile(
24
- r"^\[bookmark\]\(" # prefix
25
- r"(https?://[^\s\"]+)" # URL
26
- r"(?:\s+\"([^\"]+)\")?" # optional Title
27
- r"(?:\s+\"([^\"]+)\")?" # optional Description
28
- r"\)$"
29
- )
23
+ # Flexible pattern that can handle caption in any position
24
+ BOOKMARK_PATTERN = re.compile(r"\[bookmark\]\((https?://[^\s\"]+)\)")
30
25
 
31
26
  @classmethod
32
27
  def match_notion(cls, block: Block) -> bool:
33
28
  return block.type == BlockType.BOOKMARK and block.bookmark
34
29
 
35
30
  @classmethod
36
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
37
32
  """
38
33
  Convert a markdown bookmark into a Notion BookmarkBlock.
39
34
  """
40
- if not (m := cls.PATTERN.match(text.strip())):
41
- return None
35
+ # First remove captions to get clean text for URL extraction
36
+ clean_text = cls.remove_caption(text.strip())
42
37
 
43
- url, title, description = m.group(1), m.group(2), m.group(3)
38
+ # Use our own regex to find the bookmark URL
39
+ bookmark_match = cls.BOOKMARK_PATTERN.search(clean_text)
40
+ if not bookmark_match:
41
+ return None
44
42
 
45
- # Build caption texts
46
- parts: list[str] = []
47
- if title:
48
- parts.append(title)
49
- if description:
50
- parts.append(description)
43
+ url = bookmark_match.group(1)
51
44
 
52
- caption = []
53
- if parts:
54
- joined = " – ".join(parts)
55
- caption = TextInlineFormatter.parse_inline_formatting(joined)
45
+ caption_text = cls.extract_caption(text.strip())
46
+ caption_rich_text = cls.build_caption_rich_text(caption_text or "")
56
47
 
57
- bookmark_data = BookmarkBlock(url=url, caption=caption)
48
+ bookmark_data = BookmarkBlock(url=url, caption=caption_rich_text)
58
49
  return CreateBookmarkBlock(bookmark=bookmark_data)
59
50
 
60
51
  @classmethod
61
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
52
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
62
53
  if block.type != BlockType.BOOKMARK or block.bookmark is None:
63
54
  return None
64
55
 
@@ -67,14 +58,26 @@ class BookmarkElement(BaseBlockElement):
67
58
  if not url:
68
59
  return None
69
60
 
70
- captions = bm.caption or []
71
- if not captions:
72
- return f"[bookmark]({url})"
61
+ result = f"[bookmark]({url})"
73
62
 
74
- text = TextInlineFormatter.extract_text_with_formatting(captions)
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
75
67
 
76
- if " - " in text:
77
- title, desc = map(str.strip, text.split(" - ", 1))
78
- return f'[bookmark]({url} "{title}" "{desc}")'
68
+ return result
79
69
 
80
- return f'[bookmark]({url} "{text}")'
70
+ @classmethod
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.",
83
+ )
@@ -5,41 +5,40 @@ from typing import Optional
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from notionary.markdown.markdown_node import MarkdownNode
8
+ from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
8
9
 
9
10
 
10
11
  class BookmarkMarkdownBlockParams(BaseModel):
11
12
  url: str
12
13
  title: Optional[str] = None
13
- description: Optional[str] = None
14
+ caption: Optional[str] = None
14
15
 
15
16
 
16
- class BookmarkMarkdownNode(MarkdownNode):
17
+ class BookmarkMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
17
18
  """
18
19
  Programmatic interface for creating Notion-style bookmark Markdown blocks.
19
20
  """
20
21
 
21
22
  def __init__(
22
- self, url: str, title: Optional[str] = None, description: Optional[str] = None
23
+ self, url: str, title: Optional[str] = None, caption: Optional[str] = None
23
24
  ):
24
25
  self.url = url
25
26
  self.title = title
26
- self.description = description
27
+ self.caption = caption
27
28
 
28
29
  @classmethod
29
30
  def from_params(cls, params: BookmarkMarkdownBlockParams) -> BookmarkMarkdownNode:
30
- return cls(url=params.url, title=params.title, description=params.description)
31
+ return cls(url=params.url, title=params.title, caption=params.caption)
31
32
 
32
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)
33
39
  """
34
- Returns the Markdown representation, e.g.:
35
- [bookmark](https://example.com "Title" "Description")
36
- """
37
- parts = [f"[bookmark]({self.url}"]
38
- if self.title is not None:
39
- parts.append(f'"{self.title}"')
40
- if self.description is not None:
41
- # Wenn title fehlt, aber description da ist, trotzdem Platzhalter für title:
42
- if self.title is None:
43
- parts.append('""')
44
- parts.append(f'"{self.description}"')
45
- 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)
@@ -28,12 +28,12 @@ class BreadcrumbElement(BaseBlockElement):
28
28
  return block.type == BlockType.BREADCRUMB and block.breadcrumb
29
29
 
30
30
  @classmethod
31
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
32
  if not cls.PATTERN.match(text.strip()):
33
33
  return None
34
34
  return CreateBreadcrumbBlock(breadcrumb=BreadcrumbBlock())
35
35
 
36
36
  @classmethod
37
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
37
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
38
38
  if block.type == BlockType.BREADCRUMB and block.breadcrumb:
39
39
  return cls.BREADCRUMB_MARKER
@@ -8,6 +8,7 @@ from notionary.blocks.bulleted_list.bulleted_list_models import (
8
8
  BulletedListItemBlock,
9
9
  CreateBulletedListItemBlock,
10
10
  )
11
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
11
12
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
12
13
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
13
14
 
@@ -24,7 +25,7 @@ class BulletedListElement(BaseBlockElement):
24
25
  return block.type == BlockType.BULLETED_LIST_ITEM and block.bulleted_list_item
25
26
 
26
27
  @classmethod
27
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
28
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
28
29
  """
29
30
  Convert a markdown bulleted list item into a Notion BulletedListItemBlock.
30
31
  """
@@ -35,7 +36,7 @@ class BulletedListElement(BaseBlockElement):
35
36
  content = match.group(2)
36
37
 
37
38
  # Parse inline markdown formatting into RichTextObject list
38
- rich_text = TextInlineFormatter.parse_inline_formatting(content)
39
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
39
40
 
40
41
  # Return a properly typed Notion block
41
42
  bulleted_list_content = BulletedListItemBlock(
@@ -44,7 +45,7 @@ class BulletedListElement(BaseBlockElement):
44
45
  return CreateBulletedListItemBlock(bulleted_list_item=bulleted_list_content)
45
46
 
46
47
  @classmethod
47
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
48
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
48
49
  """Convert Notion bulleted_list_item block to Markdown."""
49
50
  if block.type != BlockType.BULLETED_LIST_ITEM or not block.bulleted_list_item:
50
51
  return None
@@ -53,5 +54,21 @@ class BulletedListElement(BaseBlockElement):
53
54
  if not rich_list:
54
55
  return "-"
55
56
 
56
- text = TextInlineFormatter.extract_text_with_formatting(rich_list)
57
+ text = await TextInlineFormatter.extract_text_with_formatting(rich_list)
57
58
  return f"- {text}"
59
+
60
+ @classmethod
61
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
62
+ """Get system prompt information for bulleted list blocks."""
63
+ return BlockElementMarkdownInformation(
64
+ block_type=cls.__name__,
65
+ description="Bulleted list items create unordered lists with bullet points",
66
+ syntax_examples=[
67
+ "- First item",
68
+ "* Second item",
69
+ "+ Third item",
70
+ "- Item with **bold text**",
71
+ "- Item with *italic text*",
72
+ ],
73
+ usage_guidelines="Use -, *, or + to create bullet points. Supports inline formatting like bold, italic, and links. Do not use for todo items (use [ ] or [x] for those).",
74
+ )
@@ -10,6 +10,7 @@ from notionary.blocks.callout.callout_models import (
10
10
  EmojiIcon,
11
11
  IconObject,
12
12
  )
13
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
13
14
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
14
15
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
15
16
 
@@ -42,7 +43,7 @@ class CalloutElement(BaseBlockElement):
42
43
  return block.type == BlockType.CALLOUT and block.callout
43
44
 
44
45
  @classmethod
45
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
46
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
46
47
  """Convert a markdown callout into a Notion CalloutBlock."""
47
48
  match = cls.PATTERN.match(text.strip())
48
49
  if not match:
@@ -55,7 +56,7 @@ class CalloutElement(BaseBlockElement):
55
56
  if not emoji:
56
57
  emoji = cls.DEFAULT_EMOJI
57
58
 
58
- rich_text = TextInlineFormatter.parse_inline_formatting(content.strip())
59
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content.strip())
59
60
 
60
61
  callout_content = CalloutBlock(
61
62
  rich_text=rich_text,
@@ -65,13 +66,13 @@ class CalloutElement(BaseBlockElement):
65
66
  return CreateCalloutBlock(callout=callout_content)
66
67
 
67
68
  @classmethod
68
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
69
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
69
70
  if block.type != BlockType.CALLOUT or not block.callout:
70
71
  return None
71
72
 
72
73
  data = block.callout
73
74
 
74
- content = TextInlineFormatter.extract_text_with_formatting(data.rich_text)
75
+ content = await TextInlineFormatter.extract_text_with_formatting(data.rich_text)
75
76
  if not content:
76
77
  return None
77
78
 
@@ -81,3 +82,18 @@ class CalloutElement(BaseBlockElement):
81
82
  if emoji_char and emoji_char != cls.DEFAULT_EMOJI:
82
83
  return f'[callout]({content} "{emoji_char}")'
83
84
  return f"[callout]({content})"
85
+
86
+ @classmethod
87
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
88
+ """Get system prompt information for callout blocks."""
89
+ return BlockElementMarkdownInformation(
90
+ block_type=cls.__name__,
91
+ description="Callout blocks create highlighted text boxes with optional custom emojis for emphasis",
92
+ syntax_examples=[
93
+ "[callout](This is important information)",
94
+ '[callout](Warning message "⚠️")',
95
+ '[callout](Success message "✅")',
96
+ "[callout](Note with default emoji)",
97
+ ],
98
+ usage_guidelines="Use for highlighting important information, warnings, tips, or notes. Default emoji is 💡. Custom emoji should be provided in quotes after the text content.",
99
+ )
@@ -1,7 +1,14 @@
1
- from notionary.blocks.child_database.child_database_models import (
2
- CreateInlineDatabaseRequest,
3
- )
1
+ """
2
+ Child Database Block Module
3
+
4
+ This module provides functionality for handling Notion child database blocks.
5
+ """
6
+
7
+ from .child_database_element import ChildDatabaseElement
8
+ from .child_database_models import ChildDatabaseBlock, CreateChildDatabaseBlock
4
9
 
5
10
  __all__ = [
6
- "CreateInlineDatabaseRequest",
11
+ "ChildDatabaseElement",
12
+ "ChildDatabaseBlock",
13
+ "CreateChildDatabaseBlock",
7
14
  ]
@@ -0,0 +1,59 @@
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.syntax_prompt_builder import BlockElementMarkdownInformation
8
+ from notionary.blocks.models import Block, BlockType
9
+ from notionary.util import LoggingMixin
10
+
11
+
12
+ class ChildDatabaseElement(BaseBlockElement, LoggingMixin):
13
+ """
14
+ Handles conversion between Markdown database references and Notion child database blocks.
15
+
16
+ Creates new databases when converting from markdown.
17
+ """
18
+
19
+ PATTERN_BRACKET = re.compile(r"^\[database:\s*(.+)\]$", re.IGNORECASE)
20
+ PATTERN_EMOJI = re.compile(r"^📊\s*(.+)$")
21
+
22
+ @classmethod
23
+ def match_notion(cls, block: Block) -> bool:
24
+ return block.type == BlockType.CHILD_DATABASE and block.child_database
25
+
26
+ @classmethod
27
+ async def markdown_to_notion(cls, text: str) -> Optional[str]:
28
+ """
29
+ Convert markdown database syntax to actual Notion database.
30
+ Returns the database_id if successful, None otherwise.
31
+ """
32
+ return None
33
+
34
+ @classmethod
35
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
36
+ if block.type != BlockType.CHILD_DATABASE or not block.child_database:
37
+ return None
38
+
39
+ title = block.child_database.title
40
+ if not title or not title.strip():
41
+ return None
42
+
43
+ # Use bracket syntax for output
44
+ return f"[database: {title.strip()}]"
45
+
46
+ @classmethod
47
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
48
+ """Get system prompt information for child database blocks."""
49
+ return BlockElementMarkdownInformation(
50
+ block_type=cls.__name__,
51
+ description="Creates new embedded databases within a Notion page",
52
+ syntax_examples=[
53
+ "[database: Project Tasks]",
54
+ "[database: Customer Information]",
55
+ "📊 Sales Pipeline",
56
+ "📊 Team Directory",
57
+ ],
58
+ usage_guidelines="Use to create new databases that will be embedded in the page. The database will be created with a basic 'Name' property and can be customized later.",
59
+ )
@@ -1,19 +1,12 @@
1
- from typing import Any
1
+ from typing import Literal
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel
4
4
 
5
- from notionary.blocks.rich_text.rich_text_models import RichTextObject
6
5
 
6
+ class ChildDatabaseBlock(BaseModel):
7
+ title: str
7
8
 
8
- class CreateInlineDatabaseRequest(BaseModel):
9
- """
10
- Minimaler Create-Payload für eine inline Database.
11
- Parent wird von der Page-Schicht gesetzt: {"type": "page_id", "page_id": "..."}.
12
- """
13
9
 
14
- parent: dict[str, str] # wird von außen injiziert
15
- title: list[
16
- RichTextObject
17
- ] # z. B. [RichTextObject.from_plain_text("Monatsübersicht")]
18
- properties: dict[str, dict[str, Any]] # mindestens eine Title-Property erforderlich
19
- is_inline: bool = True # inline = erscheint als child_database-Block auf der Page
10
+ class CreateChildDatabaseBlock(BaseModel):
11
+ type: Literal["child_database"] = "child_database"
12
+ child_database: ChildDatabaseBlock