notionary 0.2.19__py3-none-any.whl → 0.2.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +8 -4
- notionary/base_notion_client.py +3 -1
- notionary/blocks/__init__.py +2 -91
- notionary/blocks/_bootstrap.py +271 -0
- notionary/blocks/audio/__init__.py +8 -2
- notionary/blocks/audio/audio_element.py +69 -106
- notionary/blocks/audio/audio_markdown_node.py +13 -5
- notionary/blocks/audio/audio_models.py +6 -55
- notionary/blocks/base_block_element.py +42 -0
- notionary/blocks/bookmark/__init__.py +9 -2
- notionary/blocks/bookmark/bookmark_element.py +49 -139
- notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
- notionary/blocks/bookmark/bookmark_models.py +15 -0
- notionary/blocks/breadcrumbs/__init__.py +17 -0
- notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
- notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
- notionary/blocks/bulleted_list/__init__.py +12 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
- notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
- notionary/blocks/callout/__init__.py +9 -2
- notionary/blocks/callout/callout_element.py +53 -86
- notionary/blocks/callout/callout_markdown_node.py +3 -1
- notionary/blocks/callout/callout_models.py +33 -0
- notionary/blocks/child_database/__init__.py +14 -0
- notionary/blocks/child_database/child_database_element.py +61 -0
- notionary/blocks/child_database/child_database_models.py +12 -0
- notionary/blocks/child_page/__init__.py +9 -0
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/child_page/child_page_models.py +12 -0
- notionary/blocks/{shared/block_client.py → client.py} +54 -54
- notionary/blocks/code/__init__.py +6 -2
- notionary/blocks/code/code_element.py +96 -181
- notionary/blocks/code/code_markdown_node.py +64 -13
- notionary/blocks/code/code_models.py +94 -0
- notionary/blocks/column/__init__.py +25 -1
- notionary/blocks/column/column_element.py +44 -312
- notionary/blocks/column/column_list_element.py +52 -0
- notionary/blocks/column/column_list_markdown_node.py +50 -0
- notionary/blocks/column/column_markdown_node.py +59 -0
- notionary/blocks/column/column_models.py +26 -0
- notionary/blocks/divider/__init__.py +9 -2
- notionary/blocks/divider/divider_element.py +18 -49
- notionary/blocks/divider/divider_markdown_node.py +2 -1
- notionary/blocks/divider/divider_models.py +12 -0
- notionary/blocks/embed/__init__.py +9 -2
- notionary/blocks/embed/embed_element.py +65 -111
- notionary/blocks/embed/embed_markdown_node.py +3 -1
- notionary/blocks/embed/embed_models.py +14 -0
- notionary/blocks/equation/__init__.py +14 -0
- notionary/blocks/equation/equation_element.py +133 -0
- notionary/blocks/equation/equation_element_markdown_node.py +35 -0
- notionary/blocks/equation/equation_models.py +11 -0
- notionary/blocks/file/__init__.py +25 -0
- notionary/blocks/file/file_element.py +112 -0
- notionary/blocks/file/file_element_markdown_node.py +37 -0
- notionary/blocks/file/file_element_models.py +39 -0
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/__init__.py +16 -2
- notionary/blocks/heading/heading_element.py +83 -69
- notionary/blocks/heading/heading_markdown_node.py +2 -1
- notionary/blocks/heading/heading_models.py +29 -0
- notionary/blocks/image_block/__init__.py +13 -0
- notionary/blocks/image_block/image_element.py +89 -0
- notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
- notionary/blocks/image_block/image_models.py +10 -0
- 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 +174 -0
- notionary/blocks/numbered_list/__init__.py +12 -2
- notionary/blocks/numbered_list/numbered_list_element.py +48 -56
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
- notionary/blocks/numbered_list/numbered_list_models.py +17 -0
- notionary/blocks/paragraph/__init__.py +12 -2
- notionary/blocks/paragraph/paragraph_element.py +40 -66
- notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
- notionary/blocks/paragraph/paragraph_models.py +16 -0
- notionary/blocks/pdf/__init__.py +13 -0
- notionary/blocks/pdf/pdf_element.py +97 -0
- notionary/blocks/pdf/pdf_markdown_node.py +37 -0
- notionary/blocks/pdf/pdf_models.py +11 -0
- notionary/blocks/quote/__init__.py +11 -2
- notionary/blocks/quote/quote_element.py +45 -62
- notionary/blocks/quote/quote_markdown_node.py +6 -3
- notionary/blocks/quote/quote_models.py +18 -0
- notionary/blocks/registry/__init__.py +4 -0
- notionary/blocks/registry/block_registry.py +60 -121
- notionary/blocks/registry/block_registry_builder.py +115 -59
- notionary/blocks/rich_text/__init__.py +33 -0
- notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
- notionary/blocks/rich_text/rich_text_models.py +221 -0
- notionary/blocks/rich_text/text_inline_formatter.py +456 -0
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/__init__.py +16 -2
- notionary/blocks/table/table_element.py +136 -228
- notionary/blocks/table/table_markdown_node.py +2 -1
- notionary/blocks/table/table_models.py +28 -0
- notionary/blocks/table_of_contents/__init__.py +19 -0
- notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
- notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
- notionary/blocks/todo/__init__.py +9 -2
- notionary/blocks/todo/todo_element.py +52 -92
- notionary/blocks/todo/todo_markdown_node.py +2 -1
- notionary/blocks/todo/todo_models.py +19 -0
- notionary/blocks/toggle/__init__.py +13 -3
- notionary/blocks/toggle/toggle_element.py +69 -260
- notionary/blocks/toggle/toggle_markdown_node.py +25 -15
- notionary/blocks/toggle/toggle_models.py +17 -0
- notionary/blocks/toggleable_heading/__init__.py +6 -2
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
- notionary/blocks/types.py +130 -0
- notionary/blocks/video/__init__.py +8 -2
- notionary/blocks/video/video_element.py +70 -141
- notionary/blocks/video/video_element_models.py +10 -0
- notionary/blocks/video/video_markdown_node.py +13 -6
- notionary/database/client.py +26 -8
- notionary/database/database.py +13 -14
- notionary/database/database_filter_builder.py +2 -2
- notionary/database/database_provider.py +5 -4
- notionary/database/models.py +337 -0
- notionary/database/notion_database.py +6 -7
- notionary/file_upload/client.py +5 -7
- notionary/file_upload/models.py +3 -2
- notionary/file_upload/notion_file_upload.py +2 -3
- notionary/markdown/markdown_builder.py +729 -0
- notionary/markdown/markdown_document_model.py +228 -0
- notionary/{blocks → markdown}/markdown_node.py +1 -0
- notionary/models/notion_database_response.py +0 -338
- notionary/page/client.py +34 -15
- notionary/page/models.py +327 -0
- notionary/page/notion_page.py +136 -58
- notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
- notionary/page/page_content_writer.py +177 -0
- notionary/page/page_context.py +65 -0
- notionary/page/reader/handler/__init__.py +19 -0
- notionary/page/reader/handler/base_block_renderer.py +44 -0
- notionary/page/reader/handler/block_processing_context.py +35 -0
- notionary/page/reader/handler/block_rendering_context.py +48 -0
- notionary/page/reader/handler/column_list_renderer.py +51 -0
- notionary/page/reader/handler/column_renderer.py +60 -0
- notionary/page/reader/handler/line_renderer.py +73 -0
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/handler/toggle_renderer.py +69 -0
- notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
- notionary/page/reader/page_content_retriever.py +81 -0
- notionary/page/search_filter_builder.py +2 -1
- notionary/page/writer/handler/__init__.py +24 -0
- notionary/page/writer/handler/code_handler.py +72 -0
- notionary/page/writer/handler/column_handler.py +141 -0
- notionary/page/writer/handler/column_list_handler.py +139 -0
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +35 -0
- notionary/page/writer/handler/line_processing_context.py +54 -0
- notionary/page/writer/handler/regular_line_handler.py +86 -0
- notionary/page/writer/handler/table_handler.py +66 -0
- notionary/page/writer/handler/toggle_handler.py +155 -0
- notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
- notionary/page/writer/markdown_to_notion_converter.py +95 -0
- 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/notion_text_length_processor.py +150 -0
- notionary/telemetry/__init__.py +2 -2
- notionary/telemetry/service.py +3 -3
- notionary/user/__init__.py +2 -2
- notionary/user/base_notion_user.py +2 -1
- notionary/user/client.py +2 -3
- notionary/user/models.py +1 -0
- notionary/user/notion_bot_user.py +4 -5
- notionary/user/notion_user.py +3 -4
- notionary/user/notion_user_manager.py +23 -95
- notionary/util/__init__.py +3 -2
- notionary/util/fuzzy.py +2 -1
- notionary/util/logging_mixin.py +2 -2
- notionary/util/singleton_metaclass.py +1 -1
- notionary/workspace.py +6 -5
- notionary-0.2.22.dist-info/METADATA +237 -0
- notionary-0.2.22.dist-info/RECORD +200 -0
- notionary/blocks/document/__init__.py +0 -7
- notionary/blocks/document/document_element.py +0 -102
- notionary/blocks/document/document_markdown_node.py +0 -31
- notionary/blocks/image/__init__.py +0 -7
- notionary/blocks/image/image_element.py +0 -151
- notionary/blocks/markdown_builder.py +0 -356
- notionary/blocks/mention/__init__.py +0 -7
- notionary/blocks/mention/mention_element.py +0 -229
- notionary/blocks/mention/mention_markdown_node.py +0 -38
- notionary/blocks/prompts/element_prompt_builder.py +0 -83
- notionary/blocks/prompts/element_prompt_content.py +0 -41
- notionary/blocks/shared/models.py +0 -713
- notionary/blocks/shared/notion_block_element.py +0 -37
- notionary/blocks/shared/text_inline_formatter.py +0 -262
- notionary/blocks/shared/text_inline_formatter_new.py +0 -139
- notionary/database/models/page_result.py +0 -10
- notionary/models/notion_block_response.py +0 -264
- notionary/models/notion_page_response.py +0 -78
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/content/markdown_whitespace_processor.py +0 -80
- notionary/page/content/notion_text_length_utils.py +0 -87
- notionary/page/content/page_content_retriever.py +0 -60
- notionary/page/formatting/line_processor.py +0 -153
- notionary/page/formatting/markdown_to_notion_converter.py +0 -153
- notionary/page/markdown_syntax_prompt_generator.py +0 -114
- notionary/page/notion_to_markdown_converter.py +0 -179
- notionary/page/properites/property_value_extractor.py +0 -0
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.19.dist-info/METADATA +0 -225
- notionary-0.2.19.dist-info/RECORD +0 -150
- /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
- /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
- /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
- /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
- /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
- /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -0,0 +1,112 @@
|
|
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.file.file_element_models import (
|
8
|
+
CreateFileBlock,
|
9
|
+
ExternalFile,
|
10
|
+
FileBlock,
|
11
|
+
FileType,
|
12
|
+
)
|
13
|
+
from notionary.blocks.mixins.captions import CaptionMixin
|
14
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
15
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
16
|
+
|
17
|
+
|
18
|
+
class FileElement(BaseBlockElement, CaptionMixin):
|
19
|
+
"""
|
20
|
+
Handles conversion between Markdown file embeds and Notion file blocks.
|
21
|
+
|
22
|
+
Markdown file syntax:
|
23
|
+
- [file](https://example.com/document.pdf) - URL only
|
24
|
+
- [file](https://example.com/document.pdf)(caption:Annual Report) - URL with caption
|
25
|
+
- (caption:Important document)[file](https://example.com/doc.pdf) - caption before URL
|
26
|
+
|
27
|
+
Supports external file URLs with optional captions.
|
28
|
+
"""
|
29
|
+
|
30
|
+
# Simple pattern that matches just the file link, CaptionMixin handles caption separately
|
31
|
+
FILE_PATTERN = re.compile(r"\[file\]\((https?://[^\s\"]+)\)")
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def _extract_file_url(cls, text: str) -> Optional[str]:
|
35
|
+
"""Extract file URL from text, handling caption patterns."""
|
36
|
+
# First remove any captions to get clean text for URL extraction
|
37
|
+
clean_text = cls.remove_caption(text)
|
38
|
+
|
39
|
+
# Now extract the URL from clean text
|
40
|
+
match = cls.FILE_PATTERN.search(clean_text)
|
41
|
+
if match:
|
42
|
+
return match.group(1)
|
43
|
+
|
44
|
+
return None
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def match_notion(cls, block: Block) -> bool:
|
48
|
+
# Notion file block covers files
|
49
|
+
return block.type == BlockType.FILE and block.file
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
53
|
+
"""Convert markdown file link to Notion FileBlock."""
|
54
|
+
# Use our helper method to extract the URL
|
55
|
+
url = cls._extract_file_url(text.strip())
|
56
|
+
if not url:
|
57
|
+
return None
|
58
|
+
|
59
|
+
# Use mixin to extract caption (if present anywhere in text)
|
60
|
+
caption_text = cls.extract_caption(text.strip())
|
61
|
+
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
62
|
+
|
63
|
+
# Build FileBlock using FileType enum
|
64
|
+
file_block = FileBlock(
|
65
|
+
type=FileType.EXTERNAL,
|
66
|
+
external=ExternalFile(url=url),
|
67
|
+
caption=caption_rich_text,
|
68
|
+
)
|
69
|
+
|
70
|
+
return CreateFileBlock(file=file_block)
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
74
|
+
if block.type != BlockType.FILE or not block.file:
|
75
|
+
return None
|
76
|
+
|
77
|
+
fb: FileBlock = block.file
|
78
|
+
|
79
|
+
# Determine URL (only external and file types are valid for Markdown)
|
80
|
+
if fb.type == FileType.EXTERNAL and fb.external:
|
81
|
+
url = fb.external.url
|
82
|
+
elif fb.type == FileType.FILE and fb.file:
|
83
|
+
url = fb.file.url
|
84
|
+
elif fb.type == FileType.FILE_UPLOAD:
|
85
|
+
# Uploaded file has no stable URL for Markdown
|
86
|
+
return None
|
87
|
+
else:
|
88
|
+
return None
|
89
|
+
|
90
|
+
result = f"[file]({url})"
|
91
|
+
|
92
|
+
# Add caption if present
|
93
|
+
caption_markdown = await cls.format_caption_for_markdown(fb.caption or [])
|
94
|
+
if caption_markdown:
|
95
|
+
result += caption_markdown
|
96
|
+
|
97
|
+
return result
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
101
|
+
"""Get system prompt information for file blocks."""
|
102
|
+
return BlockElementMarkdownInformation(
|
103
|
+
block_type=cls.__name__,
|
104
|
+
description="File blocks embed downloadable files from external URLs with optional captions",
|
105
|
+
syntax_examples=[
|
106
|
+
"[file](https://example.com/document.pdf)",
|
107
|
+
"[file](https://example.com/document.pdf)(caption:Annual Report)",
|
108
|
+
"(caption:Q1 Data)[file](https://example.com/spreadsheet.xlsx)",
|
109
|
+
"[file](https://example.com/manual.docx)(caption:**User** manual)",
|
110
|
+
],
|
111
|
+
usage_guidelines="Use for linking to downloadable files like PDFs, documents, spreadsheets. Supports various file formats. Caption supports rich text formatting and should describe the file content or purpose.",
|
112
|
+
)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from notionary.markdown.markdown_node import MarkdownNode
|
8
|
+
from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
|
9
|
+
|
10
|
+
|
11
|
+
class FileMarkdownNodeParams(BaseModel):
|
12
|
+
url: str
|
13
|
+
caption: Optional[str] = None
|
14
|
+
|
15
|
+
|
16
|
+
class FileMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
17
|
+
"""
|
18
|
+
Programmatic interface for creating Notion-style Markdown file embeds.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, url: str, caption: Optional[str] = None):
|
22
|
+
self.url = url
|
23
|
+
self.caption = caption or ""
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def from_params(cls, params: FileMarkdownNodeParams) -> FileMarkdownNode:
|
27
|
+
return cls(url=params.url, caption=params.caption)
|
28
|
+
|
29
|
+
def to_markdown(self) -> str:
|
30
|
+
"""Return the Markdown representation.
|
31
|
+
|
32
|
+
Examples:
|
33
|
+
- [file](https://example.com/document.pdf)
|
34
|
+
- [file](https://example.com/document.pdf)(caption:User manual)
|
35
|
+
"""
|
36
|
+
base_markdown = f"[file]({self.url})"
|
37
|
+
return self.append_caption_to_markdown(base_markdown, self.caption)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Literal, Optional
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
7
|
+
|
8
|
+
|
9
|
+
class FileType(str, Enum):
|
10
|
+
EXTERNAL = "external"
|
11
|
+
FILE = "file"
|
12
|
+
FILE_UPLOAD = "file_upload"
|
13
|
+
|
14
|
+
|
15
|
+
class ExternalFile(BaseModel):
|
16
|
+
url: str
|
17
|
+
|
18
|
+
|
19
|
+
class NotionHostedFile(BaseModel):
|
20
|
+
url: str
|
21
|
+
expiry_time: str
|
22
|
+
|
23
|
+
|
24
|
+
class FileUploadFile(BaseModel):
|
25
|
+
id: str
|
26
|
+
|
27
|
+
|
28
|
+
class FileBlock(BaseModel):
|
29
|
+
caption: list[RichTextObject] = Field(default_factory=list)
|
30
|
+
type: FileType
|
31
|
+
external: Optional[ExternalFile] = None
|
32
|
+
file: Optional[NotionHostedFile] = None
|
33
|
+
file_upload: Optional[FileUploadFile] = None
|
34
|
+
name: Optional[str] = None
|
35
|
+
|
36
|
+
|
37
|
+
class CreateFileBlock(BaseModel):
|
38
|
+
type: Literal["file"] = "file"
|
39
|
+
file: FileBlock
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from typing import Protocol
|
2
|
+
|
3
|
+
from notionary.blocks.models import BlockCreateRequest
|
4
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
5
|
+
|
6
|
+
|
7
|
+
class HasRichText(Protocol):
|
8
|
+
"""Protocol for objects that have a rich_text attribute."""
|
9
|
+
|
10
|
+
rich_text: list[RichTextObject]
|
11
|
+
|
12
|
+
|
13
|
+
class HasChildren(Protocol):
|
14
|
+
"""Protocol for objects that have children blocks."""
|
15
|
+
|
16
|
+
children: list[BlockCreateRequest]
|
17
|
+
|
18
|
+
|
19
|
+
class HasRichTextAndChildren(HasRichText, HasChildren, Protocol):
|
20
|
+
"""Protocol for objects that have both rich_text and children."""
|
21
|
+
|
22
|
+
pass
|
@@ -1,7 +1,21 @@
|
|
1
|
-
from .heading_element import HeadingElement
|
2
|
-
from .heading_markdown_node import
|
1
|
+
from notionary.blocks.heading.heading_element import HeadingElement
|
2
|
+
from notionary.blocks.heading.heading_markdown_node import (
|
3
|
+
HeadingMarkdownBlockParams,
|
4
|
+
HeadingMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.heading.heading_models import (
|
7
|
+
CreateHeading1Block,
|
8
|
+
CreateHeading2Block,
|
9
|
+
CreateHeading3Block,
|
10
|
+
HeadingBlock,
|
11
|
+
)
|
3
12
|
|
4
13
|
__all__ = [
|
5
14
|
"HeadingElement",
|
15
|
+
"HeadingBlock",
|
16
|
+
"CreateHeading1Block",
|
17
|
+
"CreateHeading2Block",
|
18
|
+
"CreateHeading3Block",
|
6
19
|
"HeadingMarkdownNode",
|
20
|
+
"HeadingMarkdownBlockParams",
|
7
21
|
]
|
@@ -1,98 +1,112 @@
|
|
1
|
-
import
|
2
|
-
from typing import Dict, Any, Optional
|
1
|
+
from __future__ import annotations
|
3
2
|
|
4
|
-
|
5
|
-
from
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
import re
|
4
|
+
from typing import Optional, cast
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.heading.heading_models import (
|
8
|
+
CreateHeading1Block,
|
9
|
+
CreateHeading2Block,
|
10
|
+
CreateHeading3Block,
|
11
|
+
HeadingBlock,
|
9
12
|
)
|
10
|
-
from notionary.blocks.
|
13
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
14
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
15
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
16
|
+
from notionary.blocks.types import BlockColor
|
11
17
|
|
12
18
|
|
13
|
-
class HeadingElement(
|
19
|
+
class HeadingElement(BaseBlockElement):
|
14
20
|
"""Handles conversion between Markdown headings and Notion heading blocks."""
|
15
21
|
|
16
|
-
# Pattern: #, ## oder ###, dann mind. 1 Leerzeichen/Tab, dann mind. 1 sichtbares Zeichen (kein Whitespace-only)
|
17
22
|
PATTERN = re.compile(r"^(#{1,3})[ \t]+(.+)$")
|
18
23
|
|
19
24
|
@classmethod
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
"""Check if block is a Notion heading."""
|
31
|
-
block_type: str = block.get("type", "")
|
32
|
-
return block_type.startswith("heading_") and block_type[-1] in "123"
|
25
|
+
def match_notion(cls, block: Block) -> bool:
|
26
|
+
return (
|
27
|
+
block.type
|
28
|
+
in (
|
29
|
+
BlockType.HEADING_1,
|
30
|
+
BlockType.HEADING_2,
|
31
|
+
BlockType.HEADING_3,
|
32
|
+
)
|
33
|
+
and getattr(block, block.type.value) is not None
|
34
|
+
)
|
33
35
|
|
34
36
|
@classmethod
|
35
|
-
def markdown_to_notion(cls, text: str) ->
|
36
|
-
"""Convert markdown
|
37
|
-
match = cls.PATTERN.match(text)
|
37
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
38
|
+
"""Convert markdown headings (#, ##, ###) to Notion HeadingBlock."""
|
39
|
+
match = cls.PATTERN.match(text.strip())
|
38
40
|
if not match:
|
39
41
|
return None
|
40
42
|
|
41
43
|
level = len(match.group(1))
|
42
|
-
if
|
44
|
+
if level < 1 or level > 3:
|
43
45
|
return None
|
44
46
|
|
45
|
-
content = match.group(2).
|
46
|
-
if not content
|
47
|
-
return None
|
47
|
+
content = match.group(2).strip()
|
48
|
+
if not content:
|
49
|
+
return None
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
},
|
54
|
-
}
|
55
|
-
return [header_block]
|
51
|
+
rich_text = await TextInlineFormatter.parse_inline_formatting(content)
|
52
|
+
heading_content = HeadingBlock(
|
53
|
+
rich_text=rich_text, color=BlockColor.DEFAULT, is_toggleable=False
|
54
|
+
)
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
56
|
+
if level == 1:
|
57
|
+
return CreateHeading1Block(heading_1=heading_content)
|
58
|
+
elif level == 2:
|
59
|
+
return CreateHeading2Block(heading_2=heading_content)
|
60
|
+
else:
|
61
|
+
return CreateHeading3Block(heading_3=heading_content)
|
61
62
|
|
62
|
-
|
63
|
+
@classmethod
|
64
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
65
|
+
# Only handle heading blocks via BlockType enum
|
66
|
+
if block.type not in (
|
67
|
+
BlockType.HEADING_1,
|
68
|
+
BlockType.HEADING_2,
|
69
|
+
BlockType.HEADING_3,
|
70
|
+
):
|
63
71
|
return None
|
64
72
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
73
|
+
# Determine heading level from enum
|
74
|
+
if block.type == BlockType.HEADING_1:
|
75
|
+
level = 1
|
76
|
+
elif block.type == BlockType.HEADING_2:
|
77
|
+
level = 2
|
78
|
+
else:
|
79
|
+
level = 3
|
80
|
+
|
81
|
+
heading_obj = getattr(block, block.type.value)
|
82
|
+
if not heading_obj:
|
70
83
|
return None
|
71
84
|
|
72
|
-
heading_data =
|
73
|
-
|
85
|
+
heading_data = cast(HeadingBlock, heading_obj)
|
86
|
+
if not heading_data.rich_text:
|
87
|
+
return None
|
74
88
|
|
75
|
-
text = TextInlineFormatter.extract_text_with_formatting(
|
76
|
-
|
77
|
-
|
89
|
+
text = await TextInlineFormatter.extract_text_with_formatting(
|
90
|
+
heading_data.rich_text
|
91
|
+
)
|
92
|
+
if not text:
|
93
|
+
return None
|
78
94
|
|
79
|
-
|
80
|
-
|
81
|
-
return False
|
95
|
+
# Use hash-style for all heading levels
|
96
|
+
return f"{('#' * level)} {text}"
|
82
97
|
|
83
98
|
@classmethod
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
.
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
"
|
92
|
-
|
93
|
-
|
94
|
-
"
|
95
|
-
|
96
|
-
.
|
97
|
-
.build()
|
99
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
100
|
+
"""Get system prompt information for heading blocks."""
|
101
|
+
return BlockElementMarkdownInformation(
|
102
|
+
block_type=cls.__name__,
|
103
|
+
description="Heading blocks create hierarchical document structure with different levels",
|
104
|
+
syntax_examples=[
|
105
|
+
"# Heading Level 1",
|
106
|
+
"## Heading Level 2",
|
107
|
+
"### Heading Level 3",
|
108
|
+
"# Heading with **bold text**",
|
109
|
+
"## Heading with *italic text*",
|
110
|
+
],
|
111
|
+
usage_guidelines="Use # for main titles, ## for sections, ### for subsections. Supports inline formatting. Only levels 1-3 are supported in Notion.",
|
98
112
|
)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from notionary.blocks.models import Block
|
6
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
7
|
+
from notionary.blocks.types import BlockColor
|
8
|
+
|
9
|
+
|
10
|
+
class HeadingBlock(BaseModel):
|
11
|
+
rich_text: list[RichTextObject]
|
12
|
+
color: BlockColor = BlockColor.DEFAULT
|
13
|
+
is_toggleable: bool = False
|
14
|
+
children: list[Block] = Field(default_factory=list)
|
15
|
+
|
16
|
+
|
17
|
+
class CreateHeading1Block(BaseModel):
|
18
|
+
type: Literal["heading_1"] = "heading_1"
|
19
|
+
heading_1: HeadingBlock
|
20
|
+
|
21
|
+
|
22
|
+
class CreateHeading2Block(BaseModel):
|
23
|
+
type: Literal["heading_2"] = "heading_2"
|
24
|
+
heading_2: HeadingBlock
|
25
|
+
|
26
|
+
|
27
|
+
class CreateHeading3Block(BaseModel):
|
28
|
+
type: Literal["heading_3"] = "heading_3"
|
29
|
+
heading_3: HeadingBlock
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from notionary.blocks.image_block.image_element import ImageElement
|
2
|
+
from notionary.blocks.image_block.image_markdown_node import (
|
3
|
+
ImageMarkdownBlockParams,
|
4
|
+
ImageMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.image_block.image_models import CreateImageBlock
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"ImageElement",
|
10
|
+
"CreateImageBlock",
|
11
|
+
"ImageMarkdownNode",
|
12
|
+
"ImageMarkdownBlockParams",
|
13
|
+
]
|
@@ -0,0 +1,89 @@
|
|
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.file.file_element_models import ExternalFile, FileType
|
8
|
+
from notionary.blocks.image_block.image_models import CreateImageBlock, FileBlock
|
9
|
+
from notionary.blocks.mixins.captions import CaptionMixin
|
10
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
11
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
12
|
+
|
13
|
+
|
14
|
+
class ImageElement(BaseBlockElement, CaptionMixin):
|
15
|
+
"""
|
16
|
+
Handles conversion between Markdown images and Notion image blocks.
|
17
|
+
|
18
|
+
Markdown image syntax:
|
19
|
+
- [image](https://example.com/image.jpg) - URL only
|
20
|
+
- [image](https://example.com/image.jpg)(caption:This is a caption) - URL with caption
|
21
|
+
- (caption:Profile picture)[image](https://example.com/avatar.jpg) - caption before URL
|
22
|
+
"""
|
23
|
+
|
24
|
+
# Flexible pattern that can handle caption in any position
|
25
|
+
IMAGE_PATTERN = re.compile(r"\[image\]\((https?://[^\s\"]+)\)")
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def match_notion(cls, block: Block) -> bool:
|
29
|
+
return block.type == BlockType.IMAGE and block.image
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
33
|
+
"""Convert markdown image syntax to Notion ImageBlock."""
|
34
|
+
clean_text = cls.remove_caption(text.strip())
|
35
|
+
|
36
|
+
# Use our own regex to find the image URL
|
37
|
+
image_match = cls.IMAGE_PATTERN.search(clean_text)
|
38
|
+
if not image_match:
|
39
|
+
return None
|
40
|
+
|
41
|
+
url = image_match.group(1)
|
42
|
+
|
43
|
+
caption_text = cls.extract_caption(text.strip())
|
44
|
+
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
45
|
+
|
46
|
+
# Build ImageBlock
|
47
|
+
image_block = FileBlock(
|
48
|
+
type="external", external=ExternalFile(url=url), caption=caption_rich_text
|
49
|
+
)
|
50
|
+
|
51
|
+
return CreateImageBlock(image=image_block)
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
55
|
+
if block.type != BlockType.IMAGE or not block.image:
|
56
|
+
return None
|
57
|
+
|
58
|
+
fo = block.image
|
59
|
+
|
60
|
+
if fo.type == FileType.EXTERNAL and fo.external:
|
61
|
+
url = fo.external.url
|
62
|
+
elif fo.type == FileType.FILE and fo.file:
|
63
|
+
url = fo.file.url
|
64
|
+
else:
|
65
|
+
return None
|
66
|
+
|
67
|
+
result = f"[image]({url})"
|
68
|
+
|
69
|
+
# Add caption if present
|
70
|
+
caption_markdown = await cls.format_caption_for_markdown(fo.caption or [])
|
71
|
+
if caption_markdown:
|
72
|
+
result += caption_markdown
|
73
|
+
|
74
|
+
return result
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
78
|
+
"""Get system prompt information for image blocks."""
|
79
|
+
return BlockElementMarkdownInformation(
|
80
|
+
block_type=cls.__name__,
|
81
|
+
description="Image blocks display images from external URLs with optional captions",
|
82
|
+
syntax_examples=[
|
83
|
+
"[image](https://example.com/photo.jpg)",
|
84
|
+
"[image](https://example.com/diagram.png)(caption:Architecture Diagram)",
|
85
|
+
"(caption:Sales Chart)[image](https://example.com/chart.svg)",
|
86
|
+
"[image](https://example.com/screenshot.png)(caption:Dashboard **overview**)",
|
87
|
+
],
|
88
|
+
usage_guidelines="Use for displaying images from external URLs. Supports common image formats (jpg, png, gif, svg, webp). Caption supports rich text formatting and describes the image content.",
|
89
|
+
)
|
@@ -1,8 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from typing import Optional
|
4
|
+
|
4
5
|
from pydantic import BaseModel
|
5
|
-
|
6
|
+
|
7
|
+
from notionary.markdown.markdown_node import MarkdownNode
|
8
|
+
from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
|
6
9
|
|
7
10
|
|
8
11
|
class ImageMarkdownBlockParams(BaseModel):
|
@@ -10,10 +13,9 @@ class ImageMarkdownBlockParams(BaseModel):
|
|
10
13
|
caption: Optional[str] = None
|
11
14
|
|
12
15
|
|
13
|
-
class ImageMarkdownNode(MarkdownNode):
|
16
|
+
class ImageMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
14
17
|
"""
|
15
18
|
Programmatic interface for creating Notion-style image blocks.
|
16
|
-
Example: [image](https://example.com/image.jpg "Optional caption")
|
17
19
|
"""
|
18
20
|
|
19
21
|
def __init__(
|
@@ -28,6 +30,11 @@ class ImageMarkdownNode(MarkdownNode):
|
|
28
30
|
return cls(url=params.url, caption=params.caption)
|
29
31
|
|
30
32
|
def to_markdown(self) -> str:
|
31
|
-
|
32
|
-
|
33
|
-
|
33
|
+
"""Return the Markdown representation.
|
34
|
+
|
35
|
+
Examples:
|
36
|
+
- [image](https://example.com/screenshot.png)
|
37
|
+
- [image](https://example.com/screenshot.png)(caption:Dashboard overview)
|
38
|
+
"""
|
39
|
+
base_markdown = f"[image]({self.url})"
|
40
|
+
return self.append_caption_to_markdown(base_markdown, self.caption)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
|
4
|
+
class CaptionMarkdownNodeMixin:
|
5
|
+
"""Mixin to add caption functionality to MarkdownNode classes."""
|
6
|
+
|
7
|
+
@classmethod
|
8
|
+
def append_caption_to_markdown(
|
9
|
+
cls, base_markdown: str, caption: Optional[str]
|
10
|
+
) -> str:
|
11
|
+
"""
|
12
|
+
Append caption to existing markdown if caption is present.
|
13
|
+
Returns: base_markdown + "(caption:...)" or just base_markdown
|
14
|
+
"""
|
15
|
+
if not caption:
|
16
|
+
return base_markdown
|
17
|
+
return f"{base_markdown}(caption:{caption})"
|
18
|
+
|
19
|
+
@classmethod
|
20
|
+
def format_caption_for_markdown(cls, caption: Optional[str]) -> str:
|
21
|
+
"""
|
22
|
+
Format caption text for markdown output.
|
23
|
+
Returns: "(caption:...)" or empty string
|
24
|
+
"""
|
25
|
+
if not caption:
|
26
|
+
return ""
|
27
|
+
return f"(caption:{caption})"
|
28
|
+
|
29
|
+
def has_caption(self) -> bool:
|
30
|
+
"""Check if this node has a caption."""
|
31
|
+
return hasattr(self, "caption") and bool(getattr(self, "caption", None))
|