notionary 0.2.16__py3-none-any.whl → 0.2.18__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.
Files changed (137) hide show
  1. notionary/__init__.py +10 -5
  2. notionary/base_notion_client.py +18 -7
  3. notionary/blocks/__init__.py +55 -24
  4. notionary/blocks/audio/__init__.py +7 -0
  5. notionary/blocks/audio/audio_element.py +152 -0
  6. notionary/blocks/audio/audio_markdown_node.py +29 -0
  7. notionary/blocks/audio/audio_models.py +59 -0
  8. notionary/blocks/bookmark/__init__.py +7 -0
  9. notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
  10. notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  11. notionary/blocks/bulleted_list/__init__.py +7 -0
  12. notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
  13. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  14. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  15. notionary/blocks/callout/__init__.py +7 -0
  16. notionary/blocks/callout/callout_element.py +132 -0
  17. notionary/blocks/callout/callout_markdown_node.py +31 -0
  18. notionary/blocks/callout/callout_models.py +0 -0
  19. notionary/blocks/code/__init__.py +7 -0
  20. notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
  21. notionary/blocks/code/code_markdown_node.py +43 -0
  22. notionary/blocks/code/code_models.py +0 -0
  23. notionary/blocks/column/__init__.py +5 -0
  24. notionary/blocks/{column_element.py → column/column_element.py} +24 -55
  25. notionary/blocks/column/column_models.py +0 -0
  26. notionary/blocks/divider/__init__.py +7 -0
  27. notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
  28. notionary/blocks/divider/divider_markdown_node.py +24 -0
  29. notionary/blocks/divider/divider_models.py +0 -0
  30. notionary/blocks/document/__init__.py +7 -0
  31. notionary/blocks/document/document_element.py +102 -0
  32. notionary/blocks/document/document_markdown_node.py +31 -0
  33. notionary/blocks/document/document_models.py +0 -0
  34. notionary/blocks/embed/__init__.py +7 -0
  35. notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
  36. notionary/blocks/embed/embed_markdown_node.py +30 -0
  37. notionary/blocks/embed/embed_models.py +0 -0
  38. notionary/blocks/heading/__init__.py +7 -0
  39. notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
  40. notionary/blocks/heading/heading_markdown_node.py +29 -0
  41. notionary/blocks/heading/heading_models.py +0 -0
  42. notionary/blocks/image/__init__.py +7 -0
  43. notionary/blocks/{image_element.py → image/image_element.py} +62 -42
  44. notionary/blocks/image/image_markdown_node.py +33 -0
  45. notionary/blocks/image/image_models.py +0 -0
  46. notionary/blocks/markdown_builder.py +356 -0
  47. notionary/blocks/markdown_node.py +29 -0
  48. notionary/blocks/mention/__init__.py +7 -0
  49. notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
  50. notionary/blocks/mention/mention_markdown_node.py +38 -0
  51. notionary/blocks/mention/mention_models.py +0 -0
  52. notionary/blocks/numbered_list/__init__.py +7 -0
  53. notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
  54. notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  55. notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  56. notionary/blocks/paragraph/__init__.py +7 -0
  57. notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
  58. notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  59. notionary/blocks/paragraph/paragraph_models.py +0 -0
  60. notionary/blocks/quote/__init__.py +7 -0
  61. notionary/blocks/quote/quote_element.py +92 -0
  62. notionary/blocks/quote/quote_markdown_node.py +23 -0
  63. notionary/blocks/quote/quote_models.py +0 -0
  64. notionary/blocks/registry/block_registry.py +17 -3
  65. notionary/blocks/registry/block_registry_builder.py +90 -178
  66. notionary/blocks/shared/__init__.py +0 -0
  67. notionary/blocks/shared/block_client.py +256 -0
  68. notionary/blocks/shared/models.py +710 -0
  69. notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
  70. notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
  71. notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  72. notionary/blocks/table/__init__.py +7 -0
  73. notionary/blocks/{table_element.py → table/table_element.py} +23 -11
  74. notionary/blocks/table/table_markdown_node.py +40 -0
  75. notionary/blocks/table/table_models.py +0 -0
  76. notionary/blocks/todo/__init__.py +7 -0
  77. notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
  78. notionary/blocks/todo/todo_markdown_node.py +31 -0
  79. notionary/blocks/todo/todo_models.py +0 -0
  80. notionary/blocks/toggle/__init__.py +4 -0
  81. notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
  82. notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  83. notionary/blocks/toggle/toggle_models.py +0 -0
  84. notionary/blocks/toggleable_heading/__init__.py +9 -0
  85. notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
  86. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  87. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  88. notionary/blocks/video/__init__.py +7 -0
  89. notionary/blocks/{video_element.py → video/video_element.py} +82 -57
  90. notionary/blocks/video/video_markdown_node.py +30 -0
  91. notionary/database/__init__.py +4 -0
  92. notionary/database/database.py +481 -0
  93. notionary/database/{filter_builder.py → database_filter_builder.py} +27 -29
  94. notionary/database/{notion_database_provider.py → database_provider.py} +4 -4
  95. notionary/database/notion_database.py +45 -18
  96. notionary/file_upload/__init__.py +7 -0
  97. notionary/file_upload/client.py +254 -0
  98. notionary/file_upload/models.py +60 -0
  99. notionary/file_upload/notion_file_upload.py +387 -0
  100. notionary/page/content/markdown_whitespace_processor.py +80 -0
  101. notionary/page/content/notion_text_length_utils.py +87 -0
  102. notionary/page/content/page_content_retriever.py +2 -2
  103. notionary/page/content/page_content_writer.py +97 -148
  104. notionary/page/formatting/line_processor.py +153 -0
  105. notionary/page/formatting/markdown_to_notion_converter.py +103 -424
  106. notionary/page/notion_page.py +13 -14
  107. notionary/page/notion_to_markdown_converter.py +9 -13
  108. notionary/telemetry/views.py +15 -6
  109. notionary/user/__init__.py +11 -0
  110. notionary/user/base_notion_user.py +52 -0
  111. notionary/user/client.py +129 -0
  112. notionary/user/models.py +83 -0
  113. notionary/user/notion_bot_user.py +227 -0
  114. notionary/user/notion_user.py +256 -0
  115. notionary/user/notion_user_manager.py +173 -0
  116. notionary/user/notion_user_provider.py +1 -0
  117. notionary/util/__init__.py +3 -5
  118. notionary/util/factory_decorator.py +0 -33
  119. notionary/util/factory_only.py +37 -0
  120. notionary/util/fuzzy.py +74 -0
  121. notionary/util/logging_mixin.py +12 -12
  122. notionary/workspace.py +38 -3
  123. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/METADATA +2 -1
  124. notionary-0.2.18.dist-info/RECORD +149 -0
  125. notionary/blocks/audio_element.py +0 -144
  126. notionary/blocks/callout_element.py +0 -122
  127. notionary/blocks/notion_block_client.py +0 -26
  128. notionary/blocks/qoute_element.py +0 -169
  129. notionary/page/content/notion_page_content_chunker.py +0 -84
  130. notionary/page/formatting/spacer_rules.py +0 -483
  131. notionary/util/fuzzy_matcher.py +0 -82
  132. notionary-0.2.16.dist-info/RECORD +0 -71
  133. /notionary/{elements/__init__.py → blocks/bookmark/bookmark_models.py} +0 -0
  134. /notionary/database/{database_exceptions.py → exceptions.py} +0 -0
  135. /notionary/util/{singleton_decorator.py → singleton.py} +0 -0
  136. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/LICENSE +0 -0
  137. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/WHEEL +0 -0
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+ from notionary.blocks.markdown_node import MarkdownNode
5
+
6
+
7
+ class BulletedListMarkdownBlockParams(BaseModel):
8
+ texts: list[str]
9
+
10
+
11
+ class BulletedListMarkdownNode(MarkdownNode):
12
+ """
13
+ Programmatic interface for creating Markdown bulleted list items.
14
+ Example:
15
+ - First item
16
+ - Second item
17
+ - Third item
18
+ """
19
+
20
+ def __init__(self, texts: list[str]):
21
+ self.texts = texts
22
+
23
+ @classmethod
24
+ def from_params(
25
+ cls, params: BulletedListMarkdownBlockParams
26
+ ) -> BulletedListMarkdownNode:
27
+ return cls(texts=params.texts)
28
+
29
+ def to_markdown(self) -> str:
30
+ result = []
31
+ for text in self.texts:
32
+ result.append(f"- {text}")
33
+ return "\n".join(result)
File without changes
@@ -0,0 +1,7 @@
1
+ from .callout_element import CalloutElement
2
+ from .callout_markdown_node import CalloutMarkdownNode
3
+
4
+ __all__ = [
5
+ "CalloutElement",
6
+ "CalloutMarkdownNode",
7
+ ]
@@ -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
@@ -0,0 +1,7 @@
1
+ from .code_element import CodeElement
2
+ from .code_markdown_node import CodeMarkdownNode
3
+
4
+ __all__ = [
5
+ "CodeElement",
6
+ "CodeMarkdownNode",
7
+ ]
@@ -1,11 +1,16 @@
1
1
  import re
2
2
 
3
- from typing import Dict, Any, Optional, List, Tuple
3
+ from typing import Optional, Any
4
4
  from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
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 CodeBlockElement(NotionBlockElement):
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(CodeBlockElement.PATTERN.search(text))
36
+ return bool(cls.PATTERN.search(text))
32
37
 
33
38
  @classmethod
34
- def match_notion(cls, block: Dict[str, Any]) -> bool:
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) -> Optional[Dict[str, Any]]:
44
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
40
45
  """Convert markdown code block to Notion code block."""
41
- match = CodeBlockElement.PATTERN.search(text)
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
- block["code"]["caption"] = [
69
- {
70
- "type": "text",
71
- "text": {"content": caption.strip()},
72
- "plain_text": caption.strip(),
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: Dict[str, Any]) -> Optional[str]:
80
- """Convert Notion code block to markdown code block."""
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
- # Extract the code content
88
- content = ""
89
- for text_block in rich_text:
90
- content += text_block.get("plain_text", "")
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
- language = code_data.get("language", "")
106
+ code_content = extract_content(rich_text)
107
+ caption_text = extract_caption(caption)
93
108
 
94
- # Extract caption if present
95
- caption_text = ""
96
- caption_data = code_data.get("caption", [])
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
- # Format as a markdown code block
101
- result = f"```{language}\n{content}\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.strip():
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) -> List[Tuple[int, int, Dict[str, Any]]]:
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 CodeBlockElement.PATTERN.finditer(text):
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```', # Without caption
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
@@ -0,0 +1,5 @@
1
+ from .column_element import ColumnElement
2
+
3
+ __all__ = [
4
+ "ColumnElement",
5
+ ]
@@ -1,9 +1,12 @@
1
1
  import re
2
- from typing import Dict, Any, Optional, List, Tuple, Callable
2
+ from typing import Optional, Callable
3
3
 
4
4
  from notionary.blocks import NotionBlockElement
5
- from notionary.page.formatting.spacer_rules import SPACER_MARKER
6
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
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], List[Dict[str, Any]]]
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: Dict[str, Any]) -> bool:
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) -> Optional[Dict[str, Any]]:
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: Dict[str, Any]) -> Optional[str]:
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
- ) -> List[Tuple[int, int, Dict[str, Any]]]:
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: List[str], start_index: int, converter_callback: Callable
145
- ) -> Tuple[int, int, Dict[str, Any], int]:
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
- columns_block = cls.markdown_to_notion(lines[start_index].strip())
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: List[str],
182
+ lines: list[str],
179
183
  start_index: int,
180
- columns_children: List[Dict[str, Any]],
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: List[str],
241
- columns_children: List[Dict[str, Any]],
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: List[str]) -> List[str]:
272
- """
273
- Preprocess column content to handle special cases like first headings.
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
@@ -0,0 +1,7 @@
1
+ from .divider_element import DividerElement
2
+ from .divider_markdown_node import DividerMarkdownNode
3
+
4
+ __all__ = [
5
+ "DividerElement",
6
+ "DividerMarkdownNode",
7
+ ]
@@ -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 ElementPromptContent, ElementPromptBuilder
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) -> Optional[Dict[str, Any]]:
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
- return {"type": "divider", "divider": {}}
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]: