notionary 0.2.19__py3-none-any.whl → 0.2.22__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 +8 -4
- notionary/base_notion_client.py +3 -1
- notionary/blocks/__init__.py +2 -91
- notionary/blocks/_bootstrap.py +271 -0
- notionary/blocks/audio/__init__.py +8 -2
- notionary/blocks/audio/audio_element.py +69 -106
- notionary/blocks/audio/audio_markdown_node.py +13 -5
- notionary/blocks/audio/audio_models.py +6 -55
- notionary/blocks/base_block_element.py +42 -0
- notionary/blocks/bookmark/__init__.py +9 -2
- notionary/blocks/bookmark/bookmark_element.py +49 -139
- notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
- notionary/blocks/bookmark/bookmark_models.py +15 -0
- notionary/blocks/breadcrumbs/__init__.py +17 -0
- notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
- notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
- notionary/blocks/bulleted_list/__init__.py +12 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
- notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
- notionary/blocks/callout/__init__.py +9 -2
- notionary/blocks/callout/callout_element.py +53 -86
- notionary/blocks/callout/callout_markdown_node.py +3 -1
- notionary/blocks/callout/callout_models.py +33 -0
- notionary/blocks/child_database/__init__.py +14 -0
- notionary/blocks/child_database/child_database_element.py +61 -0
- notionary/blocks/child_database/child_database_models.py +12 -0
- notionary/blocks/child_page/__init__.py +9 -0
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/child_page/child_page_models.py +12 -0
- notionary/blocks/{shared/block_client.py → client.py} +54 -54
- notionary/blocks/code/__init__.py +6 -2
- notionary/blocks/code/code_element.py +96 -181
- notionary/blocks/code/code_markdown_node.py +64 -13
- notionary/blocks/code/code_models.py +94 -0
- notionary/blocks/column/__init__.py +25 -1
- notionary/blocks/column/column_element.py +44 -312
- notionary/blocks/column/column_list_element.py +52 -0
- notionary/blocks/column/column_list_markdown_node.py +50 -0
- notionary/blocks/column/column_markdown_node.py +59 -0
- notionary/blocks/column/column_models.py +26 -0
- notionary/blocks/divider/__init__.py +9 -2
- notionary/blocks/divider/divider_element.py +18 -49
- notionary/blocks/divider/divider_markdown_node.py +2 -1
- notionary/blocks/divider/divider_models.py +12 -0
- notionary/blocks/embed/__init__.py +9 -2
- notionary/blocks/embed/embed_element.py +65 -111
- notionary/blocks/embed/embed_markdown_node.py +3 -1
- notionary/blocks/embed/embed_models.py +14 -0
- notionary/blocks/equation/__init__.py +14 -0
- notionary/blocks/equation/equation_element.py +133 -0
- notionary/blocks/equation/equation_element_markdown_node.py +35 -0
- notionary/blocks/equation/equation_models.py +11 -0
- notionary/blocks/file/__init__.py +25 -0
- notionary/blocks/file/file_element.py +112 -0
- notionary/blocks/file/file_element_markdown_node.py +37 -0
- notionary/blocks/file/file_element_models.py +39 -0
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/__init__.py +16 -2
- notionary/blocks/heading/heading_element.py +83 -69
- notionary/blocks/heading/heading_markdown_node.py +2 -1
- notionary/blocks/heading/heading_models.py +29 -0
- notionary/blocks/image_block/__init__.py +13 -0
- notionary/blocks/image_block/image_element.py +89 -0
- notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
- notionary/blocks/image_block/image_models.py +10 -0
- 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 +174 -0
- notionary/blocks/numbered_list/__init__.py +12 -2
- notionary/blocks/numbered_list/numbered_list_element.py +48 -56
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
- notionary/blocks/numbered_list/numbered_list_models.py +17 -0
- notionary/blocks/paragraph/__init__.py +12 -2
- notionary/blocks/paragraph/paragraph_element.py +40 -66
- notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
- notionary/blocks/paragraph/paragraph_models.py +16 -0
- notionary/blocks/pdf/__init__.py +13 -0
- notionary/blocks/pdf/pdf_element.py +97 -0
- notionary/blocks/pdf/pdf_markdown_node.py +37 -0
- notionary/blocks/pdf/pdf_models.py +11 -0
- notionary/blocks/quote/__init__.py +11 -2
- notionary/blocks/quote/quote_element.py +45 -62
- notionary/blocks/quote/quote_markdown_node.py +6 -3
- notionary/blocks/quote/quote_models.py +18 -0
- notionary/blocks/registry/__init__.py +4 -0
- notionary/blocks/registry/block_registry.py +60 -121
- notionary/blocks/registry/block_registry_builder.py +115 -59
- notionary/blocks/rich_text/__init__.py +33 -0
- notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
- notionary/blocks/rich_text/rich_text_models.py +221 -0
- notionary/blocks/rich_text/text_inline_formatter.py +456 -0
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/__init__.py +16 -2
- notionary/blocks/table/table_element.py +136 -228
- notionary/blocks/table/table_markdown_node.py +2 -1
- notionary/blocks/table/table_models.py +28 -0
- notionary/blocks/table_of_contents/__init__.py +19 -0
- notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
- notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
- notionary/blocks/todo/__init__.py +9 -2
- notionary/blocks/todo/todo_element.py +52 -92
- notionary/blocks/todo/todo_markdown_node.py +2 -1
- notionary/blocks/todo/todo_models.py +19 -0
- notionary/blocks/toggle/__init__.py +13 -3
- notionary/blocks/toggle/toggle_element.py +69 -260
- notionary/blocks/toggle/toggle_markdown_node.py +25 -15
- notionary/blocks/toggle/toggle_models.py +17 -0
- notionary/blocks/toggleable_heading/__init__.py +6 -2
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
- notionary/blocks/types.py +130 -0
- notionary/blocks/video/__init__.py +8 -2
- notionary/blocks/video/video_element.py +70 -141
- notionary/blocks/video/video_element_models.py +10 -0
- notionary/blocks/video/video_markdown_node.py +13 -6
- notionary/database/client.py +26 -8
- notionary/database/database.py +13 -14
- notionary/database/database_filter_builder.py +2 -2
- notionary/database/database_provider.py +5 -4
- notionary/database/models.py +337 -0
- notionary/database/notion_database.py +6 -7
- notionary/file_upload/client.py +5 -7
- notionary/file_upload/models.py +3 -2
- notionary/file_upload/notion_file_upload.py +2 -3
- notionary/markdown/markdown_builder.py +729 -0
- notionary/markdown/markdown_document_model.py +228 -0
- notionary/{blocks → markdown}/markdown_node.py +1 -0
- notionary/models/notion_database_response.py +0 -338
- notionary/page/client.py +34 -15
- notionary/page/models.py +327 -0
- notionary/page/notion_page.py +136 -58
- notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
- notionary/page/page_content_writer.py +177 -0
- notionary/page/page_context.py +65 -0
- notionary/page/reader/handler/__init__.py +19 -0
- notionary/page/reader/handler/base_block_renderer.py +44 -0
- notionary/page/reader/handler/block_processing_context.py +35 -0
- notionary/page/reader/handler/block_rendering_context.py +48 -0
- notionary/page/reader/handler/column_list_renderer.py +51 -0
- notionary/page/reader/handler/column_renderer.py +60 -0
- notionary/page/reader/handler/line_renderer.py +73 -0
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/handler/toggle_renderer.py +69 -0
- notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
- notionary/page/reader/page_content_retriever.py +81 -0
- notionary/page/search_filter_builder.py +2 -1
- notionary/page/writer/handler/__init__.py +24 -0
- notionary/page/writer/handler/code_handler.py +72 -0
- notionary/page/writer/handler/column_handler.py +141 -0
- notionary/page/writer/handler/column_list_handler.py +139 -0
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +35 -0
- notionary/page/writer/handler/line_processing_context.py +54 -0
- notionary/page/writer/handler/regular_line_handler.py +86 -0
- notionary/page/writer/handler/table_handler.py +66 -0
- notionary/page/writer/handler/toggle_handler.py +155 -0
- notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
- notionary/page/writer/markdown_to_notion_converter.py +95 -0
- 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/notion_text_length_processor.py +150 -0
- notionary/telemetry/__init__.py +2 -2
- notionary/telemetry/service.py +3 -3
- notionary/user/__init__.py +2 -2
- notionary/user/base_notion_user.py +2 -1
- notionary/user/client.py +2 -3
- notionary/user/models.py +1 -0
- notionary/user/notion_bot_user.py +4 -5
- notionary/user/notion_user.py +3 -4
- notionary/user/notion_user_manager.py +23 -95
- notionary/util/__init__.py +3 -2
- notionary/util/fuzzy.py +2 -1
- notionary/util/logging_mixin.py +2 -2
- notionary/util/singleton_metaclass.py +1 -1
- notionary/workspace.py +6 -5
- notionary-0.2.22.dist-info/METADATA +237 -0
- notionary-0.2.22.dist-info/RECORD +200 -0
- notionary/blocks/document/__init__.py +0 -7
- notionary/blocks/document/document_element.py +0 -102
- notionary/blocks/document/document_markdown_node.py +0 -31
- notionary/blocks/image/__init__.py +0 -7
- notionary/blocks/image/image_element.py +0 -151
- notionary/blocks/markdown_builder.py +0 -356
- notionary/blocks/mention/__init__.py +0 -7
- notionary/blocks/mention/mention_element.py +0 -229
- notionary/blocks/mention/mention_markdown_node.py +0 -38
- notionary/blocks/prompts/element_prompt_builder.py +0 -83
- notionary/blocks/prompts/element_prompt_content.py +0 -41
- notionary/blocks/shared/models.py +0 -713
- notionary/blocks/shared/notion_block_element.py +0 -37
- notionary/blocks/shared/text_inline_formatter.py +0 -262
- notionary/blocks/shared/text_inline_formatter_new.py +0 -139
- notionary/database/models/page_result.py +0 -10
- notionary/models/notion_block_response.py +0 -264
- notionary/models/notion_page_response.py +0 -78
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/content/markdown_whitespace_processor.py +0 -80
- notionary/page/content/notion_text_length_utils.py +0 -87
- notionary/page/content/page_content_retriever.py +0 -60
- notionary/page/formatting/line_processor.py +0 -153
- notionary/page/formatting/markdown_to_notion_converter.py +0 -153
- notionary/page/markdown_syntax_prompt_generator.py +0 -114
- notionary/page/notion_to_markdown_converter.py +0 -179
- notionary/page/properites/property_value_extractor.py +0 -0
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.19.dist-info/METADATA +0 -225
- notionary-0.2.19.dist-info/RECORD +0 -150
- /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
- /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
- /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
- /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
- /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
- /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -0,0 +1,221 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class RichTextType(str, Enum):
|
10
|
+
"""Types of rich text objects."""
|
11
|
+
|
12
|
+
TEXT = "text"
|
13
|
+
MENTION = "mention"
|
14
|
+
EQUATION = "equation"
|
15
|
+
|
16
|
+
|
17
|
+
class MentionType(str, Enum):
|
18
|
+
"""Types of mention objects."""
|
19
|
+
|
20
|
+
USER = "user"
|
21
|
+
PAGE = "page"
|
22
|
+
DATABASE = "database"
|
23
|
+
DATE = "date"
|
24
|
+
LINK_PREVIEW = "link_preview"
|
25
|
+
TEMPLATE_MENTION = "template_mention"
|
26
|
+
|
27
|
+
|
28
|
+
class TemplateMentionType(str, Enum):
|
29
|
+
"""Types of template mentions."""
|
30
|
+
|
31
|
+
USER = "template_mention_user"
|
32
|
+
DATE = "template_mention_date"
|
33
|
+
|
34
|
+
|
35
|
+
class TextAnnotations(BaseModel):
|
36
|
+
bold: bool = False
|
37
|
+
italic: bool = False
|
38
|
+
strikethrough: bool = False
|
39
|
+
underline: bool = False
|
40
|
+
code: bool = False
|
41
|
+
color: str = "default"
|
42
|
+
|
43
|
+
|
44
|
+
class LinkObject(BaseModel):
|
45
|
+
url: str
|
46
|
+
|
47
|
+
|
48
|
+
class TextContent(BaseModel):
|
49
|
+
content: str
|
50
|
+
link: Optional[LinkObject] = None
|
51
|
+
|
52
|
+
|
53
|
+
class EquationObject(BaseModel):
|
54
|
+
expression: str
|
55
|
+
|
56
|
+
|
57
|
+
class MentionUserRef(BaseModel):
|
58
|
+
id: str # Notion user id
|
59
|
+
|
60
|
+
|
61
|
+
class MentionPageRef(BaseModel):
|
62
|
+
id: str
|
63
|
+
|
64
|
+
|
65
|
+
class MentionDatabaseRef(BaseModel):
|
66
|
+
id: str
|
67
|
+
|
68
|
+
|
69
|
+
class MentionLinkPreview(BaseModel):
|
70
|
+
url: str
|
71
|
+
|
72
|
+
|
73
|
+
class MentionDate(BaseModel):
|
74
|
+
# entspricht Notion date object (start Pflicht, end/time_zone optional)
|
75
|
+
start: str # ISO 8601 date or datetime
|
76
|
+
end: Optional[str] = None
|
77
|
+
time_zone: Optional[str] = None
|
78
|
+
|
79
|
+
|
80
|
+
class MentionTemplateMention(BaseModel):
|
81
|
+
# Notion hat zwei Template-Mention-Typen
|
82
|
+
type: TemplateMentionType
|
83
|
+
|
84
|
+
|
85
|
+
class MentionObject(BaseModel):
|
86
|
+
type: MentionType
|
87
|
+
user: Optional[MentionUserRef] = None
|
88
|
+
page: Optional[MentionPageRef] = None
|
89
|
+
database: Optional[MentionDatabaseRef] = None
|
90
|
+
date: Optional[MentionDate] = None
|
91
|
+
link_preview: Optional[MentionLinkPreview] = None
|
92
|
+
template_mention: Optional[MentionTemplateMention] = None
|
93
|
+
|
94
|
+
|
95
|
+
class RichTextObject(BaseModel):
|
96
|
+
type: RichTextType = RichTextType.TEXT
|
97
|
+
|
98
|
+
text: Optional[TextContent] = None
|
99
|
+
annotations: Optional[TextAnnotations] = None
|
100
|
+
plain_text: str = ""
|
101
|
+
href: Optional[str] = None
|
102
|
+
|
103
|
+
mention: Optional[MentionObject] = None
|
104
|
+
|
105
|
+
equation: Optional[EquationObject] = None
|
106
|
+
|
107
|
+
@classmethod
|
108
|
+
def from_plain_text(cls, content: str, **ann) -> RichTextObject:
|
109
|
+
return cls(
|
110
|
+
type=RichTextType.TEXT,
|
111
|
+
text=TextContent(content=content),
|
112
|
+
annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
|
113
|
+
plain_text=content,
|
114
|
+
)
|
115
|
+
|
116
|
+
@classmethod
|
117
|
+
def for_caption(cls, content: str) -> RichTextObject:
|
118
|
+
return cls(
|
119
|
+
type=RichTextType.TEXT,
|
120
|
+
text=TextContent(content=content),
|
121
|
+
annotations=None,
|
122
|
+
plain_text=content,
|
123
|
+
)
|
124
|
+
|
125
|
+
@classmethod
|
126
|
+
def for_code_block(cls, content: str) -> RichTextObject:
|
127
|
+
# keine annotations setzen → Notion Code-Highlight bleibt an
|
128
|
+
return cls.for_caption(content)
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def for_link(cls, content: str, url: str, **ann) -> RichTextObject:
|
132
|
+
return cls(
|
133
|
+
type=RichTextType.TEXT,
|
134
|
+
text=TextContent(content=content, link=LinkObject(url=url)),
|
135
|
+
annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
|
136
|
+
plain_text=content,
|
137
|
+
)
|
138
|
+
|
139
|
+
@classmethod
|
140
|
+
def mention_user(cls, user_id: str) -> RichTextObject:
|
141
|
+
return cls(
|
142
|
+
type=RichTextType.MENTION,
|
143
|
+
mention=MentionObject(
|
144
|
+
type=MentionType.USER, user=MentionUserRef(id=user_id)
|
145
|
+
),
|
146
|
+
annotations=TextAnnotations(),
|
147
|
+
)
|
148
|
+
|
149
|
+
@classmethod
|
150
|
+
def mention_page(cls, page_id: str) -> RichTextObject:
|
151
|
+
return cls(
|
152
|
+
type=RichTextType.MENTION,
|
153
|
+
mention=MentionObject(
|
154
|
+
type=MentionType.PAGE, page=MentionPageRef(id=page_id)
|
155
|
+
),
|
156
|
+
annotations=TextAnnotations(),
|
157
|
+
)
|
158
|
+
|
159
|
+
@classmethod
|
160
|
+
def mention_database(cls, database_id: str) -> RichTextObject:
|
161
|
+
return cls(
|
162
|
+
type=RichTextType.MENTION,
|
163
|
+
mention=MentionObject(
|
164
|
+
type=MentionType.DATABASE, database=MentionDatabaseRef(id=database_id)
|
165
|
+
),
|
166
|
+
annotations=TextAnnotations(),
|
167
|
+
)
|
168
|
+
|
169
|
+
@classmethod
|
170
|
+
def mention_link_preview(cls, url: str) -> RichTextObject:
|
171
|
+
return cls(
|
172
|
+
type=RichTextType.MENTION,
|
173
|
+
mention=MentionObject(
|
174
|
+
type=MentionType.LINK_PREVIEW, link_preview=MentionLinkPreview(url=url)
|
175
|
+
),
|
176
|
+
annotations=TextAnnotations(),
|
177
|
+
)
|
178
|
+
|
179
|
+
@classmethod
|
180
|
+
def mention_date(
|
181
|
+
cls, start: str, end: str | None = None, time_zone: str | None = None
|
182
|
+
) -> RichTextObject:
|
183
|
+
return cls(
|
184
|
+
type=RichTextType.MENTION,
|
185
|
+
mention=MentionObject(
|
186
|
+
type=MentionType.DATE,
|
187
|
+
date=MentionDate(start=start, end=end, time_zone=time_zone),
|
188
|
+
),
|
189
|
+
annotations=TextAnnotations(),
|
190
|
+
)
|
191
|
+
|
192
|
+
@classmethod
|
193
|
+
def mention_template_user(cls) -> RichTextObject:
|
194
|
+
return cls(
|
195
|
+
type=RichTextType.MENTION,
|
196
|
+
mention=MentionObject(
|
197
|
+
type=MentionType.TEMPLATE_MENTION,
|
198
|
+
template_mention=MentionTemplateMention(type=TemplateMentionType.USER),
|
199
|
+
),
|
200
|
+
annotations=TextAnnotations(),
|
201
|
+
)
|
202
|
+
|
203
|
+
@classmethod
|
204
|
+
def mention_template_date(cls) -> RichTextObject:
|
205
|
+
return cls(
|
206
|
+
type=RichTextType.MENTION,
|
207
|
+
mention=MentionObject(
|
208
|
+
type=MentionType.TEMPLATE_MENTION,
|
209
|
+
template_mention=MentionTemplateMention(type=TemplateMentionType.DATE),
|
210
|
+
),
|
211
|
+
annotations=TextAnnotations(),
|
212
|
+
)
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def equation_inline(cls, expression: str) -> RichTextObject:
|
216
|
+
return cls(
|
217
|
+
type=RichTextType.EQUATION,
|
218
|
+
equation=EquationObject(expression=expression),
|
219
|
+
annotations=TextAnnotations(),
|
220
|
+
plain_text=expression,
|
221
|
+
)
|
@@ -0,0 +1,456 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Optional, Match, List
|
3
|
+
|
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.blocks.rich_text.name_to_id_resolver import NameIdResolver
|
14
|
+
|
15
|
+
|
16
|
+
class TextInlineFormatter:
|
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."""
|
120
|
+
if not text:
|
121
|
+
return []
|
122
|
+
return await cls._split_text_into_segments(text)
|
123
|
+
|
124
|
+
@classmethod
|
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] = []
|
128
|
+
remaining = text
|
129
|
+
|
130
|
+
while 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))
|
136
|
+
break
|
137
|
+
|
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
|
186
|
+
|
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)
|
206
|
+
else:
|
207
|
+
# For non-text segments (equations, mentions, etc.), keep as-is
|
208
|
+
colored_segments.append(segment)
|
209
|
+
|
210
|
+
return colored_segments
|
211
|
+
|
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
|
219
|
+
|
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}]")
|
277
|
+
|
278
|
+
@classmethod
|
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
|
+
|
298
|
+
parts: list[str] = []
|
299
|
+
|
300
|
+
for rich_obj in rich_text:
|
301
|
+
formatted_text = await cls._convert_rich_text_to_markdown(rich_obj)
|
302
|
+
parts.append(formatted_text)
|
303
|
+
|
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)
|