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.
- notionary/blocks/_bootstrap.py +9 -1
- notionary/blocks/audio/audio_element.py +53 -28
- notionary/blocks/audio/audio_markdown_node.py +10 -4
- notionary/blocks/base_block_element.py +15 -3
- notionary/blocks/bookmark/bookmark_element.py +39 -36
- notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
- notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
- notionary/blocks/callout/callout_element.py +20 -4
- notionary/blocks/child_database/__init__.py +11 -4
- notionary/blocks/child_database/child_database_element.py +59 -0
- notionary/blocks/child_database/child_database_models.py +7 -14
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/client.py +0 -1
- notionary/blocks/code/code_element.py +51 -2
- notionary/blocks/code/code_markdown_node.py +52 -1
- notionary/blocks/column/column_element.py +9 -3
- notionary/blocks/column/column_list_element.py +18 -3
- notionary/blocks/divider/divider_element.py +3 -11
- notionary/blocks/embed/embed_element.py +27 -6
- notionary/blocks/equation/equation_element.py +94 -41
- notionary/blocks/equation/equation_element_markdown_node.py +8 -9
- notionary/blocks/file/file_element.py +56 -37
- notionary/blocks/file/file_element_markdown_node.py +9 -7
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/heading_element.py +23 -4
- notionary/blocks/image_block/image_element.py +43 -38
- notionary/blocks/image_block/image_markdown_node.py +10 -5
- notionary/blocks/mixins/captions/__init__.py +4 -0
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
- notionary/blocks/mixins/captions/caption_mixin.py +92 -0
- notionary/blocks/models.py +3 -1
- notionary/blocks/numbered_list/numbered_list_element.py +21 -4
- notionary/blocks/paragraph/paragraph_element.py +21 -5
- notionary/blocks/pdf/pdf_element.py +47 -41
- notionary/blocks/pdf/pdf_markdown_node.py +9 -7
- notionary/blocks/quote/quote_element.py +26 -9
- notionary/blocks/quote/quote_markdown_node.py +2 -2
- notionary/blocks/registry/block_registry.py +1 -46
- notionary/blocks/registry/block_registry_builder.py +8 -0
- notionary/blocks/rich_text/rich_text_models.py +62 -29
- notionary/blocks/rich_text/text_inline_formatter.py +432 -101
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/table_element.py +110 -9
- notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
- notionary/blocks/todo/todo_element.py +21 -4
- notionary/blocks/toggle/toggle_element.py +19 -3
- notionary/blocks/toggle/toggle_markdown_node.py +1 -1
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
- notionary/blocks/types.py +69 -0
- notionary/blocks/video/video_element.py +44 -39
- notionary/blocks/video/video_markdown_node.py +10 -5
- notionary/comments/__init__.py +26 -0
- notionary/comments/client.py +211 -0
- notionary/comments/models.py +129 -0
- notionary/database/client.py +23 -0
- notionary/file_upload/models.py +2 -2
- notionary/markdown/markdown_builder.py +34 -27
- notionary/page/client.py +21 -6
- notionary/page/notion_page.py +77 -2
- notionary/page/page_content_deleting_service.py +117 -0
- notionary/page/page_content_writer.py +89 -113
- notionary/page/page_context.py +64 -0
- notionary/page/reader/handler/__init__.py +2 -0
- notionary/page/reader/handler/base_block_renderer.py +4 -4
- notionary/page/reader/handler/block_rendering_context.py +5 -0
- notionary/page/reader/handler/line_renderer.py +16 -3
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/page_content_retriever.py +17 -5
- notionary/page/writer/handler/__init__.py +2 -0
- notionary/page/writer/handler/code_handler.py +12 -40
- notionary/page/writer/handler/column_handler.py +12 -12
- notionary/page/writer/handler/column_list_handler.py +13 -13
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +4 -4
- notionary/page/writer/handler/regular_line_handler.py +31 -37
- notionary/page/writer/handler/table_handler.py +8 -72
- notionary/page/writer/handler/toggle_handler.py +14 -12
- notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
- notionary/page/writer/markdown_to_notion_converter.py +28 -9
- notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
- notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +150 -0
- notionary/shared/__init__.py +5 -0
- notionary/shared/name_to_id_resolver.py +203 -0
- notionary/telemetry/service.py +0 -1
- notionary/user/notion_user_manager.py +22 -95
- notionary/util/concurrency_limiter.py +0 -0
- notionary/workspace.py +4 -4
- notionary-0.2.23.dist-info/METADATA +235 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
- notionary/page/markdown_whitespace_processor.py +0 -80
- notionary/page/notion_text_length_utils.py +0 -119
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.21.dist-info/METADATA +0 -229
- /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
notionary/blocks/_bootstrap.py
CHANGED
@@ -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[
|
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
|
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
|
25
|
+
- Caption supports rich text formatting and is optional
|
25
26
|
"""
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
41
|
-
|
42
|
-
|
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
|
-
#
|
51
|
-
|
52
|
-
|
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=
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
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
|
20
|
-
- [bookmark](https://example.com
|
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
|
-
|
24
|
-
|
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
|
-
|
41
|
-
|
35
|
+
# First remove captions to get clean text for URL extraction
|
36
|
+
clean_text = cls.remove_caption(text.strip())
|
42
37
|
|
43
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
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=
|
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
|
-
|
71
|
-
if not captions:
|
72
|
-
return f"[bookmark]({url})"
|
61
|
+
result = f"[bookmark]({url})"
|
73
62
|
|
74
|
-
|
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
|
-
|
77
|
-
title, desc = map(str.strip, text.split(" - ", 1))
|
78
|
-
return f'[bookmark]({url} "{title}" "{desc}")'
|
68
|
+
return result
|
79
69
|
|
80
|
-
|
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
|
-
|
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,
|
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.
|
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,
|
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
|
-
|
35
|
-
[bookmark](
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
2
|
-
|
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
|
-
"
|
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
|
1
|
+
from typing import Literal
|
2
2
|
|
3
|
-
from pydantic import BaseModel
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|