notionary 0.2.21__py3-none-any.whl → 0.2.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/blocks/_bootstrap.py +9 -1
- notionary/blocks/audio/audio_element.py +53 -28
- notionary/blocks/audio/audio_markdown_node.py +10 -4
- notionary/blocks/base_block_element.py +15 -3
- notionary/blocks/bookmark/bookmark_element.py +39 -36
- notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
- notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
- notionary/blocks/callout/callout_element.py +20 -4
- notionary/blocks/child_database/__init__.py +11 -4
- notionary/blocks/child_database/child_database_element.py +59 -0
- notionary/blocks/child_database/child_database_models.py +7 -14
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/client.py +0 -1
- notionary/blocks/code/code_element.py +51 -2
- notionary/blocks/code/code_markdown_node.py +52 -1
- notionary/blocks/column/column_element.py +9 -3
- notionary/blocks/column/column_list_element.py +18 -3
- notionary/blocks/divider/divider_element.py +3 -11
- notionary/blocks/embed/embed_element.py +27 -6
- notionary/blocks/equation/equation_element.py +94 -41
- notionary/blocks/equation/equation_element_markdown_node.py +8 -9
- notionary/blocks/file/file_element.py +56 -37
- notionary/blocks/file/file_element_markdown_node.py +9 -7
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/heading_element.py +23 -4
- notionary/blocks/image_block/image_element.py +43 -38
- notionary/blocks/image_block/image_markdown_node.py +10 -5
- notionary/blocks/mixins/captions/__init__.py +4 -0
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
- notionary/blocks/mixins/captions/caption_mixin.py +92 -0
- notionary/blocks/models.py +3 -1
- notionary/blocks/numbered_list/numbered_list_element.py +21 -4
- notionary/blocks/paragraph/paragraph_element.py +21 -5
- notionary/blocks/pdf/pdf_element.py +47 -41
- notionary/blocks/pdf/pdf_markdown_node.py +9 -7
- notionary/blocks/quote/quote_element.py +26 -9
- notionary/blocks/quote/quote_markdown_node.py +2 -2
- notionary/blocks/registry/block_registry.py +1 -46
- notionary/blocks/registry/block_registry_builder.py +8 -0
- notionary/blocks/rich_text/rich_text_models.py +62 -29
- notionary/blocks/rich_text/text_inline_formatter.py +432 -101
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/table_element.py +110 -9
- notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
- notionary/blocks/todo/todo_element.py +21 -4
- notionary/blocks/toggle/toggle_element.py +19 -3
- notionary/blocks/toggle/toggle_markdown_node.py +1 -1
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
- notionary/blocks/types.py +69 -0
- notionary/blocks/video/video_element.py +44 -39
- notionary/blocks/video/video_markdown_node.py +10 -5
- notionary/comments/__init__.py +26 -0
- notionary/comments/client.py +211 -0
- notionary/comments/models.py +129 -0
- notionary/database/client.py +23 -0
- notionary/file_upload/models.py +2 -2
- notionary/markdown/markdown_builder.py +34 -27
- notionary/page/client.py +21 -6
- notionary/page/notion_page.py +77 -2
- notionary/page/page_content_deleting_service.py +117 -0
- notionary/page/page_content_writer.py +89 -113
- notionary/page/page_context.py +64 -0
- notionary/page/reader/handler/__init__.py +2 -0
- notionary/page/reader/handler/base_block_renderer.py +4 -4
- notionary/page/reader/handler/block_rendering_context.py +5 -0
- notionary/page/reader/handler/line_renderer.py +16 -3
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/page_content_retriever.py +17 -5
- notionary/page/writer/handler/__init__.py +2 -0
- notionary/page/writer/handler/code_handler.py +12 -40
- notionary/page/writer/handler/column_handler.py +12 -12
- notionary/page/writer/handler/column_list_handler.py +13 -13
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +4 -4
- notionary/page/writer/handler/regular_line_handler.py +31 -37
- notionary/page/writer/handler/table_handler.py +8 -72
- notionary/page/writer/handler/toggle_handler.py +14 -12
- notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
- notionary/page/writer/markdown_to_notion_converter.py +28 -9
- notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
- notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +150 -0
- notionary/shared/__init__.py +5 -0
- notionary/shared/name_to_id_resolver.py +203 -0
- notionary/telemetry/service.py +0 -1
- notionary/user/notion_user_manager.py +22 -95
- notionary/util/concurrency_limiter.py +0 -0
- notionary/workspace.py +4 -4
- notionary-0.2.23.dist-info/METADATA +235 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
- notionary/page/markdown_whitespace_processor.py +0 -80
- notionary/page/notion_text_length_utils.py +0 -119
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.21.dist-info/METADATA +0 -229
- /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
8
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
9
|
+
from notionary.page.page_context import get_page_context
|
10
|
+
|
11
|
+
|
12
|
+
class ChildPageElement(BaseBlockElement):
|
13
|
+
"""
|
14
|
+
Handles conversion between Markdown page references and Notion child page blocks.
|
15
|
+
|
16
|
+
Creates new pages when converting from markdown.
|
17
|
+
"""
|
18
|
+
|
19
|
+
PATTERN_BRACKET = re.compile(r"^\[page:\s*(.+)\]$", re.IGNORECASE)
|
20
|
+
PATTERN_EMOJI = re.compile(r"^[📝📄]\s*(.+)$")
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def match_notion(cls, block: Block) -> bool:
|
24
|
+
return block.type == BlockType.CHILD_PAGE and getattr(block, "child_page", None)
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
async def markdown_to_notion(cls, text: str) -> Optional[BlockCreateResult]:
|
28
|
+
"""
|
29
|
+
Convert markdown page syntax to an actual Notion page.
|
30
|
+
Returns None since child_page blocks are created implicitly via Pages API (not Blocks API).
|
31
|
+
"""
|
32
|
+
context = get_page_context()
|
33
|
+
|
34
|
+
text = text.strip()
|
35
|
+
|
36
|
+
match = cls.PATTERN_BRACKET.match(text)
|
37
|
+
if not match:
|
38
|
+
match = cls.PATTERN_EMOJI.match(text)
|
39
|
+
|
40
|
+
if not match:
|
41
|
+
return None
|
42
|
+
|
43
|
+
title = match.group(1).strip()
|
44
|
+
if not title:
|
45
|
+
return None
|
46
|
+
|
47
|
+
# Reject multiline titles
|
48
|
+
if "\n" in title or "\r" in title:
|
49
|
+
return None
|
50
|
+
|
51
|
+
try:
|
52
|
+
# Create the actual page using context
|
53
|
+
await context.page_client.create_page(
|
54
|
+
title=title,
|
55
|
+
parent_page_id=context.page_id,
|
56
|
+
)
|
57
|
+
# Return None as per BaseBlockElement convention:
|
58
|
+
# child_page blocks cannot be written through the Blocks API directly.
|
59
|
+
# Creating a page under the parent page will automatically insert a child_page block.
|
60
|
+
return None
|
61
|
+
|
62
|
+
except Exception as e:
|
63
|
+
print(f"Failed to create page '{title}': {e}")
|
64
|
+
return None
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
68
|
+
if block.type != BlockType.CHILD_PAGE or not getattr(block, "child_page", None):
|
69
|
+
return None
|
70
|
+
|
71
|
+
title = block.child_page.title
|
72
|
+
if not title or not title.strip():
|
73
|
+
return None
|
74
|
+
|
75
|
+
# Use bracket syntax for output
|
76
|
+
return f"[page: {title.strip()}]"
|
77
|
+
|
78
|
+
@classmethod
|
79
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
80
|
+
"""Get system prompt information for child page blocks."""
|
81
|
+
return BlockElementMarkdownInformation(
|
82
|
+
block_type=cls.__name__,
|
83
|
+
description="Creates new sub-pages within a Notion page.",
|
84
|
+
syntax_examples=[
|
85
|
+
"[page: Meeting Notes]",
|
86
|
+
"[page: Ideas]",
|
87
|
+
"📝 Project Overview",
|
88
|
+
"📄 Research Log",
|
89
|
+
],
|
90
|
+
usage_guidelines=(
|
91
|
+
"Use to create new pages that will appear as child_page blocks in the current page. "
|
92
|
+
"Pages are created via the Pages API with the current page as parent."
|
93
|
+
),
|
94
|
+
)
|
notionary/blocks/client.py
CHANGED
@@ -24,7 +24,6 @@ class NotionBlockClient(BaseNotionClient):
|
|
24
24
|
return None
|
25
25
|
return None
|
26
26
|
|
27
|
-
# das hier ist falsch (Columns werden nicht richtig abgebildet)
|
28
27
|
async def get_blocks_by_page_id_recursively(
|
29
28
|
self, page_id: str, parent_id: Optional[str] = None
|
30
29
|
) -> list[Block]:
|
@@ -5,6 +5,7 @@ from typing import Optional
|
|
5
5
|
|
6
6
|
from notionary.blocks.base_block_element import BaseBlockElement
|
7
7
|
from notionary.blocks.code.code_models import CodeBlock, CodeLanguage, CreateCodeBlock
|
8
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
8
9
|
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
9
10
|
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
10
11
|
|
@@ -22,6 +23,7 @@ class CodeElement(BaseBlockElement):
|
|
22
23
|
|
23
24
|
DEFAULT_LANGUAGE = "plain text"
|
24
25
|
CODE_START_PATTERN = re.compile(r"^```(\w*)\s*$")
|
26
|
+
CODE_START_WITH_CAPTION_PATTERN = re.compile(r"^```(\w*)\s*(?:\"([^\"]*)\")?\s*$")
|
25
27
|
|
26
28
|
@classmethod
|
27
29
|
def match_notion(cls, block: Block) -> bool:
|
@@ -29,7 +31,7 @@ class CodeElement(BaseBlockElement):
|
|
29
31
|
return block.type == BlockType.CODE and block.code
|
30
32
|
|
31
33
|
@classmethod
|
32
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
34
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
33
35
|
"""Convert opening ```language to Notion code block."""
|
34
36
|
if not (match := cls.CODE_START_PATTERN.match(text.strip())):
|
35
37
|
return None
|
@@ -42,7 +44,39 @@ class CodeElement(BaseBlockElement):
|
|
42
44
|
return CreateCodeBlock(code=code_block)
|
43
45
|
|
44
46
|
@classmethod
|
45
|
-
def
|
47
|
+
def create_from_markdown_block(
|
48
|
+
cls, opening_line: str, code_lines: list[str]
|
49
|
+
) -> BlockCreateResult:
|
50
|
+
"""
|
51
|
+
Create a complete code block from markdown components.
|
52
|
+
"""
|
53
|
+
match = cls.CODE_START_WITH_CAPTION_PATTERN.match(opening_line.strip())
|
54
|
+
if not match:
|
55
|
+
return None
|
56
|
+
|
57
|
+
language = (match.group(1) or cls.DEFAULT_LANGUAGE).lower()
|
58
|
+
language = cls._normalize_language(language)
|
59
|
+
|
60
|
+
caption = match.group(2) if match.group(2) else None
|
61
|
+
|
62
|
+
# Create rich text content from code lines
|
63
|
+
rich_text = []
|
64
|
+
if code_lines:
|
65
|
+
content = "\n".join(code_lines)
|
66
|
+
rich_text = [RichTextObject.for_code_block(content)]
|
67
|
+
|
68
|
+
caption_list = []
|
69
|
+
if caption:
|
70
|
+
caption_list = [RichTextObject.for_caption(caption)]
|
71
|
+
|
72
|
+
code_block = CodeBlock(
|
73
|
+
rich_text=rich_text, language=language, caption=caption_list
|
74
|
+
)
|
75
|
+
|
76
|
+
return CreateCodeBlock(code=code_block)
|
77
|
+
|
78
|
+
@classmethod
|
79
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
46
80
|
"""Convert Notion code block to Markdown."""
|
47
81
|
if block.type != BlockType.CODE:
|
48
82
|
return None
|
@@ -98,3 +132,18 @@ class CodeElement(BaseBlockElement):
|
|
98
132
|
def extract_caption(caption_list: list[RichTextObject]) -> str:
|
99
133
|
"""Extract caption text from caption array."""
|
100
134
|
return "".join(rt.plain_text for rt in caption_list if rt.plain_text)
|
135
|
+
|
136
|
+
@classmethod
|
137
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
138
|
+
"""Get system prompt information for code blocks."""
|
139
|
+
return BlockElementMarkdownInformation(
|
140
|
+
block_type=cls.__name__,
|
141
|
+
description="Code blocks display syntax-highlighted code with optional language specification and captions",
|
142
|
+
syntax_examples=[
|
143
|
+
"```\nprint('Hello World')\n```",
|
144
|
+
"```python\nprint('Hello World')\n```",
|
145
|
+
"```python \"Example code\"\nprint('Hello World')\n```",
|
146
|
+
"```javascript\nconsole.log('Hello');\n```",
|
147
|
+
],
|
148
|
+
usage_guidelines="Use for displaying code snippets. Language specification enables syntax highlighting. Caption in quotes on first line provides description. Supports many programming languages.",
|
149
|
+
)
|
@@ -9,6 +9,8 @@ from notionary.markdown.markdown_node import MarkdownNode
|
|
9
9
|
class CodeMarkdownNode(MarkdownNode):
|
10
10
|
"""
|
11
11
|
Programmatic interface for creating Notion-style Markdown code blocks.
|
12
|
+
Automatically handles indentation normalization for multiline strings.
|
13
|
+
|
12
14
|
Example:
|
13
15
|
```python "Basic usage"
|
14
16
|
print("Hello, world!")
|
@@ -39,5 +41,54 @@ class CodeMarkdownNode(MarkdownNode):
|
|
39
41
|
if self.caption:
|
40
42
|
opening_fence += f' "{self.caption}"'
|
41
43
|
|
42
|
-
|
44
|
+
# Smart indentation normalization
|
45
|
+
normalized_code = self._normalize_indentation(self.code)
|
46
|
+
|
47
|
+
content = f"{opening_fence}\n{normalized_code}\n```"
|
43
48
|
return content
|
49
|
+
|
50
|
+
def _normalize_indentation(self, code: str) -> str:
|
51
|
+
"""Normalize indentation by removing common leading whitespace."""
|
52
|
+
lines = code.strip().split("\n")
|
53
|
+
|
54
|
+
if self._is_empty_or_single_line(lines):
|
55
|
+
return self._handle_simple_cases(lines)
|
56
|
+
|
57
|
+
min_indentation = self._find_minimum_indentation_excluding_first_line(lines)
|
58
|
+
return self._remove_common_indentation(lines, min_indentation)
|
59
|
+
|
60
|
+
def _is_empty_or_single_line(self, lines: list[str]) -> bool:
|
61
|
+
return not lines or len(lines) == 1
|
62
|
+
|
63
|
+
def _handle_simple_cases(self, lines: list[str]) -> str:
|
64
|
+
if not lines:
|
65
|
+
return ""
|
66
|
+
return lines[0].strip()
|
67
|
+
|
68
|
+
def _find_minimum_indentation_excluding_first_line(self, lines: list[str]) -> int:
|
69
|
+
non_empty_lines_after_first = [line for line in lines[1:] if line.strip()]
|
70
|
+
|
71
|
+
if not non_empty_lines_after_first:
|
72
|
+
return 0
|
73
|
+
|
74
|
+
return min(
|
75
|
+
len(line) - len(line.lstrip()) for line in non_empty_lines_after_first
|
76
|
+
)
|
77
|
+
|
78
|
+
def _remove_common_indentation(self, lines: list[str], min_indentation: int) -> str:
|
79
|
+
normalized_lines = [lines[0].strip()]
|
80
|
+
|
81
|
+
for line in lines[1:]:
|
82
|
+
normalized_line = self._normalize_single_line(line, min_indentation)
|
83
|
+
normalized_lines.append(normalized_line)
|
84
|
+
|
85
|
+
return "\n".join(normalized_lines)
|
86
|
+
|
87
|
+
def _normalize_single_line(self, line: str, min_indentation: int) -> str:
|
88
|
+
if not line.strip():
|
89
|
+
return ""
|
90
|
+
|
91
|
+
if len(line) > min_indentation:
|
92
|
+
return line[min_indentation:]
|
93
|
+
|
94
|
+
return line.strip()
|
@@ -1,10 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
-
from
|
4
|
+
from typing import Optional
|
5
5
|
|
6
6
|
from notionary.blocks.base_block_element import BaseBlockElement
|
7
7
|
from notionary.blocks.column.column_models import ColumnBlock, CreateColumnBlock
|
8
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
8
9
|
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
9
10
|
|
10
11
|
|
@@ -27,7 +28,7 @@ class ColumnElement(BaseBlockElement):
|
|
27
28
|
return block.type == BlockType.COLUMN and block.column
|
28
29
|
|
29
30
|
@classmethod
|
30
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
31
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
31
32
|
"""Convert `::: column [ratio]` to Notion ColumnBlock."""
|
32
33
|
if not (match := cls.COLUMN_START.match(text.strip())):
|
33
34
|
return None
|
@@ -48,7 +49,7 @@ class ColumnElement(BaseBlockElement):
|
|
48
49
|
return CreateColumnBlock(column=column_content)
|
49
50
|
|
50
51
|
@classmethod
|
51
|
-
def notion_to_markdown(cls, block: Block) -> str:
|
52
|
+
async def notion_to_markdown(cls, block: Block) -> str:
|
52
53
|
"""Convert Notion column to markdown."""
|
53
54
|
if not cls.match_notion(block):
|
54
55
|
return ""
|
@@ -57,3 +58,8 @@ class ColumnElement(BaseBlockElement):
|
|
57
58
|
return "::: column"
|
58
59
|
|
59
60
|
return f"::: column {block.column.width_ratio}"
|
61
|
+
|
62
|
+
@classmethod
|
63
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
64
|
+
"""Column elements are documented via ColumnListElement - return None to avoid duplication."""
|
65
|
+
return None
|
@@ -1,9 +1,9 @@
|
|
1
|
-
from
|
2
|
-
|
1
|
+
from typing import Optional
|
3
2
|
import re
|
4
3
|
|
5
4
|
from notionary.blocks.base_block_element import BaseBlockElement
|
6
5
|
from notionary.blocks.column.column_models import ColumnListBlock, CreateColumnListBlock
|
6
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
7
7
|
from notionary.blocks.models import Block, BlockCreateResult
|
8
8
|
from notionary.blocks.types import BlockType
|
9
9
|
|
@@ -27,7 +27,7 @@ class ColumnListElement(BaseBlockElement):
|
|
27
27
|
return block.type == BlockType.COLUMN_LIST and block.column_list
|
28
28
|
|
29
29
|
@classmethod
|
30
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
30
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
31
31
|
"""Convert `::: columns` to Notion ColumnListBlock."""
|
32
32
|
if not cls.COLUMNS_START.match(text.strip()):
|
33
33
|
return None
|
@@ -35,3 +35,18 @@ class ColumnListElement(BaseBlockElement):
|
|
35
35
|
# Empty ColumnListBlock - children (columns) added by stack processor
|
36
36
|
column_list_content = ColumnListBlock()
|
37
37
|
return CreateColumnListBlock(column_list=column_list_content)
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
@classmethod
|
41
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
42
|
+
"""Get system prompt information for column list blocks."""
|
43
|
+
return BlockElementMarkdownInformation(
|
44
|
+
block_type=cls.__name__,
|
45
|
+
description="Column list containers organize multiple columns in side-by-side layouts",
|
46
|
+
syntax_examples=[
|
47
|
+
"::: columns\n::: column\nContent 1\n:::\n::: column\nContent 2\n:::\n:::",
|
48
|
+
"::: columns\n::: column 0.6\nMain content\n:::\n::: column 0.4\nSidebar\n:::\n:::",
|
49
|
+
"::: columns\n::: column 0.25\nLeft\n:::\n::: column 0.5\nCenter\n:::\n::: column 0.25\nRight\n:::\n:::",
|
50
|
+
],
|
51
|
+
usage_guidelines="Use to create multi-column layouts with at least 2 columns. Column width ratios must add up to 1.0 when specified. Each column can contain any block content. Ends with :::.",
|
52
|
+
)
|
@@ -6,10 +6,6 @@ from typing import Optional
|
|
6
6
|
from notionary.blocks.base_block_element import BaseBlockElement
|
7
7
|
from notionary.blocks.divider.divider_models import CreateDividerBlock, DividerBlock
|
8
8
|
from notionary.blocks.models import Block, BlockCreateResult
|
9
|
-
from notionary.blocks.paragraph.paragraph_models import (
|
10
|
-
CreateParagraphBlock,
|
11
|
-
ParagraphBlock,
|
12
|
-
)
|
13
9
|
from notionary.blocks.types import BlockType
|
14
10
|
|
15
11
|
|
@@ -29,21 +25,17 @@ class DividerElement(BaseBlockElement):
|
|
29
25
|
return block.type == BlockType.DIVIDER and block.divider
|
30
26
|
|
31
27
|
@classmethod
|
32
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
28
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
33
29
|
"""Convert markdown horizontal rule to Notion divider, with preceding empty paragraph."""
|
34
30
|
if not cls.PATTERN.match(text.strip()):
|
35
31
|
return None
|
36
32
|
|
37
|
-
empty_para = ParagraphBlock(rich_text=[])
|
38
33
|
divider = DividerBlock()
|
39
34
|
|
40
|
-
return
|
41
|
-
CreateParagraphBlock(paragraph=empty_para),
|
42
|
-
CreateDividerBlock(divider=divider),
|
43
|
-
]
|
35
|
+
return CreateDividerBlock(divider=divider)
|
44
36
|
|
45
37
|
@classmethod
|
46
|
-
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
38
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
47
39
|
if block.type != BlockType.DIVIDER or not block.divider:
|
48
40
|
return None
|
49
41
|
return "---"
|
@@ -10,6 +10,7 @@ from notionary.blocks.file.file_element_models import (
|
|
10
10
|
FileUploadFile,
|
11
11
|
NotionHostedFile,
|
12
12
|
)
|
13
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
13
14
|
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
14
15
|
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
15
16
|
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
@@ -36,7 +37,7 @@ class EmbedElement(BaseBlockElement):
|
|
36
37
|
return block.type == BlockType.EMBED and block.embed
|
37
38
|
|
38
39
|
@classmethod
|
39
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
40
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
40
41
|
"""Convert markdown embed syntax to Notion EmbedBlock."""
|
41
42
|
match = cls.PATTERN.match(text.strip())
|
42
43
|
if not match:
|
@@ -53,7 +54,7 @@ class EmbedElement(BaseBlockElement):
|
|
53
54
|
return CreateEmbedBlock(embed=embed_block)
|
54
55
|
|
55
56
|
@classmethod
|
56
|
-
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
57
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
57
58
|
if block.type != BlockType.EMBED or not block.embed:
|
58
59
|
return None
|
59
60
|
|
@@ -69,9 +70,29 @@ class EmbedElement(BaseBlockElement):
|
|
69
70
|
if not fo.caption:
|
70
71
|
return f"[embed]({url})"
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
text_parts = []
|
74
|
+
for rt in fo.caption:
|
75
|
+
if rt.plain_text:
|
76
|
+
text_parts.append(rt.plain_text)
|
77
|
+
else:
|
78
|
+
formatted_text = await TextInlineFormatter.extract_text_with_formatting(
|
79
|
+
[rt]
|
80
|
+
)
|
81
|
+
text_parts.append(formatted_text)
|
82
|
+
text = "".join(text_parts)
|
76
83
|
|
77
84
|
return f'[embed]({url} "{text}")'
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
88
|
+
"""Get system prompt information for embed blocks."""
|
89
|
+
return BlockElementMarkdownInformation(
|
90
|
+
block_type=cls.__name__,
|
91
|
+
description="Embed blocks display interactive content from external URLs like videos, maps, or widgets",
|
92
|
+
syntax_examples=[
|
93
|
+
"[embed](https://youtube.com/watch?v=123)",
|
94
|
+
'[embed](https://maps.google.com/location "Map Location")',
|
95
|
+
'[embed](https://codepen.io/pen/123 "Interactive Demo")',
|
96
|
+
],
|
97
|
+
usage_guidelines="Use for embedding interactive content that supports iframe embedding. URL must be from a supported platform. Caption describes the embedded content.",
|
98
|
+
)
|
@@ -1,70 +1,113 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
+
import textwrap
|
4
5
|
from typing import Optional
|
5
6
|
|
6
7
|
from notionary.blocks.base_block_element import BaseBlockElement
|
7
8
|
from notionary.blocks.equation.equation_models import CreateEquationBlock, EquationBlock
|
9
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
8
10
|
from notionary.blocks.models import Block, BlockCreateResult
|
9
11
|
from notionary.blocks.types import BlockType
|
10
12
|
|
11
13
|
|
12
14
|
class EquationElement(BaseBlockElement):
|
13
15
|
"""
|
14
|
-
|
16
|
+
Supports standard Markdown equation syntax:
|
15
17
|
|
16
|
-
-
|
17
|
-
-
|
18
|
+
- $$E = mc^2$$ # simple equations
|
19
|
+
- $$E = mc^2 + \\frac{a}{b}$$ # complex equations with LaTeX
|
18
20
|
|
19
|
-
|
21
|
+
Uses $$...$$ parsing for block equations.
|
20
22
|
"""
|
21
23
|
|
22
|
-
|
23
|
-
r
|
24
|
+
_EQUATION_PATTERN = re.compile(
|
25
|
+
r"^\$\$\s*(?P<expression>.*?)\s*\$\$$",
|
24
26
|
re.DOTALL,
|
25
27
|
)
|
26
28
|
|
27
|
-
_UNQUOTED_PATTERN = re.compile(
|
28
|
-
r"^\[equation\]\(\s*(?P<unquoted_expr>[^)\r\n]+?)\s*\)$"
|
29
|
-
)
|
30
|
-
|
31
29
|
@classmethod
|
32
30
|
def match_notion(cls, block: Block) -> bool:
|
33
31
|
return block.type == BlockType.EQUATION and block.equation
|
34
32
|
|
35
33
|
@classmethod
|
36
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
34
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
37
35
|
input_text = text.strip()
|
38
36
|
|
39
|
-
|
40
|
-
if
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
37
|
+
equation_match = cls._EQUATION_PATTERN.match(input_text)
|
38
|
+
if not equation_match:
|
39
|
+
return None
|
40
|
+
|
41
|
+
expression = equation_match.group("expression").strip()
|
42
|
+
if not expression:
|
43
|
+
return None
|
44
|
+
|
45
|
+
return CreateEquationBlock(equation=EquationBlock(expression=expression))
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def create_from_markdown_block(
|
49
|
+
cls, opening_line: str, equation_lines: list[str]
|
50
|
+
) -> BlockCreateResult:
|
51
|
+
"""
|
52
|
+
Create a complete equation block from markdown components.
|
53
|
+
Handles multiline equations like:
|
54
|
+
$$
|
55
|
+
some
|
56
|
+
inline formula here
|
57
|
+
$$
|
58
|
+
|
59
|
+
Automatically handles:
|
60
|
+
- Indentation removal from multiline strings
|
61
|
+
- Single backslash conversion to double backslash for LaTeX line breaks
|
62
|
+
"""
|
63
|
+
# Check if opening line is just $$
|
64
|
+
if opening_line.strip() != "$$":
|
65
|
+
return None
|
66
|
+
|
67
|
+
# Process equation lines if any exist
|
68
|
+
if equation_lines:
|
69
|
+
# Remove common indentation from all lines
|
70
|
+
raw_content = "\n".join(equation_lines)
|
71
|
+
dedented_content = textwrap.dedent(raw_content)
|
72
|
+
|
73
|
+
# Fix single backslashes at line ends for LaTeX line breaks
|
74
|
+
fixed_lines = cls._fix_latex_line_breaks(dedented_content.splitlines())
|
75
|
+
expression = "\n".join(fixed_lines).strip()
|
76
|
+
|
77
|
+
if expression:
|
78
|
+
return CreateEquationBlock(
|
79
|
+
equation=EquationBlock(expression=expression)
|
80
|
+
)
|
63
81
|
|
64
82
|
return None
|
65
83
|
|
66
84
|
@classmethod
|
67
|
-
def
|
85
|
+
def _fix_latex_line_breaks(cls, lines: list[str]) -> list[str]:
|
86
|
+
"""
|
87
|
+
Fix lines that end with single backslashes by converting them to double backslashes.
|
88
|
+
This makes LaTeX line breaks work correctly when users write single backslashes.
|
89
|
+
|
90
|
+
Examples:
|
91
|
+
- "a = b + c \" -> "a = b + c \\"
|
92
|
+
- "a = b + c \\\\" -> "a = b + c \\\\" (unchanged)
|
93
|
+
"""
|
94
|
+
fixed_lines = []
|
95
|
+
|
96
|
+
for line in lines:
|
97
|
+
# Check if line ends with backslashes
|
98
|
+
backslash_match = re.search(r"(\\+)$", line)
|
99
|
+
if backslash_match:
|
100
|
+
backslashes = backslash_match.group(1)
|
101
|
+
# If odd number of backslashes, the last one needs to be doubled
|
102
|
+
if len(backslashes) % 2 == 1:
|
103
|
+
line = line + "\\"
|
104
|
+
|
105
|
+
fixed_lines.append(line)
|
106
|
+
|
107
|
+
return fixed_lines
|
108
|
+
|
109
|
+
@classmethod
|
110
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
68
111
|
if block.type != BlockType.EQUATION or not block.equation:
|
69
112
|
return None
|
70
113
|
|
@@ -72,9 +115,19 @@ class EquationElement(BaseBlockElement):
|
|
72
115
|
if not expression:
|
73
116
|
return None
|
74
117
|
|
75
|
-
|
76
|
-
if ("\n" in expression) or (")" in expression) or ('"' in expression):
|
77
|
-
escaped_expression = expression.replace("\\", "\\\\").replace('"', r"\"")
|
78
|
-
return f'[equation]("{escaped_expression}")'
|
118
|
+
return f"$${expression}$$"
|
79
119
|
|
80
|
-
|
120
|
+
@classmethod
|
121
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
122
|
+
"""Get system prompt information for equation blocks."""
|
123
|
+
return BlockElementMarkdownInformation(
|
124
|
+
block_type=cls.__name__,
|
125
|
+
description="Mathematical equations using standard Markdown LaTeX syntax",
|
126
|
+
syntax_examples=[
|
127
|
+
"$$E = mc^2$$",
|
128
|
+
"$$\\frac{a}{b} + \\sqrt{c}$$",
|
129
|
+
"$$\\int_0^\\infty e^{-x} dx = 1$$",
|
130
|
+
"$$\\sum_{i=1}^n i = \\frac{n(n+1)}{2}$$",
|
131
|
+
],
|
132
|
+
usage_guidelines="Use for mathematical expressions and formulas. Supports LaTeX syntax. Wrap equations in double dollar signs ($$).",
|
133
|
+
)
|
@@ -12,9 +12,12 @@ class EquationMarkdownBlockParams(BaseModel):
|
|
12
12
|
class EquationMarkdownNode(MarkdownNode):
|
13
13
|
"""
|
14
14
|
Programmatic interface for creating Markdown equation blocks.
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
Uses standard Markdown equation syntax with double dollar signs.
|
16
|
+
|
17
|
+
Examples:
|
18
|
+
$$E = mc^2$$
|
19
|
+
$$\\frac{a}{b} + \\sqrt{c}$$
|
20
|
+
$$\\int_0^\\infty e^{-x} dx = 1$$
|
18
21
|
"""
|
19
22
|
|
20
23
|
def __init__(self, expression: str):
|
@@ -27,10 +30,6 @@ class EquationMarkdownNode(MarkdownNode):
|
|
27
30
|
def to_markdown(self) -> str:
|
28
31
|
expr = self.expression.strip()
|
29
32
|
if not expr:
|
30
|
-
return "
|
31
|
-
|
32
|
-
if ("\n" in expr) or (")" in expr) or ('"' in expr):
|
33
|
-
escaped = expr.replace("\\", "\\\\").replace('"', r"\"")
|
34
|
-
return f'[equation]("{escaped}")'
|
33
|
+
return "$$$$"
|
35
34
|
|
36
|
-
return f"
|
35
|
+
return f"$${expr}$$"
|