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.
Files changed (149) hide show
  1. notionary/__init__.py +9 -2
  2. notionary/blocks/__init__.py +5 -0
  3. notionary/blocks/client.py +6 -4
  4. notionary/blocks/enums.py +28 -1
  5. notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
  6. notionary/blocks/rich_text/models.py +14 -0
  7. notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
  8. notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
  9. notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
  10. notionary/blocks/rich_text/rich_text_patterns.py +3 -0
  11. notionary/blocks/schemas.py +42 -10
  12. notionary/comments/__init__.py +5 -0
  13. notionary/comments/client.py +7 -10
  14. notionary/comments/factory.py +4 -6
  15. notionary/data_source/http/data_source_instance_client.py +14 -4
  16. notionary/data_source/properties/{models.py → schemas.py} +4 -8
  17. notionary/data_source/query/__init__.py +9 -0
  18. notionary/data_source/query/builder.py +38 -10
  19. notionary/data_source/query/schema.py +13 -10
  20. notionary/data_source/query/validator.py +11 -11
  21. notionary/data_source/schema/registry.py +104 -0
  22. notionary/data_source/schema/service.py +136 -0
  23. notionary/data_source/schemas.py +1 -1
  24. notionary/data_source/service.py +29 -103
  25. notionary/database/service.py +17 -60
  26. notionary/exceptions/__init__.py +5 -1
  27. notionary/exceptions/block_parsing.py +21 -0
  28. notionary/exceptions/search.py +24 -0
  29. notionary/http/client.py +9 -10
  30. notionary/http/models.py +5 -4
  31. notionary/page/content/factory.py +10 -3
  32. notionary/page/content/markdown/builder.py +76 -154
  33. notionary/page/content/markdown/nodes/__init__.py +0 -2
  34. notionary/page/content/markdown/nodes/audio.py +1 -1
  35. notionary/page/content/markdown/nodes/base.py +1 -1
  36. notionary/page/content/markdown/nodes/bookmark.py +1 -1
  37. notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
  38. notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
  39. notionary/page/content/markdown/nodes/callout.py +12 -10
  40. notionary/page/content/markdown/nodes/code.py +3 -5
  41. notionary/page/content/markdown/nodes/columns.py +39 -21
  42. notionary/page/content/markdown/nodes/container.py +64 -0
  43. notionary/page/content/markdown/nodes/divider.py +1 -1
  44. notionary/page/content/markdown/nodes/embed.py +1 -1
  45. notionary/page/content/markdown/nodes/equation.py +1 -1
  46. notionary/page/content/markdown/nodes/file.py +1 -1
  47. notionary/page/content/markdown/nodes/heading.py +26 -6
  48. notionary/page/content/markdown/nodes/image.py +1 -1
  49. notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
  50. notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
  51. notionary/page/content/markdown/nodes/numbered_list.py +28 -5
  52. notionary/page/content/markdown/nodes/paragraph.py +1 -1
  53. notionary/page/content/markdown/nodes/pdf.py +1 -1
  54. notionary/page/content/markdown/nodes/quote.py +17 -5
  55. notionary/page/content/markdown/nodes/space.py +1 -1
  56. notionary/page/content/markdown/nodes/table.py +1 -1
  57. notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
  58. notionary/page/content/markdown/nodes/todo.py +23 -7
  59. notionary/page/content/markdown/nodes/toggle.py +13 -14
  60. notionary/page/content/markdown/nodes/video.py +1 -1
  61. notionary/page/content/parser/context.py +98 -21
  62. notionary/page/content/parser/factory.py +1 -10
  63. notionary/page/content/parser/parsers/__init__.py +0 -2
  64. notionary/page/content/parser/parsers/audio.py +1 -1
  65. notionary/page/content/parser/parsers/base.py +1 -1
  66. notionary/page/content/parser/parsers/bookmark.py +1 -1
  67. notionary/page/content/parser/parsers/breadcrumb.py +1 -1
  68. notionary/page/content/parser/parsers/bulleted_list.py +52 -8
  69. notionary/page/content/parser/parsers/callout.py +55 -84
  70. notionary/page/content/parser/parsers/caption.py +1 -1
  71. notionary/page/content/parser/parsers/code.py +5 -5
  72. notionary/page/content/parser/parsers/column.py +23 -64
  73. notionary/page/content/parser/parsers/column_list.py +45 -45
  74. notionary/page/content/parser/parsers/divider.py +1 -1
  75. notionary/page/content/parser/parsers/embed.py +1 -1
  76. notionary/page/content/parser/parsers/equation.py +1 -1
  77. notionary/page/content/parser/parsers/file.py +1 -1
  78. notionary/page/content/parser/parsers/heading.py +65 -8
  79. notionary/page/content/parser/parsers/image.py +1 -1
  80. notionary/page/content/parser/parsers/numbered_list.py +52 -8
  81. notionary/page/content/parser/parsers/paragraph.py +3 -2
  82. notionary/page/content/parser/parsers/pdf.py +1 -1
  83. notionary/page/content/parser/parsers/quote.py +75 -15
  84. notionary/page/content/parser/parsers/space.py +14 -8
  85. notionary/page/content/parser/parsers/table.py +1 -1
  86. notionary/page/content/parser/parsers/table_of_contents.py +1 -1
  87. notionary/page/content/parser/parsers/todo.py +57 -19
  88. notionary/page/content/parser/parsers/toggle.py +17 -74
  89. notionary/page/content/parser/parsers/video.py +1 -1
  90. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
  91. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
  92. notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
  93. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
  94. notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
  95. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
  96. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
  97. notionary/page/content/parser/service.py +9 -0
  98. notionary/page/content/renderer/context.py +5 -2
  99. notionary/page/content/renderer/factory.py +2 -11
  100. notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
  101. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
  102. notionary/page/content/renderer/renderers/__init__.py +0 -2
  103. notionary/page/content/renderer/renderers/base.py +1 -1
  104. notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
  105. notionary/page/content/renderer/renderers/callout.py +6 -21
  106. notionary/page/content/renderer/renderers/captioned_block.py +1 -1
  107. notionary/page/content/renderer/renderers/column.py +28 -19
  108. notionary/page/content/renderer/renderers/column_list.py +24 -11
  109. notionary/page/content/renderer/renderers/heading.py +53 -27
  110. notionary/page/content/renderer/renderers/numbered_list.py +6 -5
  111. notionary/page/content/renderer/renderers/quote.py +1 -1
  112. notionary/page/content/renderer/renderers/todo.py +1 -1
  113. notionary/page/content/renderer/renderers/toggle.py +6 -7
  114. notionary/page/content/service.py +4 -1
  115. notionary/page/content/syntax/__init__.py +4 -0
  116. notionary/page/content/syntax/grammar.py +10 -0
  117. notionary/page/content/syntax/models.py +0 -2
  118. notionary/page/content/syntax/{service.py → registry.py} +31 -91
  119. notionary/page/properties/client.py +3 -3
  120. notionary/page/properties/models.py +3 -2
  121. notionary/page/properties/service.py +18 -3
  122. notionary/page/service.py +22 -80
  123. notionary/shared/entity/service.py +94 -36
  124. notionary/shared/models/cover.py +1 -1
  125. notionary/shared/typings.py +3 -0
  126. notionary/user/base.py +60 -11
  127. notionary/user/factory.py +0 -0
  128. notionary/utils/decorators.py +122 -0
  129. notionary/utils/fuzzy.py +18 -6
  130. notionary/utils/mixins/logging.py +38 -27
  131. notionary/utils/pagination.py +70 -16
  132. notionary/workspace/__init__.py +2 -1
  133. notionary/workspace/client.py +4 -2
  134. notionary/workspace/query/__init__.py +3 -0
  135. notionary/workspace/query/builder.py +25 -1
  136. notionary/workspace/query/models.py +12 -3
  137. notionary/workspace/query/service.py +57 -32
  138. notionary/workspace/service.py +31 -21
  139. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
  140. notionary-0.3.1.dist-info/RECORD +211 -0
  141. notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
  142. notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
  143. notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
  144. notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
  145. notionary/utils/async_retry.py +0 -39
  146. notionary/utils/singleton.py +0 -13
  147. notionary-0.2.28.dist-info/RECORD +0 -200
  148. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
  149. {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 CodeLanguage
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.children.append(HeadingMarkdownNode(text=text, level=1))
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.children.append(HeadingMarkdownNode(text=text, level=2))
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.children.append(HeadingMarkdownNode(text=text, level=3))
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.children.append(QuoteMarkdownNode(text=text))
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 todo(self, text: str, checked: bool = False) -> Self:
78
- self.children.append(TodoMarkdownNode(text=text, checked=checked))
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 checked_todo(self, text: str) -> Self:
82
- return self.todo(text, checked=True)
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 unchecked_todo(self, text: str) -> Self:
85
- return self.todo(text, checked=False)
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 todo_list(self, items: list[str], completed: list[bool] | None = None) -> Self:
88
- if completed is None:
89
- completed = [False] * len(items)
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
- Add a callout block with children built using the builder API.
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
- Add a toggle block with content built using the builder API.
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: CodeLanguage | None = None, caption: str | None = None) -> Self:
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=CodeLanguage.MERMAID.value, caption=caption))
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
- Add multiple columns in a layout.
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
- column_node = ColumnMarkdownNode(children=col_builder.children, width_ratio=width_ratio)
280
- columns.append(column_node)
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
- self.children.append(ColumnListMarkdownNode(columns=columns))
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 _validate_columns_args(
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
- ) -> None:
290
- if len(builder_funcs) < 2:
291
- raise ValueError("Column layout requires at least 2 columns")
292
-
293
- if width_ratios is not None:
294
- if len(width_ratios) != len(builder_funcs):
295
- raise ValueError(
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.service import SyntaxRegistry
5
+ from notionary.page.content.syntax import SyntaxRegistry
6
6
 
7
7
 
8
8
  class AudioMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
- from notionary.page.content.syntax.service import SyntaxRegistry
3
+ from notionary.page.content.syntax import SyntaxRegistry
4
4
 
5
5
 
6
6
  class MarkdownNode(ABC):
@@ -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.service import SyntaxRegistry
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.service import SyntaxRegistry
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.syntax.service import SyntaxRegistry
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(MarkdownNode):
8
- def __init__(self, texts: list[str], syntax_registry: SyntaxRegistry | None = None) -> None:
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
- bulleted_list_syntax = self._syntax_registry.get_bulleted_list_syntax()
15
- result = []
16
- for text in self.texts:
17
- result.append(f"{bulleted_list_syntax.start_delimiter} {text}")
18
- return "\n".join(result)
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.syntax.service import SyntaxRegistry
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(MarkdownNode):
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
- callout_syntax = self._syntax_registry.get_callout_syntax()
23
- start_tag = f"{callout_syntax.start_delimiter} {self.emoji}" if self.emoji else callout_syntax.start_delimiter
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
- if not self.children:
26
- return f"{start_tag}\n{self.text}\n{callout_syntax.end_delimiter}"
27
+ result += self.render_children()
27
28
 
28
- # Convert children to markdown
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
- return f"{start_tag}\n{content_text}\n{callout_syntax.end_delimiter}"
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 CodeLanguage
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.service import SyntaxRegistry
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: CodeLanguage | None = None,
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.syntax.service import SyntaxRegistry
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(MarkdownNode):
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
- column_syntax = self._syntax_registry.get_column_syntax()
21
- start_tag = (
22
- f"{column_syntax.start_delimiter} {self.width_ratio}"
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
- if not self.children:
28
- return f"{start_tag}\n{column_syntax.end_delimiter}"
25
+ def _format_column_start_tag(self) -> str:
26
+ delimiter = self._syntax_registry.get_column_syntax().start_delimiter
29
27
 
30
- # Convert children to markdown
31
- content_parts = [child.to_markdown() for child in self.children]
32
- content_text = "\n\n".join(content_parts)
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__(self, columns: list[ColumnMarkdownNode] | None = None, syntax_registry: SyntaxRegistry | None = None):
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
- column_list_syntax = self._syntax_registry.get_column_list_syntax()
44
+ start_delimiter = self._get_column_list_delimiter()
45
+
45
46
  if not self.columns:
46
- return f"{column_list_syntax.start_delimiter}\n{column_list_syntax.end_delimiter}"
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
- column_parts = [column.to_markdown() for column in self.columns]
49
- columns_content = "\n\n".join(column_parts)
64
+ return "\n" + "\n".join(rendered_columns) if rendered_columns else ""
50
65
 
51
- return f"{column_list_syntax.start_delimiter}\n{columns_content}\n{column_list_syntax.end_delimiter}"
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.service import SyntaxRegistry
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.service import SyntaxRegistry
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.service import SyntaxRegistry
4
+ from notionary.page.content.syntax import SyntaxRegistry
5
5
 
6
6
 
7
7
  class EquationMarkdownNode(MarkdownNode):