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,97 @@
|
|
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, FileBlock, FileType
|
8
|
+
from notionary.blocks.mixins.captions import CaptionMixin
|
9
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
10
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
11
|
+
from notionary.blocks.pdf.pdf_models import CreatePdfBlock
|
12
|
+
|
13
|
+
|
14
|
+
class PdfElement(BaseBlockElement, CaptionMixin):
|
15
|
+
"""
|
16
|
+
Handles conversion between Markdown PDF embeds and Notion PDF blocks.
|
17
|
+
|
18
|
+
Markdown PDF syntax:
|
19
|
+
- [pdf](https://example.com/document.pdf) - External URL
|
20
|
+
- [pdf](https://example.com/document.pdf)(caption:Annual Report 2024) - URL with caption
|
21
|
+
- (caption:User Manual)[pdf](https://example.com/manual.pdf) - caption before URL
|
22
|
+
- [pdf](notion://file_id_here)(caption:Notion hosted file) - Notion hosted file
|
23
|
+
- [pdf](upload://upload_id_here)(caption:File upload) - File upload
|
24
|
+
|
25
|
+
Supports all three PDF types: external, notion-hosted, and file uploads.
|
26
|
+
"""
|
27
|
+
|
28
|
+
# Flexible pattern that can handle caption in any position
|
29
|
+
PDF_PATTERN = re.compile(r"\[pdf\]\(((?:https?://|notion://|upload://)[^\s\"]+)\)")
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def match_notion(cls, block: Block) -> bool:
|
33
|
+
# Notion PDF block covers PDFs
|
34
|
+
return block.type == BlockType.PDF and block.pdf
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
38
|
+
"""Convert markdown PDF link to Notion FileBlock (used for PDF)."""
|
39
|
+
# Use our own regex to find the PDF URL
|
40
|
+
pdf_match = cls.PDF_PATTERN.search(text.strip())
|
41
|
+
if not pdf_match:
|
42
|
+
return None
|
43
|
+
|
44
|
+
url = pdf_match.group(1)
|
45
|
+
|
46
|
+
# Use mixin to extract caption (if present anywhere in text)
|
47
|
+
caption_text = cls.extract_caption(text.strip())
|
48
|
+
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
49
|
+
|
50
|
+
# Build FileBlock using FileType enum (reused for PDF)
|
51
|
+
pdf_block = FileBlock(
|
52
|
+
type=FileType.EXTERNAL,
|
53
|
+
external=ExternalFile(url=url),
|
54
|
+
caption=caption_rich_text,
|
55
|
+
)
|
56
|
+
|
57
|
+
return CreatePdfBlock(pdf=pdf_block)
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
61
|
+
if block.type != BlockType.PDF or not block.pdf:
|
62
|
+
return None
|
63
|
+
|
64
|
+
pb: FileBlock = block.pdf
|
65
|
+
|
66
|
+
if pb.type == FileType.EXTERNAL and pb.external:
|
67
|
+
url = pb.external.url
|
68
|
+
elif pb.type == FileType.FILE and pb.file:
|
69
|
+
url = pb.file.url
|
70
|
+
elif pb.type == FileType.FILE_UPLOAD:
|
71
|
+
return None
|
72
|
+
else:
|
73
|
+
return None
|
74
|
+
|
75
|
+
result = f"[pdf]({url})"
|
76
|
+
|
77
|
+
# Add caption if present
|
78
|
+
caption_markdown = await cls.format_caption_for_markdown(pb.caption or [])
|
79
|
+
if caption_markdown:
|
80
|
+
result += caption_markdown
|
81
|
+
|
82
|
+
return result
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
86
|
+
"""Get system prompt information for PDF blocks."""
|
87
|
+
return BlockElementMarkdownInformation(
|
88
|
+
block_type=cls.__name__,
|
89
|
+
description="PDF blocks embed and display PDF documents from external URLs with optional captions",
|
90
|
+
syntax_examples=[
|
91
|
+
"[pdf](https://example.com/document.pdf)",
|
92
|
+
"[pdf](https://example.com/report.pdf)(caption:Annual Report 2024)",
|
93
|
+
"(caption:User Manual)[pdf](https://example.com/manual.pdf)",
|
94
|
+
"[pdf](https://example.com/guide.pdf)(caption:**Important** documentation)",
|
95
|
+
],
|
96
|
+
usage_guidelines="Use for embedding PDF documents that can be viewed inline. Supports external URLs and Notion-hosted files. Caption supports rich text formatting and should describe the PDF content.",
|
97
|
+
)
|
@@ -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 PdfMarkdownNodeParams(BaseModel):
|
12
|
+
url: str
|
13
|
+
caption: Optional[str] = None
|
14
|
+
|
15
|
+
|
16
|
+
class PdfMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
17
|
+
"""
|
18
|
+
Programmatic interface for creating Notion-style Markdown PDF 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: PdfMarkdownNodeParams) -> PdfMarkdownNode:
|
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
|
+
- [pdf](https://example.com/document.pdf)
|
34
|
+
- [pdf](https://example.com/document.pdf)(caption:Critical safety information)
|
35
|
+
"""
|
36
|
+
base_markdown = f"[pdf]({self.url})"
|
37
|
+
return self.append_caption_to_markdown(base_markdown, self.caption)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from notionary.blocks.file.file_element_models import FileBlock
|
6
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
7
|
+
|
8
|
+
|
9
|
+
class CreatePdfBlock(BaseModel):
|
10
|
+
type: Literal["pdf"] = "pdf"
|
11
|
+
pdf: FileBlock
|
@@ -1,7 +1,16 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
"""Quote block handling for Notionary."""
|
2
|
+
|
3
|
+
from notionary.blocks.quote.quote_element import QuoteElement
|
4
|
+
from notionary.blocks.quote.quote_markdown_node import (
|
5
|
+
QuoteMarkdownBlockParams,
|
6
|
+
QuoteMarkdownNode,
|
7
|
+
)
|
8
|
+
from notionary.blocks.quote.quote_models import CreateQuoteBlock, QuoteBlock
|
3
9
|
|
4
10
|
__all__ = [
|
5
11
|
"QuoteElement",
|
12
|
+
"QuoteBlock",
|
13
|
+
"CreateQuoteBlock",
|
6
14
|
"QuoteMarkdownNode",
|
15
|
+
"QuoteMarkdownBlockParams",
|
7
16
|
]
|
@@ -1,34 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import re
|
2
|
-
from typing import
|
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, BlockCreateResult, BlockType
|
9
|
+
from notionary.blocks.quote.quote_models import CreateQuoteBlock, QuoteBlock
|
10
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
11
|
+
from notionary.blocks.types import BlockColor
|
3
12
|
|
4
|
-
from notionary.blocks import NotionBlockElement, NotionBlockResult
|
5
|
-
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
6
13
|
|
7
|
-
class QuoteElement(
|
14
|
+
class QuoteElement(BaseBlockElement):
|
8
15
|
"""
|
9
16
|
Handles conversion between Markdown quotes and Notion quote blocks.
|
17
|
+
|
10
18
|
Markdown quote syntax:
|
11
|
-
-
|
19
|
+
- > Simple quote text
|
20
|
+
|
21
|
+
Only single-line quotes without author metadata.
|
12
22
|
"""
|
13
23
|
|
14
|
-
|
15
|
-
PATTERN = re.compile(r'^\[quote\]\(([^\n\r]+)\)$')
|
24
|
+
PATTERN = re.compile(r"^>\s*(.+)$")
|
16
25
|
|
17
26
|
@classmethod
|
18
|
-
def
|
19
|
-
|
20
|
-
# Nur gültig, wenn etwas nicht-leeres drinsteht
|
21
|
-
return bool(m and m.group(1).strip())
|
27
|
+
def match_notion(cls, block: Block) -> bool:
|
28
|
+
return block.type == BlockType.QUOTE and block.quote
|
22
29
|
|
23
30
|
@classmethod
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
@classmethod
|
28
|
-
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
29
|
-
if not text:
|
30
|
-
return None
|
31
|
-
|
31
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
32
|
+
"""Convert markdown quote to Notion QuoteBlock."""
|
32
33
|
match = cls.PATTERN.match(text.strip())
|
33
34
|
if not match:
|
34
35
|
return None
|
@@ -37,56 +38,38 @@ class QuoteElement(NotionBlockElement):
|
|
37
38
|
if not content:
|
38
39
|
return None
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
45
|
-
if block.get("type") != "quote":
|
41
|
+
# Reject multiline quotes
|
42
|
+
if "\n" in content or "\r" in content:
|
46
43
|
return None
|
47
44
|
|
48
|
-
rich_text =
|
49
|
-
content = cls._extract_text_content(rich_text)
|
50
|
-
if not content.strip():
|
51
|
-
return None
|
45
|
+
rich_text = await TextInlineFormatter.parse_inline_formatting(content)
|
52
46
|
|
53
|
-
|
47
|
+
quote_content = QuoteBlock(rich_text=rich_text, color=BlockColor.DEFAULT)
|
48
|
+
return CreateQuoteBlock(quote=quote_content)
|
54
49
|
|
55
50
|
@classmethod
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
candidate = match.group(0)
|
60
|
-
block = cls.markdown_to_notion(candidate)
|
61
|
-
if block:
|
62
|
-
matches.append((match.start(), match.end(), block))
|
63
|
-
return matches
|
51
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
52
|
+
if block.type != BlockType.QUOTE or not block.quote:
|
53
|
+
return None
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
return False
|
55
|
+
rich = block.quote.rich_text
|
56
|
+
text = await TextInlineFormatter.extract_text_with_formatting(rich)
|
68
57
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
for t in rich_text
|
74
|
-
if t.get("type") == "text"
|
75
|
-
)
|
58
|
+
if not text.strip():
|
59
|
+
return None
|
60
|
+
|
61
|
+
return f"> {text.strip()}"
|
76
62
|
|
77
63
|
@classmethod
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
.
|
82
|
-
|
83
|
-
|
84
|
-
"
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
"[quote](Knowledge is power)",
|
90
|
-
])
|
91
|
-
.build()
|
64
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
65
|
+
"""Get system prompt information for quote blocks."""
|
66
|
+
return BlockElementMarkdownInformation(
|
67
|
+
block_type=cls.__name__,
|
68
|
+
description="Quote blocks display highlighted quotations or emphasized text",
|
69
|
+
syntax_examples=[
|
70
|
+
"> This is an important quote",
|
71
|
+
"> The only way to do great work is to love what you do",
|
72
|
+
"> Innovation distinguishes between a leader and a follower",
|
73
|
+
],
|
74
|
+
usage_guidelines="Use for quotations, important statements, or text that should be visually emphasized. Content should be meaningful and stand out from regular paragraphs.",
|
92
75
|
)
|
@@ -1,15 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from pydantic import BaseModel
|
4
|
-
|
4
|
+
|
5
|
+
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
+
|
5
7
|
|
6
8
|
class QuoteMarkdownBlockParams(BaseModel):
|
7
9
|
text: str
|
8
10
|
|
11
|
+
|
9
12
|
class QuoteMarkdownNode(MarkdownNode):
|
10
13
|
"""
|
11
14
|
Programmatic interface for creating Notion-style quote blocks.
|
12
|
-
Example:
|
15
|
+
Example: > This is a quote
|
13
16
|
"""
|
14
17
|
|
15
18
|
def __init__(self, text: str):
|
@@ -20,4 +23,4 @@ class QuoteMarkdownNode(MarkdownNode):
|
|
20
23
|
return cls(text=params.text)
|
21
24
|
|
22
25
|
def to_markdown(self) -> str:
|
23
|
-
return f"
|
26
|
+
return f"> {self.text}"
|
@@ -0,0 +1,18 @@
|
|
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 QuoteBlock(BaseModel):
|
11
|
+
rich_text: list[RichTextObject]
|
12
|
+
color: BlockColor = BlockColor.DEFAULT
|
13
|
+
children: list[Block] = Field(default_factory=list)
|
14
|
+
|
15
|
+
|
16
|
+
class CreateQuoteBlock(BaseModel):
|
17
|
+
type: Literal["quote"] = "quote"
|
18
|
+
quote: QuoteBlock
|
@@ -1,156 +1,95 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import Dict, Any, Optional, List, Set, Type
|
3
2
|
|
4
|
-
from
|
5
|
-
from notionary.page.markdown_syntax_prompt_generator import (
|
6
|
-
MarkdownSyntaxPromptGenerator,
|
7
|
-
)
|
8
|
-
from notionary.blocks import TextInlineFormatter
|
3
|
+
from typing import Optional, Type
|
9
4
|
|
10
|
-
from notionary.blocks import
|
5
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
6
|
+
from notionary.blocks.registry.block_registry_builder import BlockRegistryBuilder
|
11
7
|
from notionary.telemetry import (
|
12
8
|
ProductTelemetry,
|
13
|
-
NotionMarkdownSyntaxPromptEvent,
|
14
|
-
MarkdownToNotionConversionEvent,
|
15
|
-
NotionToMarkdownConversionEvent,
|
16
9
|
)
|
17
10
|
|
18
11
|
|
19
12
|
class BlockRegistry:
|
20
13
|
"""Registry of elements that can convert between Markdown and Notion."""
|
21
14
|
|
22
|
-
def __init__(self,
|
15
|
+
def __init__(self, builder: Optional[BlockRegistryBuilder] = None):
|
23
16
|
"""
|
24
17
|
Initialize a new registry instance.
|
25
18
|
|
26
19
|
Args:
|
27
|
-
|
20
|
+
builder: BlockRegistryBuilder instance to delegate operations to
|
28
21
|
"""
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
for element in elements:
|
34
|
-
self.register(element)
|
22
|
+
# Import here to avoid circular imports
|
23
|
+
from notionary.blocks.registry.block_registry_builder import (
|
24
|
+
BlockRegistryBuilder,
|
25
|
+
)
|
35
26
|
|
27
|
+
self._builder: BlockRegistryBuilder = builder or BlockRegistryBuilder()
|
36
28
|
self.telemetry = ProductTelemetry()
|
37
29
|
|
38
30
|
@classmethod
|
39
31
|
def create_registry(cls) -> BlockRegistry:
|
40
32
|
"""
|
41
33
|
Create a registry with all standard elements in recommended order.
|
42
|
-
|
43
|
-
This uses the BlockRegistryBuilder internally to construct a complete
|
44
|
-
registry with all available block types.
|
45
|
-
|
46
|
-
Returns:
|
47
|
-
BlockRegistry: A fully configured registry with all standard elements
|
48
34
|
"""
|
49
|
-
from notionary.blocks import
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
35
|
+
from notionary.blocks.registry.block_registry_builder import (
|
36
|
+
BlockRegistryBuilder,
|
37
|
+
)
|
38
|
+
|
39
|
+
builder = BlockRegistryBuilder()
|
40
|
+
builder = (
|
41
|
+
builder.with_headings()
|
42
|
+
.with_callouts()
|
43
|
+
.with_code()
|
44
|
+
.with_dividers()
|
45
|
+
.with_tables()
|
46
|
+
.with_bulleted_list()
|
47
|
+
.with_numbered_list()
|
48
|
+
.with_toggles()
|
49
|
+
.with_toggleable_heading_element()
|
50
|
+
.with_quotes()
|
51
|
+
.with_todos()
|
52
|
+
.with_bookmarks()
|
53
|
+
.with_images()
|
54
|
+
.with_videos()
|
55
|
+
.with_embeds()
|
56
|
+
.with_audio()
|
57
|
+
.with_columns()
|
58
|
+
.with_equation()
|
59
|
+
.with_table_of_contents()
|
60
|
+
.with_breadcrumbs()
|
61
|
+
.with_child_database()
|
62
|
+
.with_paragraphs() # position here is important - its a fallback!
|
63
|
+
)
|
64
|
+
|
65
|
+
return cls(builder=builder)
|
66
|
+
|
67
|
+
@property
|
68
|
+
def builder(self) -> BlockRegistryBuilder:
|
69
|
+
return self._builder
|
70
|
+
|
71
|
+
def register(self, element_class: Type[BaseBlockElement]) -> bool:
|
54
72
|
"""
|
55
|
-
Register an element class.
|
56
|
-
|
57
|
-
Args:
|
58
|
-
element_class: The element class to register
|
59
|
-
|
60
|
-
Returns:
|
61
|
-
bool: True if element was added, False if it already existed
|
73
|
+
Register an element class via builder.
|
62
74
|
"""
|
63
|
-
|
64
|
-
|
75
|
+
initial_count = len(self._builder._elements)
|
76
|
+
self._builder._add_element(element_class)
|
77
|
+
return len(self._builder._elements) > initial_count
|
65
78
|
|
66
|
-
|
67
|
-
self._element_types.add(element_class)
|
68
|
-
return True
|
69
|
-
|
70
|
-
def deregister(self, element_class: Type[NotionBlockElement]) -> bool:
|
79
|
+
def deregister(self, element_class: Type[BaseBlockElement]) -> bool:
|
71
80
|
"""
|
72
|
-
Deregister an element class.
|
81
|
+
Deregister an element class via builder.
|
73
82
|
"""
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
return True
|
78
|
-
return False
|
83
|
+
initial_count = len(self._builder._elements)
|
84
|
+
self._builder.remove_element(element_class)
|
85
|
+
return len(self._builder._elements) < initial_count
|
79
86
|
|
80
|
-
def contains(self, element_class: Type[
|
87
|
+
def contains(self, element_class: Type[BaseBlockElement]) -> bool:
|
81
88
|
"""
|
82
89
|
Checks if a specific element is contained in the registry.
|
83
|
-
|
84
|
-
Args:
|
85
|
-
element_class: The element class to check.
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
bool: True if the element is contained, otherwise False.
|
89
90
|
"""
|
90
|
-
return element_class in self._elements
|
91
|
-
|
92
|
-
def find_markdown_handler(self, text: str) -> Optional[Type[NotionBlockElement]]:
|
93
|
-
"""Find an element that can handle the given markdown text."""
|
94
|
-
for element in self._elements:
|
95
|
-
if element.match_markdown(text):
|
96
|
-
return element
|
97
|
-
return None
|
91
|
+
return element_class.__name__ in self._builder._elements
|
98
92
|
|
99
|
-
def
|
100
|
-
"""Convert markdown to Notion block using registered elements."""
|
101
|
-
handler = self.find_markdown_handler(text)
|
102
|
-
|
103
|
-
if handler:
|
104
|
-
self.telemetry.capture(
|
105
|
-
MarkdownToNotionConversionEvent(
|
106
|
-
handler_element_name=handler.__name__,
|
107
|
-
)
|
108
|
-
)
|
109
|
-
|
110
|
-
return handler.markdown_to_notion(text)
|
111
|
-
return None
|
112
|
-
|
113
|
-
def notion_to_markdown(self, block: Dict[str, Any]) -> Optional[str]:
|
114
|
-
"""Convert Notion block to markdown using registered elements."""
|
115
|
-
handler = self._find_notion_handler(block)
|
116
|
-
|
117
|
-
if handler:
|
118
|
-
self.telemetry.capture(
|
119
|
-
NotionToMarkdownConversionEvent(
|
120
|
-
handler_element_name=handler.__name__,
|
121
|
-
)
|
122
|
-
)
|
123
|
-
|
124
|
-
return handler.notion_to_markdown(block)
|
125
|
-
return None
|
126
|
-
|
127
|
-
def get_multiline_elements(self) -> List[Type[NotionBlockElement]]:
|
128
|
-
"""Get all registered multiline elements."""
|
129
|
-
return [element for element in self._elements if element.is_multiline()]
|
130
|
-
|
131
|
-
def get_elements(self) -> List[Type[NotionBlockElement]]:
|
93
|
+
def get_elements(self) -> list[Type[BaseBlockElement]]:
|
132
94
|
"""Get all registered elements."""
|
133
|
-
return self._elements.
|
134
|
-
|
135
|
-
def get_notion_markdown_syntax_prompt(self) -> str:
|
136
|
-
"""
|
137
|
-
Generates an LLM system prompt that describes the Markdown syntax of all registered elements.
|
138
|
-
"""
|
139
|
-
element_classes = self._elements.copy()
|
140
|
-
|
141
|
-
formatter_names = [e.__name__ for e in element_classes]
|
142
|
-
if "TextInlineFormatter" not in formatter_names:
|
143
|
-
element_classes = element_classes + [TextInlineFormatter]
|
144
|
-
|
145
|
-
self.telemetry.capture(NotionMarkdownSyntaxPromptEvent())
|
146
|
-
|
147
|
-
return MarkdownSyntaxPromptGenerator.generate_system_prompt(element_classes)
|
148
|
-
|
149
|
-
def _find_notion_handler(
|
150
|
-
self, block: Dict[str, Any]
|
151
|
-
) -> Optional[Type[NotionBlockElement]]:
|
152
|
-
"""Find an element that can handle the given Notion block."""
|
153
|
-
for element in self._elements:
|
154
|
-
if element.match_notion(block):
|
155
|
-
return element
|
156
|
-
return None
|
95
|
+
return list(self._builder._elements.values())
|