notionary 0.2.21__py3-none-any.whl → 0.2.23__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/blocks/_bootstrap.py +9 -1
- notionary/blocks/audio/audio_element.py +53 -28
- notionary/blocks/audio/audio_markdown_node.py +10 -4
- notionary/blocks/base_block_element.py +15 -3
- notionary/blocks/bookmark/bookmark_element.py +39 -36
- notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
- notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
- notionary/blocks/callout/callout_element.py +20 -4
- notionary/blocks/child_database/__init__.py +11 -4
- notionary/blocks/child_database/child_database_element.py +59 -0
- notionary/blocks/child_database/child_database_models.py +7 -14
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/client.py +0 -1
- notionary/blocks/code/code_element.py +51 -2
- notionary/blocks/code/code_markdown_node.py +52 -1
- notionary/blocks/column/column_element.py +9 -3
- notionary/blocks/column/column_list_element.py +18 -3
- notionary/blocks/divider/divider_element.py +3 -11
- notionary/blocks/embed/embed_element.py +27 -6
- notionary/blocks/equation/equation_element.py +94 -41
- notionary/blocks/equation/equation_element_markdown_node.py +8 -9
- notionary/blocks/file/file_element.py +56 -37
- notionary/blocks/file/file_element_markdown_node.py +9 -7
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/heading_element.py +23 -4
- notionary/blocks/image_block/image_element.py +43 -38
- notionary/blocks/image_block/image_markdown_node.py +10 -5
- notionary/blocks/mixins/captions/__init__.py +4 -0
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
- notionary/blocks/mixins/captions/caption_mixin.py +92 -0
- notionary/blocks/models.py +3 -1
- notionary/blocks/numbered_list/numbered_list_element.py +21 -4
- notionary/blocks/paragraph/paragraph_element.py +21 -5
- notionary/blocks/pdf/pdf_element.py +47 -41
- notionary/blocks/pdf/pdf_markdown_node.py +9 -7
- notionary/blocks/quote/quote_element.py +26 -9
- notionary/blocks/quote/quote_markdown_node.py +2 -2
- notionary/blocks/registry/block_registry.py +1 -46
- notionary/blocks/registry/block_registry_builder.py +8 -0
- notionary/blocks/rich_text/rich_text_models.py +62 -29
- notionary/blocks/rich_text/text_inline_formatter.py +432 -101
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/table_element.py +110 -9
- notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
- notionary/blocks/todo/todo_element.py +21 -4
- notionary/blocks/toggle/toggle_element.py +19 -3
- notionary/blocks/toggle/toggle_markdown_node.py +1 -1
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
- notionary/blocks/types.py +69 -0
- notionary/blocks/video/video_element.py +44 -39
- notionary/blocks/video/video_markdown_node.py +10 -5
- notionary/comments/__init__.py +26 -0
- notionary/comments/client.py +211 -0
- notionary/comments/models.py +129 -0
- notionary/database/client.py +23 -0
- notionary/file_upload/models.py +2 -2
- notionary/markdown/markdown_builder.py +34 -27
- notionary/page/client.py +21 -6
- notionary/page/notion_page.py +77 -2
- notionary/page/page_content_deleting_service.py +117 -0
- notionary/page/page_content_writer.py +89 -113
- notionary/page/page_context.py +64 -0
- notionary/page/reader/handler/__init__.py +2 -0
- notionary/page/reader/handler/base_block_renderer.py +4 -4
- notionary/page/reader/handler/block_rendering_context.py +5 -0
- notionary/page/reader/handler/line_renderer.py +16 -3
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/page_content_retriever.py +17 -5
- notionary/page/writer/handler/__init__.py +2 -0
- notionary/page/writer/handler/code_handler.py +12 -40
- notionary/page/writer/handler/column_handler.py +12 -12
- notionary/page/writer/handler/column_list_handler.py +13 -13
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +4 -4
- notionary/page/writer/handler/regular_line_handler.py +31 -37
- notionary/page/writer/handler/table_handler.py +8 -72
- notionary/page/writer/handler/toggle_handler.py +14 -12
- notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
- notionary/page/writer/markdown_to_notion_converter.py +28 -9
- notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
- notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +150 -0
- notionary/shared/__init__.py +5 -0
- notionary/shared/name_to_id_resolver.py +203 -0
- notionary/telemetry/service.py +0 -1
- notionary/user/notion_user_manager.py +22 -95
- notionary/util/concurrency_limiter.py +0 -0
- notionary/workspace.py +4 -4
- notionary-0.2.23.dist-info/METADATA +235 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
- notionary/page/markdown_whitespace_processor.py +0 -80
- notionary/page/notion_text_length_utils.py +0 -119
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.21.dist-info/METADATA +0 -229
- /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
@@ -1,125 +1,456 @@
|
|
1
1
|
import re
|
2
|
-
from typing import
|
2
|
+
from typing import Optional, Match, List
|
3
3
|
|
4
|
-
from notionary.blocks.rich_text.rich_text_models import
|
4
|
+
from notionary.blocks.rich_text.rich_text_models import (
|
5
|
+
RichTextObject,
|
6
|
+
RichTextType,
|
7
|
+
MentionType,
|
8
|
+
TemplateMentionType,
|
9
|
+
MentionDate,
|
10
|
+
MentionTemplateMention,
|
11
|
+
)
|
12
|
+
from notionary.blocks.types import BlockColor
|
13
|
+
from notionary.shared import NameIdResolver
|
5
14
|
|
6
15
|
|
7
16
|
class TextInlineFormatter:
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
(
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
"""
|
18
|
+
Supported syntax patterns:
|
19
|
+
|
20
|
+
• Bold
|
21
|
+
**bold text**
|
22
|
+
→ RichTextObject(plain_text="bold text", bold=True)
|
23
|
+
|
24
|
+
• Italic
|
25
|
+
*italic text* or _italic text_
|
26
|
+
→ RichTextObject(plain_text="italic text", italic=True)
|
27
|
+
|
28
|
+
• Underline
|
29
|
+
__underlined text__
|
30
|
+
→ RichTextObject(plain_text="underlined text", underline=True)
|
31
|
+
|
32
|
+
• Strikethrough
|
33
|
+
~~strikethrough~~
|
34
|
+
→ RichTextObject(plain_text="strikethrough", strikethrough=True)
|
35
|
+
|
36
|
+
• Inline code
|
37
|
+
`code snippet`
|
38
|
+
→ RichTextObject(plain_text="code snippet", code=True)
|
39
|
+
|
40
|
+
• Link
|
41
|
+
[link text](https://example.com)
|
42
|
+
→ RichTextObject.for_link("link text", "https://example.com")
|
43
|
+
|
44
|
+
• Inline equation
|
45
|
+
$E = mc^2$
|
46
|
+
→ RichTextObject.equation_inline("E = mc^2")
|
47
|
+
|
48
|
+
• Colored text / highlight (supports nested formatting)
|
49
|
+
(red:important) — sets text color to "red"
|
50
|
+
(blue_background:note) — sets background to "blue_background"
|
51
|
+
(red_background:**bold text**) — red background with bold formatting
|
52
|
+
→ RichTextObject(plain_text="important", color="red", bold=True)
|
53
|
+
Valid colors are any value in the BlockColor enum, e.g.:
|
54
|
+
default, gray, brown, orange, yellow, green, blue, purple, pink, red
|
55
|
+
or their `_background` variants.
|
56
|
+
|
57
|
+
• Page mention
|
58
|
+
@page[123e4567-e89b-12d3-a456-426614174000] — by ID
|
59
|
+
@page[Page Name] — by name
|
60
|
+
→ RichTextObject.mention_page("resolved-id")
|
61
|
+
|
62
|
+
• Database mention
|
63
|
+
@database[123e4567-e89b-12d3-a456-426614174000] — by ID
|
64
|
+
@database[Database Name] — by name
|
65
|
+
→ RichTextObject.mention_database("resolved-id")
|
66
|
+
"""
|
67
|
+
|
68
|
+
class Patterns:
|
69
|
+
BOLD = r"\*\*(.+?)\*\*"
|
70
|
+
ITALIC = r"\*(.+?)\*"
|
71
|
+
ITALIC_UNDERSCORE = r"_([^_]+?)_"
|
72
|
+
UNDERLINE = r"__(.+?)__"
|
73
|
+
STRIKETHROUGH = r"~~(.+?)~~"
|
74
|
+
CODE = r"`(.+?)`"
|
75
|
+
LINK = r"\[(.+?)\]\((.+?)\)"
|
76
|
+
INLINE_EQUATION = r"\$(.+?)\$"
|
77
|
+
COLOR = r"\((\w+):(.+?)\)" # (blue:colored text) or (blue_background:text)
|
78
|
+
PAGE_MENTION = r"@page\[([^\]]+)\]" # Matches both IDs and names
|
79
|
+
DATABASE_MENTION = r"@database\[([^\]]+)\]" # Matches both IDs and names
|
80
|
+
USER_MENTION = r"@user\[([^\]]+)\]" # Matches both IDs and names
|
81
|
+
|
82
|
+
# Pattern to handler mapping - cleaner approach
|
83
|
+
@classmethod
|
84
|
+
def _get_format_handlers(cls):
|
85
|
+
"""Get pattern to handler mapping - defined as method to access class methods."""
|
86
|
+
return [
|
87
|
+
(cls.Patterns.BOLD, cls._handle_bold_pattern),
|
88
|
+
(cls.Patterns.ITALIC, cls._handle_italic_pattern),
|
89
|
+
(cls.Patterns.ITALIC_UNDERSCORE, cls._handle_italic_pattern),
|
90
|
+
(cls.Patterns.UNDERLINE, cls._handle_underline_pattern),
|
91
|
+
(cls.Patterns.STRIKETHROUGH, cls._handle_strikethrough_pattern),
|
92
|
+
(cls.Patterns.CODE, cls._handle_code_pattern),
|
93
|
+
(cls.Patterns.LINK, cls._handle_link_pattern),
|
94
|
+
(cls.Patterns.INLINE_EQUATION, cls._handle_equation_pattern),
|
95
|
+
(cls.Patterns.COLOR, cls._handle_color_pattern),
|
96
|
+
(cls.Patterns.PAGE_MENTION, cls._handle_page_mention_pattern),
|
97
|
+
(cls.Patterns.DATABASE_MENTION, cls._handle_database_mention_pattern),
|
98
|
+
(cls.Patterns.USER_MENTION, cls._handle_user_mention_pattern),
|
99
|
+
]
|
100
|
+
|
101
|
+
VALID_COLORS = {color.value for color in BlockColor}
|
102
|
+
|
103
|
+
_resolver: Optional[NameIdResolver] = None
|
104
|
+
|
105
|
+
@classmethod
|
106
|
+
def set_resolver(cls, resolver: Optional[NameIdResolver]) -> None:
|
107
|
+
"""Set the name-to-ID resolver instance."""
|
108
|
+
cls._resolver = resolver
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def get_resolver(cls) -> NameIdResolver:
|
112
|
+
"""Get or create the name-to-ID resolver instance."""
|
113
|
+
if cls._resolver is None:
|
114
|
+
cls._resolver = NameIdResolver()
|
115
|
+
return cls._resolver
|
116
|
+
|
117
|
+
@classmethod
|
118
|
+
async def parse_inline_formatting(cls, text: str) -> list[RichTextObject]:
|
119
|
+
"""Main entry point: Parse markdown text into RichTextObjects."""
|
21
120
|
if not text:
|
22
121
|
return []
|
23
|
-
return cls._split_text_into_segments(text
|
122
|
+
return await cls._split_text_into_segments(text)
|
24
123
|
|
25
124
|
@classmethod
|
26
|
-
def _split_text_into_segments(
|
27
|
-
|
28
|
-
|
29
|
-
segs: list[RichTextObject] = []
|
125
|
+
async def _split_text_into_segments(cls, text: str) -> list[RichTextObject]:
|
126
|
+
"""Core parsing logic - split text based on formatting patterns."""
|
127
|
+
segments: list[RichTextObject] = []
|
30
128
|
remaining = text
|
31
129
|
|
32
130
|
while remaining:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
if not match:
|
40
|
-
segs.append(RichTextObject.from_plain_text(remaining))
|
131
|
+
earliest_match = cls._find_earliest_pattern_match(remaining)
|
132
|
+
|
133
|
+
if not earliest_match:
|
134
|
+
# No more patterns - add remaining as plain text
|
135
|
+
segments.append(RichTextObject.from_plain_text(remaining))
|
41
136
|
break
|
42
137
|
|
43
|
-
|
44
|
-
|
138
|
+
match, handler_name, position = earliest_match
|
139
|
+
|
140
|
+
# Add any plain text before the pattern
|
141
|
+
if position > 0:
|
142
|
+
plain_text = remaining[:position]
|
143
|
+
segments.append(RichTextObject.from_plain_text(plain_text))
|
144
|
+
|
145
|
+
# Convert pattern to RichTextObject(s) - handlers can now return single objects or lists
|
146
|
+
if handler_name in [
|
147
|
+
cls._handle_page_mention_pattern,
|
148
|
+
cls._handle_database_mention_pattern,
|
149
|
+
cls._handle_user_mention_pattern,
|
150
|
+
cls._handle_color_pattern, # Color pattern also needs async for recursive parsing
|
151
|
+
]:
|
152
|
+
result = await handler_name(match)
|
153
|
+
else:
|
154
|
+
result = handler_name(match)
|
155
|
+
|
156
|
+
# Handle both single RichTextObject and list of RichTextObjects
|
157
|
+
if isinstance(result, list):
|
158
|
+
segments.extend(result)
|
159
|
+
elif result:
|
160
|
+
segments.append(result)
|
161
|
+
|
162
|
+
# Continue with text after the pattern
|
163
|
+
remaining = remaining[position + len(match.group(0)) :]
|
164
|
+
|
165
|
+
return segments
|
166
|
+
|
167
|
+
@classmethod
|
168
|
+
def _find_earliest_pattern_match(
|
169
|
+
cls, text: str
|
170
|
+
) -> Optional[tuple[Match, callable, int]]:
|
171
|
+
"""Find the pattern that appears earliest in the text."""
|
172
|
+
earliest_match = None
|
173
|
+
earliest_position = len(text)
|
174
|
+
earliest_handler = None
|
175
|
+
|
176
|
+
for pattern, handler_func in cls._get_format_handlers():
|
177
|
+
match = re.search(pattern, text)
|
178
|
+
if match and match.start() < earliest_position:
|
179
|
+
earliest_match = match
|
180
|
+
earliest_position = match.start()
|
181
|
+
earliest_handler = handler_func
|
182
|
+
|
183
|
+
if earliest_match:
|
184
|
+
return earliest_match, earliest_handler, earliest_position
|
185
|
+
return None
|
45
186
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
187
|
+
@classmethod
|
188
|
+
async def _handle_color_pattern(cls, match: Match) -> List[RichTextObject]:
|
189
|
+
"""Handle colored text with support for nested formatting: (blue:**bold text**)"""
|
190
|
+
color, content = match.group(1).lower(), match.group(2)
|
191
|
+
|
192
|
+
if color not in cls.VALID_COLORS:
|
193
|
+
return [RichTextObject.from_plain_text(f"({match.group(1)}:{content})")]
|
194
|
+
|
195
|
+
# Recursively parse the content inside the color pattern for nested formatting
|
196
|
+
parsed_segments = await cls._split_text_into_segments(content)
|
197
|
+
|
198
|
+
# Apply the color to all resulting segments
|
199
|
+
colored_segments = []
|
200
|
+
for segment in parsed_segments:
|
201
|
+
# Create a new RichTextObject with the same formatting but with the color applied
|
202
|
+
if segment.type == RichTextType.TEXT:
|
203
|
+
# For text segments, we can combine the color with existing formatting
|
204
|
+
colored_segment = cls._apply_color_to_text_segment(segment, color)
|
205
|
+
colored_segments.append(colored_segment)
|
52
206
|
else:
|
53
|
-
|
207
|
+
# For non-text segments (equations, mentions, etc.), keep as-is
|
208
|
+
colored_segments.append(segment)
|
209
|
+
|
210
|
+
return colored_segments
|
54
211
|
|
55
|
-
|
212
|
+
@classmethod
|
213
|
+
def _apply_color_to_text_segment(
|
214
|
+
cls, segment: RichTextObject, color: str
|
215
|
+
) -> RichTextObject:
|
216
|
+
"""Apply color to a text segment while preserving existing formatting."""
|
217
|
+
if segment.type != RichTextType.TEXT:
|
218
|
+
return segment
|
56
219
|
|
57
|
-
|
220
|
+
# Extract existing formatting
|
221
|
+
annotations = segment.annotations
|
222
|
+
text_content = segment.text
|
223
|
+
plain_text = segment.plain_text
|
224
|
+
|
225
|
+
# Create new RichTextObject with color and existing formatting
|
226
|
+
if text_content and text_content.link:
|
227
|
+
# For links, preserve the link while adding color and formatting
|
228
|
+
return RichTextObject.for_link(
|
229
|
+
plain_text,
|
230
|
+
text_content.link.url,
|
231
|
+
bold=annotations.bold if annotations else False,
|
232
|
+
italic=annotations.italic if annotations else False,
|
233
|
+
strikethrough=annotations.strikethrough if annotations else False,
|
234
|
+
underline=annotations.underline if annotations else False,
|
235
|
+
code=annotations.code if annotations else False,
|
236
|
+
color=color,
|
237
|
+
)
|
238
|
+
else:
|
239
|
+
# For regular text, combine all formatting
|
240
|
+
return RichTextObject.from_plain_text(
|
241
|
+
plain_text,
|
242
|
+
bold=annotations.bold if annotations else False,
|
243
|
+
italic=annotations.italic if annotations else False,
|
244
|
+
strikethrough=annotations.strikethrough if annotations else False,
|
245
|
+
underline=annotations.underline if annotations else False,
|
246
|
+
code=annotations.code if annotations else False,
|
247
|
+
color=color,
|
248
|
+
)
|
249
|
+
|
250
|
+
@classmethod
|
251
|
+
async def _handle_page_mention_pattern(cls, match: Match) -> RichTextObject:
|
252
|
+
"""Handle page mentions: @page[page-id-or-name]"""
|
253
|
+
page_identifier = match.group(1)
|
254
|
+
|
255
|
+
resolver = cls.get_resolver()
|
256
|
+
page_id = await resolver.resolve_page_id(page_identifier)
|
257
|
+
|
258
|
+
if page_id:
|
259
|
+
return RichTextObject.mention_page(page_id)
|
260
|
+
else:
|
261
|
+
# If resolution fails, treat as plain text
|
262
|
+
return RichTextObject.for_caption(f"@page[{page_identifier}]")
|
263
|
+
|
264
|
+
@classmethod
|
265
|
+
async def _handle_database_mention_pattern(cls, match: Match) -> RichTextObject:
|
266
|
+
"""Handle database mentions: @database[database-id-or-name]"""
|
267
|
+
database_identifier = match.group(1)
|
268
|
+
|
269
|
+
resolver = cls.get_resolver()
|
270
|
+
database_id = await resolver.resolve_database_id(database_identifier)
|
271
|
+
|
272
|
+
if database_id:
|
273
|
+
return RichTextObject.mention_database(database_id)
|
274
|
+
else:
|
275
|
+
# If resolution fails, treat as plain text
|
276
|
+
return RichTextObject.for_caption(f"@database[{database_identifier}]")
|
58
277
|
|
59
278
|
@classmethod
|
60
|
-
def
|
61
|
-
"""
|
62
|
-
|
63
|
-
|
279
|
+
async def _handle_user_mention_pattern(cls, match: Match) -> RichTextObject:
|
280
|
+
"""Handle user mentions: @user[user-id-or-name]"""
|
281
|
+
user_identifier = match.group(1)
|
282
|
+
|
283
|
+
resolver = cls.get_resolver()
|
284
|
+
user_id = await resolver.resolve_user_id(user_identifier)
|
285
|
+
|
286
|
+
if user_id:
|
287
|
+
return RichTextObject.mention_user(user_id)
|
288
|
+
else:
|
289
|
+
# If resolution fails, treat as plain text
|
290
|
+
return RichTextObject.for_caption(f"@user[{user_identifier}]")
|
291
|
+
|
292
|
+
@classmethod
|
293
|
+
async def extract_text_with_formatting(cls, rich_text: list[RichTextObject]) -> str:
|
294
|
+
"""Convert RichTextObjects back into markdown with inline formatting."""
|
295
|
+
if not rich_text:
|
296
|
+
return ""
|
297
|
+
|
64
298
|
parts: list[str] = []
|
65
299
|
|
66
|
-
for
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# Equations
|
71
|
-
if obj.type == "equation" and obj.equation:
|
72
|
-
parts.append(f"${obj.equation.expression}$")
|
73
|
-
continue
|
74
|
-
|
75
|
-
# Mentions
|
76
|
-
if obj.type == "mention" and obj.mention:
|
77
|
-
m = obj.mention
|
78
|
-
if m.type == "page" and m.page:
|
79
|
-
parts.append(f"@[{m.page.id}]")
|
80
|
-
continue
|
81
|
-
elif m.type == "user" and m.user:
|
82
|
-
parts.append(f"@user({m.user.id})")
|
83
|
-
continue
|
84
|
-
elif m.type == "database" and m.database:
|
85
|
-
parts.append(f"@db({m.database.id})")
|
86
|
-
continue
|
87
|
-
elif m.type == "date" and m.date:
|
88
|
-
if m.date.end:
|
89
|
-
parts.append(f"{m.date.start}–{m.date.end}")
|
90
|
-
else:
|
91
|
-
parts.append(m.date.start)
|
92
|
-
continue
|
93
|
-
elif m.type == "link_preview" and m.link_preview:
|
94
|
-
# Als Link rendern
|
95
|
-
content = f"[{content}]({m.link_preview.url})"
|
96
|
-
elif m.type == "template_mention" and m.template_mention:
|
97
|
-
tm = m.template_mention.type
|
98
|
-
parts.append(
|
99
|
-
"@template_user"
|
100
|
-
if tm == "template_mention_user"
|
101
|
-
else "@template_date"
|
102
|
-
)
|
103
|
-
continue
|
104
|
-
|
105
|
-
# Normale Links (text.link)
|
106
|
-
if obj.text and obj.text.link:
|
107
|
-
url = obj.text.link.url
|
108
|
-
content = f"[{content}]({url})"
|
109
|
-
|
110
|
-
# Inline-Formatierungen
|
111
|
-
ann = obj.annotations.model_dump() if obj.annotations else {}
|
112
|
-
if ann.get("code"):
|
113
|
-
content = f"`{content}`"
|
114
|
-
if ann.get("strikethrough"):
|
115
|
-
content = f"~~{content}~~"
|
116
|
-
if ann.get("underline"):
|
117
|
-
content = f"__{content}__"
|
118
|
-
if ann.get("italic"):
|
119
|
-
content = f"*{content}*"
|
120
|
-
if ann.get("bold"):
|
121
|
-
content = f"**{content}**"
|
122
|
-
|
123
|
-
parts.append(content)
|
300
|
+
for rich_obj in rich_text:
|
301
|
+
formatted_text = await cls._convert_rich_text_to_markdown(rich_obj)
|
302
|
+
parts.append(formatted_text)
|
124
303
|
|
125
304
|
return "".join(parts)
|
305
|
+
|
306
|
+
@classmethod
|
307
|
+
async def _convert_rich_text_to_markdown(cls, obj: RichTextObject) -> str:
|
308
|
+
"""Convert single RichTextObject back to markdown format."""
|
309
|
+
|
310
|
+
# Handle special types first
|
311
|
+
if obj.type == RichTextType.EQUATION and obj.equation:
|
312
|
+
return f"${obj.equation.expression}$"
|
313
|
+
|
314
|
+
if obj.type == RichTextType.MENTION:
|
315
|
+
mention_markdown = await cls._extract_mention_markdown(obj)
|
316
|
+
if mention_markdown:
|
317
|
+
return mention_markdown
|
318
|
+
|
319
|
+
# Handle regular text with formatting
|
320
|
+
content = obj.plain_text or (obj.text.content if obj.text else "")
|
321
|
+
return cls._apply_text_formatting_to_content(obj, content)
|
322
|
+
|
323
|
+
@classmethod
|
324
|
+
async def _extract_mention_markdown(cls, obj: RichTextObject) -> Optional[str]:
|
325
|
+
"""Extract mention objects back to markdown format with human-readable names."""
|
326
|
+
if not obj.mention:
|
327
|
+
return None
|
328
|
+
|
329
|
+
mention = obj.mention
|
330
|
+
|
331
|
+
# Handle different mention types
|
332
|
+
if mention.type == MentionType.PAGE and mention.page:
|
333
|
+
return await cls._extract_page_mention_markdown(mention.page.id)
|
334
|
+
|
335
|
+
if mention.type == MentionType.DATABASE and mention.database:
|
336
|
+
return await cls._extract_database_mention_markdown(mention.database.id)
|
337
|
+
|
338
|
+
if mention.type == MentionType.USER and mention.user:
|
339
|
+
return await cls._extract_user_mention_markdown(mention.user.id)
|
340
|
+
|
341
|
+
if mention.type == MentionType.DATE and mention.date:
|
342
|
+
return cls._extract_date_mention_markdown(mention.date)
|
343
|
+
|
344
|
+
if mention.type == MentionType.TEMPLATE_MENTION and mention.template_mention:
|
345
|
+
return cls._extract_template_mention_markdown(mention.template_mention)
|
346
|
+
|
347
|
+
if mention.type == MentionType.LINK_PREVIEW and mention.link_preview:
|
348
|
+
return f"[{obj.plain_text}]({mention.link_preview.url})"
|
349
|
+
|
350
|
+
return None
|
351
|
+
|
352
|
+
@classmethod
|
353
|
+
async def _extract_page_mention_markdown(cls, page_id: str) -> str:
|
354
|
+
"""Extract page mention to markdown format."""
|
355
|
+
resolver = cls.get_resolver()
|
356
|
+
page_name = await resolver.resolve_page_name(page_id)
|
357
|
+
return f"@page[{page_name or page_id}]"
|
358
|
+
|
359
|
+
@classmethod
|
360
|
+
async def _extract_database_mention_markdown(cls, database_id: str) -> str:
|
361
|
+
"""Extract database mention to markdown format."""
|
362
|
+
resolver = cls.get_resolver()
|
363
|
+
database_name = await resolver.resolve_database_name(database_id)
|
364
|
+
return f"@database[{database_name or database_id}]"
|
365
|
+
|
366
|
+
@classmethod
|
367
|
+
async def _extract_user_mention_markdown(cls, user_id: str) -> str:
|
368
|
+
"""Extract user mention to markdown format."""
|
369
|
+
resolver = cls.get_resolver()
|
370
|
+
user_name = await resolver.resolve_user_name(user_id)
|
371
|
+
return f"@user[{user_name or user_id}]"
|
372
|
+
|
373
|
+
@classmethod
|
374
|
+
def _extract_date_mention_markdown(cls, date_mention: MentionDate) -> str:
|
375
|
+
"""Extract date mention to markdown format."""
|
376
|
+
date_range = date_mention.start
|
377
|
+
if date_mention.end:
|
378
|
+
date_range += f"–{date_mention.end}"
|
379
|
+
return date_range
|
380
|
+
|
381
|
+
@classmethod
|
382
|
+
def _extract_template_mention_markdown(
|
383
|
+
cls, template_mention: MentionTemplateMention
|
384
|
+
) -> str:
|
385
|
+
"""Extract template mention to markdown format."""
|
386
|
+
template_type = template_mention.type
|
387
|
+
return (
|
388
|
+
"@template_user"
|
389
|
+
if template_type == TemplateMentionType.USER
|
390
|
+
else "@template_date"
|
391
|
+
)
|
392
|
+
|
393
|
+
@classmethod
|
394
|
+
def _apply_text_formatting_to_content(
|
395
|
+
cls, obj: RichTextObject, content: str
|
396
|
+
) -> str:
|
397
|
+
"""Apply text formatting annotations to content in correct order."""
|
398
|
+
|
399
|
+
# Handle links first (they wrap the content)
|
400
|
+
if obj.text and obj.text.link:
|
401
|
+
content = f"[{content}]({obj.text.link.url})"
|
402
|
+
|
403
|
+
# Apply formatting annotations if they exist
|
404
|
+
if not obj.annotations:
|
405
|
+
return content
|
406
|
+
|
407
|
+
annotations = obj.annotations
|
408
|
+
|
409
|
+
# Apply formatting in inside-out order
|
410
|
+
if annotations.code:
|
411
|
+
content = f"`{content}`"
|
412
|
+
if annotations.strikethrough:
|
413
|
+
content = f"~~{content}~~"
|
414
|
+
if annotations.underline:
|
415
|
+
content = f"__{content}__"
|
416
|
+
if annotations.italic:
|
417
|
+
content = f"*{content}*"
|
418
|
+
if annotations.bold:
|
419
|
+
content = f"**{content}**"
|
420
|
+
|
421
|
+
# Handle colors (wrap everything)
|
422
|
+
if annotations.color != "default" and annotations.color in cls.VALID_COLORS:
|
423
|
+
content = f"({annotations.color}:{content})"
|
424
|
+
|
425
|
+
return content
|
426
|
+
|
427
|
+
@classmethod
|
428
|
+
def _handle_bold_pattern(cls, match: Match) -> RichTextObject:
|
429
|
+
return RichTextObject.from_plain_text(match.group(1), bold=True)
|
430
|
+
|
431
|
+
@classmethod
|
432
|
+
def _handle_italic_pattern(cls, match: Match) -> RichTextObject:
|
433
|
+
return RichTextObject.from_plain_text(match.group(1), italic=True)
|
434
|
+
|
435
|
+
@classmethod
|
436
|
+
def _handle_underline_pattern(cls, match: Match) -> RichTextObject:
|
437
|
+
return RichTextObject.from_plain_text(match.group(1), underline=True)
|
438
|
+
|
439
|
+
@classmethod
|
440
|
+
def _handle_strikethrough_pattern(cls, match: Match) -> RichTextObject:
|
441
|
+
return RichTextObject.from_plain_text(match.group(1), strikethrough=True)
|
442
|
+
|
443
|
+
@classmethod
|
444
|
+
def _handle_code_pattern(cls, match: Match) -> RichTextObject:
|
445
|
+
return RichTextObject.from_plain_text(match.group(1), code=True)
|
446
|
+
|
447
|
+
@classmethod
|
448
|
+
def _handle_link_pattern(cls, match: Match) -> RichTextObject:
|
449
|
+
link_text, url = match.group(1), match.group(2)
|
450
|
+
return RichTextObject.for_link(link_text, url)
|
451
|
+
|
452
|
+
@classmethod
|
453
|
+
def _handle_equation_pattern(cls, match: Match) -> RichTextObject:
|
454
|
+
"""Handle inline equations: $E = mc^2$"""
|
455
|
+
expression = match.group(1)
|
456
|
+
return RichTextObject.equation_inline(expression)
|