notionary 0.2.6__py3-none-any.whl → 0.2.8__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.
@@ -37,7 +37,6 @@ class PageContentWriter(LoggingMixin):
37
37
  async def append_markdown(self, markdown_text: str, append_divider=False) -> bool:
38
38
  """
39
39
  Append markdown text to a Notion page, automatically handling content length limits.
40
-
41
40
  """
42
41
  if append_divider and not self.block_registry.contains(DividerElement):
43
42
  self.logger.warning(
@@ -45,9 +44,11 @@ class PageContentWriter(LoggingMixin):
45
44
  )
46
45
  append_divider = False
47
46
 
48
- # Append divider in markdonw format as it will be converted to a Notion divider block
47
+ # Append divider in markdown format as it will be converted to a Notion divider block
49
48
  if append_divider:
50
- markdown_text = markdown_text + "\n\n---\n\n"
49
+ markdown_text = markdown_text + "\n---"
50
+
51
+ markdown_text = self._process_markdown_whitespace(markdown_text)
51
52
 
52
53
  try:
53
54
  blocks = self._markdown_to_notion_converter.convert(markdown_text)
@@ -101,3 +102,102 @@ class PageContentWriter(LoggingMixin):
101
102
  except Exception as e:
102
103
  self.logger.error("Failed to delete block: %s", str(e))
103
104
  return False
105
+
106
+ def _process_markdown_whitespace(self, markdown_text: str) -> str:
107
+ """
108
+ Process markdown text to preserve code structure while removing unnecessary indentation.
109
+ Strips all leading whitespace from regular lines, but preserves relative indentation
110
+ within code blocks.
111
+
112
+ Args:
113
+ markdown_text: Original markdown text with potential leading whitespace
114
+
115
+ Returns:
116
+ Processed markdown text with corrected whitespace
117
+ """
118
+ lines = markdown_text.split("\n")
119
+ if not lines:
120
+ return ""
121
+
122
+ processed_lines = []
123
+ in_code_block = False
124
+ current_code_block = []
125
+
126
+ for line in lines:
127
+ # Handle code block markers
128
+ if self._is_code_block_marker(line):
129
+ if not in_code_block:
130
+ # Starting a new code block
131
+ in_code_block = True
132
+ processed_lines.append(self._process_code_block_start(line))
133
+ current_code_block = []
134
+ continue
135
+
136
+ # Ending a code block
137
+ processed_lines.extend(
138
+ self._process_code_block_content(current_code_block)
139
+ )
140
+ processed_lines.append("```")
141
+ in_code_block = False
142
+ continue
143
+
144
+ # Handle code block content
145
+ if in_code_block:
146
+ current_code_block.append(line)
147
+ continue
148
+
149
+ # Handle regular text
150
+ processed_lines.append(line.lstrip())
151
+
152
+ # Handle unclosed code block
153
+ if in_code_block and current_code_block:
154
+ processed_lines.extend(self._process_code_block_content(current_code_block))
155
+ processed_lines.append("```")
156
+
157
+ return "\n".join(processed_lines)
158
+
159
+ def _is_code_block_marker(self, line: str) -> bool:
160
+ """Check if a line is a code block marker."""
161
+ return line.lstrip().startswith("```")
162
+
163
+ def _process_code_block_start(self, line: str) -> str:
164
+ """Extract and normalize the code block opening marker."""
165
+ language = line.lstrip().replace("```", "", 1).strip()
166
+ return "```" + language
167
+
168
+ def _process_code_block_content(self, code_lines: list) -> list:
169
+ """
170
+ Normalize code block indentation by removing the minimum common indentation.
171
+
172
+ Args:
173
+ code_lines: List of code block content lines
174
+
175
+ Returns:
176
+ List of processed code lines with normalized indentation
177
+ """
178
+ if not code_lines:
179
+ return []
180
+
181
+ # Find non-empty lines to determine minimum indentation
182
+ non_empty_code_lines = [line for line in code_lines if line.strip()]
183
+ if not non_empty_code_lines:
184
+ return [""] * len(code_lines) # All empty lines stay empty
185
+
186
+ # Calculate minimum indentation
187
+ min_indent = min(
188
+ len(line) - len(line.lstrip()) for line in non_empty_code_lines
189
+ )
190
+ if min_indent == 0:
191
+ return code_lines # No common indentation to remove
192
+
193
+ # Process each line
194
+ processed_code_lines = []
195
+ for line in code_lines:
196
+ if not line.strip():
197
+ processed_code_lines.append("") # Keep empty lines empty
198
+ continue
199
+
200
+ # Remove exactly the minimum indentation
201
+ processed_code_lines.append(line[min_indent:])
202
+
203
+ return processed_code_lines
@@ -1,5 +1,7 @@
1
1
  from typing import Dict, Any, List, Optional, Tuple
2
+ import re
2
3
 
4
+ from notionary.elements.column_element import ColumnElement
3
5
  from notionary.elements.registry.block_registry import BlockRegistry
4
6
  from notionary.elements.registry.block_registry_builder import (
5
7
  BlockRegistryBuilder,
@@ -9,9 +11,11 @@ from notionary.elements.registry.block_registry_builder import (
9
11
  class MarkdownToNotionConverter:
10
12
  """Converts Markdown text to Notion API block format with support for pipe syntax for nested structures."""
11
13
 
12
- SPACER_MARKER = "<!-- spacer -->"
14
+ SPACER_MARKER = "---spacer---"
13
15
  TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
14
16
  PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
17
+ HEADING_PATTERN = r"^(#{1,6})\s+(.+)$"
18
+ DIVIDER_PATTERN = r"^-{3,}$"
15
19
 
16
20
  def __init__(self, block_registry: Optional[BlockRegistry] = None):
17
21
  """Initialize the converter with an optional custom block registry."""
@@ -19,14 +23,21 @@ class MarkdownToNotionConverter:
19
23
  block_registry or BlockRegistryBuilder().create_full_registry()
20
24
  )
21
25
 
26
+ if self._block_registry.contains(ColumnElement):
27
+ ColumnElement.set_converter_callback(self.convert)
28
+
22
29
  def convert(self, markdown_text: str) -> List[Dict[str, Any]]:
23
30
  """Convert markdown text to Notion API block format."""
24
31
  if not markdown_text:
25
32
  return []
26
33
 
34
+ # Preprocess markdown to add spacers before headings and dividers
35
+ processed_markdown = self._add_spacers_before_elements(markdown_text)
36
+ print("Processed Markdown:", processed_markdown)
37
+
27
38
  # Collect all blocks with their positions in the text
28
39
  all_blocks_with_positions = self._collect_all_blocks_with_positions(
29
- markdown_text
40
+ processed_markdown
30
41
  )
31
42
 
32
43
  # Sort all blocks by their position in the text
@@ -38,6 +49,135 @@ class MarkdownToNotionConverter:
38
49
  # Process spacing between blocks
39
50
  return self._process_block_spacing(blocks)
40
51
 
52
+ def _add_spacers_before_elements(self, markdown_text: str) -> str:
53
+ """Add spacer markers before every heading (except the first one) and before every divider,
54
+ but ignore content inside code blocks and consecutive headings."""
55
+ lines = markdown_text.split("\n")
56
+ processed_lines = []
57
+ found_first_heading = False
58
+ in_code_block = False
59
+ last_line_was_spacer = False
60
+ last_non_empty_was_heading = False
61
+
62
+ i = 0
63
+ while i < len(lines):
64
+ line = lines[i]
65
+
66
+ # Check for code block boundaries and handle accordingly
67
+ if self._is_code_block_marker(line):
68
+ in_code_block = not in_code_block
69
+ processed_lines.append(line)
70
+ if line.strip(): # If not empty
71
+ last_non_empty_was_heading = False
72
+ last_line_was_spacer = False
73
+ i += 1
74
+ continue
75
+
76
+ # Skip processing markdown inside code blocks
77
+ if in_code_block:
78
+ processed_lines.append(line)
79
+ if line.strip(): # If not empty
80
+ last_non_empty_was_heading = False
81
+ last_line_was_spacer = False
82
+ i += 1
83
+ continue
84
+
85
+ # Process line with context about consecutive headings
86
+ result = self._process_line_for_spacers(
87
+ line,
88
+ processed_lines,
89
+ found_first_heading,
90
+ last_line_was_spacer,
91
+ last_non_empty_was_heading,
92
+ )
93
+
94
+ last_line_was_spacer = result["added_spacer"]
95
+
96
+ # Update tracking of consecutive headings and first heading
97
+ if line.strip(): # Not empty line
98
+ is_heading = re.match(self.HEADING_PATTERN, line) is not None
99
+ if is_heading:
100
+ if not found_first_heading:
101
+ found_first_heading = True
102
+ last_non_empty_was_heading = True
103
+ elif line.strip() != self.SPACER_MARKER: # Not a spacer or heading
104
+ last_non_empty_was_heading = False
105
+
106
+ i += 1
107
+
108
+ return "\n".join(processed_lines)
109
+
110
+ def _is_code_block_marker(self, line: str) -> bool:
111
+ """Check if a line is a code block marker (start or end)."""
112
+ return line.strip().startswith("```")
113
+
114
+ def _process_line_for_spacers(
115
+ self,
116
+ line: str,
117
+ processed_lines: List[str],
118
+ found_first_heading: bool,
119
+ last_line_was_spacer: bool,
120
+ last_non_empty_was_heading: bool,
121
+ ) -> Dict[str, bool]:
122
+ """
123
+ Process a single line to add spacers before headings and dividers if needed.
124
+
125
+ Args:
126
+ line: The line to process
127
+ processed_lines: List of already processed lines to append to
128
+ found_first_heading: Whether the first heading has been found
129
+ last_line_was_spacer: Whether the last added line was a spacer
130
+ last_non_empty_was_heading: Whether the last non-empty line was a heading
131
+
132
+ Returns:
133
+ Dictionary with processing results
134
+ """
135
+ added_spacer = False
136
+ line_stripped = line.strip()
137
+ is_empty = not line_stripped
138
+
139
+ # Skip empty lines
140
+ if is_empty:
141
+ processed_lines.append(line)
142
+ return {"added_spacer": False}
143
+
144
+ # Check if line is a heading
145
+ if re.match(self.HEADING_PATTERN, line):
146
+ if (
147
+ found_first_heading
148
+ and not last_line_was_spacer
149
+ and not last_non_empty_was_heading
150
+ ):
151
+ # Add spacer only if:
152
+ # 1. Not the first heading
153
+ # 2. Last non-empty line was not a heading
154
+ # 3. Last line was not already a spacer
155
+ processed_lines.append(self.SPACER_MARKER)
156
+ added_spacer = True
157
+
158
+ processed_lines.append(line)
159
+
160
+ # Check if line is a divider
161
+ elif re.match(self.DIVIDER_PATTERN, line):
162
+ if not last_line_was_spacer:
163
+ # Only add a single spacer line before dividers (no extra line breaks)
164
+ processed_lines.append(self.SPACER_MARKER)
165
+ added_spacer = True
166
+
167
+ processed_lines.append(line)
168
+
169
+ # Check if this line itself is a spacer
170
+ elif line_stripped == self.SPACER_MARKER:
171
+ # Never add consecutive spacers
172
+ if not last_line_was_spacer:
173
+ processed_lines.append(line)
174
+ added_spacer = True
175
+
176
+ else:
177
+ processed_lines.append(line)
178
+
179
+ return {"added_spacer": added_spacer}
180
+
41
181
  def _collect_all_blocks_with_positions(
42
182
  self, markdown_text: str
43
183
  ) -> List[Tuple[int, int, Dict[str, Any]]]:
@@ -75,13 +215,10 @@ class MarkdownToNotionConverter:
75
215
  if not toggleable_elements:
76
216
  return []
77
217
 
78
- # Process each toggleable element type
79
218
  for element in toggleable_elements:
80
- if hasattr(element, "find_matches"):
81
- # Find matches with context awareness
82
- matches = element.find_matches(text, self.convert, context_aware=True)
83
- if matches:
84
- toggleable_blocks.extend(matches)
219
+ matches = element.find_matches(text, self.convert, context_aware=True)
220
+ if matches:
221
+ toggleable_blocks.extend(matches)
85
222
 
86
223
  return toggleable_blocks
87
224
 
@@ -112,9 +249,6 @@ class MarkdownToNotionConverter:
112
249
 
113
250
  multiline_blocks = []
114
251
  for element in multiline_elements:
115
- if not hasattr(element, "find_matches"):
116
- continue
117
-
118
252
  matches = element.find_matches(text)
119
253
 
120
254
  if not matches:
@@ -202,8 +336,6 @@ class MarkdownToNotionConverter:
202
336
 
203
337
  def _is_pipe_syntax_line(self, line: str) -> bool:
204
338
  """Check if a line uses pipe syntax (for nested content)."""
205
- import re
206
-
207
339
  return bool(re.match(self.PIPE_CONTENT_PATTERN, line))
208
340
 
209
341
  def _process_line(
@@ -1,6 +1,6 @@
1
- from textwrap import dedent
2
1
  from typing import Type, List
3
2
  from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.text_inline_formatter import TextInlineFormatter
4
4
 
5
5
 
6
6
  class MarkdownSyntaxPromptGenerator:
@@ -11,10 +11,8 @@ class MarkdownSyntaxPromptGenerator:
11
11
  and formats them optimally for LLMs.
12
12
  """
13
13
 
14
- SYSTEM_PROMPT_TEMPLATE = dedent(
15
- """
16
- You are a knowledgeable assistant that helps users create content for Notion pages.
17
- Notion supports standard Markdown with some special extensions for creating rich content.
14
+ SYSTEM_PROMPT_TEMPLATE = """
15
+ You create content for Notion pages using Markdown syntax with special Notion extensions.
18
16
 
19
17
  # Understanding Notion Blocks
20
18
 
@@ -27,30 +25,36 @@ class MarkdownSyntaxPromptGenerator:
27
25
 
28
26
  1. Do NOT start content with a level 1 heading (# Heading). In Notion, the page title is already displayed in the metadata, so starting with an H1 heading is redundant. Begin with H2 (## Heading) or lower for section headings.
29
27
 
30
- 2. INLINE FORMATTING - VERY IMPORTANT:
31
- You can use inline formatting within almost any block type.
32
- Combine **bold**, _italic_, `code`, and other formatting as needed.
33
- Format text to create visual hierarchy and emphasize important points.
34
- DO NOT overuse formatting - be strategic with formatting for best readability.
35
-
36
- 3. BACKTICK HANDLING - EXTREMELY IMPORTANT:
37
- ❌ NEVER wrap entire content or responses in triple backticks (```).
38
- DO NOT use triple backticks (```) for anything except CODE BLOCKS or DIAGRAMS.
39
- DO NOT use triple backticks to mark or highlight regular text or examples.
40
- USE triple backticks ONLY for actual programming code, pseudocode, or specialized notation.
41
- For inline code, use single backticks (`code`).
42
- When showing Markdown syntax examples, use inline code formatting with single backticks.
43
-
44
- 4. BLOCK SEPARATION - IMPORTANT:
45
- Use empty lines between different blocks to ensure proper rendering in Notion.
46
- For major logical sections, use the spacer element (see documentation below).
47
- ⚠️ While headings can sometimes work without an empty line before the following paragraph, including empty lines between all block types ensures consistent rendering.
48
-
49
- 5. CONTENT FORMATTING - CRITICAL:
50
- DO NOT include introductory phrases like "I understand that..." or "Here's the content...".
51
- Provide ONLY the requested content directly without any prefacing text or meta-commentary.
52
- Generate just the content itself, formatted according to these guidelines."""
53
- )
28
+ 2. BACKTICK HANDLING - EXTREMELY IMPORTANT:
29
+ - NEVER wrap entire content or responses in triple backticks (```).
30
+ - DO NOT use triple backticks (```) for anything except CODE BLOCKS or DIAGRAMS.
31
+ - DO NOT use triple backticks to mark or highlight regular text or examples.
32
+ - USE triple backticks ONLY for actual programming code, pseudocode, or specialized notation.
33
+ - For inline code, use single backticks (`code`).
34
+ - When showing Markdown syntax examples, use inline code formatting with single backticks.
35
+
36
+ 3. CONTENT FORMATTING - CRITICAL:
37
+ - DO NOT include introductory phrases like "I understand that..." or "Here's the content...".
38
+ - Provide ONLY the requested content directly without any prefacing text or meta-commentary.
39
+ - Generate just the content itself, formatted according to these guidelines.
40
+ - USE INLINE FORMATTING to enhance readability:
41
+ - Use *italic* for emphasis, terminology, and definitions
42
+ - Use `code` for technical terms, file paths, variables, and commands
43
+ - Use **bold** sparingly for truly important information
44
+ - Use appropriate inline formatting naturally throughout the content, but don't overuse it
45
+
46
+ 4. USER INSTRUCTIONS - VERY IMPORTANT:
47
+ - Follow the user's formatting instructions EXACTLY and in the specified order
48
+ - When the user requests specific elements (e.g., "first a callout, then 4 bullet points"), create them in that precise sequence
49
+ - Adhere strictly to any structural requirements provided by the user
50
+ - Do not deviate from or reinterpret the user's formatting requests
51
+
52
+ 5. ADD EMOJIS TO HEADINGS - REQUIRED UNLESS EXPLICITLY TOLD NOT TO:
53
+ - ALWAYS add appropriate emojis at the beginning of headings to improve structure and readability
54
+ - Choose emojis that represent the content or theme of each section
55
+ - Format as: ## 🚀 Heading Text (with space after emoji)
56
+ - Only omit emojis if the user explicitly instructs you not to use them
57
+ """
54
58
 
55
59
  @staticmethod
56
60
  def generate_element_doc(element_class: Type[NotionBlockElement]) -> str:
@@ -0,0 +1,271 @@
1
+ Metadata-Version: 2.4
2
+ Name: notionary
3
+ Version: 0.2.8
4
+ Summary: A toolkit to convert between Markdown and Notion blocks
5
+ Home-page: https://github.com/mathisarends/notionary
6
+ Author: Mathis Arends
7
+ Author-email: mathisarends27@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: httpx>=0.28.0
14
+ Requires-Dist: python-dotenv>=1.1.0
15
+ Requires-Dist: pydantic>=2.11.4
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
27
+ # Notionary 📝
28
+
29
+ [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/downloads/)
30
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
31
+
32
+ **Notionary** is a powerful Python library for interacting with the Notion API, making it easy to create, update, and manage Notion pages and databases programmatically with a clean, intuitive interface. It's specifically designed to be the foundation for AI-driven Notion content generation.
33
+
34
+ ---
35
+
36
+ ## Features
37
+
38
+ - **Rich Markdown Support**: Create Notion pages using intuitive Markdown syntax with custom extensions
39
+ - **Dynamic Database Operations**: Create, update, and query database entries with schema auto-detection
40
+ - **Extensible Block Registry**: Add, customize, or remove Notion block elements with a flexible registry pattern
41
+ - **LLM-Ready Prompts**: Generate system prompts explaining Markdown syntax for LLMs to create Notion content
42
+ - **Async-First Design**: Built for modern Python with full async/await support
43
+ - **Schema-Based Validation**: Automatic property validation based on database schemas
44
+ - **Intelligent Content Conversion**: Bidirectional conversion between Markdown and Notion blocks
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install notionary
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ### Creating and Managing Pages
59
+
60
+ ```python
61
+ import asyncio
62
+ from notionary import NotionPage
63
+
64
+ async def main():
65
+ # Create a page from URL
66
+ page = NotionPage.from_url("https://www.notion.so/your-page-url")
67
+
68
+ # Or find by name
69
+ page = await NotionPage.from_page_name("My Project Page")
70
+
71
+ # Update page metadata
72
+ await page.set_title("Updated Title")
73
+ await page.set_emoji_icon("🚀")
74
+ await page.set_random_gradient_cover()
75
+
76
+ # Add markdown content
77
+ markdown = """
78
+ # Project Overview
79
+
80
+ !> [💡] This page was created programmatically using Notionary.
81
+
82
+ ## Features
83
+ - **Rich** Markdown support
84
+ - Async functionality
85
+ - Custom syntax extensions
86
+
87
+ +++ Implementation Details
88
+ | Notionary uses a custom converter to transform Markdown into Notion blocks.
89
+ | This makes it easy to create rich content programmatically.
90
+ """
91
+
92
+ await page.replace_content(markdown)
93
+
94
+ if __name__ == "__main__":
95
+ asyncio.run(main())
96
+ ```
97
+
98
+ ### Working with Databases
99
+
100
+ ```python
101
+ import asyncio
102
+ from notionary import NotionDatabase, DatabaseDiscovery
103
+
104
+ async def main():
105
+ # Discover available databases
106
+ discovery = DatabaseDiscovery()
107
+ await discovery()
108
+
109
+ # Connect to a database by name
110
+ db = await NotionDatabase.from_database_name("Projects")
111
+
112
+ # Create a new page in the database
113
+ page = await db.create_blank_page()
114
+
115
+ # Set properties
116
+ await page.set_property_value_by_name("Status", "In Progress")
117
+ await page.set_property_value_by_name("Priority", "High")
118
+
119
+ # Query pages from database
120
+ async for page in db.iter_pages():
121
+ title = await page.get_title()
122
+ print(f"Page: {title}")
123
+
124
+ if __name__ == "__main__":
125
+ asyncio.run(main())
126
+ ```
127
+
128
+ ## Custom Markdown Syntax
129
+
130
+ Notionary extends standard Markdown with special syntax to support Notion-specific features:
131
+
132
+ ### Text Formatting
133
+
134
+ - Standard: `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``
135
+ - Links: `[text](url)`
136
+ - Quotes: `> This is a quote`
137
+ - Divider: `---`
138
+
139
+ ### Callouts
140
+
141
+ ```markdown
142
+ !> [💡] This is a default callout with the light bulb emoji
143
+ !> [🔔] This is a notification with a bell emoji
144
+ !> [⚠️] Warning: This is an important note
145
+ ```
146
+
147
+ ### Toggles
148
+
149
+ ```markdown
150
+ +++ How to use Notionary
151
+ | 1. Initialize with NotionPage
152
+ | 2. Update metadata with set_title(), set_emoji_icon(), etc.
153
+ | 3. Add content with replace_content() or append_markdown()
154
+ ```
155
+
156
+ ### Multi-Column Layout
157
+
158
+ ```markdown
159
+ ::: columns
160
+ ::: column
161
+
162
+ ## Left Column
163
+
164
+ - Item 1
165
+ - Item 2
166
+ - Item 3
167
+ :::
168
+ ::: column
169
+
170
+ ## Right Column
171
+
172
+ This text appears in the second column. Multi-column layouts are perfect for:
173
+
174
+ - Comparing features
175
+ - Creating side-by-side content
176
+ - Improving readability of wide content
177
+ :::
178
+ :::
179
+ ```
180
+
181
+ ### Code Blocks
182
+
183
+ ```python
184
+ def hello_world():
185
+ print("Hello from Notionary!")
186
+ ```
187
+
188
+ ### To-do Lists
189
+
190
+ ```markdown
191
+ - [ ] Define project scope
192
+ - [x] Create timeline
193
+ - [ ] Assign resources
194
+ ```
195
+
196
+ ### Tables
197
+
198
+ ```markdown
199
+ | Feature | Status | Priority |
200
+ | --------------- | ----------- | -------- |
201
+ | API Integration | Complete | High |
202
+ | Documentation | In Progress | Medium |
203
+ ```
204
+
205
+ ### More Elements
206
+
207
+ ```markdown
208
+ ![Caption](https://example.com/image.jpg)
209
+ @[Caption](https://youtube.com/watch?v=...)
210
+ [bookmark](https://example.com "Title" "Description")
211
+ ```
212
+
213
+ ## Block Registry & Customization
214
+
215
+ ```python
216
+ from notionary import NotionPage, BlockRegistryBuilder
217
+
218
+ # Create a custom registry with only the elements you need
219
+ custom_registry = (
220
+ BlockRegistryBuilder()
221
+ .with_headings()
222
+ .with_callouts()
223
+ .with_toggles()
224
+ .with_columns() # Include multi-column support
225
+ .with_code()
226
+ .with_todos()
227
+ .with_paragraphs()
228
+ .build()
229
+ )
230
+
231
+ # Apply this registry to a page
232
+ page = NotionPage.from_url("https://www.notion.so/your-page-url")
233
+ page.block_registry = custom_registry
234
+
235
+ # Replace content using only supported elements
236
+ await page.replace_content("# Custom heading with selected elements only")
237
+ ```
238
+
239
+ ## AI-Ready: Generate LLM Prompts
240
+
241
+ ```python
242
+ from notionary import BlockRegistryBuilder
243
+
244
+ # Create a registry with all standard elements
245
+ registry = BlockRegistryBuilder.create_full_registry()
246
+
247
+ # Generate the LLM system prompt
248
+ llm_system_prompt = registry.get_notion_markdown_syntax_prompt()
249
+ print(llm_system_prompt)
250
+ ```
251
+
252
+ ## Examples
253
+
254
+ See the `examples/` folder for:
255
+
256
+ - [Database discovery and querying](examples/database_discovery_example.py)
257
+ - [Rich page creation with Markdown](examples/page_example.py)
258
+ - [Database management](examples/database_management_example.py)
259
+ - [Iterating through database entries](examples/database_iteration_example.py)
260
+ - [Temporary usage & debugging](examples/temp.py)
261
+
262
+ ## Perfect for AI Agents and Automation
263
+
264
+ - **LLM Integration**: Generate Notion-compatible content with any LLM using the system prompt generator
265
+ - **Dynamic Content Generation**: AI agents can generate content in Markdown and render it directly as Notion pages
266
+ - **Schema-Aware Operations**: Automatically validate and format properties based on database schemas
267
+ - **Simplified API**: Clean, intuitive interface for both human developers and AI systems
268
+
269
+ ## Contributing
270
+
271
+ Contributions welcome — feel free to submit a pull request!