notionary 0.2.19__py3-none-any.whl → 0.2.21__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 +263 -0
- notionary/blocks/audio/__init__.py +8 -2
- notionary/blocks/audio/audio_element.py +42 -104
- notionary/blocks/audio/audio_markdown_node.py +3 -1
- notionary/blocks/audio/audio_models.py +6 -55
- notionary/blocks/base_block_element.py +30 -0
- notionary/blocks/bookmark/__init__.py +9 -2
- notionary/blocks/bookmark/bookmark_element.py +46 -139
- notionary/blocks/bookmark/bookmark_markdown_node.py +3 -1
- 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 +40 -55
- 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 +40 -89
- notionary/blocks/callout/callout_markdown_node.py +3 -1
- notionary/blocks/callout/callout_models.py +33 -0
- notionary/blocks/child_database/__init__.py +7 -0
- notionary/blocks/child_database/child_database_models.py +19 -0
- notionary/blocks/child_page/__init__.py +9 -0
- notionary/blocks/child_page/child_page_models.py +12 -0
- notionary/blocks/{shared/block_client.py → client.py} +55 -54
- notionary/blocks/code/__init__.py +6 -2
- notionary/blocks/code/code_element.py +53 -187
- notionary/blocks/code/code_markdown_node.py +13 -13
- notionary/blocks/code/code_models.py +94 -0
- notionary/blocks/column/__init__.py +25 -1
- notionary/blocks/column/column_element.py +40 -314
- notionary/blocks/column/column_list_element.py +37 -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 +26 -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 +47 -114
- 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 +80 -0
- notionary/blocks/equation/equation_element_markdown_node.py +36 -0
- notionary/blocks/equation/equation_models.py +11 -0
- notionary/blocks/file/__init__.py +25 -0
- notionary/blocks/file/file_element.py +93 -0
- notionary/blocks/file/file_element_markdown_node.py +35 -0
- notionary/blocks/file/file_element_models.py +39 -0
- notionary/blocks/heading/__init__.py +16 -2
- notionary/blocks/heading/heading_element.py +67 -72
- 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 +84 -0
- notionary/blocks/{image → image_block}/image_markdown_node.py +3 -1
- notionary/blocks/image_block/image_models.py +10 -0
- notionary/blocks/models.py +172 -0
- notionary/blocks/numbered_list/__init__.py +12 -2
- notionary/blocks/numbered_list/numbered_list_element.py +33 -58
- 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 +27 -69
- 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 +91 -0
- notionary/blocks/pdf/pdf_markdown_node.py +35 -0
- notionary/blocks/pdf/pdf_models.py +11 -0
- notionary/blocks/quote/__init__.py +11 -2
- notionary/blocks/quote/quote_element.py +31 -65
- notionary/blocks/quote/quote_markdown_node.py +4 -1
- notionary/blocks/quote/quote_models.py +18 -0
- notionary/blocks/registry/__init__.py +4 -0
- notionary/blocks/registry/block_registry.py +75 -91
- notionary/blocks/registry/block_registry_builder.py +107 -59
- notionary/blocks/rich_text/__init__.py +33 -0
- notionary/blocks/rich_text/rich_text_models.py +188 -0
- notionary/blocks/rich_text/text_inline_formatter.py +125 -0
- notionary/blocks/table/__init__.py +16 -2
- notionary/blocks/table/table_element.py +48 -241
- 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 +51 -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 +38 -95
- 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 +57 -264
- notionary/blocks/toggle/toggle_markdown_node.py +24 -14
- notionary/blocks/toggle/toggle_models.py +17 -0
- notionary/blocks/toggleable_heading/__init__.py +6 -2
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +74 -244
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
- notionary/blocks/types.py +61 -0
- notionary/blocks/video/__init__.py +8 -2
- notionary/blocks/video/video_element.py +67 -143
- notionary/blocks/video/video_element_models.py +10 -0
- notionary/blocks/video/video_markdown_node.py +3 -1
- notionary/database/client.py +3 -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 +2 -1
- notionary/file_upload/notion_file_upload.py +2 -3
- notionary/markdown/markdown_builder.py +722 -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 +9 -10
- notionary/page/models.py +327 -0
- notionary/page/notion_page.py +99 -52
- notionary/page/notion_text_length_utils.py +119 -0
- notionary/page/{content/page_content_writer.py → page_content_writer.py} +88 -38
- notionary/page/reader/handler/__init__.py +17 -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 +43 -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 +60 -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 +69 -0
- notionary/page/search_filter_builder.py +2 -1
- notionary/page/writer/handler/__init__.py +22 -0
- notionary/page/writer/handler/code_handler.py +100 -0
- notionary/page/writer/handler/column_handler.py +141 -0
- notionary/page/writer/handler/column_list_handler.py +139 -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 +92 -0
- notionary/page/writer/handler/table_handler.py +130 -0
- notionary/page/writer/handler/toggle_handler.py +153 -0
- notionary/page/writer/handler/toggleable_heading_handler.py +167 -0
- notionary/page/writer/markdown_to_notion_converter.py +76 -0
- notionary/telemetry/__init__.py +2 -2
- notionary/telemetry/service.py +4 -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 +3 -2
- notionary/user/notion_user_provider.py +1 -1
- 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 +3 -2
- {notionary-0.2.19.dist-info → notionary-0.2.21.dist-info}/METADATA +12 -8
- notionary-0.2.21.dist-info/RECORD +185 -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/__init__.py +0 -0
- 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/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/database/models/page_result.py +0 -10
- notionary/elements/__init__.py +0 -0
- 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/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-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/page/{content/markdown_whitespace_processor.py → markdown_whitespace_processor.py} +0 -0
- /notionary/{blocks/mention/mention_models.py → page/reader/handler/context.py} +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.21.dist-info}/LICENSE +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.21.dist-info}/WHEEL +0 -0
@@ -1,7 +1,14 @@
|
|
1
|
-
from .embed_element import EmbedElement
|
2
|
-
from .embed_markdown_node import
|
1
|
+
from notionary.blocks.embed.embed_element import EmbedElement
|
2
|
+
from notionary.blocks.embed.embed_markdown_node import (
|
3
|
+
EmbedMarkdownBlockParams,
|
4
|
+
EmbedMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.embed.embed_models import CreateEmbedBlock, EmbedBlock
|
3
7
|
|
4
8
|
__all__ = [
|
5
9
|
"EmbedElement",
|
10
|
+
"EmbedBlock",
|
11
|
+
"CreateEmbedBlock",
|
6
12
|
"EmbedMarkdownNode",
|
13
|
+
"EmbedMarkdownBlockParams",
|
7
14
|
]
|
@@ -1,144 +1,77 @@
|
|
1
|
-
import
|
2
|
-
from typing import Dict, Any, Optional, List
|
1
|
+
from __future__ import annotations
|
3
2
|
|
4
|
-
|
5
|
-
from
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
import re
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.embed.embed_models import CreateEmbedBlock, EmbedBlock
|
8
|
+
from notionary.blocks.file.file_element_models import (
|
9
|
+
ExternalFile,
|
10
|
+
FileUploadFile,
|
11
|
+
NotionHostedFile,
|
9
12
|
)
|
13
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
14
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
15
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
10
16
|
|
11
17
|
|
12
|
-
class EmbedElement(
|
18
|
+
class EmbedElement(BaseBlockElement):
|
13
19
|
"""
|
14
20
|
Handles conversion between Markdown embeds and Notion embed blocks.
|
15
21
|
|
16
22
|
Markdown embed syntax:
|
17
|
-
- [embed](https://example.com) -
|
18
|
-
- [embed](https://example.com "Caption") -
|
19
|
-
|
20
|
-
Where:
|
21
|
-
- URL is the required embed URL
|
22
|
-
- Caption is an optional descriptive text (enclosed in quotes)
|
23
|
-
|
24
|
-
Supports various URL types including websites, PDFs, Google Maps, Google Drive,
|
25
|
-
Twitter/X posts, and other sources that Notion can embed.
|
23
|
+
- [embed](https://example.com) - URL only
|
24
|
+
- [embed](https://example.com "Caption") - URL + caption
|
26
25
|
"""
|
27
26
|
|
28
|
-
# Regex pattern for embed syntax with optional caption
|
29
27
|
PATTERN = re.compile(
|
30
|
-
r"^\[embed\]\(" #
|
31
|
-
|
32
|
-
|
33
|
-
|
28
|
+
r"^\[embed\]\(" # prefix
|
29
|
+
r"(https?://[^\s\"]+)" # URL
|
30
|
+
r"(?:\s+\"([^\"]+)\")?" # optional caption
|
31
|
+
r"\)$"
|
34
32
|
)
|
35
33
|
|
36
34
|
@classmethod
|
37
|
-
def
|
38
|
-
|
39
|
-
return text.strip().startswith("[embed]") and bool(
|
40
|
-
EmbedElement.PATTERN.match(text.strip())
|
41
|
-
)
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
def match_notion(cls, block: Dict[str, Any]) -> bool:
|
45
|
-
"""Check if block is a Notion embed."""
|
46
|
-
return block.get("type") == "embed"
|
35
|
+
def match_notion(cls, block: Block) -> bool:
|
36
|
+
return block.type == BlockType.EMBED and block.embed
|
47
37
|
|
48
38
|
@classmethod
|
49
|
-
def markdown_to_notion(cls, text: str) ->
|
50
|
-
"""Convert markdown embed to Notion
|
51
|
-
|
52
|
-
if not
|
39
|
+
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
40
|
+
"""Convert markdown embed syntax to Notion EmbedBlock."""
|
41
|
+
match = cls.PATTERN.match(text.strip())
|
42
|
+
if not match:
|
53
43
|
return None
|
54
44
|
|
55
|
-
url =
|
56
|
-
caption = embed_match.group(2)
|
57
|
-
|
58
|
-
if not url:
|
59
|
-
return None
|
60
|
-
|
61
|
-
embed_data = {"url": url}
|
62
|
-
|
63
|
-
# Add caption if provided
|
64
|
-
if caption:
|
65
|
-
embed_data["caption"] = [{"type": "text", "text": {"content": caption}}]
|
66
|
-
else:
|
67
|
-
embed_data["caption"] = []
|
68
|
-
|
69
|
-
# Prepare the embed block
|
70
|
-
embed_block = {"type": "embed", "embed": embed_data}
|
45
|
+
url, rich_text = match.group(1), match.group(2) or ""
|
71
46
|
|
72
|
-
#
|
73
|
-
|
47
|
+
# Build EmbedBlock
|
48
|
+
embed_block = EmbedBlock(url=url, caption=[])
|
49
|
+
if rich_text.strip():
|
50
|
+
rich_text_obj = RichTextObject.from_plain_text(rich_text.strip())
|
51
|
+
embed_block.caption = [rich_text_obj]
|
74
52
|
|
75
|
-
return
|
53
|
+
return CreateEmbedBlock(embed=embed_block)
|
76
54
|
|
77
55
|
@classmethod
|
78
|
-
def notion_to_markdown(cls, block:
|
79
|
-
|
80
|
-
if block.get("type") != "embed":
|
56
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
57
|
+
if block.type != BlockType.EMBED or not block.embed:
|
81
58
|
return None
|
82
59
|
|
83
|
-
|
84
|
-
url = embed_data.get("url", "")
|
60
|
+
fo = block.embed
|
85
61
|
|
86
|
-
if
|
62
|
+
if isinstance(fo, (ExternalFile, NotionHostedFile)):
|
63
|
+
url = fo.url
|
64
|
+
elif isinstance(fo, FileUploadFile):
|
65
|
+
return None
|
66
|
+
else:
|
87
67
|
return None
|
88
68
|
|
89
|
-
|
90
|
-
|
91
|
-
if not caption_rich_text:
|
92
|
-
# Simple embed with URL only
|
69
|
+
if not fo.caption:
|
93
70
|
return f"[embed]({url})"
|
94
71
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
if caption:
|
99
|
-
return f'[embed]({url} "{caption}")'
|
100
|
-
|
101
|
-
return f"[embed]({url})"
|
102
|
-
|
103
|
-
@classmethod
|
104
|
-
def is_multiline(cls) -> bool:
|
105
|
-
"""Embeds are single-line elements."""
|
106
|
-
return False
|
107
|
-
|
108
|
-
@classmethod
|
109
|
-
def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
|
110
|
-
"""Extract plain text content from Notion rich_text elements."""
|
111
|
-
result = ""
|
112
|
-
for text_obj in rich_text:
|
113
|
-
if text_obj.get("type") == "text":
|
114
|
-
result += text_obj.get("text", {}).get("content", "")
|
115
|
-
elif "plain_text" in text_obj:
|
116
|
-
result += text_obj.get("plain_text", "")
|
117
|
-
return result
|
118
|
-
|
119
|
-
@classmethod
|
120
|
-
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
121
|
-
"""
|
122
|
-
Returns structured LLM prompt metadata for the embed element.
|
123
|
-
"""
|
124
|
-
return (
|
125
|
-
ElementPromptBuilder()
|
126
|
-
.with_description(
|
127
|
-
"Embeds external content from websites, PDFs, Google Maps, and other sources directly in your document."
|
128
|
-
)
|
129
|
-
.with_usage_guidelines(
|
130
|
-
"Use embeds when you want to include external content that isn't just a video or image. "
|
131
|
-
"Embeds are great for interactive content, reference materials, or live data sources."
|
132
|
-
)
|
133
|
-
.with_syntax('[embed](https://example.com "Optional caption")')
|
134
|
-
.with_examples(
|
135
|
-
[
|
136
|
-
"[embed](https://drive.google.com/file/d/123456/view)",
|
137
|
-
'[embed](https://www.google.com/maps?q=San+Francisco "Our office location")',
|
138
|
-
'[embed](https://twitter.com/NotionHQ/status/1234567890 "Latest announcement")',
|
139
|
-
'[embed](https://github.com/username/repo "Project documentation")',
|
140
|
-
'[embed](https://example.com/important-reference.pdf "Course materials")',
|
141
|
-
]
|
142
|
-
)
|
143
|
-
.build()
|
72
|
+
text = "".join(
|
73
|
+
rt.plain_text or TextInlineFormatter.extract_text_with_formatting([rt])
|
74
|
+
for rt in fo.caption
|
144
75
|
)
|
76
|
+
|
77
|
+
return f'[embed]({url} "{text}")'
|
@@ -1,8 +1,10 @@
|
|
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
|
6
8
|
|
7
9
|
|
8
10
|
class EmbedMarkdownBlockParams(BaseModel):
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
from typing_extensions import Literal
|
3
|
+
|
4
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
5
|
+
|
6
|
+
|
7
|
+
class EmbedBlock(BaseModel):
|
8
|
+
url: str
|
9
|
+
caption: list[RichTextObject] = Field(default_factory=list)
|
10
|
+
|
11
|
+
|
12
|
+
class CreateEmbedBlock(BaseModel):
|
13
|
+
type: Literal["embed"] = "embed"
|
14
|
+
embed: EmbedBlock
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from notionary.blocks.equation.equation_element import EquationElement
|
2
|
+
from notionary.blocks.equation.equation_element_markdown_node import (
|
3
|
+
EquationMarkdownBlockParams,
|
4
|
+
EquationMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.equation.equation_models import CreateEquationBlock, EquationBlock
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"EquationElement",
|
10
|
+
"EquationBlock",
|
11
|
+
"CreateEquationBlock",
|
12
|
+
"EquationMarkdownNode",
|
13
|
+
"EquationMarkdownBlockParams",
|
14
|
+
]
|
@@ -0,0 +1,80 @@
|
|
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.equation.equation_models import CreateEquationBlock, EquationBlock
|
8
|
+
from notionary.blocks.models import Block, BlockCreateResult
|
9
|
+
from notionary.blocks.types import BlockType
|
10
|
+
|
11
|
+
|
12
|
+
class EquationElement(BaseBlockElement):
|
13
|
+
"""
|
14
|
+
Only supports bracket style (analog zu [video]):
|
15
|
+
|
16
|
+
- [equation](E = mc^2) # unquoted: keine ')' oder Newlines
|
17
|
+
- [equation]("E = mc^2 + \\frac{a}{b}") # quoted: erlaubt ')' & Newlines & \"
|
18
|
+
|
19
|
+
No $$...$$ parsing.
|
20
|
+
"""
|
21
|
+
|
22
|
+
_QUOTED_PATTERN = re.compile(
|
23
|
+
r'^\[equation\]\(\s*"(?P<quoted_expr>(?:[^"\\]|\\.)*)"\s*\)$',
|
24
|
+
re.DOTALL,
|
25
|
+
)
|
26
|
+
|
27
|
+
_UNQUOTED_PATTERN = re.compile(
|
28
|
+
r"^\[equation\]\(\s*(?P<unquoted_expr>[^)\r\n]+?)\s*\)$"
|
29
|
+
)
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def match_notion(cls, block: Block) -> bool:
|
33
|
+
return block.type == BlockType.EQUATION and block.equation
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
37
|
+
input_text = text.strip()
|
38
|
+
|
39
|
+
# Try quoted form first: [equation]("...")
|
40
|
+
if quoted_match := cls._QUOTED_PATTERN.match(input_text):
|
41
|
+
raw_expression = quoted_match.group("quoted_expr")
|
42
|
+
# Unescape \" and \\ for Notion
|
43
|
+
unescaped_expression = raw_expression.encode("utf-8").decode(
|
44
|
+
"unicode_escape"
|
45
|
+
)
|
46
|
+
unescaped_expression = unescaped_expression.replace('\\"', '"')
|
47
|
+
final_expression = unescaped_expression.strip()
|
48
|
+
|
49
|
+
return (
|
50
|
+
CreateEquationBlock(equation=EquationBlock(expression=final_expression))
|
51
|
+
if final_expression
|
52
|
+
else None
|
53
|
+
)
|
54
|
+
|
55
|
+
# Try unquoted form: [equation](...)
|
56
|
+
if unquoted_match := cls._UNQUOTED_PATTERN.match(input_text):
|
57
|
+
raw_expression = unquoted_match.group("unquoted_expr").strip()
|
58
|
+
return (
|
59
|
+
CreateEquationBlock(equation=EquationBlock(expression=raw_expression))
|
60
|
+
if raw_expression
|
61
|
+
else None
|
62
|
+
)
|
63
|
+
|
64
|
+
return None
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
68
|
+
if block.type != BlockType.EQUATION or not block.equation:
|
69
|
+
return None
|
70
|
+
|
71
|
+
expression = (block.equation.expression or "").strip()
|
72
|
+
if not expression:
|
73
|
+
return None
|
74
|
+
|
75
|
+
# Use quoted form if expression contains risky characters
|
76
|
+
if ("\n" in expression) or (")" in expression) or ('"' in expression):
|
77
|
+
escaped_expression = expression.replace("\\", "\\\\").replace('"', r"\"")
|
78
|
+
return f'[equation]("{escaped_expression}")'
|
79
|
+
|
80
|
+
return f"[equation]({expression})"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
+
|
7
|
+
|
8
|
+
class EquationMarkdownBlockParams(BaseModel):
|
9
|
+
expression: str
|
10
|
+
|
11
|
+
|
12
|
+
class EquationMarkdownNode(MarkdownNode):
|
13
|
+
"""
|
14
|
+
Programmatic interface for creating Markdown equation blocks.
|
15
|
+
Example:
|
16
|
+
[equation](E = mc^2)
|
17
|
+
[equation]("f(x) = \\sin(x) + \\cos(x)")
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, expression: str):
|
21
|
+
self.expression = expression
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def from_params(cls, params: EquationMarkdownBlockParams) -> EquationMarkdownNode:
|
25
|
+
return cls(expression=params.expression)
|
26
|
+
|
27
|
+
def to_markdown(self) -> str:
|
28
|
+
expr = self.expression.strip()
|
29
|
+
if not expr:
|
30
|
+
return "[equation]()"
|
31
|
+
|
32
|
+
if ("\n" in expr) or (")" in expr) or ('"' in expr):
|
33
|
+
escaped = expr.replace("\\", "\\\\").replace('"', r"\"")
|
34
|
+
return f'[equation]("{escaped}")'
|
35
|
+
|
36
|
+
return f"[equation]({expr})"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from notionary.blocks.file.file_element import FileElement
|
2
|
+
from notionary.blocks.file.file_element_markdown_node import (
|
3
|
+
FileMarkdownNode,
|
4
|
+
FileMarkdownNodeParams,
|
5
|
+
)
|
6
|
+
from notionary.blocks.file.file_element_models import (
|
7
|
+
CreateFileBlock,
|
8
|
+
ExternalFile,
|
9
|
+
FileBlock,
|
10
|
+
FileType,
|
11
|
+
FileUploadFile,
|
12
|
+
NotionHostedFile,
|
13
|
+
)
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"FileElement",
|
17
|
+
"FileType",
|
18
|
+
"ExternalFile",
|
19
|
+
"NotionHostedFile",
|
20
|
+
"FileUploadFile",
|
21
|
+
"FileBlock",
|
22
|
+
"CreateFileBlock",
|
23
|
+
"FileMarkdownNode",
|
24
|
+
"FileMarkdownNodeParams",
|
25
|
+
]
|
@@ -0,0 +1,93 @@
|
|
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.models import Block, BlockCreateResult, BlockType
|
14
|
+
from notionary.blocks.paragraph.paragraph_models import (
|
15
|
+
CreateParagraphBlock,
|
16
|
+
ParagraphBlock,
|
17
|
+
)
|
18
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
19
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
20
|
+
|
21
|
+
|
22
|
+
class FileElement(BaseBlockElement):
|
23
|
+
"""
|
24
|
+
Handles conversion between Markdown file embeds and Notion file blocks.
|
25
|
+
|
26
|
+
Markdown file syntax:
|
27
|
+
- [file](https://example.com/document.pdf "Caption")
|
28
|
+
- [file](https://example.com/document.pdf)
|
29
|
+
|
30
|
+
Supports external file URLs with optional captions.
|
31
|
+
"""
|
32
|
+
|
33
|
+
PATTERN = re.compile(
|
34
|
+
r"^\[file\]\(" # prefix
|
35
|
+
r'(https?://[^\s\)"]+)' # URL
|
36
|
+
r'(?:\s+"([^"]*)")?' # optional caption
|
37
|
+
r"\)$"
|
38
|
+
)
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def match_notion(cls, block: Block) -> bool:
|
42
|
+
# Notion file block covers files
|
43
|
+
return block.type == BlockType.FILE and block.file
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
47
|
+
"""Convert markdown file link to Notion FileBlock followed by an empty paragraph."""
|
48
|
+
match = cls.PATTERN.match(text.strip())
|
49
|
+
if not match:
|
50
|
+
return None
|
51
|
+
|
52
|
+
url, caption_text = match.group(1), match.group(2) or ""
|
53
|
+
|
54
|
+
# Build FileBlock using FileType enum
|
55
|
+
file_block = FileBlock(
|
56
|
+
type=FileType.EXTERNAL, external=ExternalFile(url=url), caption=[]
|
57
|
+
)
|
58
|
+
if caption_text.strip():
|
59
|
+
rt = RichTextObject.from_plain_text(caption_text)
|
60
|
+
file_block.caption = [rt]
|
61
|
+
|
62
|
+
empty_para = ParagraphBlock(rich_text=[])
|
63
|
+
|
64
|
+
return [
|
65
|
+
CreateFileBlock(file=file_block),
|
66
|
+
CreateParagraphBlock(paragraph=empty_para),
|
67
|
+
]
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
71
|
+
if block.type != BlockType.FILE or not block.file:
|
72
|
+
return None
|
73
|
+
|
74
|
+
fb: FileBlock = block.file
|
75
|
+
|
76
|
+
# Determine URL (only external and file types are valid for Markdown)
|
77
|
+
if fb.type == FileType.EXTERNAL and fb.external:
|
78
|
+
url = fb.external.url
|
79
|
+
elif fb.type == FileType.FILE and fb.file:
|
80
|
+
url = fb.file.url
|
81
|
+
elif fb.type == FileType.FILE_UPLOAD:
|
82
|
+
# Uploaded file has no stable URL for Markdown
|
83
|
+
return None
|
84
|
+
else:
|
85
|
+
return None
|
86
|
+
|
87
|
+
if not fb.caption:
|
88
|
+
return f"[file]({url})"
|
89
|
+
|
90
|
+
caption_md = TextInlineFormatter.extract_text_with_formatting(fb.caption)
|
91
|
+
if caption_md:
|
92
|
+
return f'[file]({url} "{caption_md}")'
|
93
|
+
return f"[file]({url})"
|
@@ -0,0 +1,35 @@
|
|
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
|
+
|
9
|
+
|
10
|
+
class FileMarkdownNodeParams(BaseModel):
|
11
|
+
url: str
|
12
|
+
caption: Optional[str] = None
|
13
|
+
|
14
|
+
|
15
|
+
class FileMarkdownNode(MarkdownNode):
|
16
|
+
"""
|
17
|
+
Programmatic interface for creating Notion-style Markdown file embeds.
|
18
|
+
Example: [file](https://example.com/file.pdf "My Caption")
|
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
|
+
"""
|
31
|
+
Convert to markdown as [file](url "caption") or [file](url) if caption is empty.
|
32
|
+
"""
|
33
|
+
if self.caption:
|
34
|
+
return f'[file]({self.url} "{self.caption}")'
|
35
|
+
return f"[file]({self.url})"
|
@@ -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
|
@@ -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
|
]
|