notionary 0.1.24__py3-none-any.whl → 0.1.26__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 (35) hide show
  1. notionary/elements/audio_element.py +41 -38
  2. notionary/elements/bookmark_element.py +36 -27
  3. notionary/elements/bulleted_list_element.py +28 -21
  4. notionary/elements/callout_element.py +39 -31
  5. notionary/elements/code_block_element.py +38 -26
  6. notionary/elements/divider_element.py +29 -18
  7. notionary/elements/embed_element.py +37 -28
  8. notionary/elements/heading_element.py +39 -24
  9. notionary/elements/image_element.py +33 -24
  10. notionary/elements/mention_element.py +40 -29
  11. notionary/elements/notion_block_element.py +13 -31
  12. notionary/elements/numbered_list_element.py +29 -20
  13. notionary/elements/paragraph_element.py +37 -31
  14. notionary/elements/prompts/element_prompt_content.py +91 -8
  15. notionary/elements/prompts/synthax_prompt_builder.py +64 -17
  16. notionary/elements/qoute_element.py +72 -74
  17. notionary/elements/registry/block_element_registry.py +1 -1
  18. notionary/elements/registry/block_element_registry_builder.py +6 -9
  19. notionary/elements/table_element.py +49 -36
  20. notionary/elements/text_inline_formatter.py +23 -15
  21. notionary/elements/{todo_lists.py → todo_element.py} +34 -25
  22. notionary/elements/toggle_element.py +184 -108
  23. notionary/elements/toggleable_heading_element.py +269 -0
  24. notionary/elements/video_element.py +37 -28
  25. notionary/page/content/page_content_manager.py +5 -8
  26. notionary/page/markdown_to_notion_converter.py +269 -274
  27. notionary/page/notion_page.py +1 -1
  28. notionary/page/notion_to_markdown_converter.py +20 -95
  29. {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/METADATA +1 -1
  30. notionary-0.1.26.dist-info/RECORD +58 -0
  31. {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/WHEEL +1 -1
  32. notionary/elements/column_element.py +0 -307
  33. notionary-0.1.24.dist-info/RECORD +0 -58
  34. {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/licenses/LICENSE +0 -0
  35. {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,18 @@
1
1
  from typing import Dict, Any, Optional
2
- from typing_extensions import override
3
2
 
4
3
  from notionary.elements.notion_block_element import NotionBlockElement
5
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
4
+ from notionary.elements.prompts.element_prompt_content import (
5
+ ElementPromptBuilder,
6
+ ElementPromptContent,
7
+ )
6
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
7
9
 
8
10
 
9
11
  class ParagraphElement(NotionBlockElement):
10
12
  """Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
11
13
 
12
- @override
13
- @staticmethod
14
- def match_markdown(text: str) -> bool:
14
+ @classmethod
15
+ def match_markdown(cls, text: str) -> bool:
15
16
  """
16
17
  Check if text is a markdown paragraph.
17
18
  Paragraphs are essentially any text that isn't matched by other block elements.
@@ -19,15 +20,13 @@ class ParagraphElement(NotionBlockElement):
19
20
  """
20
21
  return True
21
22
 
22
- @override
23
- @staticmethod
24
- def match_notion(block: Dict[str, Any]) -> bool:
23
+ @classmethod
24
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
25
25
  """Check if block is a Notion paragraph."""
26
26
  return block.get("type") == "paragraph"
27
27
 
28
- @override
29
- @staticmethod
30
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
28
+ @classmethod
29
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
31
30
  """Convert markdown paragraph to Notion paragraph block."""
32
31
  if not text.strip():
33
32
  return None
@@ -39,9 +38,8 @@ class ParagraphElement(NotionBlockElement):
39
38
  },
40
39
  }
41
40
 
42
- @override
43
- @staticmethod
44
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
41
+ @classmethod
42
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
45
43
  """Convert Notion paragraph block to markdown paragraph."""
46
44
  if block.get("type") != "paragraph":
47
45
  return None
@@ -52,26 +50,34 @@ class ParagraphElement(NotionBlockElement):
52
50
  text = TextInlineFormatter.extract_text_with_formatting(rich_text)
53
51
  return text if text else None
54
52
 
55
- @override
56
- @staticmethod
57
- def is_multiline() -> bool:
53
+ @classmethod
54
+ def is_multiline(cls) -> bool:
58
55
  return False
59
56
 
60
57
  @classmethod
61
58
  def get_llm_prompt_content(cls) -> ElementPromptContent:
62
59
  """
63
- Returns structured LLM prompt metadata for the paragraph element.
60
+ Returns structured LLM prompt metadata for the paragraph element,
61
+ including information about supported inline formatting.
64
62
  """
65
- return {
66
- "description": "Creates standard paragraph blocks for regular text content.",
67
- "when_to_use": (
68
- "Use paragraphs for normal text content. Paragraphs are the default block type and will be used "
69
- "when no other specific formatting is applied."
70
- ),
71
- "syntax": "Just write text normally without any special prefix",
72
- "examples": [
73
- "This is a simple paragraph with plain text.",
74
- "This paragraph has **bold** and *italic* formatting.",
75
- "You can also include [links](https://example.com) or `inline code`.",
76
- ],
77
- }
63
+ return (
64
+ ElementPromptBuilder()
65
+ .with_description(
66
+ "Creates standard paragraph blocks for regular text content with support for inline formatting: "
67
+ "**bold**, *italic*, `code`, ~~strikethrough~~, __underline__, and [links](url)."
68
+ )
69
+ .with_usage_guidelines(
70
+ "Use for normal text content. Paragraphs are the default block type when no specific formatting is applied. "
71
+ "Apply inline formatting to highlight key points or provide links to resources."
72
+ )
73
+ .with_syntax("Just write text normally without any special prefix")
74
+ .with_examples(
75
+ [
76
+ "This is a simple paragraph with plain text.",
77
+ "This paragraph has **bold** and *italic* formatting.",
78
+ "You can include [links](https://example.com) or `inline code`.",
79
+ "Advanced formatting: ~~strikethrough~~ and __underlined text__.",
80
+ ]
81
+ )
82
+ .build()
83
+ )
@@ -1,9 +1,11 @@
1
- from typing import NotRequired, TypedDict, List
1
+ from dataclasses import field, dataclass
2
+ from typing import Optional, List, Self
2
3
 
3
4
 
4
- class ElementPromptContent(TypedDict):
5
+ @dataclass
6
+ class ElementPromptContent:
5
7
  """
6
- Typed dictionary defining the standardized structure for element prompt content.
8
+ Dataclass defining the standardized structure for element prompt content.
7
9
  This ensures consistent formatting across all Notion block elements.
8
10
  """
9
11
 
@@ -13,12 +15,93 @@ class ElementPromptContent(TypedDict):
13
15
  syntax: str
14
16
  """The exact markdown syntax pattern used to create this element."""
15
17
 
16
- examples: List[str]
17
- """List of practical usage examples showing the element in context."""
18
-
19
18
  when_to_use: str
20
19
  """Guidelines explaining the appropriate scenarios for using this element."""
21
-
22
- avoid: NotRequired[str]
20
+
21
+ examples: List[str] = field(default_factory=list)
22
+ """List of practical usage examples showing the element in context."""
23
+
24
+ avoid: Optional[str] = None
23
25
  """Optional field listing scenarios when this element should be avoided."""
24
26
 
27
+ def __post_init__(self):
28
+ """Validates that the content meets minimum requirements."""
29
+ if not self.description:
30
+ raise ValueError("Description is required")
31
+ if not self.syntax:
32
+ raise ValueError("Syntax is required")
33
+ if not self.examples:
34
+ raise ValueError("At least one example is required")
35
+ if not self.when_to_use:
36
+ raise ValueError("Usage guidelines are required")
37
+
38
+
39
+ class ElementPromptBuilder:
40
+ """
41
+ Builder class for creating ElementPromptContent with a fluent interface.
42
+ Provides better IDE support and validation for creating prompts.
43
+ """
44
+
45
+ def __init__(self) -> None:
46
+ self._description: Optional[str] = None
47
+ self._syntax: Optional[str] = None
48
+ self._examples: List[str] = []
49
+ self._when_to_use: Optional[str] = None
50
+ self._avoid: Optional[str] = None
51
+
52
+ def with_description(self, description: str) -> Self:
53
+ """Set the description of the element."""
54
+ self._description = description
55
+ return self
56
+
57
+ def with_syntax(self, syntax: str) -> Self:
58
+ """Set the syntax pattern for the element."""
59
+ self._syntax = syntax
60
+ return self
61
+
62
+ def add_example(self, example: str) -> Self:
63
+ """Add a usage example for the element."""
64
+ self._examples.append(example)
65
+ return self
66
+
67
+ def with_examples(self, examples: List[str]) -> Self:
68
+ """Set the list of usage examples for the element."""
69
+ self._examples = examples.copy()
70
+ return self
71
+
72
+ def with_usage_guidelines(self, when_to_use: str) -> Self:
73
+ """Set the usage guidelines for the element."""
74
+ self._when_to_use = when_to_use
75
+ return self
76
+
77
+ def with_avoidance_guidelines(self, avoid: str) -> Self:
78
+ """Set the scenarios when this element should be avoided."""
79
+ self._avoid = avoid
80
+ return self
81
+
82
+ def build(self) -> ElementPromptContent:
83
+ """
84
+ Build and validate the ElementPromptContent object.
85
+
86
+ Returns:
87
+ A valid ElementPromptContent object.
88
+
89
+ Raises:
90
+ ValueError: If any required field is missing.
91
+ """
92
+ if not self._description:
93
+ raise ValueError("Description is required")
94
+ if not self._syntax:
95
+ raise ValueError("Syntax is required")
96
+ if not self._examples:
97
+ raise ValueError("At least one example is required")
98
+ if not self._when_to_use:
99
+ raise ValueError("Usage guidelines are required")
100
+
101
+ return ElementPromptContent(
102
+ description=self._description,
103
+ syntax=self._syntax,
104
+ examples=self._examples,
105
+ when_to_use=self._when_to_use,
106
+ avoid=self._avoid,
107
+ )
@@ -13,33 +13,81 @@ class MarkdownSyntaxPromptBuilder:
13
13
  SYSTEM_PROMPT_TEMPLATE = """You are a knowledgeable assistant that helps users create content for Notion pages.
14
14
  Notion supports standard Markdown with some special extensions for creating rich content.
15
15
 
16
+ # Understanding Notion Blocks
17
+ Notion documents are composed of individual blocks. Each block has a specific type (paragraph, heading, list item, etc.) and format.
18
+ The Markdown syntax you use directly maps to these Notion blocks.
19
+
20
+ ## Inline Formatting
21
+ Inline formatting can be used within most block types to style your text. You can combine multiple formatting options.
22
+ **Syntax:** **bold**, *italic*, `code`, ~~strikethrough~~, __underline__, [text](url)
23
+ **Examples:**
24
+ - This text has a **bold** word.
25
+ - This text has an *italic* word.
26
+ - This text has `code` formatting.
27
+ - This text has ~~strikethrough~~ formatting.
28
+ - This text has __underlined__ formatting.
29
+ - This has a [hyperlink](https://example.com).
30
+ - You can **combine *different* formatting** styles.
31
+
32
+ **When to use:** Use inline formatting to highlight important words, provide emphasis, show code or paths, or add hyperlinks. This helps create visual hierarchy and improves scanability.
33
+
34
+ ## Spacers and Block Separation
35
+ There are two ways to create visual separation between blocks:
36
+
37
+ 1. **Empty Lines**: Simply add a blank line between blocks
38
+ **Syntax:** Press Enter twice between blocks
39
+ **Example:**
40
+ First paragraph.
41
+
42
+ Second paragraph after an empty line.
43
+
44
+ 2. **HTML Comment Spacer**: For more deliberate spacing between logical sections
45
+ **Syntax:** <!-- spacer -->
46
+ **Example:**
47
+ ## First Section
48
+ Content here.
49
+ <!-- spacer -->
50
+ ## Second Section
51
+ More content here.
52
+
53
+ **When to use:** Use empty lines for basic separation between blocks. Use the HTML comment spacer (<!-- spacer -->) to create more obvious visual separation between major logical sections of your document.
54
+
16
55
  {element_docs}
17
56
 
18
57
  CRITICAL USAGE GUIDELINES:
19
58
 
20
59
  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.
21
60
 
22
- 2. BACKTICK HANDLING - EXTREMELY IMPORTANT:
61
+ 2. INLINE FORMATTING - VERY IMPORTANT:
62
+ ✅ You can use inline formatting within almost any block type.
63
+ ✅ Combine **bold**, *italic*, `code`, and other formatting as needed.
64
+ ✅ Format text to create visual hierarchy and emphasize important points.
65
+ ❌ DO NOT overuse formatting - be strategic with formatting for best readability.
66
+
67
+ 3. BACKTICK HANDLING - EXTREMELY IMPORTANT:
23
68
  ❌ NEVER wrap entire content or responses in triple backticks (```).
24
69
  ❌ DO NOT use triple backticks (```) for anything except CODE BLOCKS or DIAGRAMS.
25
70
  ❌ DO NOT use triple backticks to mark or highlight regular text or examples.
26
71
  ✅ USE triple backticks ONLY for actual programming code, pseudocode, or specialized notation.
72
+ ✅ For inline code, use single backticks (`code`).
27
73
  ✅ When showing Markdown syntax examples, use inline code formatting with single backticks.
28
74
 
29
- 3. Use inline formatting (bold, italic, etc.) across all content to enhance readability.
30
- Proper typography is essential for creating scannable, well-structured documents.
31
-
32
- 4. Notion's extensions to Markdown provide richer formatting options than standard Markdown
33
- while maintaining the familiar Markdown syntax for basic elements.
75
+ 4. BLOCK SEPARATION - IMPORTANT:
76
+ Use empty lines between different blocks to ensure proper rendering in Notion.
77
+ ✅ For major logical sections, add the HTML comment spacer: <!-- spacer -->
78
+ This spacer creates better visual breaks between key sections of your document.
79
+ ⚠️ While headings can sometimes work without an empty line before the following paragraph, including empty lines between all block types ensures consistent rendering.
34
80
 
35
- 5. Always structure content with clear headings, lists, and paragraphs to create visually appealing
36
- and well-organized documents.
81
+ 5. TOGGLE BLOCKS - NOTE:
82
+ ✅ For toggle blocks and collapsible headings, use pipe prefixes (|) for content.
83
+ ✅ Each line within a toggle should start with a pipe character followed by a space.
84
+ ❌ Do not use the pipe character for any other blocks.
37
85
 
38
86
  6. CONTENT FORMATTING - CRITICAL:
39
87
  ❌ DO NOT include introductory phrases like "I understand that..." or "Here's the content...".
40
88
  ✅ Provide ONLY the requested content directly without any prefacing text or meta-commentary.
41
89
  ✅ Generate just the content itself, formatted according to these guidelines.
42
- """
90
+ """
43
91
 
44
92
  @staticmethod
45
93
  def generate_element_doc(element_class: Type[NotionBlockElement]) -> str:
@@ -59,17 +107,16 @@ CRITICAL USAGE GUIDELINES:
59
107
  # Get the element content
60
108
  content = element_class.get_llm_prompt_content()
61
109
 
62
- # Format the element documentation in a compact way
63
110
  doc_parts = [
64
111
  f"## {element_name}",
65
- f"{content['description']}",
66
- f"**Syntax:** {content['syntax']}",
67
- f"**Example:** {content['examples'][0]}" if content["examples"] else "",
68
- f"**When to use:** {content['when_to_use']}",
112
+ f"{content.description}",
113
+ f"**Syntax:** {content.syntax}",
114
+ f"**Example:** {content.examples[0]}" if content.examples else "",
115
+ f"**When to use:** {content.when_to_use}",
69
116
  ]
70
-
71
- if "avoid" in content and content["avoid"]:
72
- doc_parts.append(f"**Avoid:** {content['avoid']}")
117
+
118
+ if content.avoid:
119
+ doc_parts.append(f"**Avoid:** {content.avoid}")
73
120
 
74
121
  return "\n".join([part for part in doc_parts if part])
75
122
 
@@ -1,85 +1,78 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
3
  from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.elements.prompts.element_prompt_content import ElementPromptContent
4
+ from notionary.elements.prompts.element_prompt_content import (
5
+ ElementPromptBuilder,
6
+ ElementPromptContent,
7
+ )
5
8
 
6
9
 
7
10
  class QuoteElement(NotionBlockElement):
8
11
  """Class for converting between Markdown blockquotes and Notion quote blocks."""
9
12
 
10
- @staticmethod
11
- def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
13
+ # Regular expression pattern to match Markdown blockquote lines
14
+ # Matches lines that start with optional whitespace, followed by '>',
15
+ # then optional whitespace, and captures any text after that
16
+ quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
17
+
18
+ @classmethod
19
+ def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
12
20
  """
13
21
  Find all blockquote matches in the text and return their positions and blocks.
14
-
15
- Args:
16
- text: The input markdown text
17
-
18
- Returns:
19
- List of tuples (start_pos, end_pos, block)
20
22
  """
21
- quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
22
23
  matches = []
24
+ quote_matches = list(QuoteElement.quote_pattern.finditer(text))
23
25
 
24
- # Find all potential quote line matches
25
- quote_matches = list(quote_pattern.finditer(text))
26
26
  if not quote_matches:
27
27
  return []
28
28
 
29
- # Group consecutive quote lines
30
- i = 0
31
- while i < len(quote_matches):
32
- start_match = quote_matches[i]
29
+ current_match_index = 0
30
+ while current_match_index < len(quote_matches):
31
+ start_match = quote_matches[current_match_index]
33
32
  start_pos = start_match.start()
34
33
 
35
- # Find consecutive quote lines
36
- j = i + 1
37
- while j < len(quote_matches):
38
- # Check if this is the next line (considering newlines)
39
- if (
40
- text[quote_matches[j - 1].end() : quote_matches[j].start()].count(
41
- "\n"
42
- )
43
- == 1
44
- or
45
- # Or if it's an empty line followed by a quote line
46
- (
47
- text[
48
- quote_matches[j - 1].end() : quote_matches[j].start()
49
- ].strip()
50
- == ""
51
- and text[
52
- quote_matches[j - 1].end() : quote_matches[j].start()
53
- ].count("\n")
54
- <= 2
55
- )
56
- ):
57
- j += 1
58
- else:
59
- break
60
-
61
- end_pos = quote_matches[j - 1].end()
34
+ next_match_index = current_match_index + 1
35
+ while next_match_index < len(
36
+ quote_matches
37
+ ) and QuoteElement.is_consecutive_quote(
38
+ text, quote_matches, next_match_index
39
+ ):
40
+ next_match_index += 1
41
+
42
+ end_pos = quote_matches[next_match_index - 1].end()
62
43
  quote_text = text[start_pos:end_pos]
63
44
 
64
- # Create the block
65
45
  block = QuoteElement.markdown_to_notion(quote_text)
66
46
  if block:
67
47
  matches.append((start_pos, end_pos, block))
68
48
 
69
- i = j
49
+ current_match_index = next_match_index
70
50
 
71
51
  return matches
72
52
 
73
- @staticmethod
74
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
53
+ @classmethod
54
+ def is_consecutive_quote(cls, text: str, quote_matches: List, index: int) -> bool:
55
+ """Checks if the current quote is part of the previous quote sequence."""
56
+ prev_end = quote_matches[index - 1].end()
57
+ curr_start = quote_matches[index].start()
58
+ gap_text = text[prev_end:curr_start]
59
+
60
+ if gap_text.count("\n") == 1:
61
+ return True
62
+
63
+ if gap_text.strip() == "" and gap_text.count("\n") <= 2:
64
+ return True
65
+
66
+ return False
67
+
68
+ @classmethod
69
+ def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
75
70
  """Convert markdown blockquote to Notion block."""
76
71
  if not text:
77
72
  return None
78
73
 
79
- quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
80
-
81
74
  # Check if it's a blockquote
82
- if not quote_pattern.search(text):
75
+ if not QuoteElement.quote_pattern.search(text):
83
76
  return None
84
77
 
85
78
  # Extract quote content
@@ -88,7 +81,7 @@ class QuoteElement(NotionBlockElement):
88
81
 
89
82
  # Extract content from each line
90
83
  for line in lines:
91
- quote_match = quote_pattern.match(line)
84
+ quote_match = QuoteElement.quote_pattern.match(line)
92
85
  if quote_match:
93
86
  content = quote_match.group(1)
94
87
  quote_lines.append(content)
@@ -105,8 +98,8 @@ class QuoteElement(NotionBlockElement):
105
98
 
106
99
  return {"type": "quote", "quote": {"rich_text": rich_text, "color": "default"}}
107
100
 
108
- @staticmethod
109
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
101
+ @classmethod
102
+ def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
110
103
  """Convert Notion quote block to markdown."""
111
104
  if block.get("type") != "quote":
112
105
  return None
@@ -126,24 +119,23 @@ class QuoteElement(NotionBlockElement):
126
119
 
127
120
  return "\n".join(formatted_lines)
128
121
 
129
- @staticmethod
130
- def match_markdown(text: str) -> bool:
122
+ @classmethod
123
+ def match_markdown(cls, text: str) -> bool:
131
124
  """Check if this element can handle the given markdown text."""
132
- quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
133
- return bool(quote_pattern.search(text))
125
+ return bool(QuoteElement.quote_pattern.search(text))
134
126
 
135
- @staticmethod
136
- def match_notion(block: Dict[str, Any]) -> bool:
127
+ @classmethod
128
+ def match_notion(cls, block: Dict[str, Any]) -> bool:
137
129
  """Check if this element can handle the given Notion block."""
138
130
  return block.get("type") == "quote"
139
131
 
140
- @staticmethod
141
- def is_multiline() -> bool:
132
+ @classmethod
133
+ def is_multiline(cls) -> bool:
142
134
  """Blockquotes can span multiple lines."""
143
135
  return True
144
136
 
145
- @staticmethod
146
- def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
137
+ @classmethod
138
+ def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
147
139
  """Extract plain text content from Notion rich_text elements."""
148
140
  result = ""
149
141
  for text_obj in rich_text:
@@ -158,16 +150,22 @@ class QuoteElement(NotionBlockElement):
158
150
  """
159
151
  Returns structured LLM prompt metadata for the quote element.
160
152
  """
161
- return {
162
- "description": "Creates blockquotes that visually distinguish quoted text.",
163
- "when_to_use": (
153
+ return (
154
+ ElementPromptBuilder()
155
+ .with_description(
156
+ "Creates blockquotes that visually distinguish quoted text."
157
+ )
158
+ .with_usage_guidelines(
164
159
  "Use blockquotes for quoting external sources, highlighting important statements, "
165
160
  "or creating visual emphasis for key information."
166
- ),
167
- "syntax": "> Quoted text",
168
- "examples": [
169
- "> This is a simple blockquote",
170
- "> This is a multi-line quote\n> that continues on the next line",
171
- "> Important note:\n> This quote spans\n> multiple lines.",
172
- ],
173
- }
161
+ )
162
+ .with_syntax("> Quoted text")
163
+ .with_examples(
164
+ [
165
+ "> This is a simple blockquote",
166
+ "> This is a multi-line quote\n> that continues on the next line",
167
+ "> Important note:\n> This quote spans\n> multiple lines.",
168
+ ]
169
+ )
170
+ .build()
171
+ )
@@ -33,7 +33,7 @@ class BlockElementRegistry:
33
33
  self._elements.remove(element_class)
34
34
  return True
35
35
  return False
36
-
36
+
37
37
  def contains(self, element_class: Type[NotionBlockElement]) -> bool:
38
38
  """
39
39
  Check if the registry contains the specified element class.
@@ -18,13 +18,13 @@ from notionary.elements.callout_element import CalloutElement
18
18
  from notionary.elements.code_block_element import CodeBlockElement
19
19
  from notionary.elements.divider_element import DividerElement
20
20
  from notionary.elements.table_element import TableElement
21
- from notionary.elements.todo_lists import TodoElement
21
+ from notionary.elements.todo_element import TodoElement
22
22
  from notionary.elements.qoute_element import QuoteElement
23
23
  from notionary.elements.image_element import ImageElement
24
+ from notionary.elements.toggleable_heading_element import ToggleableHeadingElement
24
25
  from notionary.elements.video_element import VideoElement
25
26
  from notionary.elements.toggle_element import ToggleElement
26
27
  from notionary.elements.bookmark_element import BookmarkElement
27
- from notionary.elements.column_element import ColumnElement
28
28
 
29
29
 
30
30
  class BlockElementRegistryBuilder:
@@ -51,7 +51,6 @@ class BlockElementRegistryBuilder:
51
51
  .with_code()
52
52
  .with_dividers()
53
53
  .with_tables()
54
- .with_columns()
55
54
  .with_bulleted_list()
56
55
  .with_numbered_list()
57
56
  .with_toggles()
@@ -64,6 +63,7 @@ class BlockElementRegistryBuilder:
64
63
  .with_audio()
65
64
  .with_mention()
66
65
  .with_paragraphs()
66
+ .with_toggleable_heading_element()
67
67
  ).build()
68
68
 
69
69
  def add_element(
@@ -174,12 +174,6 @@ class BlockElementRegistryBuilder:
174
174
  """
175
175
  return self.add_element(TableElement)
176
176
 
177
- def with_columns(self) -> BlockElementRegistryBuilder:
178
- """
179
- Add support for column elements.
180
- """
181
- return self.add_element(ColumnElement)
182
-
183
177
  def with_bulleted_list(self) -> BlockElementRegistryBuilder:
184
178
  """
185
179
  Add support for bulleted list elements (unordered lists).
@@ -249,6 +243,9 @@ class BlockElementRegistryBuilder:
249
243
  def with_mention(self) -> BlockElementRegistryBuilder:
250
244
  return self.add_element(MentionElement)
251
245
 
246
+ def with_toggleable_heading_element(self) -> BlockElementRegistryBuilder:
247
+ return self.add_element(ToggleableHeadingElement)
248
+
252
249
  def build(self) -> BlockElementRegistry:
253
250
  """
254
251
  Build and return the configured BlockElementRegistry instance.