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.
- notionary/elements/audio_element.py +41 -38
- notionary/elements/bookmark_element.py +36 -27
- notionary/elements/bulleted_list_element.py +28 -21
- notionary/elements/callout_element.py +39 -31
- notionary/elements/code_block_element.py +38 -26
- notionary/elements/divider_element.py +29 -18
- notionary/elements/embed_element.py +37 -28
- notionary/elements/heading_element.py +39 -24
- notionary/elements/image_element.py +33 -24
- notionary/elements/mention_element.py +40 -29
- notionary/elements/notion_block_element.py +13 -31
- notionary/elements/numbered_list_element.py +29 -20
- notionary/elements/paragraph_element.py +37 -31
- notionary/elements/prompts/element_prompt_content.py +91 -8
- notionary/elements/prompts/synthax_prompt_builder.py +64 -17
- notionary/elements/qoute_element.py +72 -74
- notionary/elements/registry/block_element_registry.py +1 -1
- notionary/elements/registry/block_element_registry_builder.py +6 -9
- notionary/elements/table_element.py +49 -36
- notionary/elements/text_inline_formatter.py +23 -15
- notionary/elements/{todo_lists.py → todo_element.py} +34 -25
- notionary/elements/toggle_element.py +184 -108
- notionary/elements/toggleable_heading_element.py +269 -0
- notionary/elements/video_element.py +37 -28
- notionary/page/content/page_content_manager.py +5 -8
- notionary/page/markdown_to_notion_converter.py +269 -274
- notionary/page/notion_page.py +1 -1
- notionary/page/notion_to_markdown_converter.py +20 -95
- {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/METADATA +1 -1
- notionary-0.1.26.dist-info/RECORD +58 -0
- {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/WHEEL +1 -1
- notionary/elements/column_element.py +0 -307
- notionary-0.1.24.dist-info/RECORD +0 -58
- {notionary-0.1.24.dist-info → notionary-0.1.26.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
@
|
13
|
-
|
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
|
-
@
|
23
|
-
|
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
|
-
@
|
29
|
-
|
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
|
-
@
|
43
|
-
|
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
|
-
@
|
56
|
-
|
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
|
-
|
67
|
-
|
68
|
-
"
|
69
|
-
"
|
70
|
-
)
|
71
|
-
|
72
|
-
|
73
|
-
"
|
74
|
-
|
75
|
-
|
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
|
1
|
+
from dataclasses import field, dataclass
|
2
|
+
from typing import Optional, List, Self
|
2
3
|
|
3
4
|
|
4
|
-
|
5
|
+
@dataclass
|
6
|
+
class ElementPromptContent:
|
5
7
|
"""
|
6
|
-
|
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
|
-
|
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.
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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.
|
36
|
-
and
|
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
|
66
|
-
f"**Syntax:** {content
|
67
|
-
f"**Example:** {content
|
68
|
-
f"**When to use:** {content
|
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
|
72
|
-
doc_parts.append(f"**Avoid:** {content
|
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
|
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
|
-
|
11
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
49
|
+
current_match_index = next_match_index
|
70
50
|
|
71
51
|
return matches
|
72
52
|
|
73
|
-
@
|
74
|
-
def
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
133
|
-
return bool(quote_pattern.search(text))
|
125
|
+
return bool(QuoteElement.quote_pattern.search(text))
|
134
126
|
|
135
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
163
|
-
|
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
|
-
"
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
+
)
|
@@ -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.
|
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.
|