notionary 0.1.2__py3-none-any.whl → 0.1.3__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/core/__init__.py +0 -0
- notionary/core/converters/__init__.py +50 -0
- notionary/core/converters/elements/__init__.py +0 -0
- notionary/core/converters/elements/bookmark_element.py +224 -0
- notionary/core/converters/elements/callout_element.py +179 -0
- notionary/core/converters/elements/code_block_element.py +153 -0
- notionary/core/converters/elements/column_element.py +294 -0
- notionary/core/converters/elements/divider_element.py +73 -0
- notionary/core/converters/elements/heading_element.py +84 -0
- notionary/core/converters/elements/image_element.py +130 -0
- notionary/core/converters/elements/list_element.py +130 -0
- notionary/core/converters/elements/notion_block_element.py +51 -0
- notionary/core/converters/elements/paragraph_element.py +73 -0
- notionary/core/converters/elements/qoute_element.py +242 -0
- notionary/core/converters/elements/table_element.py +306 -0
- notionary/core/converters/elements/text_inline_formatter.py +294 -0
- notionary/core/converters/elements/todo_lists.py +114 -0
- notionary/core/converters/elements/toggle_element.py +205 -0
- notionary/core/converters/elements/video_element.py +159 -0
- notionary/core/converters/markdown_to_notion_converter.py +482 -0
- notionary/core/converters/notion_to_markdown_converter.py +45 -0
- notionary/core/converters/registry/__init__.py +0 -0
- notionary/core/converters/registry/block_element_registry.py +234 -0
- notionary/core/converters/registry/block_element_registry_builder.py +280 -0
- notionary/core/database/database_info_service.py +43 -0
- notionary/core/database/database_query_service.py +73 -0
- notionary/core/database/database_schema_service.py +57 -0
- notionary/core/database/models/page_result.py +10 -0
- notionary/core/database/notion_database_manager.py +332 -0
- notionary/core/database/notion_database_manager_factory.py +233 -0
- notionary/core/database/notion_database_schema.py +415 -0
- notionary/core/database/notion_database_writer.py +390 -0
- notionary/core/database/page_service.py +161 -0
- notionary/core/notion_client.py +134 -0
- notionary/core/page/meta_data/metadata_editor.py +37 -0
- notionary/core/page/notion_page_manager.py +110 -0
- notionary/core/page/page_content_manager.py +85 -0
- notionary/core/page/property_formatter.py +97 -0
- notionary/exceptions/database_exceptions.py +76 -0
- notionary/exceptions/page_creation_exception.py +9 -0
- notionary/util/logging_mixin.py +47 -0
- notionary/util/singleton_decorator.py +20 -0
- notionary/util/uuid_utils.py +24 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
- notionary-0.1.3.dist-info/RECORD +49 -0
- notionary-0.1.2.dist-info/RECORD +0 -6
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional
|
3
|
+
from typing_extensions import override
|
4
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
5
|
+
from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
|
6
|
+
|
7
|
+
|
8
|
+
class BulletedListElement(NotionBlockElement):
|
9
|
+
"""Class for converting between Markdown bullet lists and Notion bulleted list items."""
|
10
|
+
|
11
|
+
@override
|
12
|
+
@staticmethod
|
13
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
14
|
+
"""Convert markdown bulleted list item to Notion block."""
|
15
|
+
pattern = re.compile(
|
16
|
+
r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$"
|
17
|
+
) # Avoid matching todo items
|
18
|
+
list_match = pattern.match(text)
|
19
|
+
if not list_match:
|
20
|
+
return None
|
21
|
+
|
22
|
+
content = list_match.group(2)
|
23
|
+
|
24
|
+
# Use parse_inline_formatting to handle rich text
|
25
|
+
rich_text = TextInlineFormatter.parse_inline_formatting(content)
|
26
|
+
|
27
|
+
return {
|
28
|
+
"type": "bulleted_list_item",
|
29
|
+
"bulleted_list_item": {"rich_text": rich_text, "color": "default"},
|
30
|
+
}
|
31
|
+
|
32
|
+
@override
|
33
|
+
@staticmethod
|
34
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
35
|
+
"""Convert Notion bulleted list item block to markdown."""
|
36
|
+
if block.get("type") != "bulleted_list_item":
|
37
|
+
return None
|
38
|
+
|
39
|
+
rich_text = block.get("bulleted_list_item", {}).get("rich_text", [])
|
40
|
+
content = TextInlineFormatter.extract_text_with_formatting(rich_text)
|
41
|
+
|
42
|
+
return f"- {content}"
|
43
|
+
|
44
|
+
@override
|
45
|
+
@staticmethod
|
46
|
+
def match_markdown(text: str) -> bool:
|
47
|
+
"""Check if this element can handle the given markdown text."""
|
48
|
+
pattern = re.compile(r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$")
|
49
|
+
return bool(pattern.match(text))
|
50
|
+
|
51
|
+
@override
|
52
|
+
@staticmethod
|
53
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
54
|
+
"""Check if this element can handle the given Notion block."""
|
55
|
+
return block.get("type") == "bulleted_list_item"
|
56
|
+
|
57
|
+
@override
|
58
|
+
@classmethod
|
59
|
+
def get_llm_prompt_content(cls) -> dict:
|
60
|
+
"""Returns information for LLM prompts about this element."""
|
61
|
+
return {
|
62
|
+
"description": "Creates bulleted list items for unordered lists.",
|
63
|
+
"when_to_use": "Use for lists where order doesn't matter, such as features, options, or items without hierarchy.",
|
64
|
+
"syntax": ["- Item text", "* Item text", "+ Item text"],
|
65
|
+
"examples": ["- First item\n- Second item\n- Third item"],
|
66
|
+
}
|
67
|
+
|
68
|
+
|
69
|
+
class NumberedListElement:
|
70
|
+
"""Class for converting between Markdown numbered lists and Notion numbered list items."""
|
71
|
+
|
72
|
+
@override
|
73
|
+
@staticmethod
|
74
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
75
|
+
"""Convert markdown numbered list item to Notion block."""
|
76
|
+
pattern = re.compile(r"^\s*(\d+)\.\s+(.+)$")
|
77
|
+
numbered_match = pattern.match(text)
|
78
|
+
if not numbered_match:
|
79
|
+
return None
|
80
|
+
|
81
|
+
content = numbered_match.group(2)
|
82
|
+
|
83
|
+
# Use parse_inline_formatting to handle rich text
|
84
|
+
rich_text = TextInlineFormatter.parse_inline_formatting(content)
|
85
|
+
|
86
|
+
return {
|
87
|
+
"type": "numbered_list_item",
|
88
|
+
"numbered_list_item": {"rich_text": rich_text, "color": "default"},
|
89
|
+
}
|
90
|
+
|
91
|
+
@override
|
92
|
+
@staticmethod
|
93
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
94
|
+
"""Convert Notion numbered list item block to markdown."""
|
95
|
+
if block.get("type") != "numbered_list_item":
|
96
|
+
return None
|
97
|
+
|
98
|
+
rich_text = block.get("numbered_list_item", {}).get("rich_text", [])
|
99
|
+
content = TextInlineFormatter.extract_text_with_formatting(rich_text)
|
100
|
+
|
101
|
+
return f"1. {content}"
|
102
|
+
|
103
|
+
@override
|
104
|
+
@staticmethod
|
105
|
+
def match_markdown(text: str) -> bool:
|
106
|
+
"""Check if this element can handle the given markdown text."""
|
107
|
+
pattern = re.compile(r"^\s*\d+\.\s+(.+)$")
|
108
|
+
return bool(pattern.match(text))
|
109
|
+
|
110
|
+
@override
|
111
|
+
@staticmethod
|
112
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
113
|
+
"""Check if this element can handle the given Notion block."""
|
114
|
+
return block.get("type") == "numbered_list_item"
|
115
|
+
|
116
|
+
@override
|
117
|
+
@staticmethod
|
118
|
+
def is_multiline() -> bool:
|
119
|
+
return False
|
120
|
+
|
121
|
+
@override
|
122
|
+
@classmethod
|
123
|
+
def get_llm_prompt_content(cls) -> dict:
|
124
|
+
"""Returns information for LLM prompts about this element."""
|
125
|
+
return {
|
126
|
+
"description": "Creates numbered list items for ordered sequences.",
|
127
|
+
"when_to_use": "Use for lists where order matters, such as steps, rankings, or sequential items.",
|
128
|
+
"syntax": ["1. Item text"],
|
129
|
+
"examples": ["1. First step\n2. Second step\n3. Third step"],
|
130
|
+
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import inspect
|
2
|
+
from typing import Dict, Any, Optional
|
3
|
+
from abc import ABC
|
4
|
+
|
5
|
+
|
6
|
+
class NotionBlockElement(ABC):
|
7
|
+
"""Base class for elements that can be converted between Markdown and Notion."""
|
8
|
+
|
9
|
+
@staticmethod
|
10
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
11
|
+
"""Convert markdown to Notion block."""
|
12
|
+
|
13
|
+
@staticmethod
|
14
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
15
|
+
"""Convert Notion block to markdown."""
|
16
|
+
|
17
|
+
@staticmethod
|
18
|
+
def match_markdown(text: str) -> bool:
|
19
|
+
"""Check if this element can handle the given markdown text."""
|
20
|
+
return bool(NotionBlockElement.markdown_to_notion(text))
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
24
|
+
"""Check if this element can handle the given Notion block."""
|
25
|
+
return bool(NotionBlockElement.notion_to_markdown(block))
|
26
|
+
|
27
|
+
@staticmethod
|
28
|
+
def is_multiline() -> bool:
|
29
|
+
return False
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def get_llm_documentation(cls) -> str:
|
33
|
+
"""
|
34
|
+
Returns documentation specifically formatted for LLM system prompts.
|
35
|
+
Can be overridden by subclasses to provide custom LLM-friendly documentation.
|
36
|
+
|
37
|
+
By default, returns the class docstring.
|
38
|
+
"""
|
39
|
+
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def get_llm_prompt_content(cls) -> dict:
|
43
|
+
"""
|
44
|
+
Returns a dictionary with information for LLM prompts about this element.
|
45
|
+
This default implementation extracts information from the class docstring.
|
46
|
+
Subclasses should override this method to provide more structured information.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Dictionary with documentation information
|
50
|
+
"""
|
51
|
+
return {"description": inspect.cleandoc(cls.__doc__ or ""), "examples": []}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from typing import Dict, Any, Optional
|
2
|
+
from typing_extensions import override
|
3
|
+
|
4
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
5
|
+
from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
|
6
|
+
|
7
|
+
|
8
|
+
class ParagraphElement(NotionBlockElement):
|
9
|
+
"""Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
|
10
|
+
|
11
|
+
@override
|
12
|
+
@staticmethod
|
13
|
+
def match_markdown(text: str) -> bool:
|
14
|
+
"""
|
15
|
+
Check if text is a markdown paragraph.
|
16
|
+
Paragraphs are essentially any text that isn't matched by other block elements.
|
17
|
+
Since paragraphs are the fallback element, this always returns True.
|
18
|
+
"""
|
19
|
+
return True
|
20
|
+
|
21
|
+
@override
|
22
|
+
@staticmethod
|
23
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
24
|
+
"""Check if block is a Notion paragraph."""
|
25
|
+
return block.get("type") == "paragraph"
|
26
|
+
|
27
|
+
@override
|
28
|
+
@staticmethod
|
29
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
30
|
+
"""Convert markdown paragraph to Notion paragraph block."""
|
31
|
+
if not text.strip():
|
32
|
+
return None
|
33
|
+
|
34
|
+
return {
|
35
|
+
"type": "paragraph",
|
36
|
+
"paragraph": {
|
37
|
+
"rich_text": TextInlineFormatter.parse_inline_formatting(text)
|
38
|
+
},
|
39
|
+
}
|
40
|
+
|
41
|
+
@override
|
42
|
+
@staticmethod
|
43
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
44
|
+
"""Convert Notion paragraph block to markdown paragraph."""
|
45
|
+
if block.get("type") != "paragraph":
|
46
|
+
return None
|
47
|
+
|
48
|
+
paragraph_data = block.get("paragraph", {})
|
49
|
+
rich_text = paragraph_data.get("rich_text", [])
|
50
|
+
|
51
|
+
text = TextInlineFormatter.extract_text_with_formatting(rich_text)
|
52
|
+
return text if text else None
|
53
|
+
|
54
|
+
@override
|
55
|
+
@staticmethod
|
56
|
+
def is_multiline() -> bool:
|
57
|
+
return False
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def get_llm_prompt_content(cls) -> dict:
|
61
|
+
"""Returns information for LLM prompts about this element."""
|
62
|
+
return {
|
63
|
+
"description": "Creates standard paragraph blocks for regular text content.",
|
64
|
+
"when_to_use": "Use paragraphs for normal text content. Paragraphs are the default block type and will be used when no other specific formatting is applied.",
|
65
|
+
"syntax": ["Just write text normally without any special prefix"],
|
66
|
+
"notes": [
|
67
|
+
"Paragraphs support inline formatting like **bold**, *italic*, ~~strikethrough~~, `code`, and [links](url)"
|
68
|
+
],
|
69
|
+
"examples": [
|
70
|
+
"This is a simple paragraph with plain text.",
|
71
|
+
"This paragraph has **bold** and *italic* formatting.",
|
72
|
+
],
|
73
|
+
}
|
@@ -0,0 +1,242 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple
|
3
|
+
from typing_extensions import override
|
4
|
+
|
5
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
6
|
+
|
7
|
+
|
8
|
+
class QuoteElement(NotionBlockElement):
|
9
|
+
"""Class for converting between Markdown blockquotes and Notion quote blocks with background color support."""
|
10
|
+
|
11
|
+
# Mapping von Markdown-Farbnamen zu Notion-Farbnamen
|
12
|
+
COLOR_MAPPING = {
|
13
|
+
"gray": "gray_background",
|
14
|
+
"brown": "brown_background",
|
15
|
+
"orange": "orange_background",
|
16
|
+
"yellow": "yellow_background",
|
17
|
+
"green": "green_background",
|
18
|
+
"blue": "blue_background",
|
19
|
+
"purple": "purple_background",
|
20
|
+
"pink": "pink_background",
|
21
|
+
"red": "red_background",
|
22
|
+
}
|
23
|
+
|
24
|
+
# Umgekehrtes Mapping für die Rückkonvertierung
|
25
|
+
REVERSE_COLOR_MAPPING = {v: k for k, v in COLOR_MAPPING.items()}
|
26
|
+
|
27
|
+
@staticmethod
|
28
|
+
def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
29
|
+
"""
|
30
|
+
Find all blockquote matches in the text and return their positions and blocks.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
text: The input markdown text
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
List of tuples (start_pos, end_pos, block)
|
37
|
+
"""
|
38
|
+
quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
|
39
|
+
matches = []
|
40
|
+
|
41
|
+
# Find all potential quote line matches
|
42
|
+
quote_matches = list(quote_pattern.finditer(text))
|
43
|
+
if not quote_matches:
|
44
|
+
return []
|
45
|
+
|
46
|
+
# Group consecutive quote lines
|
47
|
+
i = 0
|
48
|
+
while i < len(quote_matches):
|
49
|
+
start_match = quote_matches[i]
|
50
|
+
start_pos = start_match.start()
|
51
|
+
|
52
|
+
# Find consecutive quote lines
|
53
|
+
j = i + 1
|
54
|
+
while j < len(quote_matches):
|
55
|
+
# Check if this is the next line (considering newlines)
|
56
|
+
if (
|
57
|
+
text[quote_matches[j - 1].end() : quote_matches[j].start()].count(
|
58
|
+
"\n"
|
59
|
+
)
|
60
|
+
== 1
|
61
|
+
or
|
62
|
+
# Or if it's an empty line followed by a quote line
|
63
|
+
(
|
64
|
+
text[
|
65
|
+
quote_matches[j - 1].end() : quote_matches[j].start()
|
66
|
+
].strip()
|
67
|
+
== ""
|
68
|
+
and text[
|
69
|
+
quote_matches[j - 1].end() : quote_matches[j].start()
|
70
|
+
].count("\n")
|
71
|
+
<= 2
|
72
|
+
)
|
73
|
+
):
|
74
|
+
j += 1
|
75
|
+
else:
|
76
|
+
break
|
77
|
+
|
78
|
+
end_pos = quote_matches[j - 1].end()
|
79
|
+
quote_text = text[start_pos:end_pos]
|
80
|
+
|
81
|
+
# Create the block
|
82
|
+
block = QuoteElement.markdown_to_notion(quote_text)
|
83
|
+
if block:
|
84
|
+
matches.append((start_pos, end_pos, block))
|
85
|
+
|
86
|
+
i = j
|
87
|
+
|
88
|
+
return matches
|
89
|
+
|
90
|
+
@override
|
91
|
+
@staticmethod
|
92
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
93
|
+
"""Convert markdown blockquote to Notion block with background color support."""
|
94
|
+
if not text:
|
95
|
+
return None
|
96
|
+
|
97
|
+
quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
|
98
|
+
|
99
|
+
# Check if it's a blockquote
|
100
|
+
if not quote_pattern.search(text):
|
101
|
+
return None
|
102
|
+
|
103
|
+
# Extract quote content
|
104
|
+
lines = text.split("\n")
|
105
|
+
quote_lines = []
|
106
|
+
color = "default" # Standardfarbe
|
107
|
+
|
108
|
+
# Überprüfen, ob der erste Nicht-Leerzeichen-Inhalt eine Farbangabe ist
|
109
|
+
first_line = None
|
110
|
+
for line in lines:
|
111
|
+
quote_match = quote_pattern.match(line)
|
112
|
+
if quote_match and quote_match.group(1).strip():
|
113
|
+
first_line = quote_match.group(1).strip()
|
114
|
+
break
|
115
|
+
|
116
|
+
# Farbangabe in eckigen Klammern prüfen
|
117
|
+
if first_line:
|
118
|
+
color_match = re.match(r"^\[background:(\w+)\]\s*(.*)", first_line)
|
119
|
+
if color_match:
|
120
|
+
potential_color = color_match.group(1).lower()
|
121
|
+
if potential_color in QuoteElement.COLOR_MAPPING:
|
122
|
+
color = QuoteElement.COLOR_MAPPING[potential_color]
|
123
|
+
# Erste Zeile ohne Farbangabe neu hinzufügen
|
124
|
+
first_line = color_match.group(2)
|
125
|
+
|
126
|
+
# Inhalte extrahieren
|
127
|
+
processing_first_color_line = True
|
128
|
+
for line in lines:
|
129
|
+
quote_match = quote_pattern.match(line)
|
130
|
+
if quote_match:
|
131
|
+
content = quote_match.group(1)
|
132
|
+
# Farbangabe in der ersten Zeile entfernen
|
133
|
+
if (
|
134
|
+
processing_first_color_line
|
135
|
+
and content.strip()
|
136
|
+
and re.match(r"^\[background:(\w+)\]", content.strip())
|
137
|
+
):
|
138
|
+
content = re.sub(r"^\[background:(\w+)\]\s*", "", content)
|
139
|
+
processing_first_color_line = False
|
140
|
+
quote_lines.append(content)
|
141
|
+
elif not line.strip() and quote_lines:
|
142
|
+
# Allow empty lines within the quote
|
143
|
+
quote_lines.append("")
|
144
|
+
|
145
|
+
if not quote_lines:
|
146
|
+
return None
|
147
|
+
|
148
|
+
quote_content = "\n".join(quote_lines).strip()
|
149
|
+
|
150
|
+
# Create rich_text elements directly
|
151
|
+
rich_text = [{"type": "text", "text": {"content": quote_content}}]
|
152
|
+
|
153
|
+
return {"type": "quote", "quote": {"rich_text": rich_text, "color": color}}
|
154
|
+
|
155
|
+
@override
|
156
|
+
@staticmethod
|
157
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
158
|
+
"""Convert Notion quote block to markdown with background color support."""
|
159
|
+
if block.get("type") != "quote":
|
160
|
+
return None
|
161
|
+
|
162
|
+
rich_text = block.get("quote", {}).get("rich_text", [])
|
163
|
+
color = block.get("quote", {}).get("color", "default")
|
164
|
+
|
165
|
+
# Extract the text content
|
166
|
+
content = QuoteElement._extract_text_content(rich_text)
|
167
|
+
|
168
|
+
# Format as markdown blockquote
|
169
|
+
lines = content.split("\n")
|
170
|
+
formatted_lines = []
|
171
|
+
|
172
|
+
# Füge die Farbinformation zur ersten Zeile hinzu, falls nicht default
|
173
|
+
if color != "default" and color in QuoteElement.REVERSE_COLOR_MAPPING:
|
174
|
+
markdown_color = QuoteElement.REVERSE_COLOR_MAPPING.get(color)
|
175
|
+
first_line = lines[0] if lines else ""
|
176
|
+
formatted_lines.append(f"> [background:{markdown_color}] {first_line}")
|
177
|
+
lines = lines[1:] if len(lines) > 1 else []
|
178
|
+
|
179
|
+
# Füge die restlichen Zeilen hinzu
|
180
|
+
for line in lines:
|
181
|
+
formatted_lines.append(f"> {line}")
|
182
|
+
|
183
|
+
return "\n".join(formatted_lines)
|
184
|
+
|
185
|
+
@override
|
186
|
+
@staticmethod
|
187
|
+
def match_markdown(text: str) -> bool:
|
188
|
+
"""Check if this element can handle the given markdown text."""
|
189
|
+
quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
|
190
|
+
return bool(quote_pattern.search(text))
|
191
|
+
|
192
|
+
@override
|
193
|
+
@staticmethod
|
194
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
195
|
+
"""Check if this element can handle the given Notion block."""
|
196
|
+
return block.get("type") == "quote"
|
197
|
+
|
198
|
+
@override
|
199
|
+
@staticmethod
|
200
|
+
def is_multiline() -> bool:
|
201
|
+
"""Blockquotes can span multiple lines."""
|
202
|
+
return True
|
203
|
+
|
204
|
+
@staticmethod
|
205
|
+
def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
|
206
|
+
"""Extract plain text content from Notion rich_text elements."""
|
207
|
+
result = ""
|
208
|
+
for text_obj in rich_text:
|
209
|
+
if text_obj.get("type") == "text":
|
210
|
+
result += text_obj.get("text", {}).get("content", "")
|
211
|
+
elif "plain_text" in text_obj:
|
212
|
+
result += text_obj.get("plain_text", "")
|
213
|
+
return result
|
214
|
+
|
215
|
+
@override
|
216
|
+
@classmethod
|
217
|
+
def get_llm_prompt_content(cls) -> dict:
|
218
|
+
"""Returns information for LLM prompts about this element."""
|
219
|
+
return {
|
220
|
+
"description": "Creates blockquotes that visually distinguish quoted text with optional background colors.",
|
221
|
+
"when_to_use": "Use blockquotes for quoting external sources, highlighting important statements, or creating visual emphasis for key information.",
|
222
|
+
"syntax": [
|
223
|
+
"> Text - Simple blockquote",
|
224
|
+
"> [background:color] Text - Blockquote with colored background",
|
225
|
+
],
|
226
|
+
"color_options": [
|
227
|
+
"gray",
|
228
|
+
"brown",
|
229
|
+
"orange",
|
230
|
+
"yellow",
|
231
|
+
"green",
|
232
|
+
"blue",
|
233
|
+
"purple",
|
234
|
+
"pink",
|
235
|
+
"red",
|
236
|
+
],
|
237
|
+
"examples": [
|
238
|
+
"> This is a simple blockquote without any color",
|
239
|
+
"> [background:blue] This is a blockquote with blue background",
|
240
|
+
"> Multi-line quotes\n> continue like this\n> across several lines",
|
241
|
+
],
|
242
|
+
}
|