notionary 0.2.17__py3-none-any.whl → 0.2.19__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 +3 -2
- notionary/blocks/__init__.py +54 -25
- notionary/blocks/audio/__init__.py +7 -0
- notionary/blocks/audio/audio_element.py +152 -0
- notionary/blocks/audio/audio_markdown_node.py +29 -0
- notionary/blocks/audio/audio_models.py +59 -0
- notionary/blocks/bookmark/__init__.py +7 -0
- notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
- notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
- notionary/blocks/bookmark/bookmark_models.py +0 -0
- notionary/blocks/bulleted_list/__init__.py +7 -0
- notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
- notionary/blocks/callout/__init__.py +7 -0
- notionary/blocks/callout/callout_element.py +132 -0
- notionary/blocks/callout/callout_markdown_node.py +31 -0
- notionary/blocks/callout/callout_models.py +0 -0
- notionary/blocks/code/__init__.py +7 -0
- notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
- notionary/blocks/code/code_markdown_node.py +43 -0
- notionary/blocks/code/code_models.py +0 -0
- notionary/blocks/column/__init__.py +5 -0
- notionary/blocks/{column_element.py → column/column_element.py} +24 -55
- notionary/blocks/column/column_models.py +0 -0
- notionary/blocks/divider/__init__.py +7 -0
- notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
- notionary/blocks/divider/divider_markdown_node.py +24 -0
- notionary/blocks/divider/divider_models.py +0 -0
- notionary/blocks/document/__init__.py +7 -0
- notionary/blocks/document/document_element.py +102 -0
- notionary/blocks/document/document_markdown_node.py +31 -0
- notionary/blocks/document/document_models.py +0 -0
- notionary/blocks/embed/__init__.py +7 -0
- notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
- notionary/blocks/embed/embed_markdown_node.py +30 -0
- notionary/blocks/embed/embed_models.py +0 -0
- notionary/blocks/heading/__init__.py +7 -0
- notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
- notionary/blocks/heading/heading_markdown_node.py +29 -0
- notionary/blocks/heading/heading_models.py +0 -0
- notionary/blocks/image/__init__.py +7 -0
- notionary/blocks/{image_element.py → image/image_element.py} +62 -42
- notionary/blocks/image/image_markdown_node.py +33 -0
- notionary/blocks/image/image_models.py +0 -0
- notionary/blocks/markdown_builder.py +356 -0
- notionary/blocks/markdown_node.py +29 -0
- notionary/blocks/mention/__init__.py +7 -0
- notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
- notionary/blocks/mention/mention_markdown_node.py +38 -0
- notionary/blocks/mention/mention_models.py +0 -0
- notionary/blocks/numbered_list/__init__.py +7 -0
- notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
- notionary/blocks/numbered_list/numbered_list_models.py +0 -0
- notionary/blocks/paragraph/__init__.py +7 -0
- notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
- notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
- notionary/blocks/paragraph/paragraph_models.py +0 -0
- notionary/blocks/quote/__init__.py +7 -0
- notionary/blocks/quote/quote_element.py +92 -0
- notionary/blocks/quote/quote_markdown_node.py +23 -0
- notionary/blocks/quote/quote_models.py +0 -0
- notionary/blocks/registry/block_registry.py +17 -3
- notionary/blocks/registry/block_registry_builder.py +90 -178
- notionary/blocks/shared/__init__.py +0 -0
- notionary/blocks/shared/block_client.py +256 -0
- notionary/blocks/shared/models.py +713 -0
- notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
- notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
- notionary/blocks/shared/text_inline_formatter_new.py +139 -0
- notionary/blocks/table/__init__.py +7 -0
- notionary/blocks/{table_element.py → table/table_element.py} +23 -11
- notionary/blocks/table/table_markdown_node.py +40 -0
- notionary/blocks/table/table_models.py +0 -0
- notionary/blocks/todo/__init__.py +7 -0
- notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
- notionary/blocks/todo/todo_markdown_node.py +31 -0
- notionary/blocks/todo/todo_models.py +0 -0
- notionary/blocks/toggle/__init__.py +4 -0
- notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
- notionary/blocks/toggle/toggle_markdown_node.py +35 -0
- notionary/blocks/toggle/toggle_models.py +0 -0
- notionary/blocks/toggleable_heading/__init__.py +9 -0
- notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
- notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/blocks/video/__init__.py +7 -0
- notionary/blocks/{video_element.py → video/video_element.py} +82 -57
- notionary/blocks/video/video_markdown_node.py +30 -0
- notionary/file_upload/notion_file_upload.py +1 -1
- notionary/page/content/markdown_whitespace_processor.py +80 -0
- notionary/page/content/notion_text_length_utils.py +87 -0
- notionary/page/content/page_content_retriever.py +18 -10
- notionary/page/content/page_content_writer.py +97 -148
- notionary/page/formatting/line_processor.py +153 -0
- notionary/page/formatting/markdown_to_notion_converter.py +104 -425
- notionary/page/notion_page.py +9 -11
- notionary/page/notion_to_markdown_converter.py +9 -13
- notionary/util/factory_decorator.py +0 -0
- notionary/workspace.py +0 -1
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/METADATA +1 -1
- notionary-0.2.19.dist-info/RECORD +150 -0
- notionary/blocks/audio_element.py +0 -144
- notionary/blocks/callout_element.py +0 -122
- notionary/blocks/document_element.py +0 -194
- notionary/blocks/notion_block_client.py +0 -26
- notionary/blocks/qoute_element.py +0 -169
- notionary/page/content/notion_page_content_chunker.py +0 -84
- notionary/page/formatting/spacer_rules.py +0 -483
- notionary-0.2.17.dist-info/RECORD +0 -85
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/LICENSE +0 -0
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/WHEEL +0 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List
|
3
|
+
|
4
|
+
from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
|
5
|
+
from notionary.blocks import (
|
6
|
+
NotionBlockElement,
|
7
|
+
ElementPromptContent,
|
8
|
+
ElementPromptBuilder,
|
9
|
+
NotionBlockResult,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class CalloutElement(NotionBlockElement):
|
14
|
+
"""
|
15
|
+
Handles conversion between Markdown callouts and Notion callout blocks.
|
16
|
+
|
17
|
+
Markdown callout syntax:
|
18
|
+
- [callout](Text) - Simple callout with default emoji
|
19
|
+
- [callout](Text "emoji") - Callout with custom emoji
|
20
|
+
|
21
|
+
Where:
|
22
|
+
- Text is the required callout content
|
23
|
+
- emoji is an optional emoji character (enclosed in quotes)
|
24
|
+
"""
|
25
|
+
|
26
|
+
# Regex pattern for callout syntax with optional emoji
|
27
|
+
PATTERN = re.compile(
|
28
|
+
r"^\[callout\]\(" # [callout]( prefix
|
29
|
+
+ r'([^"]+?)' # Text content (required)
|
30
|
+
+ r'(?:\s+"([^"]+)")?' # Optional emoji in quotes
|
31
|
+
+ r"\)$" # closing parenthesis
|
32
|
+
)
|
33
|
+
|
34
|
+
# Default values
|
35
|
+
DEFAULT_EMOJI = "💡"
|
36
|
+
DEFAULT_COLOR = "gray_background"
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def match_markdown(cls, text: str) -> bool:
|
40
|
+
"""Check if text is a markdown callout."""
|
41
|
+
return text.strip().startswith("[callout]") and bool(
|
42
|
+
CalloutElement.PATTERN.match(text.strip())
|
43
|
+
)
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def match_notion(cls, block: Dict[str, Any]) -> bool:
|
47
|
+
"""Check if block is a Notion callout."""
|
48
|
+
return block.get("type") == "callout"
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
52
|
+
"""Convert markdown callout to Notion callout block."""
|
53
|
+
callout_match = CalloutElement.PATTERN.match(text.strip())
|
54
|
+
if not callout_match:
|
55
|
+
return None
|
56
|
+
|
57
|
+
content = callout_match.group(1)
|
58
|
+
emoji = callout_match.group(2)
|
59
|
+
|
60
|
+
if not content:
|
61
|
+
return None
|
62
|
+
|
63
|
+
# Use default emoji if none provided
|
64
|
+
if not emoji:
|
65
|
+
emoji = CalloutElement.DEFAULT_EMOJI
|
66
|
+
|
67
|
+
callout_data = {
|
68
|
+
"rich_text": TextInlineFormatter.parse_inline_formatting(content.strip()),
|
69
|
+
"icon": {"type": "emoji", "emoji": emoji},
|
70
|
+
"color": CalloutElement.DEFAULT_COLOR,
|
71
|
+
}
|
72
|
+
|
73
|
+
return {"type": "callout", "callout": callout_data}
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
77
|
+
"""Convert Notion callout block to markdown callout."""
|
78
|
+
if block.get("type") != "callout":
|
79
|
+
return None
|
80
|
+
|
81
|
+
callout_data = block.get("callout", {})
|
82
|
+
rich_text = callout_data.get("rich_text", [])
|
83
|
+
icon = callout_data.get("icon", {})
|
84
|
+
|
85
|
+
content = TextInlineFormatter.extract_text_with_formatting(rich_text)
|
86
|
+
if not content:
|
87
|
+
return None
|
88
|
+
|
89
|
+
emoji = CalloutElement._extract_emoji(icon)
|
90
|
+
|
91
|
+
if emoji and emoji != CalloutElement.DEFAULT_EMOJI:
|
92
|
+
return f'[callout]({content} "{emoji}")'
|
93
|
+
|
94
|
+
return f"[callout]({content})"
|
95
|
+
|
96
|
+
@classmethod
|
97
|
+
def is_multiline(cls) -> bool:
|
98
|
+
"""Callouts are single-line elements."""
|
99
|
+
return False
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def _extract_emoji(cls, icon: Dict[str, Any]) -> str:
|
103
|
+
"""Extract emoji from Notion icon object."""
|
104
|
+
if icon and icon.get("type") == "emoji":
|
105
|
+
return icon.get("emoji", "")
|
106
|
+
return ""
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
110
|
+
"""
|
111
|
+
Returns structured LLM prompt metadata for the callout element.
|
112
|
+
"""
|
113
|
+
return (
|
114
|
+
ElementPromptBuilder()
|
115
|
+
.with_description(
|
116
|
+
"Creates a callout block to highlight important information with an icon."
|
117
|
+
)
|
118
|
+
.with_usage_guidelines(
|
119
|
+
"Use callouts when you want to draw attention to important information, "
|
120
|
+
"tips, warnings, or notes that stand out from the main content."
|
121
|
+
)
|
122
|
+
.with_syntax('[callout](Text content "Optional emoji")')
|
123
|
+
.with_examples(
|
124
|
+
[
|
125
|
+
"[callout](This is a default callout with the light bulb emoji)",
|
126
|
+
'[callout](This is a callout with a bell emoji "🔔")',
|
127
|
+
'[callout](Warning: This is an important note "⚠️")',
|
128
|
+
'[callout](Tip: Add emoji that matches your content\'s purpose "💡")',
|
129
|
+
]
|
130
|
+
)
|
131
|
+
.build()
|
132
|
+
)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
from pydantic import BaseModel
|
5
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
6
|
+
|
7
|
+
|
8
|
+
class CalloutMarkdownBlockParams(BaseModel):
|
9
|
+
text: str
|
10
|
+
emoji: Optional[str] = None
|
11
|
+
|
12
|
+
|
13
|
+
class CalloutMarkdownNode(MarkdownNode):
|
14
|
+
"""
|
15
|
+
Programmatic interface for creating Notion-style callout Markdown blocks.
|
16
|
+
Example: [callout](This is important "⚠️")
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, text: str, emoji: Optional[str] = None):
|
20
|
+
self.text = text
|
21
|
+
self.emoji = emoji
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def from_params(cls, params: CalloutMarkdownBlockParams) -> CalloutMarkdownNode:
|
25
|
+
return cls(text=params.text, emoji=params.emoji)
|
26
|
+
|
27
|
+
def to_markdown(self) -> str:
|
28
|
+
if self.emoji and self.emoji != "💡":
|
29
|
+
return f'[callout]({self.text} "{self.emoji}")'
|
30
|
+
else:
|
31
|
+
return f"[callout]({self.text})"
|
File without changes
|
@@ -1,11 +1,16 @@
|
|
1
1
|
import re
|
2
2
|
|
3
|
-
from typing import
|
3
|
+
from typing import Optional, Any
|
4
4
|
from notionary.blocks import NotionBlockElement
|
5
|
-
from notionary.blocks import
|
5
|
+
from notionary.blocks import (
|
6
|
+
ElementPromptContent,
|
7
|
+
ElementPromptBuilder,
|
8
|
+
NotionBlockResult,
|
9
|
+
)
|
10
|
+
from notionary.blocks.shared.models import RichTextObject
|
6
11
|
|
7
12
|
|
8
|
-
class
|
13
|
+
class CodeElement(NotionBlockElement):
|
9
14
|
"""
|
10
15
|
Handles conversion between Markdown code blocks and Notion code blocks.
|
11
16
|
|
@@ -28,17 +33,17 @@ class CodeBlockElement(NotionBlockElement):
|
|
28
33
|
@classmethod
|
29
34
|
def match_markdown(cls, text: str) -> bool:
|
30
35
|
"""Check if text contains a markdown code block."""
|
31
|
-
return bool(
|
36
|
+
return bool(cls.PATTERN.search(text))
|
32
37
|
|
33
38
|
@classmethod
|
34
|
-
def match_notion(cls, block:
|
39
|
+
def match_notion(cls, block: dict[str, any]) -> bool:
|
35
40
|
"""Check if block is a Notion code block."""
|
36
41
|
return block.get("type") == "code"
|
37
42
|
|
38
43
|
@classmethod
|
39
|
-
def markdown_to_notion(cls, text: str) ->
|
44
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
40
45
|
"""Convert markdown code block to Notion code block."""
|
41
|
-
match =
|
46
|
+
match = cls.PATTERN.search(text)
|
42
47
|
if not match:
|
43
48
|
return None
|
44
49
|
|
@@ -49,65 +54,73 @@ class CodeBlockElement(NotionBlockElement):
|
|
49
54
|
if content.endswith("\n"):
|
50
55
|
content = content[:-1]
|
51
56
|
|
57
|
+
# Create code block with rich text
|
58
|
+
content_rich_text = RichTextObject.from_plain_text(content)
|
59
|
+
|
52
60
|
block = {
|
53
61
|
"type": "code",
|
54
62
|
"code": {
|
55
|
-
"rich_text": [
|
56
|
-
{
|
57
|
-
"type": "text",
|
58
|
-
"text": {"content": content},
|
59
|
-
"plain_text": content,
|
60
|
-
}
|
61
|
-
],
|
63
|
+
"rich_text": [content_rich_text.model_dump()],
|
62
64
|
"language": language,
|
63
65
|
},
|
64
66
|
}
|
65
67
|
|
66
68
|
# Add caption if provided
|
67
69
|
if caption and caption.strip():
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
}
|
74
|
-
]
|
70
|
+
caption_rich_text = RichTextObject.from_plain_text(caption.strip())
|
71
|
+
block["code"]["caption"] = [caption_rich_text.model_dump()]
|
72
|
+
|
73
|
+
# Leerer Paragraph nach dem Code-Block
|
74
|
+
empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
|
75
75
|
|
76
|
-
return block
|
76
|
+
return [block, empty_paragraph]
|
77
77
|
|
78
78
|
@classmethod
|
79
|
-
def notion_to_markdown(cls, block:
|
80
|
-
"""Convert Notion code block to
|
79
|
+
def notion_to_markdown(cls, block: dict[str, Any]) -> Optional[str]:
|
80
|
+
"""Convert Notion code block to Markdown."""
|
81
81
|
if block.get("type") != "code":
|
82
82
|
return None
|
83
83
|
|
84
84
|
code_data = block.get("code", {})
|
85
|
+
language = code_data.get("language", "")
|
85
86
|
rich_text = code_data.get("rich_text", [])
|
87
|
+
caption = code_data.get("caption", [])
|
88
|
+
|
89
|
+
def extract_content(rich_text_list):
|
90
|
+
"""Extract code content from rich_text array."""
|
91
|
+
return "".join(
|
92
|
+
text.get("text", {}).get("content", "")
|
93
|
+
if text.get("type") == "text"
|
94
|
+
else text.get("plain_text", "")
|
95
|
+
for text in rich_text_list
|
96
|
+
)
|
86
97
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
98
|
+
def extract_caption(caption_list):
|
99
|
+
"""Extract caption text from caption array."""
|
100
|
+
return "".join(
|
101
|
+
c.get("text", {}).get("content", "")
|
102
|
+
for c in caption_list
|
103
|
+
if c.get("type") == "text"
|
104
|
+
)
|
91
105
|
|
92
|
-
|
106
|
+
code_content = extract_content(rich_text)
|
107
|
+
caption_text = extract_caption(caption)
|
93
108
|
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
for caption_block in caption_data:
|
98
|
-
caption_text += caption_block.get("plain_text", "")
|
109
|
+
# Handle language - convert "plain text" back to empty string for markdown
|
110
|
+
if language == "plain text":
|
111
|
+
language = ""
|
99
112
|
|
100
|
-
#
|
101
|
-
result = f"```{language}\n{
|
113
|
+
# Build markdown code block
|
114
|
+
result = f"```{language}\n{code_content}\n```" if language else f"```\n{code_content}\n```"
|
102
115
|
|
103
116
|
# Add caption if present
|
104
|
-
if caption_text
|
117
|
+
if caption_text:
|
105
118
|
result += f"\nCaption: {caption_text}"
|
106
119
|
|
107
120
|
return result
|
108
121
|
|
109
122
|
@classmethod
|
110
|
-
def find_matches(cls, text: str) ->
|
123
|
+
def find_matches(cls, text: str) -> list[tuple[int, int, dict[str, any]]]:
|
111
124
|
"""
|
112
125
|
Find all code block matches in the text and return their positions.
|
113
126
|
|
@@ -118,7 +131,7 @@ class CodeBlockElement(NotionBlockElement):
|
|
118
131
|
List of tuples with (start_pos, end_pos, block)
|
119
132
|
"""
|
120
133
|
matches = []
|
121
|
-
for match in
|
134
|
+
for match in CodeElement.PATTERN.finditer(text):
|
122
135
|
language = match.group(1) or "plain text"
|
123
136
|
content = match.group(2)
|
124
137
|
caption = match.group(3)
|
@@ -187,7 +200,7 @@ class CodeBlockElement(NotionBlockElement):
|
|
187
200
|
"```python\nprint('Hello, world!')\n```\nCaption: Basic Python greeting example",
|
188
201
|
'```json\n{"name": "Alice", "age": 30}\n```\nCaption: User data structure',
|
189
202
|
"```mermaid\nflowchart TD\n A --> B\n```\nCaption: Simple flow diagram",
|
190
|
-
'```bash\ngit commit -m "Initial commit"\n```',
|
203
|
+
'```bash\ngit commit -m "Initial commit"\n```',
|
191
204
|
]
|
192
205
|
)
|
193
206
|
.with_avoidance_guidelines(
|
@@ -200,3 +213,22 @@ class CodeBlockElement(NotionBlockElement):
|
|
200
213
|
)
|
201
214
|
.build()
|
202
215
|
)
|
216
|
+
|
217
|
+
@staticmethod
|
218
|
+
def extract_content(rich_text_list: list[dict[str, Any]]) -> str:
|
219
|
+
"""Extract code content from rich_text array."""
|
220
|
+
return "".join(
|
221
|
+
text.get("text", {}).get("content", "")
|
222
|
+
if text.get("type") == "text"
|
223
|
+
else text.get("plain_text", "")
|
224
|
+
for text in rich_text_list
|
225
|
+
)
|
226
|
+
|
227
|
+
@staticmethod
|
228
|
+
def extract_caption(caption_list: list[dict[str, Any]]) -> str:
|
229
|
+
"""Extract caption text from caption array."""
|
230
|
+
return "".join(
|
231
|
+
c.get("text", {}).get("content", "")
|
232
|
+
for c in caption_list
|
233
|
+
if c.get("type") == "text"
|
234
|
+
)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
from pydantic import BaseModel
|
5
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
6
|
+
|
7
|
+
|
8
|
+
class CodeMarkdownBlockParams(BaseModel):
|
9
|
+
code: str
|
10
|
+
language: Optional[str] = None
|
11
|
+
caption: Optional[str] = None
|
12
|
+
|
13
|
+
|
14
|
+
class CodeMarkdownNode(MarkdownNode):
|
15
|
+
"""
|
16
|
+
Programmatic interface for creating Notion-style Markdown code blocks.
|
17
|
+
Example:
|
18
|
+
```python
|
19
|
+
print("Hello, world!")
|
20
|
+
```
|
21
|
+
Caption: Basic usage
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
code: str,
|
27
|
+
language: Optional[str] = None,
|
28
|
+
caption: Optional[str] = None,
|
29
|
+
):
|
30
|
+
self.code = code
|
31
|
+
self.language = language or ""
|
32
|
+
self.caption = caption
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def from_params(cls, params: CodeMarkdownBlockParams) -> CodeMarkdownNode:
|
36
|
+
return cls(code=params.code, language=params.language, caption=params.caption)
|
37
|
+
|
38
|
+
def to_markdown(self) -> str:
|
39
|
+
lang = self.language or ""
|
40
|
+
content = f"```{lang}\n{self.code}\n```"
|
41
|
+
if self.caption:
|
42
|
+
content += f"\nCaption: {self.caption}"
|
43
|
+
return content
|
File without changes
|
@@ -1,9 +1,12 @@
|
|
1
1
|
import re
|
2
|
-
from typing import
|
2
|
+
from typing import Optional, Callable
|
3
3
|
|
4
4
|
from notionary.blocks import NotionBlockElement
|
5
|
-
from notionary.
|
6
|
-
|
5
|
+
from notionary.blocks import (
|
6
|
+
ElementPromptContent,
|
7
|
+
ElementPromptBuilder,
|
8
|
+
NotionBlockResult,
|
9
|
+
)
|
7
10
|
|
8
11
|
|
9
12
|
class ColumnElement(NotionBlockElement):
|
@@ -31,7 +34,7 @@ class ColumnElement(NotionBlockElement):
|
|
31
34
|
|
32
35
|
@classmethod
|
33
36
|
def set_converter_callback(
|
34
|
-
cls, callback: Callable[[str],
|
37
|
+
cls, callback: Callable[[str], list[dict[str, any]]]
|
35
38
|
) -> None:
|
36
39
|
"""
|
37
40
|
Setze die Callback-Funktion, die zum Konvertieren von Markdown zu Notion-Blöcken verwendet wird.
|
@@ -47,12 +50,12 @@ class ColumnElement(NotionBlockElement):
|
|
47
50
|
return bool(ColumnElement.COLUMNS_START.match(text.strip()))
|
48
51
|
|
49
52
|
@staticmethod
|
50
|
-
def match_notion(block:
|
53
|
+
def match_notion(block: dict[str, any]) -> bool:
|
51
54
|
"""Check if block is a Notion column_list."""
|
52
55
|
return block.get("type") == "column_list"
|
53
56
|
|
54
57
|
@staticmethod
|
55
|
-
def markdown_to_notion(text: str) ->
|
58
|
+
def markdown_to_notion(text: str) -> NotionBlockResult:
|
56
59
|
"""
|
57
60
|
Convert markdown column syntax to Notion column blocks.
|
58
61
|
|
@@ -64,10 +67,10 @@ class ColumnElement(NotionBlockElement):
|
|
64
67
|
|
65
68
|
# Create an empty column_list block
|
66
69
|
# Child columns will be added by the column processor
|
67
|
-
return {"type": "column_list", "column_list": {"children": []}}
|
70
|
+
return [{"type": "column_list", "column_list": {"children": []}}]
|
68
71
|
|
69
72
|
@staticmethod
|
70
|
-
def notion_to_markdown(block:
|
73
|
+
def notion_to_markdown(block: dict[str, any]) -> Optional[str]:
|
71
74
|
"""Convert Notion column_list block to markdown column syntax."""
|
72
75
|
if block.get("type") != "column_list":
|
73
76
|
return None
|
@@ -100,7 +103,7 @@ class ColumnElement(NotionBlockElement):
|
|
100
103
|
@classmethod
|
101
104
|
def find_matches(
|
102
105
|
cls, text: str, converter_callback: Optional[Callable] = None
|
103
|
-
) ->
|
106
|
+
) -> list[tuple[int, int, dict[str, any]]]:
|
104
107
|
"""
|
105
108
|
Find all column block matches in the text and return their positions and blocks.
|
106
109
|
|
@@ -141,8 +144,8 @@ class ColumnElement(NotionBlockElement):
|
|
141
144
|
|
142
145
|
@classmethod
|
143
146
|
def _process_column_block(
|
144
|
-
cls, lines:
|
145
|
-
) ->
|
147
|
+
cls, lines: list[str], start_index: int, converter_callback: Callable
|
148
|
+
) -> tuple[int, int, dict[str, any], int]:
|
146
149
|
"""
|
147
150
|
Process a complete column block structure from the given starting line.
|
148
151
|
|
@@ -155,7 +158,8 @@ class ColumnElement(NotionBlockElement):
|
|
155
158
|
Tuple of (start_pos, end_pos, block, next_line_index)
|
156
159
|
"""
|
157
160
|
columns_start = start_index
|
158
|
-
|
161
|
+
columns_blocks = cls.markdown_to_notion(lines[start_index].strip())
|
162
|
+
columns_block = columns_blocks[0] if columns_blocks else None
|
159
163
|
columns_children = []
|
160
164
|
|
161
165
|
next_index = cls._collect_columns(
|
@@ -163,7 +167,7 @@ class ColumnElement(NotionBlockElement):
|
|
163
167
|
)
|
164
168
|
|
165
169
|
# Add columns to the main block
|
166
|
-
if columns_children:
|
170
|
+
if columns_children and columns_block:
|
167
171
|
columns_block["column_list"]["children"] = columns_children
|
168
172
|
|
169
173
|
# Calculate positions
|
@@ -175,9 +179,9 @@ class ColumnElement(NotionBlockElement):
|
|
175
179
|
@classmethod
|
176
180
|
def _collect_columns(
|
177
181
|
cls,
|
178
|
-
lines:
|
182
|
+
lines: list[str],
|
179
183
|
start_index: int,
|
180
|
-
columns_children:
|
184
|
+
columns_children: list[dict[str, any]],
|
181
185
|
converter_callback: Callable,
|
182
186
|
) -> int:
|
183
187
|
"""
|
@@ -237,8 +241,8 @@ class ColumnElement(NotionBlockElement):
|
|
237
241
|
|
238
242
|
@staticmethod
|
239
243
|
def _finalize_column(
|
240
|
-
column_content:
|
241
|
-
columns_children:
|
244
|
+
column_content: list[str],
|
245
|
+
columns_children: list[dict[str, any]],
|
242
246
|
in_column: bool,
|
243
247
|
converter_callback: Callable,
|
244
248
|
) -> None:
|
@@ -268,44 +272,9 @@ class ColumnElement(NotionBlockElement):
|
|
268
272
|
return True
|
269
273
|
|
270
274
|
@staticmethod
|
271
|
-
def _preprocess_column_content(lines:
|
272
|
-
"""
|
273
|
-
|
274
|
-
|
275
|
-
This removes any spacer markers that might have been added before the first
|
276
|
-
heading in a column, as each column should have its own heading context.
|
277
|
-
|
278
|
-
Args:
|
279
|
-
lines: The lines of content for the column
|
280
|
-
|
281
|
-
Returns:
|
282
|
-
Processed lines ready for conversion
|
283
|
-
"""
|
284
|
-
processed_lines = []
|
285
|
-
found_first_heading = False
|
286
|
-
|
287
|
-
i = 0
|
288
|
-
while i < len(lines):
|
289
|
-
line = lines[i]
|
290
|
-
|
291
|
-
# Check if this is a heading line
|
292
|
-
if re.match(r"^(#{1,6})\s+(.+)$", line.strip()):
|
293
|
-
# If it's the first heading, look ahead to check for spacer
|
294
|
-
if (
|
295
|
-
not found_first_heading
|
296
|
-
and i > 0
|
297
|
-
and processed_lines
|
298
|
-
and processed_lines[-1] == SPACER_MARKER
|
299
|
-
):
|
300
|
-
# Remove spacer before first heading in column
|
301
|
-
processed_lines.pop()
|
302
|
-
|
303
|
-
found_first_heading = True
|
304
|
-
|
305
|
-
processed_lines.append(line)
|
306
|
-
i += 1
|
307
|
-
|
308
|
-
return processed_lines
|
275
|
+
def _preprocess_column_content(lines: list[str]) -> list[str]:
|
276
|
+
"""Remove all spacer markers from column content."""
|
277
|
+
return [line for line in lines if line.strip() != "---spacer---"]
|
309
278
|
|
310
279
|
@classmethod
|
311
280
|
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
File without changes
|
@@ -2,7 +2,11 @@ import re
|
|
2
2
|
from typing import Dict, Any, Optional
|
3
3
|
|
4
4
|
from notionary.blocks import NotionBlockElement
|
5
|
-
from notionary.blocks import
|
5
|
+
from notionary.blocks import (
|
6
|
+
ElementPromptContent,
|
7
|
+
ElementPromptBuilder,
|
8
|
+
NotionBlockResult,
|
9
|
+
)
|
6
10
|
|
7
11
|
|
8
12
|
class DividerElement(NotionBlockElement):
|
@@ -26,12 +30,16 @@ class DividerElement(NotionBlockElement):
|
|
26
30
|
return block.get("type") == "divider"
|
27
31
|
|
28
32
|
@classmethod
|
29
|
-
def markdown_to_notion(cls, text: str) ->
|
33
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
30
34
|
"""Convert markdown divider to Notion divider block."""
|
31
35
|
if not DividerElement.match_markdown(text):
|
32
36
|
return None
|
33
37
|
|
34
|
-
|
38
|
+
empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
|
39
|
+
|
40
|
+
divider_block = {"type": "divider", "divider": {}}
|
41
|
+
|
42
|
+
return [empty_paragraph, divider_block]
|
35
43
|
|
36
44
|
@classmethod
|
37
45
|
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
5
|
+
|
6
|
+
|
7
|
+
class DividerMarkdownBlockParams(BaseModel):
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
class DividerMarkdownNode(MarkdownNode):
|
12
|
+
"""
|
13
|
+
Programmatic interface for creating Markdown divider lines (---).
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self):
|
17
|
+
pass # Keine Attribute notwendig
|
18
|
+
|
19
|
+
@classmethod
|
20
|
+
def from_params(cls, params: DividerMarkdownBlockParams) -> DividerMarkdownNode:
|
21
|
+
return cls()
|
22
|
+
|
23
|
+
def to_markdown(self) -> str:
|
24
|
+
return "---"
|
File without changes
|