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.
- notionary/database/database_discovery.py +24 -24
- notionary/elements/code_block_element.py +59 -18
- notionary/elements/column_element.py +353 -0
- notionary/elements/divider_element.py +1 -1
- notionary/elements/heading_element.py +4 -1
- notionary/elements/registry/block_registry.py +1 -1
- notionary/elements/registry/block_registry_builder.py +8 -0
- notionary/page/content/page_content_writer.py +103 -3
- notionary/page/markdown_to_notion_converter.py +145 -13
- notionary/prompting/markdown_syntax_prompt_generator.py +33 -29
- notionary-0.2.8.dist-info/METADATA +271 -0
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/RECORD +15 -14
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/WHEEL +1 -1
- notionary-0.2.6.dist-info/METADATA +0 -256
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/top_level.txt +0 -0
@@ -26,29 +26,7 @@ class DatabaseDiscovery(LoggingMixin):
|
|
26
26
|
self._client = client if client else NotionClient()
|
27
27
|
self.logger.info("DatabaseDiscovery initialized")
|
28
28
|
|
29
|
-
async def
|
30
|
-
"""
|
31
|
-
Discover all accessible databases and return their titles and IDs.
|
32
|
-
|
33
|
-
Args:
|
34
|
-
page_size: The number of databases to fetch per request
|
35
|
-
|
36
|
-
Returns:
|
37
|
-
List of tuples containing (database_title, database_id)
|
38
|
-
"""
|
39
|
-
databases = []
|
40
|
-
|
41
|
-
async for database in self._iter_databases(page_size):
|
42
|
-
db_id = database.get("id")
|
43
|
-
if not db_id:
|
44
|
-
continue
|
45
|
-
|
46
|
-
title = self._extract_database_title(database)
|
47
|
-
databases.append((title, db_id))
|
48
|
-
|
49
|
-
return databases
|
50
|
-
|
51
|
-
async def discover_and_print(self, page_size: int = 100) -> List[Tuple[str, str]]:
|
29
|
+
async def __call__(self, page_size: int = 100) -> List[Tuple[str, str]]:
|
52
30
|
"""
|
53
31
|
Discover databases and print the results in a nicely formatted way.
|
54
32
|
|
@@ -61,7 +39,7 @@ class DatabaseDiscovery(LoggingMixin):
|
|
61
39
|
Returns:
|
62
40
|
The same list of databases as discover() for further processing
|
63
41
|
"""
|
64
|
-
databases = await self.
|
42
|
+
databases = await self._discover(page_size)
|
65
43
|
|
66
44
|
if not databases:
|
67
45
|
print("\n⚠️ No databases found!")
|
@@ -78,6 +56,28 @@ class DatabaseDiscovery(LoggingMixin):
|
|
78
56
|
|
79
57
|
return databases
|
80
58
|
|
59
|
+
async def _discover(self, page_size: int = 100) -> List[Tuple[str, str]]:
|
60
|
+
"""
|
61
|
+
Discover all accessible databases and return their titles and IDs.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
page_size: The number of databases to fetch per request
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
List of tuples containing (database_title, database_id)
|
68
|
+
"""
|
69
|
+
databases = []
|
70
|
+
|
71
|
+
async for database in self._iter_databases(page_size):
|
72
|
+
db_id = database.get("id")
|
73
|
+
if not db_id:
|
74
|
+
continue
|
75
|
+
|
76
|
+
title = self._extract_database_title(database)
|
77
|
+
databases.append((title, db_id))
|
78
|
+
|
79
|
+
return databases
|
80
|
+
|
81
81
|
async def _iter_databases(
|
82
82
|
self, page_size: int = 100
|
83
83
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
@@ -15,13 +15,17 @@ class CodeBlockElement(NotionBlockElement):
|
|
15
15
|
```language
|
16
16
|
code content
|
17
17
|
```
|
18
|
+
Caption: optional caption text
|
18
19
|
|
19
20
|
Where:
|
20
21
|
- language is optional and specifies the programming language
|
21
22
|
- code content is the code to be displayed
|
23
|
+
- Caption line is optional and must appear immediately after the closing ```
|
22
24
|
"""
|
23
25
|
|
24
|
-
PATTERN = re.compile(
|
26
|
+
PATTERN = re.compile(
|
27
|
+
r"```(\w*)\n([\s\S]+?)```(?:\n(?:Caption|caption):\s*(.+))?", re.MULTILINE
|
28
|
+
)
|
25
29
|
|
26
30
|
@classmethod
|
27
31
|
def match_markdown(cls, text: str) -> bool:
|
@@ -42,25 +46,18 @@ class CodeBlockElement(NotionBlockElement):
|
|
42
46
|
|
43
47
|
language = match.group(1) or "plain text"
|
44
48
|
content = match.group(2)
|
49
|
+
caption = match.group(3)
|
45
50
|
|
46
51
|
if content.endswith("\n"):
|
47
52
|
content = content[:-1]
|
48
53
|
|
49
|
-
|
54
|
+
block = {
|
50
55
|
"type": "code",
|
51
56
|
"code": {
|
52
57
|
"rich_text": [
|
53
58
|
{
|
54
59
|
"type": "text",
|
55
60
|
"text": {"content": content},
|
56
|
-
"annotations": {
|
57
|
-
"bold": False,
|
58
|
-
"italic": False,
|
59
|
-
"strikethrough": False,
|
60
|
-
"underline": False,
|
61
|
-
"code": False,
|
62
|
-
"color": "default",
|
63
|
-
},
|
64
61
|
"plain_text": content,
|
65
62
|
}
|
66
63
|
],
|
@@ -68,6 +65,18 @@ class CodeBlockElement(NotionBlockElement):
|
|
68
65
|
},
|
69
66
|
}
|
70
67
|
|
68
|
+
# Add caption if provided
|
69
|
+
if caption and caption.strip():
|
70
|
+
block["code"]["caption"] = [
|
71
|
+
{
|
72
|
+
"type": "text",
|
73
|
+
"text": {"content": caption.strip()},
|
74
|
+
"plain_text": caption.strip(),
|
75
|
+
}
|
76
|
+
]
|
77
|
+
|
78
|
+
return block
|
79
|
+
|
71
80
|
@classmethod
|
72
81
|
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
73
82
|
"""Convert Notion code block to markdown code block."""
|
@@ -84,8 +93,20 @@ class CodeBlockElement(NotionBlockElement):
|
|
84
93
|
|
85
94
|
language = code_data.get("language", "")
|
86
95
|
|
96
|
+
# Extract caption if present
|
97
|
+
caption_text = ""
|
98
|
+
caption_data = code_data.get("caption", [])
|
99
|
+
for caption_block in caption_data:
|
100
|
+
caption_text += caption_block.get("plain_text", "")
|
101
|
+
|
87
102
|
# Format as a markdown code block
|
88
|
-
|
103
|
+
result = f"```{language}\n{content}\n```"
|
104
|
+
|
105
|
+
# Add caption if present
|
106
|
+
if caption_text.strip():
|
107
|
+
result += f"\nCaption: {caption_text}"
|
108
|
+
|
109
|
+
return result
|
89
110
|
|
90
111
|
@classmethod
|
91
112
|
def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
@@ -102,6 +123,7 @@ class CodeBlockElement(NotionBlockElement):
|
|
102
123
|
for match in CodeBlockElement.PATTERN.finditer(text):
|
103
124
|
language = match.group(1) or "plain text"
|
104
125
|
content = match.group(2)
|
126
|
+
caption = match.group(3)
|
105
127
|
|
106
128
|
# Remove trailing newline if present
|
107
129
|
if content.endswith("\n"):
|
@@ -121,6 +143,16 @@ class CodeBlockElement(NotionBlockElement):
|
|
121
143
|
},
|
122
144
|
}
|
123
145
|
|
146
|
+
# Add caption if provided
|
147
|
+
if caption and caption.strip():
|
148
|
+
block["code"]["caption"] = [
|
149
|
+
{
|
150
|
+
"type": "text",
|
151
|
+
"text": {"content": caption.strip()},
|
152
|
+
"plain_text": caption.strip(),
|
153
|
+
}
|
154
|
+
]
|
155
|
+
|
124
156
|
matches.append((match.start(), match.end(), block))
|
125
157
|
|
126
158
|
return matches
|
@@ -139,25 +171,34 @@ class CodeBlockElement(NotionBlockElement):
|
|
139
171
|
.with_description(
|
140
172
|
"Use fenced code blocks to format content as code. Supports language annotations like "
|
141
173
|
"'python', 'json', or 'mermaid'. Useful for displaying code, configurations, command-line "
|
142
|
-
"examples, or diagram syntax. Also suitable for explaining or visualizing systems with diagram languages."
|
174
|
+
"examples, or diagram syntax. Also suitable for explaining or visualizing systems with diagram languages. "
|
175
|
+
"Code blocks can include optional captions for better documentation."
|
143
176
|
)
|
144
177
|
.with_usage_guidelines(
|
145
178
|
"Use code blocks when you want to present technical content like code snippets, terminal commands, "
|
146
|
-
"JSON structures, or system diagrams. Especially helpful when structure and formatting are essential."
|
179
|
+
"JSON structures, or system diagrams. Especially helpful when structure and formatting are essential. "
|
180
|
+
"Add captions to provide context, explanations, or titles for your code blocks."
|
181
|
+
)
|
182
|
+
.with_syntax(
|
183
|
+
"```language\ncode content\n```\nCaption: optional caption text\n\n"
|
184
|
+
"OR\n\n"
|
185
|
+
"```language\ncode content\n```"
|
147
186
|
)
|
148
|
-
.with_syntax("```language\ncode content\n```")
|
149
187
|
.with_examples(
|
150
188
|
[
|
151
|
-
"```python\nprint('Hello, world!')\n
|
152
|
-
'```json\n{"name": "Alice", "age": 30}\n
|
153
|
-
"```mermaid\nflowchart TD\n A --> B\n
|
189
|
+
"```python\nprint('Hello, world!')\n```\nCaption: Basic Python greeting example",
|
190
|
+
'```json\n{"name": "Alice", "age": 30}\n```\nCaption: User data structure',
|
191
|
+
"```mermaid\nflowchart TD\n A --> B\n```\nCaption: Simple flow diagram",
|
192
|
+
'```bash\ngit commit -m "Initial commit"\n```', # Without caption
|
154
193
|
]
|
155
194
|
)
|
156
195
|
.with_avoidance_guidelines(
|
157
196
|
"NEVER EVER wrap markdown content with ```markdown. Markdown should be written directly without code block formatting. "
|
158
197
|
"NEVER use ```markdown under any circumstances. "
|
159
198
|
"For Mermaid diagrams, use ONLY the default styling without colors, backgrounds, or custom styling attributes. "
|
160
|
-
"Keep Mermaid diagrams simple and minimal without any styling or color modifications."
|
199
|
+
"Keep Mermaid diagrams simple and minimal without any styling or color modifications. "
|
200
|
+
"Captions must appear immediately after the closing ``` on a new line starting with 'Caption:' - "
|
201
|
+
"no empty lines between the code block and the caption."
|
161
202
|
)
|
162
203
|
.build()
|
163
204
|
)
|
@@ -0,0 +1,353 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
|
+
|
4
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
5
|
+
from notionary.prompting.element_prompt_content import ElementPromptContent
|
6
|
+
|
7
|
+
|
8
|
+
class ColumnElement(NotionBlockElement):
|
9
|
+
"""
|
10
|
+
Handles conversion between custom Markdown column syntax and Notion column blocks.
|
11
|
+
|
12
|
+
Markdown column syntax:
|
13
|
+
::: columns
|
14
|
+
::: column
|
15
|
+
Content for first column
|
16
|
+
:::
|
17
|
+
::: column
|
18
|
+
Content for second column
|
19
|
+
:::
|
20
|
+
:::
|
21
|
+
|
22
|
+
This creates a column layout in Notion with the specified content in each column.
|
23
|
+
"""
|
24
|
+
|
25
|
+
COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
|
26
|
+
COLUMN_START = re.compile(r"^:::\s*column\s*$")
|
27
|
+
BLOCK_END = re.compile(r"^:::\s*$")
|
28
|
+
|
29
|
+
_converter_callback = None
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def set_converter_callback(
|
33
|
+
cls, callback: Callable[[str], List[Dict[str, Any]]]
|
34
|
+
) -> None:
|
35
|
+
"""
|
36
|
+
Setze die Callback-Funktion, die zum Konvertieren von Markdown zu Notion-Blöcken verwendet wird.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
callback: Funktion, die Markdown-Text annimmt und eine Liste von Notion-Blöcken zurückgibt
|
40
|
+
"""
|
41
|
+
cls._converter_callback = callback
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def match_markdown(text: str) -> bool:
|
45
|
+
"""Check if text starts a columns block."""
|
46
|
+
return bool(ColumnElement.COLUMNS_START.match(text.strip()))
|
47
|
+
|
48
|
+
@staticmethod
|
49
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
50
|
+
"""Check if block is a Notion column_list."""
|
51
|
+
return block.get("type") == "column_list"
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
55
|
+
"""
|
56
|
+
Convert markdown column syntax to Notion column blocks.
|
57
|
+
|
58
|
+
Note: This only processes the first line (columns start).
|
59
|
+
The full column content needs to be processed separately.
|
60
|
+
"""
|
61
|
+
if not ColumnElement.COLUMNS_START.match(text.strip()):
|
62
|
+
return None
|
63
|
+
|
64
|
+
# Create an empty column_list block
|
65
|
+
# Child columns will be added by the column processor
|
66
|
+
return {"type": "column_list", "column_list": {"children": []}}
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
70
|
+
"""Convert Notion column_list block to markdown column syntax."""
|
71
|
+
if block.get("type") != "column_list":
|
72
|
+
return None
|
73
|
+
|
74
|
+
column_children = block.get("column_list", {}).get("children", [])
|
75
|
+
|
76
|
+
# Start the columns block
|
77
|
+
result = ["::: columns"]
|
78
|
+
|
79
|
+
# Process each column
|
80
|
+
for column_block in column_children:
|
81
|
+
if column_block.get("type") == "column":
|
82
|
+
result.append("::: column")
|
83
|
+
|
84
|
+
for _ in column_block.get("column", {}).get("children", []):
|
85
|
+
result.append(" [Column content]") # Placeholder
|
86
|
+
|
87
|
+
result.append(":::")
|
88
|
+
|
89
|
+
# End the columns block
|
90
|
+
result.append(":::")
|
91
|
+
|
92
|
+
return "\n".join(result)
|
93
|
+
|
94
|
+
@staticmethod
|
95
|
+
def is_multiline() -> bool:
|
96
|
+
"""Column blocks span multiple lines."""
|
97
|
+
return True
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def find_matches(
|
101
|
+
cls, text: str, converter_callback: Optional[Callable] = None
|
102
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
103
|
+
"""
|
104
|
+
Find all column block matches in the text and return their positions and blocks.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
text: The input markdown text
|
108
|
+
converter_callback: Optional callback to convert nested content
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
List of tuples (start_pos, end_pos, block)
|
112
|
+
"""
|
113
|
+
# Wenn ein Callback übergeben wurde, nutze diesen, sonst die gespeicherte Referenz
|
114
|
+
converter = converter_callback or cls._converter_callback
|
115
|
+
if not converter:
|
116
|
+
raise ValueError(
|
117
|
+
"No converter callback provided for ColumnElement. Call set_converter_callback first or provide converter_callback parameter."
|
118
|
+
)
|
119
|
+
|
120
|
+
matches = []
|
121
|
+
lines = text.split("\n")
|
122
|
+
i = 0
|
123
|
+
|
124
|
+
while i < len(lines):
|
125
|
+
# Skip non-column lines
|
126
|
+
if not ColumnElement.COLUMNS_START.match(lines[i].strip()):
|
127
|
+
i += 1
|
128
|
+
continue
|
129
|
+
|
130
|
+
# Process a column block and add to matches
|
131
|
+
column_block_info = cls._process_column_block(
|
132
|
+
lines=lines, start_index=i, converter_callback=converter
|
133
|
+
)
|
134
|
+
matches.append(column_block_info)
|
135
|
+
|
136
|
+
# Skip to the end of the processed column block
|
137
|
+
i = column_block_info[3] # i is returned as the 4th element in the tuple
|
138
|
+
|
139
|
+
return [(start, end, block) for start, end, block, _ in matches]
|
140
|
+
|
141
|
+
@classmethod
|
142
|
+
def _process_column_block(
|
143
|
+
cls, lines: List[str], start_index: int, converter_callback: Callable
|
144
|
+
) -> Tuple[int, int, Dict[str, Any], int]:
|
145
|
+
"""
|
146
|
+
Process a complete column block structure from the given starting line.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
lines: All lines of the text
|
150
|
+
start_index: Index of the column block start line
|
151
|
+
converter_callback: Callback function to convert markdown to notion blocks
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
Tuple of (start_pos, end_pos, block, next_line_index)
|
155
|
+
"""
|
156
|
+
columns_start = start_index
|
157
|
+
columns_block = cls.markdown_to_notion(lines[start_index].strip())
|
158
|
+
columns_children = []
|
159
|
+
|
160
|
+
next_index = cls._collect_columns(
|
161
|
+
lines, start_index + 1, columns_children, converter_callback
|
162
|
+
)
|
163
|
+
|
164
|
+
# Add columns to the main block
|
165
|
+
if columns_children:
|
166
|
+
columns_block["column_list"]["children"] = columns_children
|
167
|
+
|
168
|
+
# Calculate positions
|
169
|
+
start_pos = sum(len(lines[j]) + 1 for j in range(columns_start))
|
170
|
+
end_pos = sum(len(lines[j]) + 1 for j in range(next_index))
|
171
|
+
|
172
|
+
return (start_pos, end_pos, columns_block, next_index)
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
def _collect_columns(
|
176
|
+
cls,
|
177
|
+
lines: List[str],
|
178
|
+
start_index: int,
|
179
|
+
columns_children: List[Dict[str, Any]],
|
180
|
+
converter_callback: Callable,
|
181
|
+
) -> int:
|
182
|
+
"""
|
183
|
+
Collect all columns within a column block structure.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
lines: All lines of the text
|
187
|
+
start_index: Index to start collecting from
|
188
|
+
columns_children: List to append collected columns to
|
189
|
+
converter_callback: Callback function to convert column content
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Next line index after all columns have been processed
|
193
|
+
"""
|
194
|
+
i = start_index
|
195
|
+
in_column = False
|
196
|
+
column_content = []
|
197
|
+
|
198
|
+
while i < len(lines):
|
199
|
+
current_line = lines[i].strip()
|
200
|
+
|
201
|
+
if cls.COLUMNS_START.match(current_line):
|
202
|
+
break
|
203
|
+
|
204
|
+
if cls.COLUMN_START.match(current_line):
|
205
|
+
cls._finalize_column(
|
206
|
+
column_content, columns_children, in_column, converter_callback
|
207
|
+
)
|
208
|
+
column_content = []
|
209
|
+
in_column = True
|
210
|
+
i += 1
|
211
|
+
continue
|
212
|
+
|
213
|
+
if cls.BLOCK_END.match(current_line) and in_column:
|
214
|
+
cls._finalize_column(
|
215
|
+
column_content, columns_children, in_column, converter_callback
|
216
|
+
)
|
217
|
+
column_content = []
|
218
|
+
in_column = False
|
219
|
+
i += 1
|
220
|
+
continue
|
221
|
+
|
222
|
+
if cls.BLOCK_END.match(current_line) and not in_column:
|
223
|
+
i += 1
|
224
|
+
break
|
225
|
+
|
226
|
+
if in_column:
|
227
|
+
column_content.append(lines[i])
|
228
|
+
|
229
|
+
i += 1
|
230
|
+
|
231
|
+
cls._finalize_column(
|
232
|
+
column_content, columns_children, in_column, converter_callback
|
233
|
+
)
|
234
|
+
|
235
|
+
return i
|
236
|
+
|
237
|
+
@staticmethod
|
238
|
+
def _finalize_column(
|
239
|
+
column_content: List[str],
|
240
|
+
columns_children: List[Dict[str, Any]],
|
241
|
+
in_column: bool,
|
242
|
+
converter_callback: Callable,
|
243
|
+
) -> None:
|
244
|
+
"""
|
245
|
+
Finalize a column by processing its content and adding it to the columns_children list.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
column_content: Content lines of the column
|
249
|
+
columns_children: List to append the column block to
|
250
|
+
in_column: Whether we're currently in a column (if False, does nothing)
|
251
|
+
converter_callback: Callback function to convert column content
|
252
|
+
"""
|
253
|
+
if not (in_column and column_content):
|
254
|
+
return
|
255
|
+
|
256
|
+
processed_content = ColumnElement._preprocess_column_content(column_content)
|
257
|
+
|
258
|
+
column_blocks = converter_callback("\n".join(processed_content))
|
259
|
+
|
260
|
+
# Create column block
|
261
|
+
column_block = {"type": "column", "column": {"children": column_blocks}}
|
262
|
+
columns_children.append(column_block)
|
263
|
+
|
264
|
+
@classmethod
|
265
|
+
def is_multiline(cls) -> bool:
|
266
|
+
"""Column blocks span multiple lines."""
|
267
|
+
return True
|
268
|
+
|
269
|
+
@staticmethod
|
270
|
+
def _preprocess_column_content(lines: List[str]) -> List[str]:
|
271
|
+
"""
|
272
|
+
Preprocess column content to handle special cases like first headings.
|
273
|
+
|
274
|
+
This removes any spacer markers that might have been added before the first
|
275
|
+
heading in a column, as each column should have its own heading context.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
lines: The lines of content for the column
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
Processed lines ready for conversion
|
282
|
+
"""
|
283
|
+
from notionary.page.markdown_to_notion_converter import MarkdownToNotionConverter
|
284
|
+
|
285
|
+
processed_lines = []
|
286
|
+
found_first_heading = False
|
287
|
+
spacer_marker = MarkdownToNotionConverter.SPACER_MARKER
|
288
|
+
|
289
|
+
i = 0
|
290
|
+
while i < len(lines):
|
291
|
+
line = lines[i]
|
292
|
+
|
293
|
+
# Check if this is a heading line
|
294
|
+
if re.match(r"^(#{1,6})\s+(.+)$", line.strip()):
|
295
|
+
# If it's the first heading, look ahead to check for spacer
|
296
|
+
if not found_first_heading and i > 0 and processed_lines and processed_lines[-1] == spacer_marker:
|
297
|
+
# Remove spacer before first heading in column
|
298
|
+
processed_lines.pop()
|
299
|
+
|
300
|
+
found_first_heading = True
|
301
|
+
|
302
|
+
processed_lines.append(line)
|
303
|
+
i += 1
|
304
|
+
|
305
|
+
return processed_lines
|
306
|
+
|
307
|
+
@classmethod
|
308
|
+
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
309
|
+
"""
|
310
|
+
Returns structured LLM prompt metadata for the column layout element.
|
311
|
+
"""
|
312
|
+
return {
|
313
|
+
"description": "Creates a multi-column layout that displays content side by side.",
|
314
|
+
"when_to_use": (
|
315
|
+
"Use columns sparingly, only for direct comparisons or when parallel presentation significantly improves readability. "
|
316
|
+
"Best for pros/cons lists, feature comparisons, or pairing images with descriptions. "
|
317
|
+
"Avoid overusing as it can complicate document structure."
|
318
|
+
),
|
319
|
+
"syntax": (
|
320
|
+
"::: columns\n"
|
321
|
+
"::: column\n"
|
322
|
+
"Content for first column\n"
|
323
|
+
":::\n"
|
324
|
+
"::: column\n"
|
325
|
+
"Content for second column\n"
|
326
|
+
":::\n"
|
327
|
+
":::"
|
328
|
+
),
|
329
|
+
"examples": [
|
330
|
+
"::: columns\n"
|
331
|
+
"::: column\n"
|
332
|
+
"## Features\n"
|
333
|
+
"- Fast response time\n"
|
334
|
+
"- Intuitive interface\n"
|
335
|
+
"- Regular updates\n"
|
336
|
+
":::\n"
|
337
|
+
"::: column\n"
|
338
|
+
"## Benefits\n"
|
339
|
+
"- Increased productivity\n"
|
340
|
+
"- Better collaboration\n"
|
341
|
+
"- Simplified workflows\n"
|
342
|
+
":::\n"
|
343
|
+
":::",
|
344
|
+
"::: columns\n"
|
345
|
+
"::: column\n"
|
346
|
+
"\n"
|
347
|
+
":::\n"
|
348
|
+
"::: column\n"
|
349
|
+
"This text appears next to the image, creating a media-with-caption style layout that's perfect for documentation or articles.\n"
|
350
|
+
":::\n"
|
351
|
+
":::",
|
352
|
+
],
|
353
|
+
}
|
@@ -57,7 +57,7 @@ class DividerElement(NotionBlockElement):
|
|
57
57
|
"Creates a horizontal divider line to visually separate sections of content."
|
58
58
|
)
|
59
59
|
.with_usage_guidelines(
|
60
|
-
"Use
|
60
|
+
"Use dividers only sparingly and only when the user explicitly asks for them. Dividers create strong visual breaks between content sections, so they should not be used unless specifically requested by the user."
|
61
61
|
)
|
62
62
|
.with_syntax("---")
|
63
63
|
.with_examples(
|
@@ -85,7 +85,10 @@ class HeadingElement(NotionBlockElement):
|
|
85
85
|
.with_usage_guidelines(
|
86
86
|
"Use to group content into sections and define a visual hierarchy."
|
87
87
|
)
|
88
|
+
.with_avoidance_guidelines(
|
89
|
+
"Only H1-H3 syntax is supported. H4 and deeper heading levels are not available."
|
90
|
+
)
|
88
91
|
.with_syntax("## Your Heading Text")
|
89
92
|
.with_standard_markdown()
|
90
93
|
.build()
|
91
|
-
)
|
94
|
+
)
|
@@ -126,7 +126,7 @@ class BlockRegistry:
|
|
126
126
|
|
127
127
|
formatter_names = [e.__name__ for e in element_classes]
|
128
128
|
if "TextInlineFormatter" not in formatter_names:
|
129
|
-
element_classes = [TextInlineFormatter]
|
129
|
+
element_classes = element_classes + [TextInlineFormatter]
|
130
130
|
|
131
131
|
return MarkdownSyntaxPromptGenerator.generate_system_prompt(element_classes)
|
132
132
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
from typing import List, Type
|
3
3
|
from collections import OrderedDict
|
4
4
|
|
5
|
+
from notionary.elements.column_element import ColumnElement
|
5
6
|
from notionary.elements.notion_block_element import NotionBlockElement
|
6
7
|
|
7
8
|
from notionary.elements.audio_element import AudioElement
|
@@ -63,6 +64,7 @@ class BlockRegistryBuilder:
|
|
63
64
|
.with_videos()
|
64
65
|
.with_embeds()
|
65
66
|
.with_audio()
|
67
|
+
.with_columns()
|
66
68
|
.with_mention()
|
67
69
|
.with_paragraphs()
|
68
70
|
.with_toggleable_heading_element()
|
@@ -262,6 +264,12 @@ class BlockRegistryBuilder:
|
|
262
264
|
def with_toggleable_heading_element(self) -> BlockRegistryBuilder:
|
263
265
|
return self.add_element(ToggleableHeadingElement)
|
264
266
|
|
267
|
+
def with_columns(self) -> BlockRegistryBuilder:
|
268
|
+
"""
|
269
|
+
Add support for column elements.
|
270
|
+
"""
|
271
|
+
return self.add_element(ColumnElement)
|
272
|
+
|
265
273
|
def build(self) -> BlockRegistry:
|
266
274
|
"""
|
267
275
|
Build and return the configured BlockRegistry instance.
|