notionary 0.2.28__py3-none-any.whl → 0.3.1__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 +9 -2
- notionary/blocks/__init__.py +5 -0
- notionary/blocks/client.py +6 -4
- notionary/blocks/enums.py +28 -1
- notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
- notionary/blocks/rich_text/models.py +14 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
- notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
- notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
- notionary/blocks/rich_text/rich_text_patterns.py +3 -0
- notionary/blocks/schemas.py +42 -10
- notionary/comments/__init__.py +5 -0
- notionary/comments/client.py +7 -10
- notionary/comments/factory.py +4 -6
- notionary/data_source/http/data_source_instance_client.py +14 -4
- notionary/data_source/properties/{models.py → schemas.py} +4 -8
- notionary/data_source/query/__init__.py +9 -0
- notionary/data_source/query/builder.py +38 -10
- notionary/data_source/query/schema.py +13 -10
- notionary/data_source/query/validator.py +11 -11
- notionary/data_source/schema/registry.py +104 -0
- notionary/data_source/schema/service.py +136 -0
- notionary/data_source/schemas.py +1 -1
- notionary/data_source/service.py +29 -103
- notionary/database/service.py +17 -60
- notionary/exceptions/__init__.py +5 -1
- notionary/exceptions/block_parsing.py +21 -0
- notionary/exceptions/search.py +24 -0
- notionary/http/client.py +9 -10
- notionary/http/models.py +5 -4
- notionary/page/content/factory.py +10 -3
- notionary/page/content/markdown/builder.py +76 -154
- notionary/page/content/markdown/nodes/__init__.py +0 -2
- notionary/page/content/markdown/nodes/audio.py +1 -1
- notionary/page/content/markdown/nodes/base.py +1 -1
- notionary/page/content/markdown/nodes/bookmark.py +1 -1
- notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
- notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
- notionary/page/content/markdown/nodes/callout.py +12 -10
- notionary/page/content/markdown/nodes/code.py +3 -5
- notionary/page/content/markdown/nodes/columns.py +39 -21
- notionary/page/content/markdown/nodes/container.py +64 -0
- notionary/page/content/markdown/nodes/divider.py +1 -1
- notionary/page/content/markdown/nodes/embed.py +1 -1
- notionary/page/content/markdown/nodes/equation.py +1 -1
- notionary/page/content/markdown/nodes/file.py +1 -1
- notionary/page/content/markdown/nodes/heading.py +26 -6
- notionary/page/content/markdown/nodes/image.py +1 -1
- notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
- notionary/page/content/markdown/nodes/numbered_list.py +28 -5
- notionary/page/content/markdown/nodes/paragraph.py +1 -1
- notionary/page/content/markdown/nodes/pdf.py +1 -1
- notionary/page/content/markdown/nodes/quote.py +17 -5
- notionary/page/content/markdown/nodes/space.py +1 -1
- notionary/page/content/markdown/nodes/table.py +1 -1
- notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
- notionary/page/content/markdown/nodes/todo.py +23 -7
- notionary/page/content/markdown/nodes/toggle.py +13 -14
- notionary/page/content/markdown/nodes/video.py +1 -1
- notionary/page/content/parser/context.py +98 -21
- notionary/page/content/parser/factory.py +1 -10
- notionary/page/content/parser/parsers/__init__.py +0 -2
- notionary/page/content/parser/parsers/audio.py +1 -1
- notionary/page/content/parser/parsers/base.py +1 -1
- notionary/page/content/parser/parsers/bookmark.py +1 -1
- notionary/page/content/parser/parsers/breadcrumb.py +1 -1
- notionary/page/content/parser/parsers/bulleted_list.py +52 -8
- notionary/page/content/parser/parsers/callout.py +55 -84
- notionary/page/content/parser/parsers/caption.py +1 -1
- notionary/page/content/parser/parsers/code.py +5 -5
- notionary/page/content/parser/parsers/column.py +23 -64
- notionary/page/content/parser/parsers/column_list.py +45 -45
- notionary/page/content/parser/parsers/divider.py +1 -1
- notionary/page/content/parser/parsers/embed.py +1 -1
- notionary/page/content/parser/parsers/equation.py +1 -1
- notionary/page/content/parser/parsers/file.py +1 -1
- notionary/page/content/parser/parsers/heading.py +65 -8
- notionary/page/content/parser/parsers/image.py +1 -1
- notionary/page/content/parser/parsers/numbered_list.py +52 -8
- notionary/page/content/parser/parsers/paragraph.py +3 -2
- notionary/page/content/parser/parsers/pdf.py +1 -1
- notionary/page/content/parser/parsers/quote.py +75 -15
- notionary/page/content/parser/parsers/space.py +14 -8
- notionary/page/content/parser/parsers/table.py +1 -1
- notionary/page/content/parser/parsers/table_of_contents.py +1 -1
- notionary/page/content/parser/parsers/todo.py +57 -19
- notionary/page/content/parser/parsers/toggle.py +17 -74
- notionary/page/content/parser/parsers/video.py +1 -1
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
- notionary/page/content/parser/service.py +9 -0
- notionary/page/content/renderer/context.py +5 -2
- notionary/page/content/renderer/factory.py +2 -11
- notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
- notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
- notionary/page/content/renderer/renderers/__init__.py +0 -2
- notionary/page/content/renderer/renderers/base.py +1 -1
- notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
- notionary/page/content/renderer/renderers/callout.py +6 -21
- notionary/page/content/renderer/renderers/captioned_block.py +1 -1
- notionary/page/content/renderer/renderers/column.py +28 -19
- notionary/page/content/renderer/renderers/column_list.py +24 -11
- notionary/page/content/renderer/renderers/heading.py +53 -27
- notionary/page/content/renderer/renderers/numbered_list.py +6 -5
- notionary/page/content/renderer/renderers/quote.py +1 -1
- notionary/page/content/renderer/renderers/todo.py +1 -1
- notionary/page/content/renderer/renderers/toggle.py +6 -7
- notionary/page/content/service.py +4 -1
- notionary/page/content/syntax/__init__.py +4 -0
- notionary/page/content/syntax/grammar.py +10 -0
- notionary/page/content/syntax/models.py +0 -2
- notionary/page/content/syntax/{service.py → registry.py} +31 -91
- notionary/page/properties/client.py +3 -3
- notionary/page/properties/models.py +3 -2
- notionary/page/properties/service.py +18 -3
- notionary/page/service.py +22 -80
- notionary/shared/entity/service.py +94 -36
- notionary/shared/models/cover.py +1 -1
- notionary/shared/typings.py +3 -0
- notionary/user/base.py +60 -11
- notionary/user/factory.py +0 -0
- notionary/utils/decorators.py +122 -0
- notionary/utils/fuzzy.py +18 -6
- notionary/utils/mixins/logging.py +38 -27
- notionary/utils/pagination.py +70 -16
- notionary/workspace/__init__.py +2 -1
- notionary/workspace/client.py +4 -2
- notionary/workspace/query/__init__.py +3 -0
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +12 -3
- notionary/workspace/query/service.py +57 -32
- notionary/workspace/service.py +31 -21
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
- notionary-0.3.1.dist-info/RECORD +211 -0
- notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
- notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
- notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
- notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
- notionary/utils/async_retry.py +0 -39
- notionary/utils/singleton.py +0 -13
- notionary-0.2.28.dist-info/RECORD +0 -200
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Self
|
|
5
5
|
|
|
6
|
-
from notionary.blocks.enums import
|
|
6
|
+
from notionary.blocks.enums import CodingLanguage
|
|
7
7
|
from notionary.page.content.markdown.nodes import (
|
|
8
8
|
AudioMarkdownNode,
|
|
9
9
|
BookmarkMarkdownNode,
|
|
@@ -28,27 +28,24 @@ from notionary.page.content.markdown.nodes import (
|
|
|
28
28
|
TableMarkdownNode,
|
|
29
29
|
TableOfContentsMarkdownNode,
|
|
30
30
|
TodoMarkdownNode,
|
|
31
|
-
ToggleableHeadingMarkdownNode,
|
|
32
31
|
ToggleMarkdownNode,
|
|
33
32
|
VideoMarkdownNode,
|
|
34
33
|
)
|
|
34
|
+
from notionary.page.content.markdown.nodes.container import flatten_children
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class MarkdownBuilder:
|
|
38
38
|
def __init__(self) -> None:
|
|
39
39
|
self.children: list[MarkdownNode] = []
|
|
40
40
|
|
|
41
|
-
def h1(self, text: str) -> Self:
|
|
42
|
-
self.
|
|
43
|
-
return self
|
|
41
|
+
def h1(self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None) -> Self:
|
|
42
|
+
return self._add_heading(text, 1, builder_func)
|
|
44
43
|
|
|
45
|
-
def h2(self, text: str) -> Self:
|
|
46
|
-
self.
|
|
47
|
-
return self
|
|
44
|
+
def h2(self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None) -> Self:
|
|
45
|
+
return self._add_heading(text, 2, builder_func)
|
|
48
46
|
|
|
49
|
-
def h3(self, text: str) -> Self:
|
|
50
|
-
self.
|
|
51
|
-
return self
|
|
47
|
+
def h3(self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None) -> Self:
|
|
48
|
+
return self._add_heading(text, 3, builder_func)
|
|
52
49
|
|
|
53
50
|
def paragraph(self, text: str) -> Self:
|
|
54
51
|
self.children.append(ParagraphMarkdownNode(text=text))
|
|
@@ -58,8 +55,9 @@ class MarkdownBuilder:
|
|
|
58
55
|
self.children.append(SpaceMarkdownNode())
|
|
59
56
|
return self
|
|
60
57
|
|
|
61
|
-
def quote(self, text: str) -> Self:
|
|
62
|
-
self.
|
|
58
|
+
def quote(self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None) -> Self:
|
|
59
|
+
children = self._build_children(builder_func)
|
|
60
|
+
self.children.append(QuoteMarkdownNode(text=text, children=children))
|
|
63
61
|
return self
|
|
64
62
|
|
|
65
63
|
def divider(self) -> Self:
|
|
@@ -70,24 +68,48 @@ class MarkdownBuilder:
|
|
|
70
68
|
self.children.append(NumberedListMarkdownNode(texts=items))
|
|
71
69
|
return self
|
|
72
70
|
|
|
71
|
+
def numbered_list_item(
|
|
72
|
+
self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None
|
|
73
|
+
) -> Self:
|
|
74
|
+
children = self._build_children(builder_func)
|
|
75
|
+
wrapped = flatten_children(children)
|
|
76
|
+
child_nodes = [wrapped] if wrapped else None
|
|
77
|
+
self.children.append(NumberedListMarkdownNode(texts=[text], children=child_nodes))
|
|
78
|
+
return self
|
|
79
|
+
|
|
73
80
|
def bulleted_list(self, items: list[str]) -> Self:
|
|
74
81
|
self.children.append(BulletedListMarkdownNode(texts=items))
|
|
75
82
|
return self
|
|
76
83
|
|
|
77
|
-
def
|
|
78
|
-
self
|
|
84
|
+
def bulleted_list_item(
|
|
85
|
+
self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None
|
|
86
|
+
) -> Self:
|
|
87
|
+
children = self._build_children(builder_func)
|
|
88
|
+
wrapped = flatten_children(children)
|
|
89
|
+
child_nodes = [wrapped] if wrapped else None
|
|
90
|
+
self.children.append(BulletedListMarkdownNode(texts=[text], children=child_nodes))
|
|
79
91
|
return self
|
|
80
92
|
|
|
81
|
-
def
|
|
82
|
-
|
|
93
|
+
def todo(
|
|
94
|
+
self,
|
|
95
|
+
text: str,
|
|
96
|
+
checked: bool = False,
|
|
97
|
+
builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None,
|
|
98
|
+
) -> Self:
|
|
99
|
+
children = self._build_children(builder_func)
|
|
100
|
+
self.children.append(TodoMarkdownNode(text=text, checked=checked, children=children))
|
|
101
|
+
return self
|
|
83
102
|
|
|
84
|
-
def
|
|
85
|
-
return self.todo(text, checked=
|
|
103
|
+
def checked_todo(self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None) -> Self:
|
|
104
|
+
return self.todo(text, checked=True, builder_func=builder_func)
|
|
86
105
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
def unchecked_todo(
|
|
107
|
+
self, text: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None
|
|
108
|
+
) -> Self:
|
|
109
|
+
return self.todo(text, checked=False, builder_func=builder_func)
|
|
90
110
|
|
|
111
|
+
def todo_list(self, items: list[str], completed: list[bool] | None = None) -> Self:
|
|
112
|
+
completed = completed or [False] * len(items)
|
|
91
113
|
for i, item in enumerate(items):
|
|
92
114
|
is_done = completed[i] if i < len(completed) else False
|
|
93
115
|
self.children.append(TodoMarkdownNode(text=item, checked=is_done))
|
|
@@ -103,75 +125,13 @@ class MarkdownBuilder:
|
|
|
103
125
|
emoji: str | None = None,
|
|
104
126
|
builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None = None,
|
|
105
127
|
) -> Self:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
text: The callout text content
|
|
111
|
-
emoji: Optional emoji for the callout icon
|
|
112
|
-
builder_func: Optional function that receives a MarkdownBuilder and returns it configured
|
|
113
|
-
|
|
114
|
-
Example:
|
|
115
|
-
builder.callout_with_children("Important note", "⚠️", lambda c:
|
|
116
|
-
c.paragraph("Additional details here")
|
|
117
|
-
.bulleted_list(["Point 1", "Point 2"])
|
|
118
|
-
)
|
|
119
|
-
"""
|
|
120
|
-
if builder_func is None:
|
|
121
|
-
self.children.append(CalloutMarkdownNode(text=text, emoji=emoji))
|
|
122
|
-
return self
|
|
123
|
-
|
|
124
|
-
callout_builder = MarkdownBuilder()
|
|
125
|
-
builder_func(callout_builder)
|
|
126
|
-
self.children.append(CalloutMarkdownNode(text=text, emoji=emoji, children=callout_builder.children))
|
|
128
|
+
children = self._build_children(builder_func)
|
|
129
|
+
self.children.append(CalloutMarkdownNode(text=text, emoji=emoji, children=children))
|
|
127
130
|
return self
|
|
128
131
|
|
|
129
132
|
def toggle(self, title: str, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder]) -> Self:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
title: The toggle title/header text
|
|
135
|
-
builder_func: Function that receives a MarkdownBuilder and returns it configured
|
|
136
|
-
|
|
137
|
-
Example:
|
|
138
|
-
builder.toggle("Advanced Settings", lambda t:
|
|
139
|
-
t.h3("Configuration")
|
|
140
|
-
.paragraph("Settings description")
|
|
141
|
-
.table(["Setting", "Value"], [["Debug", "True"]])
|
|
142
|
-
.callout("Important note", "⚠️")
|
|
143
|
-
)
|
|
144
|
-
"""
|
|
145
|
-
toggle_builder = MarkdownBuilder()
|
|
146
|
-
builder_func(toggle_builder)
|
|
147
|
-
self.children.append(ToggleMarkdownNode(title=title, children=toggle_builder.children))
|
|
148
|
-
return self
|
|
149
|
-
|
|
150
|
-
def toggleable_heading(
|
|
151
|
-
self,
|
|
152
|
-
text: str,
|
|
153
|
-
level: int,
|
|
154
|
-
builder_func: Callable[[MarkdownBuilder], MarkdownBuilder],
|
|
155
|
-
) -> Self:
|
|
156
|
-
"""
|
|
157
|
-
Add a toggleable heading with content built using the builder API.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
text: The heading text content
|
|
161
|
-
level: Heading level (1-3)
|
|
162
|
-
builder_func: Function that receives a MarkdownBuilder and returns it configured
|
|
163
|
-
|
|
164
|
-
Example:
|
|
165
|
-
builder.toggleable_heading("Advanced Section", 2, lambda t:
|
|
166
|
-
t.paragraph("Introduction to this section")
|
|
167
|
-
.numbered_list(["Step 1", "Step 2", "Step 3"])
|
|
168
|
-
.code("example_code()", "python")
|
|
169
|
-
.table(["Feature", "Status"], [["API", "Ready"]])
|
|
170
|
-
)
|
|
171
|
-
"""
|
|
172
|
-
toggle_builder = MarkdownBuilder()
|
|
173
|
-
builder_func(toggle_builder)
|
|
174
|
-
self.children.append(ToggleableHeadingMarkdownNode(text=text, level=level, children=toggle_builder.children))
|
|
133
|
+
children = self._build_children(builder_func)
|
|
134
|
+
self.children.append(ToggleMarkdownNode(title=title, children=children))
|
|
175
135
|
return self
|
|
176
136
|
|
|
177
137
|
def image(self, url: str, caption: str | None = None) -> Self:
|
|
@@ -202,22 +162,18 @@ class MarkdownBuilder:
|
|
|
202
162
|
self.children.append(EmbedMarkdownNode(url=url, caption=caption))
|
|
203
163
|
return self
|
|
204
164
|
|
|
205
|
-
def code(self, code: str, language:
|
|
165
|
+
def code(self, code: str, language: CodingLanguage | None = None, caption: str | None = None) -> Self:
|
|
206
166
|
self.children.append(CodeMarkdownNode(code=code, language=language, caption=caption))
|
|
207
167
|
return self
|
|
208
168
|
|
|
209
169
|
def mermaid(self, diagram: str, caption: str | None = None) -> Self:
|
|
210
|
-
self.children.append(CodeMarkdownNode(code=diagram, language=
|
|
170
|
+
self.children.append(CodeMarkdownNode(code=diagram, language=CodingLanguage.MERMAID.value, caption=caption))
|
|
211
171
|
return self
|
|
212
172
|
|
|
213
173
|
def table(self, headers: list[str], rows: list[list[str]]) -> Self:
|
|
214
174
|
self.children.append(TableMarkdownNode(headers=headers, rows=rows))
|
|
215
175
|
return self
|
|
216
176
|
|
|
217
|
-
def add_custom(self, node: MarkdownNode) -> Self:
|
|
218
|
-
self.children.append(node)
|
|
219
|
-
return self
|
|
220
|
-
|
|
221
177
|
def breadcrumb(self) -> Self:
|
|
222
178
|
self.children.append(BreadcrumbMarkdownNode())
|
|
223
179
|
return self
|
|
@@ -235,70 +191,36 @@ class MarkdownBuilder:
|
|
|
235
191
|
*builder_funcs: Callable[[MarkdownBuilder], MarkdownBuilder],
|
|
236
192
|
width_ratios: list[float] | None = None,
|
|
237
193
|
) -> Self:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
*builder_funcs: Multiple functions, each building one column
|
|
243
|
-
width_ratios: Optional list of width ratios (0.0 to 1.0).
|
|
244
|
-
If None, columns have equal width.
|
|
245
|
-
Length must match number of builder_funcs.
|
|
246
|
-
|
|
247
|
-
Examples:
|
|
248
|
-
# Equal width (original API unchanged):
|
|
249
|
-
builder.columns(
|
|
250
|
-
lambda col: col.h2("Left").paragraph("Left content"),
|
|
251
|
-
lambda col: col.h2("Right").paragraph("Right content")
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
# Custom ratios:
|
|
255
|
-
builder.columns(
|
|
256
|
-
lambda col: col.h2("Main").paragraph("70% width"),
|
|
257
|
-
lambda col: col.h2("Sidebar").paragraph("30% width"),
|
|
258
|
-
width_ratios=[0.7, 0.3]
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
# Three columns with custom ratios:
|
|
262
|
-
builder.columns(
|
|
263
|
-
lambda col: col.h3("Nav").paragraph("Navigation"),
|
|
264
|
-
lambda col: col.h2("Main").paragraph("Main content"),
|
|
265
|
-
lambda col: col.h3("Ads").paragraph("Advertisement"),
|
|
266
|
-
width_ratios=[0.2, 0.6, 0.2]
|
|
267
|
-
)
|
|
268
|
-
"""
|
|
269
|
-
self._validate_columns_args(builder_funcs, width_ratios)
|
|
270
|
-
|
|
271
|
-
# Create all columns
|
|
272
|
-
columns = []
|
|
273
|
-
for i, builder_func in enumerate(builder_funcs):
|
|
274
|
-
width_ratio = width_ratios[i] if width_ratios else None
|
|
275
|
-
|
|
276
|
-
col_builder = MarkdownBuilder()
|
|
277
|
-
builder_func(col_builder)
|
|
194
|
+
columns = self._build_columns(builder_funcs, width_ratios)
|
|
195
|
+
self.children.append(ColumnListMarkdownNode(columns=columns))
|
|
196
|
+
return self
|
|
278
197
|
|
|
279
|
-
|
|
280
|
-
|
|
198
|
+
def build(self) -> str:
|
|
199
|
+
return "\n\n".join(child.to_markdown() for child in self.children if child is not None)
|
|
281
200
|
|
|
282
|
-
|
|
201
|
+
def _add_heading(
|
|
202
|
+
self, text: str, level: int, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None
|
|
203
|
+
) -> Self:
|
|
204
|
+
children = self._build_children(builder_func)
|
|
205
|
+
self.children.append(HeadingMarkdownNode(text=text, level=level, children=children))
|
|
283
206
|
return self
|
|
284
207
|
|
|
285
|
-
def
|
|
208
|
+
def _build_children(self, builder_func: Callable[[MarkdownBuilder], MarkdownBuilder] | None) -> list[MarkdownNode]:
|
|
209
|
+
if builder_func is None:
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
builder = MarkdownBuilder()
|
|
213
|
+
builder_func(builder)
|
|
214
|
+
return builder.children
|
|
215
|
+
|
|
216
|
+
def _build_columns(
|
|
286
217
|
self,
|
|
287
218
|
builder_funcs: tuple[Callable[[MarkdownBuilder], MarkdownBuilder], ...],
|
|
288
219
|
width_ratios: list[float] | None,
|
|
289
|
-
) ->
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
f"width_ratios length ({len(width_ratios)}) must match number of columns ({len(builder_funcs)})"
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
ratio_sum = sum(width_ratios)
|
|
300
|
-
if not (0.9 <= ratio_sum <= 1.1): # Allow small floating point errors
|
|
301
|
-
raise ValueError(f"width_ratios should sum to 1.0, got {ratio_sum}")
|
|
302
|
-
|
|
303
|
-
def build(self) -> str:
|
|
304
|
-
return "\n\n".join(child.to_markdown() for child in self.children if child is not None)
|
|
220
|
+
) -> list[ColumnMarkdownNode]:
|
|
221
|
+
columns = []
|
|
222
|
+
for i, builder_func in enumerate(builder_funcs):
|
|
223
|
+
width_ratio = width_ratios[i] if width_ratios else None
|
|
224
|
+
children = self._build_children(builder_func)
|
|
225
|
+
columns.append(ColumnMarkdownNode(children=children, width_ratio=width_ratio))
|
|
226
|
+
return columns
|
|
@@ -21,7 +21,6 @@ from .table import TableMarkdownNode
|
|
|
21
21
|
from .table_of_contents import TableOfContentsMarkdownNode
|
|
22
22
|
from .todo import TodoMarkdownNode
|
|
23
23
|
from .toggle import ToggleMarkdownNode
|
|
24
|
-
from .toggleable_heading import ToggleableHeadingMarkdownNode
|
|
25
24
|
from .video import VideoMarkdownNode
|
|
26
25
|
|
|
27
26
|
__all__ = [
|
|
@@ -49,6 +48,5 @@ __all__ = [
|
|
|
49
48
|
"TableOfContentsMarkdownNode",
|
|
50
49
|
"TodoMarkdownNode",
|
|
51
50
|
"ToggleMarkdownNode",
|
|
52
|
-
"ToggleableHeadingMarkdownNode",
|
|
53
51
|
"VideoMarkdownNode",
|
|
54
52
|
]
|
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class AudioMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class BookmarkMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class BreadcrumbMarkdownNode(MarkdownNode):
|
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class BulletedListMarkdownNode(
|
|
8
|
-
def __init__(
|
|
8
|
+
class BulletedListMarkdownNode(ContainerNode):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
texts: list[str],
|
|
12
|
+
children: list[MarkdownNode | None] | None = None,
|
|
13
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
14
|
+
) -> None:
|
|
9
15
|
super().__init__(syntax_registry=syntax_registry)
|
|
10
16
|
self.texts = texts
|
|
17
|
+
self.children = children or []
|
|
11
18
|
|
|
12
19
|
@override
|
|
13
20
|
def to_markdown(self) -> str:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
list_items = [self._render_list_item(index, text) for index, text in enumerate(self.texts)]
|
|
22
|
+
return "\n".join(list_items)
|
|
23
|
+
|
|
24
|
+
def _render_list_item(self, index: int, text: str) -> str:
|
|
25
|
+
delimiter = self._get_list_delimiter()
|
|
26
|
+
item_line = f"{delimiter}{text}"
|
|
27
|
+
|
|
28
|
+
child = self._get_child_for_item(index)
|
|
29
|
+
if child:
|
|
30
|
+
child_content = self.render_child(child)
|
|
31
|
+
return f"{item_line}\n{child_content}"
|
|
32
|
+
|
|
33
|
+
return item_line
|
|
34
|
+
|
|
35
|
+
def _get_list_delimiter(self) -> str:
|
|
36
|
+
return self._syntax_registry.get_bulleted_list_syntax().start_delimiter
|
|
37
|
+
|
|
38
|
+
def _get_child_for_item(self, index: int) -> MarkdownNode | None:
|
|
39
|
+
if not self.children or index >= len(self.children):
|
|
40
|
+
return None
|
|
41
|
+
return self.children[index]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class CalloutMarkdownNode(
|
|
8
|
+
class CalloutMarkdownNode(ContainerNode):
|
|
8
9
|
def __init__(
|
|
9
10
|
self,
|
|
10
11
|
text: str,
|
|
@@ -19,14 +20,15 @@ class CalloutMarkdownNode(MarkdownNode):
|
|
|
19
20
|
|
|
20
21
|
@override
|
|
21
22
|
def to_markdown(self) -> str:
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
callout_content = self._format_callout_content()
|
|
24
|
+
start_delimiter = self._syntax_registry.get_callout_syntax().start_delimiter
|
|
25
|
+
result = f"{start_delimiter}({callout_content})"
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
return f"{start_tag}\n{self.text}\n{callout_syntax.end_delimiter}"
|
|
27
|
+
result += self.render_children()
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
content_parts = [self.text] + [child.to_markdown() for child in self.children]
|
|
30
|
-
content_text = "\n\n".join(content_parts)
|
|
29
|
+
return result
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
def _format_callout_content(self) -> str:
|
|
32
|
+
if self.emoji:
|
|
33
|
+
return f'{self.text} "{self.emoji}"'
|
|
34
|
+
return self.text
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
|
-
from notionary.blocks.enums import
|
|
3
|
+
from notionary.blocks.enums import CodingLanguage
|
|
4
4
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
5
5
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
6
|
-
from notionary.page.content.syntax
|
|
6
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class CodeMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
10
|
-
"""Code node for creating Notion-style code blocks."""
|
|
11
|
-
|
|
12
10
|
def __init__(
|
|
13
11
|
self,
|
|
14
12
|
code: str,
|
|
15
|
-
language:
|
|
13
|
+
language: CodingLanguage | None = None,
|
|
16
14
|
caption: str | None = None,
|
|
17
15
|
syntax_registry: SyntaxRegistry | None = None,
|
|
18
16
|
) -> None:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class ColumnMarkdownNode(
|
|
8
|
+
class ColumnMarkdownNode(ContainerNode):
|
|
8
9
|
def __init__(
|
|
9
10
|
self,
|
|
10
11
|
children: list[MarkdownNode] | None = None,
|
|
@@ -17,35 +18,52 @@ class ColumnMarkdownNode(MarkdownNode):
|
|
|
17
18
|
|
|
18
19
|
@override
|
|
19
20
|
def to_markdown(self) -> str:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if self.width_ratio is not None
|
|
24
|
-
else column_syntax.start_delimiter
|
|
25
|
-
)
|
|
21
|
+
start_tag = self._format_column_start_tag()
|
|
22
|
+
result = start_tag + self.render_children()
|
|
23
|
+
return result
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
def _format_column_start_tag(self) -> str:
|
|
26
|
+
delimiter = self._syntax_registry.get_column_syntax().start_delimiter
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return f"{start_tag}\n{content_text}\n{column_syntax.end_delimiter}"
|
|
28
|
+
if self.width_ratio is not None:
|
|
29
|
+
return f"{delimiter} {self.width_ratio}"
|
|
30
|
+
return delimiter
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
class ColumnListMarkdownNode(MarkdownNode):
|
|
38
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
columns: list[ColumnMarkdownNode] | None = None,
|
|
37
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
38
|
+
):
|
|
39
39
|
super().__init__(syntax_registry=syntax_registry)
|
|
40
40
|
self.columns = columns or []
|
|
41
41
|
|
|
42
42
|
@override
|
|
43
43
|
def to_markdown(self) -> str:
|
|
44
|
-
|
|
44
|
+
start_delimiter = self._get_column_list_delimiter()
|
|
45
|
+
|
|
45
46
|
if not self.columns:
|
|
46
|
-
return
|
|
47
|
+
return start_delimiter
|
|
48
|
+
|
|
49
|
+
result = start_delimiter + self._render_columns()
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
def _get_column_list_delimiter(self) -> str:
|
|
53
|
+
return self._syntax_registry.get_column_list_syntax().start_delimiter
|
|
54
|
+
|
|
55
|
+
def _render_columns(self) -> str:
|
|
56
|
+
rendered_columns = []
|
|
57
|
+
|
|
58
|
+
for column in self.columns:
|
|
59
|
+
column_markdown = column.to_markdown()
|
|
60
|
+
if column_markdown:
|
|
61
|
+
indented = self._indent_column(column_markdown)
|
|
62
|
+
rendered_columns.append(indented)
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
columns_content = "\n\n".join(column_parts)
|
|
64
|
+
return "\n" + "\n".join(rendered_columns) if rendered_columns else ""
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _indent_column(text: str, indent: str = " ") -> str:
|
|
68
|
+
lines = text.split("\n")
|
|
69
|
+
return "\n".join(f"{indent}{line}" if line.strip() else line for line in lines)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
2
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
3
|
+
from notionary.page.content.syntax.grammar import MarkdownGrammar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def flatten_children(children: list[MarkdownNode]) -> MarkdownNode | None:
|
|
7
|
+
"""Convert a list of child nodes to a single node for container compatibility."""
|
|
8
|
+
if not children:
|
|
9
|
+
return None
|
|
10
|
+
if len(children) == 1:
|
|
11
|
+
return children[0]
|
|
12
|
+
return _MultiChildWrapper(children)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _MultiChildWrapper(MarkdownNode):
|
|
16
|
+
def __init__(self, children: list[MarkdownNode]) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.children = children
|
|
19
|
+
|
|
20
|
+
def to_markdown(self) -> str:
|
|
21
|
+
return "\n".join(child.to_markdown() for child in self.children if child)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ContainerNode(MarkdownNode):
|
|
25
|
+
children: list[MarkdownNode]
|
|
26
|
+
|
|
27
|
+
def __init__(self, syntax_registry: SyntaxRegistry | None = None) -> None:
|
|
28
|
+
super().__init__(syntax_registry=syntax_registry)
|
|
29
|
+
grammar = self._get_grammar(syntax_registry)
|
|
30
|
+
self._spaces_per_nesting_level = grammar.spaces_per_nesting_level
|
|
31
|
+
|
|
32
|
+
def render_children(self, indent_level: int = 1) -> str:
|
|
33
|
+
if not self.children:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
indent = self._calculate_indent(indent_level)
|
|
37
|
+
rendered = [self._indent_child(child, indent) for child in self.children]
|
|
38
|
+
rendered = [text for text in rendered if text]
|
|
39
|
+
|
|
40
|
+
return f"\n{'\n'.join(rendered)}" if rendered else ""
|
|
41
|
+
|
|
42
|
+
def render_child(self, child: MarkdownNode, indent_level: int = 1) -> str:
|
|
43
|
+
child_markdown = child.to_markdown()
|
|
44
|
+
if not child_markdown:
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
indent = self._calculate_indent(indent_level)
|
|
48
|
+
return self._indent_text(child_markdown, indent)
|
|
49
|
+
|
|
50
|
+
def _calculate_indent(self, level: int) -> str:
|
|
51
|
+
return " " * (self._spaces_per_nesting_level * level)
|
|
52
|
+
|
|
53
|
+
def _indent_child(self, child: MarkdownNode, indent: str) -> str:
|
|
54
|
+
child_markdown = child.to_markdown()
|
|
55
|
+
return self._indent_text(child_markdown, indent) if child_markdown else ""
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _indent_text(text: str, indent: str) -> str:
|
|
59
|
+
lines = text.split("\n")
|
|
60
|
+
return "\n".join(f"{indent}{line}" if line.strip() else line for line in lines)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _get_grammar(syntax_registry: SyntaxRegistry | None) -> MarkdownGrammar:
|
|
64
|
+
return syntax_registry._markdown_grammar if syntax_registry else MarkdownGrammar()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class DividerMarkdownNode(MarkdownNode):
|
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class EmbedMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class EquationMarkdownNode(MarkdownNode):
|