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.
Files changed (220) hide show
  1. notionary/__init__.py +8 -4
  2. notionary/base_notion_client.py +3 -1
  3. notionary/blocks/__init__.py +2 -91
  4. notionary/blocks/_bootstrap.py +271 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +69 -106
  7. notionary/blocks/audio/audio_markdown_node.py +13 -5
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +42 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +49 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
  13. notionary/blocks/bookmark/bookmark_models.py +15 -0
  14. notionary/blocks/breadcrumbs/__init__.py +17 -0
  15. notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
  16. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
  17. notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
  18. notionary/blocks/bulleted_list/__init__.py +12 -2
  19. notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
  20. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
  21. notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
  22. notionary/blocks/callout/__init__.py +9 -2
  23. notionary/blocks/callout/callout_element.py +53 -86
  24. notionary/blocks/callout/callout_markdown_node.py +3 -1
  25. notionary/blocks/callout/callout_models.py +33 -0
  26. notionary/blocks/child_database/__init__.py +14 -0
  27. notionary/blocks/child_database/child_database_element.py +61 -0
  28. notionary/blocks/child_database/child_database_models.py +12 -0
  29. notionary/blocks/child_page/__init__.py +9 -0
  30. notionary/blocks/child_page/child_page_element.py +94 -0
  31. notionary/blocks/child_page/child_page_models.py +12 -0
  32. notionary/blocks/{shared/block_client.py → client.py} +54 -54
  33. notionary/blocks/code/__init__.py +6 -2
  34. notionary/blocks/code/code_element.py +96 -181
  35. notionary/blocks/code/code_markdown_node.py +64 -13
  36. notionary/blocks/code/code_models.py +94 -0
  37. notionary/blocks/column/__init__.py +25 -1
  38. notionary/blocks/column/column_element.py +44 -312
  39. notionary/blocks/column/column_list_element.py +52 -0
  40. notionary/blocks/column/column_list_markdown_node.py +50 -0
  41. notionary/blocks/column/column_markdown_node.py +59 -0
  42. notionary/blocks/column/column_models.py +26 -0
  43. notionary/blocks/divider/__init__.py +9 -2
  44. notionary/blocks/divider/divider_element.py +18 -49
  45. notionary/blocks/divider/divider_markdown_node.py +2 -1
  46. notionary/blocks/divider/divider_models.py +12 -0
  47. notionary/blocks/embed/__init__.py +9 -2
  48. notionary/blocks/embed/embed_element.py +65 -111
  49. notionary/blocks/embed/embed_markdown_node.py +3 -1
  50. notionary/blocks/embed/embed_models.py +14 -0
  51. notionary/blocks/equation/__init__.py +14 -0
  52. notionary/blocks/equation/equation_element.py +133 -0
  53. notionary/blocks/equation/equation_element_markdown_node.py +35 -0
  54. notionary/blocks/equation/equation_models.py +11 -0
  55. notionary/blocks/file/__init__.py +25 -0
  56. notionary/blocks/file/file_element.py +112 -0
  57. notionary/blocks/file/file_element_markdown_node.py +37 -0
  58. notionary/blocks/file/file_element_models.py +39 -0
  59. notionary/blocks/guards.py +22 -0
  60. notionary/blocks/heading/__init__.py +16 -2
  61. notionary/blocks/heading/heading_element.py +83 -69
  62. notionary/blocks/heading/heading_markdown_node.py +2 -1
  63. notionary/blocks/heading/heading_models.py +29 -0
  64. notionary/blocks/image_block/__init__.py +13 -0
  65. notionary/blocks/image_block/image_element.py +89 -0
  66. notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
  67. notionary/blocks/image_block/image_models.py +10 -0
  68. notionary/blocks/mixins/captions/__init__.py +4 -0
  69. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  70. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  71. notionary/blocks/models.py +174 -0
  72. notionary/blocks/numbered_list/__init__.py +12 -2
  73. notionary/blocks/numbered_list/numbered_list_element.py +48 -56
  74. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  75. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  76. notionary/blocks/paragraph/__init__.py +12 -2
  77. notionary/blocks/paragraph/paragraph_element.py +40 -66
  78. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  79. notionary/blocks/paragraph/paragraph_models.py +16 -0
  80. notionary/blocks/pdf/__init__.py +13 -0
  81. notionary/blocks/pdf/pdf_element.py +97 -0
  82. notionary/blocks/pdf/pdf_markdown_node.py +37 -0
  83. notionary/blocks/pdf/pdf_models.py +11 -0
  84. notionary/blocks/quote/__init__.py +11 -2
  85. notionary/blocks/quote/quote_element.py +45 -62
  86. notionary/blocks/quote/quote_markdown_node.py +6 -3
  87. notionary/blocks/quote/quote_models.py +18 -0
  88. notionary/blocks/registry/__init__.py +4 -0
  89. notionary/blocks/registry/block_registry.py +60 -121
  90. notionary/blocks/registry/block_registry_builder.py +115 -59
  91. notionary/blocks/rich_text/__init__.py +33 -0
  92. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  93. notionary/blocks/rich_text/rich_text_models.py +221 -0
  94. notionary/blocks/rich_text/text_inline_formatter.py +456 -0
  95. notionary/blocks/syntax_prompt_builder.py +137 -0
  96. notionary/blocks/table/__init__.py +16 -2
  97. notionary/blocks/table/table_element.py +136 -228
  98. notionary/blocks/table/table_markdown_node.py +2 -1
  99. notionary/blocks/table/table_models.py +28 -0
  100. notionary/blocks/table_of_contents/__init__.py +19 -0
  101. notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
  102. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  103. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  104. notionary/blocks/todo/__init__.py +9 -2
  105. notionary/blocks/todo/todo_element.py +52 -92
  106. notionary/blocks/todo/todo_markdown_node.py +2 -1
  107. notionary/blocks/todo/todo_models.py +19 -0
  108. notionary/blocks/toggle/__init__.py +13 -3
  109. notionary/blocks/toggle/toggle_element.py +69 -260
  110. notionary/blocks/toggle/toggle_markdown_node.py +25 -15
  111. notionary/blocks/toggle/toggle_models.py +17 -0
  112. notionary/blocks/toggleable_heading/__init__.py +6 -2
  113. notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
  114. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  115. notionary/blocks/types.py +130 -0
  116. notionary/blocks/video/__init__.py +8 -2
  117. notionary/blocks/video/video_element.py +70 -141
  118. notionary/blocks/video/video_element_models.py +10 -0
  119. notionary/blocks/video/video_markdown_node.py +13 -6
  120. notionary/database/client.py +26 -8
  121. notionary/database/database.py +13 -14
  122. notionary/database/database_filter_builder.py +2 -2
  123. notionary/database/database_provider.py +5 -4
  124. notionary/database/models.py +337 -0
  125. notionary/database/notion_database.py +6 -7
  126. notionary/file_upload/client.py +5 -7
  127. notionary/file_upload/models.py +3 -2
  128. notionary/file_upload/notion_file_upload.py +2 -3
  129. notionary/markdown/markdown_builder.py +729 -0
  130. notionary/markdown/markdown_document_model.py +228 -0
  131. notionary/{blocks → markdown}/markdown_node.py +1 -0
  132. notionary/models/notion_database_response.py +0 -338
  133. notionary/page/client.py +34 -15
  134. notionary/page/models.py +327 -0
  135. notionary/page/notion_page.py +136 -58
  136. notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
  137. notionary/page/page_content_writer.py +177 -0
  138. notionary/page/page_context.py +65 -0
  139. notionary/page/reader/handler/__init__.py +19 -0
  140. notionary/page/reader/handler/base_block_renderer.py +44 -0
  141. notionary/page/reader/handler/block_processing_context.py +35 -0
  142. notionary/page/reader/handler/block_rendering_context.py +48 -0
  143. notionary/page/reader/handler/column_list_renderer.py +51 -0
  144. notionary/page/reader/handler/column_renderer.py +60 -0
  145. notionary/page/reader/handler/line_renderer.py +73 -0
  146. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  147. notionary/page/reader/handler/toggle_renderer.py +69 -0
  148. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  149. notionary/page/reader/page_content_retriever.py +81 -0
  150. notionary/page/search_filter_builder.py +2 -1
  151. notionary/page/writer/handler/__init__.py +24 -0
  152. notionary/page/writer/handler/code_handler.py +72 -0
  153. notionary/page/writer/handler/column_handler.py +141 -0
  154. notionary/page/writer/handler/column_list_handler.py +139 -0
  155. notionary/page/writer/handler/equation_handler.py +74 -0
  156. notionary/page/writer/handler/line_handler.py +35 -0
  157. notionary/page/writer/handler/line_processing_context.py +54 -0
  158. notionary/page/writer/handler/regular_line_handler.py +86 -0
  159. notionary/page/writer/handler/table_handler.py +66 -0
  160. notionary/page/writer/handler/toggle_handler.py +155 -0
  161. notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
  162. notionary/page/writer/markdown_to_notion_converter.py +95 -0
  163. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  164. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  165. notionary/page/writer/notion_text_length_processor.py +150 -0
  166. notionary/telemetry/__init__.py +2 -2
  167. notionary/telemetry/service.py +3 -3
  168. notionary/user/__init__.py +2 -2
  169. notionary/user/base_notion_user.py +2 -1
  170. notionary/user/client.py +2 -3
  171. notionary/user/models.py +1 -0
  172. notionary/user/notion_bot_user.py +4 -5
  173. notionary/user/notion_user.py +3 -4
  174. notionary/user/notion_user_manager.py +23 -95
  175. notionary/util/__init__.py +3 -2
  176. notionary/util/fuzzy.py +2 -1
  177. notionary/util/logging_mixin.py +2 -2
  178. notionary/util/singleton_metaclass.py +1 -1
  179. notionary/workspace.py +6 -5
  180. notionary-0.2.22.dist-info/METADATA +237 -0
  181. notionary-0.2.22.dist-info/RECORD +200 -0
  182. notionary/blocks/document/__init__.py +0 -7
  183. notionary/blocks/document/document_element.py +0 -102
  184. notionary/blocks/document/document_markdown_node.py +0 -31
  185. notionary/blocks/image/__init__.py +0 -7
  186. notionary/blocks/image/image_element.py +0 -151
  187. notionary/blocks/markdown_builder.py +0 -356
  188. notionary/blocks/mention/__init__.py +0 -7
  189. notionary/blocks/mention/mention_element.py +0 -229
  190. notionary/blocks/mention/mention_markdown_node.py +0 -38
  191. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  192. notionary/blocks/prompts/element_prompt_content.py +0 -41
  193. notionary/blocks/shared/models.py +0 -713
  194. notionary/blocks/shared/notion_block_element.py +0 -37
  195. notionary/blocks/shared/text_inline_formatter.py +0 -262
  196. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  197. notionary/database/models/page_result.py +0 -10
  198. notionary/models/notion_block_response.py +0 -264
  199. notionary/models/notion_page_response.py +0 -78
  200. notionary/models/search_response.py +0 -0
  201. notionary/page/__init__.py +0 -0
  202. notionary/page/content/markdown_whitespace_processor.py +0 -80
  203. notionary/page/content/notion_text_length_utils.py +0 -87
  204. notionary/page/content/page_content_retriever.py +0 -60
  205. notionary/page/formatting/line_processor.py +0 -153
  206. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  207. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  208. notionary/page/notion_to_markdown_converter.py +0 -179
  209. notionary/page/properites/property_value_extractor.py +0 -0
  210. notionary/user/notion_user_provider.py +0 -1
  211. notionary-0.2.19.dist-info/METADATA +0 -225
  212. notionary-0.2.19.dist-info/RECORD +0 -150
  213. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  214. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  215. /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
  216. /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
  217. /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
  218. /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
  219. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  220. {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)