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.
- notionary/__init__.py +8 -4
- notionary/base_notion_client.py +3 -1
- notionary/blocks/__init__.py +2 -91
- notionary/blocks/_bootstrap.py +263 -0
- notionary/blocks/audio/__init__.py +8 -2
- notionary/blocks/audio/audio_element.py +42 -104
- notionary/blocks/audio/audio_markdown_node.py +3 -1
- notionary/blocks/audio/audio_models.py +6 -55
- notionary/blocks/base_block_element.py +30 -0
- notionary/blocks/bookmark/__init__.py +9 -2
- notionary/blocks/bookmark/bookmark_element.py +46 -139
- notionary/blocks/bookmark/bookmark_markdown_node.py +3 -1
- 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 +40 -55
- 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 +40 -89
- notionary/blocks/callout/callout_markdown_node.py +3 -1
- notionary/blocks/callout/callout_models.py +33 -0
- notionary/blocks/child_database/__init__.py +7 -0
- notionary/blocks/child_database/child_database_models.py +19 -0
- notionary/blocks/child_page/__init__.py +9 -0
- notionary/blocks/child_page/child_page_models.py +12 -0
- notionary/blocks/{shared/block_client.py → client.py} +55 -54
- notionary/blocks/code/__init__.py +6 -2
- notionary/blocks/code/code_element.py +53 -187
- notionary/blocks/code/code_markdown_node.py +13 -13
- notionary/blocks/code/code_models.py +94 -0
- notionary/blocks/column/__init__.py +25 -1
- notionary/blocks/column/column_element.py +40 -314
- notionary/blocks/column/column_list_element.py +37 -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 +26 -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 +47 -114
- 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 +80 -0
- notionary/blocks/equation/equation_element_markdown_node.py +36 -0
- notionary/blocks/equation/equation_models.py +11 -0
- notionary/blocks/file/__init__.py +25 -0
- notionary/blocks/file/file_element.py +93 -0
- notionary/blocks/file/file_element_markdown_node.py +35 -0
- notionary/blocks/file/file_element_models.py +39 -0
- notionary/blocks/heading/__init__.py +16 -2
- notionary/blocks/heading/heading_element.py +67 -72
- 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 +84 -0
- notionary/blocks/{image → image_block}/image_markdown_node.py +3 -1
- notionary/blocks/image_block/image_models.py +10 -0
- notionary/blocks/models.py +172 -0
- notionary/blocks/numbered_list/__init__.py +12 -2
- notionary/blocks/numbered_list/numbered_list_element.py +33 -58
- 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 +27 -69
- 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 +91 -0
- notionary/blocks/pdf/pdf_markdown_node.py +35 -0
- notionary/blocks/pdf/pdf_models.py +11 -0
- notionary/blocks/quote/__init__.py +11 -2
- notionary/blocks/quote/quote_element.py +31 -65
- notionary/blocks/quote/quote_markdown_node.py +4 -1
- notionary/blocks/quote/quote_models.py +18 -0
- notionary/blocks/registry/__init__.py +4 -0
- notionary/blocks/registry/block_registry.py +75 -91
- notionary/blocks/registry/block_registry_builder.py +107 -59
- notionary/blocks/rich_text/__init__.py +33 -0
- notionary/blocks/rich_text/rich_text_models.py +188 -0
- notionary/blocks/rich_text/text_inline_formatter.py +125 -0
- notionary/blocks/table/__init__.py +16 -2
- notionary/blocks/table/table_element.py +48 -241
- 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 +51 -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 +38 -95
- 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 +57 -264
- notionary/blocks/toggle/toggle_markdown_node.py +24 -14
- notionary/blocks/toggle/toggle_models.py +17 -0
- notionary/blocks/toggleable_heading/__init__.py +6 -2
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +74 -244
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
- notionary/blocks/types.py +61 -0
- notionary/blocks/video/__init__.py +8 -2
- notionary/blocks/video/video_element.py +67 -143
- notionary/blocks/video/video_element_models.py +10 -0
- notionary/blocks/video/video_markdown_node.py +3 -1
- notionary/database/client.py +3 -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 +2 -1
- notionary/file_upload/notion_file_upload.py +2 -3
- notionary/markdown/markdown_builder.py +722 -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 +9 -10
- notionary/page/models.py +327 -0
- notionary/page/notion_page.py +99 -52
- notionary/page/notion_text_length_utils.py +119 -0
- notionary/page/{content/page_content_writer.py → page_content_writer.py} +88 -38
- notionary/page/reader/handler/__init__.py +17 -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 +43 -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 +60 -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 +69 -0
- notionary/page/search_filter_builder.py +2 -1
- notionary/page/writer/handler/__init__.py +22 -0
- notionary/page/writer/handler/code_handler.py +100 -0
- notionary/page/writer/handler/column_handler.py +141 -0
- notionary/page/writer/handler/column_list_handler.py +139 -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 +92 -0
- notionary/page/writer/handler/table_handler.py +130 -0
- notionary/page/writer/handler/toggle_handler.py +153 -0
- notionary/page/writer/handler/toggleable_heading_handler.py +167 -0
- notionary/page/writer/markdown_to_notion_converter.py +76 -0
- notionary/telemetry/__init__.py +2 -2
- notionary/telemetry/service.py +4 -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 +3 -2
- notionary/user/notion_user_provider.py +1 -1
- 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 +3 -2
- {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/METADATA +12 -8
- notionary-0.2.21.dist-info/RECORD +185 -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/__init__.py +0 -0
- notionary/blocks/shared/models.py +0 -710
- 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/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- 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/notion_text_length_utils.py +0 -87
- notionary/page/content/page_content_retriever.py +0 -52
- 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-0.2.18.dist-info/RECORD +0 -149
- /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/page/{content/markdown_whitespace_processor.py → markdown_whitespace_processor.py} +0 -0
- /notionary/{blocks/mention/mention_models.py → page/reader/handler/context.py} +0 -0
- {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/LICENSE +0 -0
- {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
|
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
|
4
|
+
from typing import Optional
|
3
5
|
|
4
|
-
from notionary.blocks import
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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(
|
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
|
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.
|
31
|
+
return block.type == BlockType.TABLE and block.table
|
54
32
|
|
55
33
|
@classmethod
|
56
|
-
def markdown_to_notion(cls, text: str) ->
|
57
|
-
"""Convert
|
58
|
-
if not
|
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
|
-
|
75
|
-
|
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
|
-
#
|
88
|
-
|
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
|
51
|
+
return CreateTableBlock(table=table_block)
|
91
52
|
|
92
53
|
@classmethod
|
93
|
-
def notion_to_markdown(cls, block:
|
54
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
94
55
|
"""Convert Notion table block to markdown table."""
|
95
|
-
if block.
|
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.
|
99
|
-
children = block.
|
62
|
+
table_data = block.table
|
63
|
+
children = block.children or []
|
100
64
|
|
101
65
|
if not children:
|
102
|
-
table_width = table_data.
|
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.
|
83
|
+
if child.type != "table_row":
|
84
|
+
continue
|
85
|
+
|
86
|
+
if not child.table_row:
|
122
87
|
continue
|
123
88
|
|
124
|
-
row_data = child.
|
125
|
-
cells = row_data.
|
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.
|
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
|
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
|
213
|
-
"""
|
214
|
-
|
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()))
|