notionary 0.1.1__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/__init__.py +9 -0
- 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.1.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
- notionary-0.1.3.dist-info/RECORD +49 -0
- notionary-0.1.3.dist-info/top_level.txt +1 -0
- notionary-0.1.1.dist-info/RECORD +0 -5
- notionary-0.1.1.dist-info/top_level.txt +0 -1
- {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
- {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,306 @@
|
|
1
|
+
# File: elements/tables.py
|
2
|
+
|
3
|
+
from typing import Dict, Any, Optional, List, Tuple
|
4
|
+
from typing_extensions import override
|
5
|
+
import re
|
6
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
7
|
+
from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
|
8
|
+
|
9
|
+
|
10
|
+
class TableElement(NotionBlockElement):
|
11
|
+
"""
|
12
|
+
Handles conversion between Markdown tables and Notion table blocks.
|
13
|
+
|
14
|
+
Markdown table syntax:
|
15
|
+
| Header 1 | Header 2 | Header 3 |
|
16
|
+
| -------- | -------- | -------- |
|
17
|
+
| Cell 1 | Cell 2 | Cell 3 |
|
18
|
+
| Cell 4 | Cell 5 | Cell 6 |
|
19
|
+
|
20
|
+
The second line with dashes and optional colons defines column alignment.
|
21
|
+
"""
|
22
|
+
|
23
|
+
# Patterns for detecting Markdown tables
|
24
|
+
ROW_PATTERN = re.compile(r"^\s*\|(.+)\|\s*$")
|
25
|
+
SEPARATOR_PATTERN = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
|
26
|
+
|
27
|
+
@override
|
28
|
+
@staticmethod
|
29
|
+
def match_markdown(text: str) -> bool:
|
30
|
+
"""Check if text contains a markdown table."""
|
31
|
+
lines = text.split("\n")
|
32
|
+
|
33
|
+
if len(lines) < 3:
|
34
|
+
return False
|
35
|
+
|
36
|
+
for i, line in enumerate(lines[:-2]):
|
37
|
+
if (
|
38
|
+
TableElement.ROW_PATTERN.match(line)
|
39
|
+
and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
|
40
|
+
and TableElement.ROW_PATTERN.match(lines[i + 2])
|
41
|
+
):
|
42
|
+
return True
|
43
|
+
|
44
|
+
return False
|
45
|
+
|
46
|
+
@override
|
47
|
+
@staticmethod
|
48
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
49
|
+
"""Check if block is a Notion table."""
|
50
|
+
return block.get("type") == "table"
|
51
|
+
|
52
|
+
@override
|
53
|
+
@staticmethod
|
54
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
55
|
+
"""Convert markdown table to Notion table block."""
|
56
|
+
if not TableElement.match_markdown(text):
|
57
|
+
return None
|
58
|
+
|
59
|
+
lines = text.split("\n")
|
60
|
+
|
61
|
+
table_start = TableElement._find_table_start(lines)
|
62
|
+
if table_start is None:
|
63
|
+
return None
|
64
|
+
|
65
|
+
table_end = TableElement._find_table_end(lines, table_start)
|
66
|
+
table_lines = lines[table_start:table_end]
|
67
|
+
|
68
|
+
rows = TableElement._extract_table_rows(table_lines)
|
69
|
+
if not rows:
|
70
|
+
return None
|
71
|
+
|
72
|
+
column_count = len(rows[0])
|
73
|
+
TableElement._normalize_row_lengths(rows, column_count)
|
74
|
+
|
75
|
+
return {
|
76
|
+
"type": "table",
|
77
|
+
"table": {
|
78
|
+
"table_width": column_count,
|
79
|
+
"has_column_header": True,
|
80
|
+
"has_row_header": False,
|
81
|
+
"children": TableElement._create_table_rows(rows),
|
82
|
+
},
|
83
|
+
}
|
84
|
+
|
85
|
+
@override
|
86
|
+
@staticmethod
|
87
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
88
|
+
"""Convert Notion table block to markdown table."""
|
89
|
+
if block.get("type") != "table":
|
90
|
+
return None
|
91
|
+
|
92
|
+
table_data = block.get("table", {})
|
93
|
+
children = block.get("children", [])
|
94
|
+
|
95
|
+
if not children:
|
96
|
+
table_width = table_data.get("table_width", 3)
|
97
|
+
|
98
|
+
header = (
|
99
|
+
"| " + " | ".join([f"Column {i+1}" for i in range(table_width)]) + " |"
|
100
|
+
)
|
101
|
+
separator = (
|
102
|
+
"| " + " | ".join(["--------" for _ in range(table_width)]) + " |"
|
103
|
+
)
|
104
|
+
data_row = (
|
105
|
+
"| " + " | ".join([" " for _ in range(table_width)]) + " |"
|
106
|
+
)
|
107
|
+
|
108
|
+
table_rows = [header, separator, data_row]
|
109
|
+
return "\n".join(table_rows)
|
110
|
+
|
111
|
+
table_rows = []
|
112
|
+
header_processed = False
|
113
|
+
|
114
|
+
for child in children:
|
115
|
+
if child.get("type") != "table_row":
|
116
|
+
continue
|
117
|
+
|
118
|
+
row_data = child.get("table_row", {})
|
119
|
+
cells = row_data.get("cells", [])
|
120
|
+
|
121
|
+
row_cells = []
|
122
|
+
for cell in cells:
|
123
|
+
cell_text = TextInlineFormatter.extract_text_with_formatting(cell)
|
124
|
+
row_cells.append(cell_text or "")
|
125
|
+
|
126
|
+
row = "| " + " | ".join(row_cells) + " |"
|
127
|
+
table_rows.append(row)
|
128
|
+
|
129
|
+
if not header_processed and table_data.get("has_column_header", True):
|
130
|
+
header_processed = True
|
131
|
+
separator = (
|
132
|
+
"| " + " | ".join(["--------" for _ in range(len(cells))]) + " |"
|
133
|
+
)
|
134
|
+
table_rows.append(separator)
|
135
|
+
|
136
|
+
if not table_rows:
|
137
|
+
return None
|
138
|
+
|
139
|
+
if len(table_rows) == 1 and table_data.get("has_column_header", True):
|
140
|
+
cells_count = len(children[0].get("table_row", {}).get("cells", []))
|
141
|
+
separator = (
|
142
|
+
"| " + " | ".join(["--------" for _ in range(cells_count)]) + " |"
|
143
|
+
)
|
144
|
+
table_rows.insert(1, separator)
|
145
|
+
|
146
|
+
return "\n".join(table_rows)
|
147
|
+
|
148
|
+
@override
|
149
|
+
@staticmethod
|
150
|
+
def is_multiline() -> bool:
|
151
|
+
"""Indicates if this element handles content that spans multiple lines."""
|
152
|
+
return True
|
153
|
+
|
154
|
+
@staticmethod
|
155
|
+
def _find_table_start(lines: List[str]) -> Optional[int]:
|
156
|
+
"""Find the start index of a table in the lines."""
|
157
|
+
for i in range(len(lines) - 2):
|
158
|
+
if (
|
159
|
+
TableElement.ROW_PATTERN.match(lines[i])
|
160
|
+
and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
|
161
|
+
and TableElement.ROW_PATTERN.match(lines[i + 2])
|
162
|
+
):
|
163
|
+
return i
|
164
|
+
return None
|
165
|
+
|
166
|
+
@staticmethod
|
167
|
+
def _find_table_end(lines: List[str], start_idx: int) -> int:
|
168
|
+
"""Find the end index of a table, starting from start_idx."""
|
169
|
+
end_idx = start_idx + 3 # Minimum: Header, Separator, one data row
|
170
|
+
while end_idx < len(lines) and TableElement.ROW_PATTERN.match(lines[end_idx]):
|
171
|
+
end_idx += 1
|
172
|
+
return end_idx
|
173
|
+
|
174
|
+
@staticmethod
|
175
|
+
def _extract_table_rows(table_lines: List[str]) -> List[List[str]]:
|
176
|
+
"""Extract row contents from table lines, excluding separator line."""
|
177
|
+
rows = []
|
178
|
+
for i, line in enumerate(table_lines):
|
179
|
+
if i != 1 and TableElement.ROW_PATTERN.match(line): # Skip separator line
|
180
|
+
cells = TableElement._parse_table_row(line)
|
181
|
+
if cells:
|
182
|
+
rows.append(cells)
|
183
|
+
return rows
|
184
|
+
|
185
|
+
@staticmethod
|
186
|
+
def _normalize_row_lengths(rows: List[List[str]], column_count: int) -> None:
|
187
|
+
"""Normalize row lengths to the specified column count."""
|
188
|
+
for row in rows:
|
189
|
+
if len(row) < column_count:
|
190
|
+
row.extend([""] * (column_count - len(row)))
|
191
|
+
elif len(row) > column_count:
|
192
|
+
del row[column_count:]
|
193
|
+
|
194
|
+
@staticmethod
|
195
|
+
def _parse_table_row(row_text: str) -> List[str]:
|
196
|
+
"""Convert table row text to cell contents."""
|
197
|
+
row_content = row_text.strip()
|
198
|
+
|
199
|
+
if row_content.startswith("|"):
|
200
|
+
row_content = row_content[1:]
|
201
|
+
if row_content.endswith("|"):
|
202
|
+
row_content = row_content[:-1]
|
203
|
+
|
204
|
+
return [cell.strip() for cell in row_content.split("|")]
|
205
|
+
|
206
|
+
@staticmethod
|
207
|
+
def _create_table_rows(rows: List[List[str]]) -> List[Dict[str, Any]]:
|
208
|
+
"""Create Notion table rows from cell contents."""
|
209
|
+
table_rows = []
|
210
|
+
|
211
|
+
for row in rows:
|
212
|
+
cells_data = []
|
213
|
+
|
214
|
+
for cell_content in row:
|
215
|
+
rich_text = TextInlineFormatter.parse_inline_formatting(cell_content)
|
216
|
+
|
217
|
+
if not rich_text:
|
218
|
+
rich_text = [
|
219
|
+
{
|
220
|
+
"type": "text",
|
221
|
+
"text": {"content": ""},
|
222
|
+
"annotations": {
|
223
|
+
"bold": False,
|
224
|
+
"italic": False,
|
225
|
+
"strikethrough": False,
|
226
|
+
"underline": False,
|
227
|
+
"code": False,
|
228
|
+
"color": "default",
|
229
|
+
},
|
230
|
+
"plain_text": "",
|
231
|
+
"href": None,
|
232
|
+
}
|
233
|
+
]
|
234
|
+
|
235
|
+
cells_data.append(rich_text)
|
236
|
+
|
237
|
+
table_rows.append({"type": "table_row", "table_row": {"cells": cells_data}})
|
238
|
+
|
239
|
+
return table_rows
|
240
|
+
|
241
|
+
@staticmethod
|
242
|
+
def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
243
|
+
"""
|
244
|
+
Find all tables in the text and return their positions.
|
245
|
+
|
246
|
+
Args:
|
247
|
+
text: The text to search in
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
List of tuples with (start_pos, end_pos, block)
|
251
|
+
"""
|
252
|
+
matches = []
|
253
|
+
lines = text.split("\n")
|
254
|
+
|
255
|
+
i = 0
|
256
|
+
while i < len(lines) - 2:
|
257
|
+
if (
|
258
|
+
TableElement.ROW_PATTERN.match(lines[i])
|
259
|
+
and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
|
260
|
+
and TableElement.ROW_PATTERN.match(lines[i + 2])
|
261
|
+
):
|
262
|
+
|
263
|
+
start_line = i
|
264
|
+
end_line = TableElement._find_table_end(lines, start_line)
|
265
|
+
|
266
|
+
start_pos = TableElement._calculate_position(lines, 0, start_line)
|
267
|
+
end_pos = start_pos + TableElement._calculate_position(
|
268
|
+
lines, start_line, end_line
|
269
|
+
)
|
270
|
+
|
271
|
+
table_text = "\n".join(lines[start_line:end_line])
|
272
|
+
table_block = TableElement.markdown_to_notion(table_text)
|
273
|
+
|
274
|
+
if table_block:
|
275
|
+
matches.append((start_pos, end_pos, table_block))
|
276
|
+
|
277
|
+
i = end_line
|
278
|
+
else:
|
279
|
+
i += 1
|
280
|
+
|
281
|
+
return matches
|
282
|
+
|
283
|
+
@staticmethod
|
284
|
+
def _calculate_position(lines: List[str], start: int, end: int) -> int:
|
285
|
+
"""Calculate the text position in characters from line start to end."""
|
286
|
+
position = 0
|
287
|
+
for i in range(start, end):
|
288
|
+
position += len(lines[i]) + 1 # +1 for newline
|
289
|
+
return position
|
290
|
+
|
291
|
+
@classmethod
|
292
|
+
def get_llm_prompt_content(cls) -> dict:
|
293
|
+
"""Returns information for LLM prompts about this element."""
|
294
|
+
return {
|
295
|
+
"description": "Creates formatted tables with rows and columns for structured data.",
|
296
|
+
"when_to_use": "Use tables to organize and present structured data in a grid format, making information easier to compare and analyze. Tables are ideal for data sets, comparison charts, pricing information, or any content that benefits from columnar organization.",
|
297
|
+
"notes": [
|
298
|
+
"The header row is required and will be displayed differently in Notion",
|
299
|
+
"The separator row with dashes is required to define the table structure",
|
300
|
+
"Table cells support inline formatting such as **bold** and *italic*",
|
301
|
+
],
|
302
|
+
"examples": [
|
303
|
+
"| Product | Price | Stock |\n| ------- | ----- | ----- |\n| Widget A | $10.99 | 42 |\n| Widget B | $14.99 | 27 |",
|
304
|
+
"| Name | Role | Department |\n| ---- | ---- | ---------- |\n| John Smith | Manager | Marketing |\n| Jane Doe | Director | Sales |",
|
305
|
+
],
|
306
|
+
}
|
@@ -0,0 +1,294 @@
|
|
1
|
+
from typing import Dict, Any, List, Tuple
|
2
|
+
import re
|
3
|
+
|
4
|
+
|
5
|
+
class TextInlineFormatter:
|
6
|
+
"""
|
7
|
+
Handles conversion between Markdown inline formatting and Notion rich text elements.
|
8
|
+
|
9
|
+
Supports various formatting options:
|
10
|
+
- Bold: **text**
|
11
|
+
- Italic: *text* or _text_
|
12
|
+
- Underline: __text__
|
13
|
+
- Strikethrough: ~~text~~
|
14
|
+
- Code: `text`
|
15
|
+
- Links: [text](url)
|
16
|
+
- Highlights: ==text== (default yellow) or ==color:text== (custom color)
|
17
|
+
"""
|
18
|
+
|
19
|
+
# Format patterns for matching Markdown formatting
|
20
|
+
FORMAT_PATTERNS = [
|
21
|
+
(r"\*\*(.+?)\*\*", {"bold": True}),
|
22
|
+
(r"\*(.+?)\*", {"italic": True}),
|
23
|
+
(r"_(.+?)_", {"italic": True}),
|
24
|
+
(r"__(.+?)__", {"underline": True}),
|
25
|
+
(r"~~(.+?)~~", {"strikethrough": True}),
|
26
|
+
(r"`(.+?)`", {"code": True}),
|
27
|
+
(r"\[(.+?)\]\((.+?)\)", {"link": True}),
|
28
|
+
(r"==([a-z_]+):(.+?)==", {"highlight": True}),
|
29
|
+
(r"==(.+?)==", {"highlight_default": True}),
|
30
|
+
]
|
31
|
+
|
32
|
+
# Valid colors for highlighting
|
33
|
+
VALID_COLORS = [
|
34
|
+
"default",
|
35
|
+
"gray",
|
36
|
+
"brown",
|
37
|
+
"orange",
|
38
|
+
"yellow",
|
39
|
+
"green",
|
40
|
+
"blue",
|
41
|
+
"purple",
|
42
|
+
"pink",
|
43
|
+
"red",
|
44
|
+
"gray_background",
|
45
|
+
"brown_background",
|
46
|
+
"orange_background",
|
47
|
+
"yellow_background",
|
48
|
+
"green_background",
|
49
|
+
"blue_background",
|
50
|
+
"purple_background",
|
51
|
+
"pink_background",
|
52
|
+
"red_background",
|
53
|
+
]
|
54
|
+
|
55
|
+
@classmethod
|
56
|
+
def parse_inline_formatting(cls, text: str) -> List[Dict[str, Any]]:
|
57
|
+
"""
|
58
|
+
Parse inline text formatting into Notion rich_text format.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
text: Markdown text with inline formatting
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
List of Notion rich_text objects
|
65
|
+
"""
|
66
|
+
if not text:
|
67
|
+
return []
|
68
|
+
|
69
|
+
return cls._split_text_into_segments(text, cls.FORMAT_PATTERNS)
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def _split_text_into_segments(
|
73
|
+
cls, text: str, format_patterns: List[Tuple]
|
74
|
+
) -> List[Dict[str, Any]]:
|
75
|
+
"""
|
76
|
+
Split text into segments by formatting markers and convert to Notion rich_text format.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
text: Text to split
|
80
|
+
format_patterns: List of (regex pattern, formatting dict) tuples
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
List of Notion rich_text objects
|
84
|
+
"""
|
85
|
+
segments = []
|
86
|
+
remaining_text = text
|
87
|
+
|
88
|
+
while remaining_text:
|
89
|
+
earliest_match = None
|
90
|
+
earliest_format = None
|
91
|
+
earliest_pos = len(remaining_text)
|
92
|
+
|
93
|
+
# Find the earliest formatting marker
|
94
|
+
for pattern, formatting in format_patterns:
|
95
|
+
match = re.search(pattern, remaining_text)
|
96
|
+
if match and match.start() < earliest_pos:
|
97
|
+
earliest_match = match
|
98
|
+
earliest_format = formatting
|
99
|
+
earliest_pos = match.start()
|
100
|
+
|
101
|
+
if earliest_match is None:
|
102
|
+
if remaining_text:
|
103
|
+
segments.append(cls._create_text_element(remaining_text, {}))
|
104
|
+
break
|
105
|
+
|
106
|
+
if earliest_pos > 0:
|
107
|
+
segments.append(
|
108
|
+
cls._create_text_element(remaining_text[:earliest_pos], {})
|
109
|
+
)
|
110
|
+
|
111
|
+
if "highlight" in earliest_format:
|
112
|
+
color = earliest_match.group(1)
|
113
|
+
content = earliest_match.group(2)
|
114
|
+
|
115
|
+
if color not in cls.VALID_COLORS:
|
116
|
+
if not color.endswith("_background"):
|
117
|
+
color = f"{color}_background"
|
118
|
+
|
119
|
+
if color not in cls.VALID_COLORS:
|
120
|
+
color = "yellow_background"
|
121
|
+
|
122
|
+
segments.append(cls._create_text_element(content, {"color": color}))
|
123
|
+
|
124
|
+
elif "highlight_default" in earliest_format:
|
125
|
+
content = earliest_match.group(1)
|
126
|
+
segments.append(
|
127
|
+
cls._create_text_element(content, {"color": "yellow_background"})
|
128
|
+
)
|
129
|
+
|
130
|
+
elif "link" in earliest_format:
|
131
|
+
content = earliest_match.group(1)
|
132
|
+
url = earliest_match.group(2)
|
133
|
+
segments.append(cls._create_link_element(content, url))
|
134
|
+
|
135
|
+
else:
|
136
|
+
content = earliest_match.group(1)
|
137
|
+
segments.append(cls._create_text_element(content, earliest_format))
|
138
|
+
|
139
|
+
# Move past the processed segment
|
140
|
+
remaining_text = remaining_text[
|
141
|
+
earliest_pos + len(earliest_match.group(0)) :
|
142
|
+
]
|
143
|
+
|
144
|
+
return segments
|
145
|
+
|
146
|
+
@classmethod
|
147
|
+
def _create_text_element(
|
148
|
+
cls, text: str, formatting: Dict[str, Any]
|
149
|
+
) -> Dict[str, Any]:
|
150
|
+
"""
|
151
|
+
Create a Notion text element with formatting.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
text: The text content
|
155
|
+
formatting: Dictionary of formatting options
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
Notion rich_text element
|
159
|
+
"""
|
160
|
+
annotations = cls._default_annotations()
|
161
|
+
|
162
|
+
# Apply formatting
|
163
|
+
for key, value in formatting.items():
|
164
|
+
if key == "color":
|
165
|
+
annotations["color"] = value
|
166
|
+
elif key in annotations:
|
167
|
+
annotations[key] = value
|
168
|
+
|
169
|
+
return {
|
170
|
+
"type": "text",
|
171
|
+
"text": {"content": text},
|
172
|
+
"annotations": annotations,
|
173
|
+
"plain_text": text,
|
174
|
+
}
|
175
|
+
|
176
|
+
@classmethod
|
177
|
+
def _create_link_element(cls, text: str, url: str) -> Dict[str, Any]:
|
178
|
+
"""
|
179
|
+
Create a Notion link element.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
text: The link text
|
183
|
+
url: The URL
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
Notion rich_text element with link
|
187
|
+
"""
|
188
|
+
return {
|
189
|
+
"type": "text",
|
190
|
+
"text": {"content": text, "link": {"url": url}},
|
191
|
+
"annotations": cls._default_annotations(),
|
192
|
+
"plain_text": text,
|
193
|
+
}
|
194
|
+
|
195
|
+
@classmethod
|
196
|
+
def extract_text_with_formatting(cls, rich_text: List[Dict[str, Any]]) -> str:
|
197
|
+
"""
|
198
|
+
Convert Notion rich_text elements back to Markdown formatted text.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
rich_text: List of Notion rich_text elements
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
Markdown formatted text
|
205
|
+
"""
|
206
|
+
formatted_parts = []
|
207
|
+
|
208
|
+
for text_obj in rich_text:
|
209
|
+
# Fallback: If plain_text is missing, use text['content']
|
210
|
+
content = text_obj.get("plain_text")
|
211
|
+
if content is None:
|
212
|
+
content = text_obj.get("text", {}).get("content", "")
|
213
|
+
|
214
|
+
annotations = text_obj.get("annotations", {})
|
215
|
+
|
216
|
+
if annotations.get("code", False):
|
217
|
+
content = f"`{content}`"
|
218
|
+
if annotations.get("strikethrough", False):
|
219
|
+
content = f"~~{content}~~"
|
220
|
+
if annotations.get("underline", False):
|
221
|
+
content = f"__{content}__"
|
222
|
+
if annotations.get("italic", False):
|
223
|
+
content = f"*{content}*"
|
224
|
+
if annotations.get("bold", False):
|
225
|
+
content = f"**{content}**"
|
226
|
+
|
227
|
+
color = annotations.get("color", "default")
|
228
|
+
if color != "default":
|
229
|
+
content = f"=={color.replace('_background', '')}:{content}=="
|
230
|
+
|
231
|
+
text_data = text_obj.get("text", {})
|
232
|
+
link_data = text_data.get("link")
|
233
|
+
if link_data:
|
234
|
+
url = link_data.get("url", "")
|
235
|
+
content = f"[{content}]({url})"
|
236
|
+
|
237
|
+
formatted_parts.append(content)
|
238
|
+
|
239
|
+
return "".join(formatted_parts)
|
240
|
+
|
241
|
+
@classmethod
|
242
|
+
def _default_annotations(cls) -> Dict[str, bool]:
|
243
|
+
"""
|
244
|
+
Create default annotations object.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
Default Notion text annotations
|
248
|
+
"""
|
249
|
+
return {
|
250
|
+
"bold": False,
|
251
|
+
"italic": False,
|
252
|
+
"strikethrough": False,
|
253
|
+
"underline": False,
|
254
|
+
"code": False,
|
255
|
+
"color": "default",
|
256
|
+
}
|
257
|
+
|
258
|
+
@classmethod
|
259
|
+
def get_llm_prompt_content(cls) -> Dict[str, Any]:
|
260
|
+
"""
|
261
|
+
Returns information about inline text formatting capabilities for LLM prompts.
|
262
|
+
|
263
|
+
This method provides documentation about supported inline formatting options
|
264
|
+
that can be used across all block elements.
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
A dictionary with descriptions, syntax examples, and usage guidelines
|
268
|
+
"""
|
269
|
+
return {
|
270
|
+
"description": "Standard Markdown formatting is supported in all text blocks. Additionally, a custom highlight syntax is available for emphasizing important information. To create vertical spacing between elements, use the special spacer tag.",
|
271
|
+
"syntax": [
|
272
|
+
"**text** - Bold text",
|
273
|
+
"*text* or _text_ - Italic text",
|
274
|
+
"__text__ - Underlined text",
|
275
|
+
"~~text~~ - Strikethrough text",
|
276
|
+
"`text` - Inline code",
|
277
|
+
"[text](url) - Link",
|
278
|
+
"==text== - Default highlight (yellow background)",
|
279
|
+
"==color:text== - Colored highlight (e.g., ==red:warning==)",
|
280
|
+
"<!-- spacer --> - Creates vertical spacing between elements",
|
281
|
+
],
|
282
|
+
"examples": [
|
283
|
+
"This is a **bold** statement with some *italic* words.",
|
284
|
+
"This feature is ~~deprecated~~ as of version 2.0.",
|
285
|
+
"Edit the `config.json` file to configure settings.",
|
286
|
+
"Check our [documentation](https://docs.example.com) for more details.",
|
287
|
+
"==This is an important note== that you should remember.",
|
288
|
+
"==red:Warning:== This action cannot be undone.",
|
289
|
+
"==blue:Note:== Common colors include red, blue, green, yellow, purple.",
|
290
|
+
"First paragraph content.\n\n<!-- spacer -->\n\nSecond paragraph with additional spacing above.",
|
291
|
+
],
|
292
|
+
"highlight_usage": "The highlight syntax (==text== and ==color:text==) should be used to emphasize important information, warnings, notes, or other content that needs to stand out. This is particularly useful for making content more scannable at a glance.",
|
293
|
+
"spacer_usage": "Use the <!-- spacer --> tag on its own line to create additional vertical spacing between elements. This is useful for improving readability by visually separating sections of content. Multiple spacer tags can be used for greater spacing.",
|
294
|
+
}
|