notionary 0.2.18__py3-none-any.whl → 0.2.21__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 (204) 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 +263 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +42 -104
  7. notionary/blocks/audio/audio_markdown_node.py +3 -1
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +30 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +46 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +3 -1
  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 +40 -55
  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 +40 -89
  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 +7 -0
  27. notionary/blocks/child_database/child_database_models.py +19 -0
  28. notionary/blocks/child_page/__init__.py +9 -0
  29. notionary/blocks/child_page/child_page_models.py +12 -0
  30. notionary/blocks/{shared/block_client.py → client.py} +55 -54
  31. notionary/blocks/code/__init__.py +6 -2
  32. notionary/blocks/code/code_element.py +53 -187
  33. notionary/blocks/code/code_markdown_node.py +13 -13
  34. notionary/blocks/code/code_models.py +94 -0
  35. notionary/blocks/column/__init__.py +25 -1
  36. notionary/blocks/column/column_element.py +40 -314
  37. notionary/blocks/column/column_list_element.py +37 -0
  38. notionary/blocks/column/column_list_markdown_node.py +50 -0
  39. notionary/blocks/column/column_markdown_node.py +59 -0
  40. notionary/blocks/column/column_models.py +26 -0
  41. notionary/blocks/divider/__init__.py +9 -2
  42. notionary/blocks/divider/divider_element.py +26 -49
  43. notionary/blocks/divider/divider_markdown_node.py +2 -1
  44. notionary/blocks/divider/divider_models.py +12 -0
  45. notionary/blocks/embed/__init__.py +9 -2
  46. notionary/blocks/embed/embed_element.py +47 -114
  47. notionary/blocks/embed/embed_markdown_node.py +3 -1
  48. notionary/blocks/embed/embed_models.py +14 -0
  49. notionary/blocks/equation/__init__.py +14 -0
  50. notionary/blocks/equation/equation_element.py +80 -0
  51. notionary/blocks/equation/equation_element_markdown_node.py +36 -0
  52. notionary/blocks/equation/equation_models.py +11 -0
  53. notionary/blocks/file/__init__.py +25 -0
  54. notionary/blocks/file/file_element.py +93 -0
  55. notionary/blocks/file/file_element_markdown_node.py +35 -0
  56. notionary/blocks/file/file_element_models.py +39 -0
  57. notionary/blocks/heading/__init__.py +16 -2
  58. notionary/blocks/heading/heading_element.py +67 -72
  59. notionary/blocks/heading/heading_markdown_node.py +2 -1
  60. notionary/blocks/heading/heading_models.py +29 -0
  61. notionary/blocks/image_block/__init__.py +13 -0
  62. notionary/blocks/image_block/image_element.py +84 -0
  63. notionary/blocks/{image → image_block}/image_markdown_node.py +3 -1
  64. notionary/blocks/image_block/image_models.py +10 -0
  65. notionary/blocks/models.py +172 -0
  66. notionary/blocks/numbered_list/__init__.py +12 -2
  67. notionary/blocks/numbered_list/numbered_list_element.py +33 -58
  68. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  69. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  70. notionary/blocks/paragraph/__init__.py +12 -2
  71. notionary/blocks/paragraph/paragraph_element.py +27 -69
  72. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  73. notionary/blocks/paragraph/paragraph_models.py +16 -0
  74. notionary/blocks/pdf/__init__.py +13 -0
  75. notionary/blocks/pdf/pdf_element.py +91 -0
  76. notionary/blocks/pdf/pdf_markdown_node.py +35 -0
  77. notionary/blocks/pdf/pdf_models.py +11 -0
  78. notionary/blocks/quote/__init__.py +11 -2
  79. notionary/blocks/quote/quote_element.py +31 -65
  80. notionary/blocks/quote/quote_markdown_node.py +4 -1
  81. notionary/blocks/quote/quote_models.py +18 -0
  82. notionary/blocks/registry/__init__.py +4 -0
  83. notionary/blocks/registry/block_registry.py +75 -91
  84. notionary/blocks/registry/block_registry_builder.py +107 -59
  85. notionary/blocks/rich_text/__init__.py +33 -0
  86. notionary/blocks/rich_text/rich_text_models.py +188 -0
  87. notionary/blocks/rich_text/text_inline_formatter.py +125 -0
  88. notionary/blocks/table/__init__.py +16 -2
  89. notionary/blocks/table/table_element.py +48 -241
  90. notionary/blocks/table/table_markdown_node.py +2 -1
  91. notionary/blocks/table/table_models.py +28 -0
  92. notionary/blocks/table_of_contents/__init__.py +19 -0
  93. notionary/blocks/table_of_contents/table_of_contents_element.py +51 -0
  94. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  95. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  96. notionary/blocks/todo/__init__.py +9 -2
  97. notionary/blocks/todo/todo_element.py +38 -95
  98. notionary/blocks/todo/todo_markdown_node.py +2 -1
  99. notionary/blocks/todo/todo_models.py +19 -0
  100. notionary/blocks/toggle/__init__.py +13 -3
  101. notionary/blocks/toggle/toggle_element.py +57 -264
  102. notionary/blocks/toggle/toggle_markdown_node.py +24 -14
  103. notionary/blocks/toggle/toggle_models.py +17 -0
  104. notionary/blocks/toggleable_heading/__init__.py +6 -2
  105. notionary/blocks/toggleable_heading/toggleable_heading_element.py +74 -244
  106. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  107. notionary/blocks/types.py +61 -0
  108. notionary/blocks/video/__init__.py +8 -2
  109. notionary/blocks/video/video_element.py +67 -143
  110. notionary/blocks/video/video_element_models.py +10 -0
  111. notionary/blocks/video/video_markdown_node.py +3 -1
  112. notionary/database/client.py +3 -8
  113. notionary/database/database.py +13 -14
  114. notionary/database/database_filter_builder.py +2 -2
  115. notionary/database/database_provider.py +5 -4
  116. notionary/database/models.py +337 -0
  117. notionary/database/notion_database.py +6 -7
  118. notionary/file_upload/client.py +5 -7
  119. notionary/file_upload/models.py +2 -1
  120. notionary/file_upload/notion_file_upload.py +2 -3
  121. notionary/markdown/markdown_builder.py +722 -0
  122. notionary/markdown/markdown_document_model.py +228 -0
  123. notionary/{blocks → markdown}/markdown_node.py +1 -0
  124. notionary/models/notion_database_response.py +0 -338
  125. notionary/page/client.py +9 -10
  126. notionary/page/models.py +327 -0
  127. notionary/page/notion_page.py +99 -52
  128. notionary/page/notion_text_length_utils.py +119 -0
  129. notionary/page/{content/page_content_writer.py → page_content_writer.py} +88 -38
  130. notionary/page/reader/handler/__init__.py +17 -0
  131. notionary/page/reader/handler/base_block_renderer.py +44 -0
  132. notionary/page/reader/handler/block_processing_context.py +35 -0
  133. notionary/page/reader/handler/block_rendering_context.py +43 -0
  134. notionary/page/reader/handler/column_list_renderer.py +51 -0
  135. notionary/page/reader/handler/column_renderer.py +60 -0
  136. notionary/page/reader/handler/line_renderer.py +60 -0
  137. notionary/page/reader/handler/toggle_renderer.py +69 -0
  138. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  139. notionary/page/reader/page_content_retriever.py +69 -0
  140. notionary/page/search_filter_builder.py +2 -1
  141. notionary/page/writer/handler/__init__.py +22 -0
  142. notionary/page/writer/handler/code_handler.py +100 -0
  143. notionary/page/writer/handler/column_handler.py +141 -0
  144. notionary/page/writer/handler/column_list_handler.py +139 -0
  145. notionary/page/writer/handler/line_handler.py +35 -0
  146. notionary/page/writer/handler/line_processing_context.py +54 -0
  147. notionary/page/writer/handler/regular_line_handler.py +92 -0
  148. notionary/page/writer/handler/table_handler.py +130 -0
  149. notionary/page/writer/handler/toggle_handler.py +153 -0
  150. notionary/page/writer/handler/toggleable_heading_handler.py +167 -0
  151. notionary/page/writer/markdown_to_notion_converter.py +76 -0
  152. notionary/telemetry/__init__.py +2 -2
  153. notionary/telemetry/service.py +4 -3
  154. notionary/user/__init__.py +2 -2
  155. notionary/user/base_notion_user.py +2 -1
  156. notionary/user/client.py +2 -3
  157. notionary/user/models.py +1 -0
  158. notionary/user/notion_bot_user.py +4 -5
  159. notionary/user/notion_user.py +3 -4
  160. notionary/user/notion_user_manager.py +3 -2
  161. notionary/user/notion_user_provider.py +1 -1
  162. notionary/util/__init__.py +3 -2
  163. notionary/util/fuzzy.py +2 -1
  164. notionary/util/logging_mixin.py +2 -2
  165. notionary/util/singleton_metaclass.py +1 -1
  166. notionary/workspace.py +3 -2
  167. {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/METADATA +12 -8
  168. notionary-0.2.21.dist-info/RECORD +185 -0
  169. notionary/blocks/document/__init__.py +0 -7
  170. notionary/blocks/document/document_element.py +0 -102
  171. notionary/blocks/document/document_markdown_node.py +0 -31
  172. notionary/blocks/image/__init__.py +0 -7
  173. notionary/blocks/image/image_element.py +0 -151
  174. notionary/blocks/markdown_builder.py +0 -356
  175. notionary/blocks/mention/__init__.py +0 -7
  176. notionary/blocks/mention/mention_element.py +0 -229
  177. notionary/blocks/mention/mention_markdown_node.py +0 -38
  178. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  179. notionary/blocks/prompts/element_prompt_content.py +0 -41
  180. notionary/blocks/shared/__init__.py +0 -0
  181. notionary/blocks/shared/models.py +0 -710
  182. notionary/blocks/shared/notion_block_element.py +0 -37
  183. notionary/blocks/shared/text_inline_formatter.py +0 -262
  184. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  185. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  186. notionary/database/models/page_result.py +0 -10
  187. notionary/models/notion_block_response.py +0 -264
  188. notionary/models/notion_page_response.py +0 -78
  189. notionary/models/search_response.py +0 -0
  190. notionary/page/__init__.py +0 -0
  191. notionary/page/content/notion_text_length_utils.py +0 -87
  192. notionary/page/content/page_content_retriever.py +0 -52
  193. notionary/page/formatting/line_processor.py +0 -153
  194. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  195. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  196. notionary/page/notion_to_markdown_converter.py +0 -179
  197. notionary/page/properites/property_value_extractor.py +0 -0
  198. notionary-0.2.18.dist-info/RECORD +0 -149
  199. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  200. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  201. /notionary/page/{content/markdown_whitespace_processor.py → markdown_whitespace_processor.py} +0 -0
  202. /notionary/{blocks/mention/mention_models.py → page/reader/handler/context.py} +0 -0
  203. {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/LICENSE +0 -0
  204. {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/WHEEL +0 -0
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+ from typing_extensions import Literal
7
+
8
+
9
+ class TextAnnotations(BaseModel):
10
+ bold: bool = False
11
+ italic: bool = False
12
+ strikethrough: bool = False
13
+ underline: bool = False
14
+ code: bool = False
15
+ color: str = "default"
16
+
17
+
18
+ class LinkObject(BaseModel):
19
+ url: str
20
+
21
+
22
+ class TextContent(BaseModel):
23
+ content: str
24
+ link: Optional[LinkObject] = None
25
+
26
+
27
+ class EquationObject(BaseModel):
28
+ expression: str
29
+
30
+
31
+ class MentionUserRef(BaseModel):
32
+ id: str # Notion user id
33
+
34
+
35
+ class MentionPageRef(BaseModel):
36
+ id: str
37
+
38
+
39
+ class MentionDatabaseRef(BaseModel):
40
+ id: str
41
+
42
+
43
+ class MentionLinkPreview(BaseModel):
44
+ url: str
45
+
46
+
47
+ class MentionDate(BaseModel):
48
+ # entspricht Notion date object (start Pflicht, end/time_zone optional)
49
+ start: str # ISO 8601 date or datetime
50
+ end: Optional[str] = None
51
+ time_zone: Optional[str] = None
52
+
53
+
54
+ class MentionTemplateMention(BaseModel):
55
+ # Notion hat zwei Template-Mention-Typen
56
+ type: Literal["template_mention_user", "template_mention_date"]
57
+
58
+
59
+ class MentionObject(BaseModel):
60
+ type: Literal[
61
+ "user", "page", "database", "date", "link_preview", "template_mention"
62
+ ]
63
+ user: Optional[MentionUserRef] = None
64
+ page: Optional[MentionPageRef] = None
65
+ database: Optional[MentionDatabaseRef] = None
66
+ date: Optional[MentionDate] = None
67
+ link_preview: Optional[MentionLinkPreview] = None
68
+ template_mention: Optional[MentionTemplateMention] = None
69
+
70
+
71
+ class RichTextObject(BaseModel):
72
+ type: Literal["text", "mention", "equation"] = "text"
73
+
74
+ text: Optional[TextContent] = None
75
+ annotations: Optional[TextAnnotations] = None
76
+ plain_text: str = ""
77
+ href: Optional[str] = None
78
+
79
+ mention: Optional[MentionObject] = None
80
+
81
+ equation: Optional[EquationObject] = None
82
+
83
+ @classmethod
84
+ def from_plain_text(cls, content: str, **ann) -> RichTextObject:
85
+ return cls(
86
+ type="text",
87
+ text=TextContent(content=content),
88
+ annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
89
+ plain_text=content,
90
+ )
91
+
92
+ @classmethod
93
+ def for_code_block(cls, content: str) -> RichTextObject:
94
+ # keine annotations setzen → Notion Code-Highlight bleibt an
95
+ return cls(
96
+ type="text",
97
+ text=TextContent(content=content),
98
+ annotations=None,
99
+ plain_text=content,
100
+ )
101
+
102
+ @classmethod
103
+ def for_link(cls, content: str, url: str, **ann) -> RichTextObject:
104
+ return cls(
105
+ type="text",
106
+ text=TextContent(content=content, link=LinkObject(url=url)),
107
+ annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
108
+ plain_text=content,
109
+ )
110
+
111
+ @classmethod
112
+ def mention_user(cls, user_id: str) -> RichTextObject:
113
+ return cls(
114
+ type="mention",
115
+ mention=MentionObject(type="user", user=MentionUserRef(id=user_id)),
116
+ annotations=TextAnnotations(),
117
+ )
118
+
119
+ @classmethod
120
+ def mention_page(cls, page_id: str) -> RichTextObject:
121
+ return cls(
122
+ type="mention",
123
+ mention=MentionObject(type="page", page=MentionPageRef(id=page_id)),
124
+ annotations=TextAnnotations(),
125
+ )
126
+
127
+ @classmethod
128
+ def mention_database(cls, database_id: str) -> RichTextObject:
129
+ return cls(
130
+ type="mention",
131
+ mention=MentionObject(
132
+ type="database", database=MentionDatabaseRef(id=database_id)
133
+ ),
134
+ annotations=TextAnnotations(),
135
+ )
136
+
137
+ @classmethod
138
+ def mention_link_preview(cls, url: str) -> RichTextObject:
139
+ return cls(
140
+ type="mention",
141
+ mention=MentionObject(
142
+ type="link_preview", link_preview=MentionLinkPreview(url=url)
143
+ ),
144
+ annotations=TextAnnotations(),
145
+ )
146
+
147
+ @classmethod
148
+ def mention_date(
149
+ cls, start: str, end: str | None = None, time_zone: str | None = None
150
+ ) -> RichTextObject:
151
+ return cls(
152
+ type="mention",
153
+ mention=MentionObject(
154
+ type="date", date=MentionDate(start=start, end=end, time_zone=time_zone)
155
+ ),
156
+ annotations=TextAnnotations(),
157
+ )
158
+
159
+ @classmethod
160
+ def mention_template_user(cls) -> RichTextObject:
161
+ return cls(
162
+ type="mention",
163
+ mention=MentionObject(
164
+ type="template_mention",
165
+ template_mention=MentionTemplateMention(type="template_mention_user"),
166
+ ),
167
+ annotations=TextAnnotations(),
168
+ )
169
+
170
+ @classmethod
171
+ def mention_template_date(cls) -> RichTextObject:
172
+ return cls(
173
+ type="mention",
174
+ mention=MentionObject(
175
+ type="template_mention",
176
+ template_mention=MentionTemplateMention(type="template_mention_date"),
177
+ ),
178
+ annotations=TextAnnotations(),
179
+ )
180
+
181
+ @classmethod
182
+ def equation_inline(cls, expression: str) -> RichTextObject:
183
+ return cls(
184
+ type="equation",
185
+ equation=EquationObject(expression=expression),
186
+ annotations=TextAnnotations(),
187
+ plain_text=expression, # Notion liefert plain_text serverseitig; für Roundtrip hilfreich
188
+ )
@@ -0,0 +1,125 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
5
+
6
+
7
+ class TextInlineFormatter:
8
+ FORMAT_PATTERNS: list[tuple[str, dict[str, Any]]] = [
9
+ (r"\*\*(.+?)\*\*", {"bold": True}),
10
+ (r"\*(.+?)\*", {"italic": True}),
11
+ (r"_(.+?)_", {"italic": True}),
12
+ (r"__(.+?)__", {"underline": True}),
13
+ (r"~~(.+?)~~", {"strikethrough": True}),
14
+ (r"`(.+?)`", {"code": True}),
15
+ (r"\[(.+?)\]\((.+?)\)", {"link": True}),
16
+ (r"@\[([0-9a-f-]+)\]", {"mention_page": True}), # weiterhin deine Kurzsyntax
17
+ ]
18
+
19
+ @classmethod
20
+ def parse_inline_formatting(cls, text: str) -> list[RichTextObject]:
21
+ if not text:
22
+ return []
23
+ return cls._split_text_into_segments(text, cls.FORMAT_PATTERNS)
24
+
25
+ @classmethod
26
+ def _split_text_into_segments(
27
+ cls, text: str, patterns: list[tuple[str, dict[str, Any]]]
28
+ ) -> list[RichTextObject]:
29
+ segs: list[RichTextObject] = []
30
+ remaining = text
31
+
32
+ while remaining:
33
+ match, fmt, pos = None, None, len(remaining)
34
+ for pattern, f in patterns:
35
+ m = re.search(pattern, remaining)
36
+ if m and m.start() < pos:
37
+ match, fmt, pos = m, f, m.start()
38
+
39
+ if not match:
40
+ segs.append(RichTextObject.from_plain_text(remaining))
41
+ break
42
+
43
+ if pos > 0:
44
+ segs.append(RichTextObject.from_plain_text(remaining[:pos]))
45
+
46
+ if "link" in fmt:
47
+ segs.append(RichTextObject.for_link(match.group(1), match.group(2)))
48
+ elif "mention_page" in fmt:
49
+ segs.append(RichTextObject.mention_page(match.group(1)))
50
+ elif "code" in fmt:
51
+ segs.append(RichTextObject.from_plain_text(match.group(1), code=True))
52
+ else:
53
+ segs.append(RichTextObject.from_plain_text(match.group(1), **fmt))
54
+
55
+ remaining = remaining[pos + len(match.group(0)) :]
56
+
57
+ return segs
58
+
59
+ @classmethod
60
+ def extract_text_with_formatting(cls, rich_text: list[RichTextObject]) -> str:
61
+ """
62
+ Convert a list of RichTextObjects back into Markdown inline syntax.
63
+ """
64
+ parts: list[str] = []
65
+
66
+ for obj in rich_text:
67
+ # Basisinhalt
68
+ content = obj.plain_text or (obj.text.content if obj.text else "")
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)
124
+
125
+ return "".join(parts)
@@ -1,7 +1,21 @@
1
- from .table_element import TableElement
2
- from .table_markdown_node import TableMarkdownNode
1
+ from notionary.blocks.table.table_element import TableElement
2
+ from notionary.blocks.table.table_markdown_node import (
3
+ TableMarkdownBlockParams,
4
+ TableMarkdownNode,
5
+ )
6
+ from notionary.blocks.table.table_models import (
7
+ CreateTableBlock,
8
+ CreateTableRowBlock,
9
+ TableBlock,
10
+ TableRowBlock,
11
+ )
3
12
 
4
13
  __all__ = [
5
14
  "TableElement",
15
+ "TableBlock",
16
+ "TableRowBlock",
17
+ "CreateTableRowBlock",
18
+ "CreateTableBlock",
6
19
  "TableMarkdownNode",
20
+ "TableMarkdownBlockParams",
7
21
  ]
@@ -1,106 +1,69 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional, List, Tuple
4
+ from typing import Optional
3
5
 
4
- from notionary.blocks import (
5
- NotionBlockElement,
6
- NotionBlockResult,
7
- ElementPromptContent,
8
- ElementPromptBuilder,
9
- )
10
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.models import Block, BlockCreateResult
8
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
9
+ from notionary.blocks.table.table_models import CreateTableBlock, TableBlock
10
+ from notionary.blocks.types import BlockType
11
11
 
12
12
 
13
- class TableElement(NotionBlockElement):
13
+ class TableElement(BaseBlockElement):
14
14
  """
15
15
  Handles conversion between Markdown tables and Notion table blocks.
16
+ Now integrated into the LineProcessor stack system.
16
17
 
17
18
  Markdown table syntax:
18
19
  | Header 1 | Header 2 | Header 3 |
19
- | -------- | -------- | -------- |
20
- | Cell 1 | Cell 2 | Cell 3 |
21
- | Cell 4 | Cell 5 | Cell 6 |
22
-
23
- The second line with dashes and optional colons defines column alignment.
20
+ [table rows as child lines]
24
21
  """
25
22
 
23
+ # Pattern für Table-Zeilen (jede Zeile die mit | startet und endet)
26
24
  ROW_PATTERN = re.compile(r"^\s*\|(.+)\|\s*$")
25
+ # Pattern für Separator-Zeilen
27
26
  SEPARATOR_PATTERN = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
28
27
 
29
28
  @classmethod
30
- def match_markdown(cls, text: str) -> bool:
31
- """
32
- Check if text contains a markdown table.
33
- Accepts tables with only header + separator, as well as header + separator + data rows.
34
- """
35
- lines = text.split("\n")
36
-
37
- if len(lines) < 2:
38
- return False
39
-
40
- # Akzeptiere Header + Separator auch ohne Datenzeile
41
- for i, line in enumerate(lines[:-1]):
42
- if (
43
- cls.ROW_PATTERN.match(line)
44
- and cls.SEPARATOR_PATTERN.match(lines[i + 1])
45
- ):
46
- return True
47
-
48
- return False
49
-
50
- @classmethod
51
- def match_notion(cls, block: Dict[str, Any]) -> bool:
29
+ def match_notion(cls, block: Block) -> bool:
52
30
  """Check if block is a Notion table."""
53
- return block.get("type") == "table"
31
+ return block.type == BlockType.TABLE and block.table
54
32
 
55
33
  @classmethod
56
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
57
- """Convert markdown table to Notion table block."""
58
- if not TableElement.match_markdown(text):
59
- return None
60
-
61
- lines = text.split("\n")
62
-
63
- table_start = TableElement._find_table_start(lines)
64
- if table_start is None:
65
- return None
66
-
67
- table_end = TableElement._find_table_end(lines, table_start)
68
- table_lines = lines[table_start:table_end]
69
-
70
- rows = TableElement._extract_table_rows(table_lines)
71
- if not rows:
34
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
35
+ """Convert opening table row to Notion table block."""
36
+ if not cls.ROW_PATTERN.match(text.strip()):
72
37
  return None
73
38
 
74
- column_count = len(rows[0])
75
- TableElement._normalize_row_lengths(rows, column_count)
76
-
77
- table_block = {
78
- "type": "table",
79
- "table": {
80
- "table_width": column_count,
81
- "has_column_header": True,
82
- "has_row_header": False,
83
- "children": TableElement._create_table_rows(rows),
84
- },
85
- }
39
+ # Parse the header row to determine column count
40
+ header_cells = cls._parse_table_row(text)
41
+ col_count = len(header_cells)
86
42
 
87
- # Leerer Paragraph nach der Tabelle
88
- empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
43
+ # Create empty TableBlock - content will be added by stack processor
44
+ table_block = TableBlock(
45
+ table_width=col_count,
46
+ has_column_header=True,
47
+ has_row_header=False,
48
+ children=[], # Will be populated by stack processor
49
+ )
89
50
 
90
- return [table_block, empty_paragraph]
51
+ return CreateTableBlock(table=table_block)
91
52
 
92
53
  @classmethod
93
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
54
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
94
55
  """Convert Notion table block to markdown table."""
95
- if block.get("type") != "table":
56
+ if block.type != BlockType.TABLE:
57
+ return None
58
+
59
+ if not block.table:
96
60
  return None
97
61
 
98
- table_data = block.get("table", {})
99
- children = block.get("children", [])
62
+ table_data = block.table
63
+ children = block.children or []
100
64
 
101
65
  if not children:
102
- table_width = table_data.get("table_width", 3)
103
-
66
+ table_width = table_data.table_width or 3
104
67
  header = (
105
68
  "| " + " | ".join([f"Column {i+1}" for i in range(table_width)]) + " |"
106
69
  )
@@ -110,7 +73,6 @@ class TableElement(NotionBlockElement):
110
73
  data_row = (
111
74
  "| " + " | ".join([" " for _ in range(table_width)]) + " |"
112
75
  )
113
-
114
76
  table_rows = [header, separator, data_row]
115
77
  return "\n".join(table_rows)
116
78
 
@@ -118,11 +80,14 @@ class TableElement(NotionBlockElement):
118
80
  header_processed = False
119
81
 
120
82
  for child in children:
121
- if child.get("type") != "table_row":
83
+ if child.type != "table_row":
84
+ continue
85
+
86
+ if not child.table_row:
122
87
  continue
123
88
 
124
- row_data = child.get("table_row", {})
125
- cells = row_data.get("cells", [])
89
+ row_data = child.table_row
90
+ cells = row_data.cells or []
126
91
 
127
92
  row_cells = []
128
93
  for cell in cells:
@@ -132,72 +97,17 @@ class TableElement(NotionBlockElement):
132
97
  row = "| " + " | ".join(row_cells) + " |"
133
98
  table_rows.append(row)
134
99
 
135
- if not header_processed and table_data.get("has_column_header", True):
100
+ if not header_processed and table_data.has_column_header:
136
101
  header_processed = True
137
102
  separator = (
138
103
  "| " + " | ".join(["--------" for _ in range(len(cells))]) + " |"
139
104
  )
140
105
  table_rows.append(separator)
141
106
 
142
- if not table_rows:
143
- return None
144
-
145
- if len(table_rows) == 1 and table_data.get("has_column_header", True):
146
- cells_count = len(children[0].get("table_row", {}).get("cells", []))
147
- separator = (
148
- "| " + " | ".join(["--------" for _ in range(cells_count)]) + " |"
149
- )
150
- table_rows.insert(1, separator)
151
-
152
107
  return "\n".join(table_rows)
153
108
 
154
109
  @classmethod
155
- def is_multiline(cls) -> bool:
156
- """Indicates if this element handles content that spans multiple lines."""
157
- return True
158
-
159
- @classmethod
160
- def _find_table_start(cls, lines: List[str]) -> Optional[int]:
161
- """Find the start index of a table in the lines."""
162
- for i in range(len(lines) - 2):
163
- if (
164
- TableElement.ROW_PATTERN.match(lines[i])
165
- and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
166
- and TableElement.ROW_PATTERN.match(lines[i + 2])
167
- ):
168
- return i
169
- return None
170
-
171
- @classmethod
172
- def _find_table_end(cls, lines: List[str], start_idx: int) -> int:
173
- """Find the end index of a table, starting from start_idx."""
174
- end_idx = start_idx + 3 # Minimum: Header, Separator, one data row
175
- while end_idx < len(lines) and TableElement.ROW_PATTERN.match(lines[end_idx]):
176
- end_idx += 1
177
- return end_idx
178
-
179
- @classmethod
180
- def _extract_table_rows(cls, table_lines: List[str]) -> List[List[str]]:
181
- """Extract row contents from table lines, excluding separator line."""
182
- rows = []
183
- for i, line in enumerate(table_lines):
184
- if i != 1 and TableElement.ROW_PATTERN.match(line): # Skip separator line
185
- cells = TableElement._parse_table_row(line)
186
- if cells:
187
- rows.append(cells)
188
- return rows
189
-
190
- @classmethod
191
- def _normalize_row_lengths(cls, rows: List[List[str]], column_count: int) -> None:
192
- """Normalize row lengths to the specified column count."""
193
- for row in rows:
194
- if len(row) < column_count:
195
- row.extend([""] * (column_count - len(row)))
196
- elif len(row) > column_count:
197
- del row[column_count:]
198
-
199
- @classmethod
200
- def _parse_table_row(cls, row_text: str) -> List[str]:
110
+ def _parse_table_row(cls, row_text: str) -> list[str]:
201
111
  """Convert table row text to cell contents."""
202
112
  row_content = row_text.strip()
203
113
 
@@ -209,109 +119,6 @@ class TableElement(NotionBlockElement):
209
119
  return [cell.strip() for cell in row_content.split("|")]
210
120
 
211
121
  @classmethod
212
- def _create_table_rows(cls, rows: List[List[str]]) -> List[Dict[str, Any]]:
213
- """Create Notion table rows from cell contents."""
214
- table_rows = []
215
-
216
- for row in rows:
217
- cells_data = []
218
-
219
- for cell_content in row:
220
- rich_text = TextInlineFormatter.parse_inline_formatting(cell_content)
221
-
222
- if not rich_text:
223
- rich_text = [
224
- {
225
- "type": "text",
226
- "text": {"content": ""},
227
- "annotations": {
228
- "bold": False,
229
- "italic": False,
230
- "strikethrough": False,
231
- "underline": False,
232
- "code": False,
233
- "color": "default",
234
- },
235
- "plain_text": "",
236
- "href": None,
237
- }
238
- ]
239
-
240
- cells_data.append(rich_text)
241
-
242
- table_rows.append({"type": "table_row", "table_row": {"cells": cells_data}})
243
-
244
- return table_rows
245
-
246
- @classmethod
247
- def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
248
- """
249
- Find all tables in the text and return their positions.
250
-
251
- Args:
252
- text: The text to search in
253
-
254
- Returns:
255
- List of tuples with (start_pos, end_pos, block)
256
- """
257
- matches = []
258
- lines = text.split("\n")
259
-
260
- i = 0
261
- while i < len(lines) - 2:
262
- if (
263
- TableElement.ROW_PATTERN.match(lines[i])
264
- and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
265
- and TableElement.ROW_PATTERN.match(lines[i + 2])
266
- ):
267
-
268
- start_line = i
269
- end_line = TableElement._find_table_end(lines, start_line)
270
-
271
- start_pos = TableElement._calculate_position(lines, 0, start_line)
272
- end_pos = start_pos + TableElement._calculate_position(
273
- lines, start_line, end_line
274
- )
275
-
276
- table_text = "\n".join(lines[start_line:end_line])
277
- table_block = TableElement.markdown_to_notion(table_text)
278
-
279
- if table_block:
280
- matches.append((start_pos, end_pos, table_block))
281
-
282
- i = end_line
283
- else:
284
- i += 1
285
-
286
- return matches
287
-
288
- @classmethod
289
- def _calculate_position(cls, lines: List[str], start: int, end: int) -> int:
290
- """Calculate the text position in characters from line start to end."""
291
- position = 0
292
- for i in range(start, end):
293
- position += len(lines[i]) + 1 # +1 for newline
294
- return position
295
-
296
- @classmethod
297
- def get_llm_prompt_content(cls) -> ElementPromptContent:
298
- """Returns information for LLM prompts about this element."""
299
- return (
300
- ElementPromptBuilder()
301
- .with_description(
302
- "Creates formatted tables with rows and columns for structured data."
303
- )
304
- .with_usage_guidelines(
305
- "Use tables to organize and present structured data in a grid format, making information easier to compare and analyze. Tables are ideal for data sets, comparison charts, pricing information, or any content that benefits from columnar organization."
306
- )
307
- .with_syntax(
308
- "| Header 1 | Header 2 | Header 3 |\n| -------- | -------- | -------- |\n| Cell 1 | Cell 2 | Cell 3 |"
309
- )
310
- .with_examples(
311
- [
312
- "| Product | Price | Stock |\n| ------- | ----- | ----- |\n| Widget A | $10.99 | 42 |\n| Widget B | $14.99 | 27 |",
313
- "| Name | Role | Department |\n| ---- | ---- | ---------- |\n| John Smith | Manager | Marketing |\n| Jane Doe | Director | Sales |",
314
- ]
315
- )
316
- .build()
317
- )
122
+ def is_table_row(cls, line: str) -> bool:
123
+ """Check if a line is a valid table row."""
124
+ return bool(cls.ROW_PATTERN.match(line.strip()))