notionary 0.1.25__py3-none-any.whl → 0.1.26__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/elements/audio_element.py +41 -38
- notionary/elements/bookmark_element.py +36 -27
- notionary/elements/bulleted_list_element.py +28 -21
- notionary/elements/callout_element.py +39 -31
- notionary/elements/code_block_element.py +38 -26
- notionary/elements/divider_element.py +29 -18
- notionary/elements/embed_element.py +37 -28
- notionary/elements/heading_element.py +39 -24
- notionary/elements/image_element.py +33 -24
- notionary/elements/mention_element.py +40 -29
- notionary/elements/notion_block_element.py +13 -31
- notionary/elements/numbered_list_element.py +29 -20
- notionary/elements/paragraph_element.py +37 -31
- notionary/elements/prompts/element_prompt_content.py +91 -7
- notionary/elements/prompts/synthax_prompt_builder.py +63 -16
- notionary/elements/qoute_element.py +72 -74
- notionary/elements/registry/block_element_registry_builder.py +6 -9
- notionary/elements/table_element.py +49 -36
- notionary/elements/text_inline_formatter.py +23 -15
- notionary/elements/{todo_lists.py → todo_element.py} +34 -25
- notionary/elements/toggle_element.py +184 -108
- notionary/elements/toggleable_heading_element.py +269 -0
- notionary/elements/video_element.py +37 -28
- notionary/page/content/page_content_manager.py +3 -8
- notionary/page/markdown_to_notion_converter.py +269 -274
- notionary/page/notion_page.py +2 -4
- notionary/page/notion_to_markdown_converter.py +20 -95
- {notionary-0.1.25.dist-info → notionary-0.1.26.dist-info}/METADATA +1 -1
- notionary-0.1.26.dist-info/RECORD +58 -0
- {notionary-0.1.25.dist-info → notionary-0.1.26.dist-info}/WHEEL +1 -1
- notionary/elements/column_element.py +0 -307
- notionary-0.1.25.dist-info/RECORD +0 -58
- {notionary-0.1.25.dist-info → notionary-0.1.26.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.25.dist-info → notionary-0.1.26.dist-info}/top_level.txt +0 -0
@@ -7,180 +7,157 @@ from notionary.elements.registry.block_element_registry_builder import (
|
|
7
7
|
|
8
8
|
|
9
9
|
class MarkdownToNotionConverter:
|
10
|
+
"""Converts Markdown text to Notion API block format with support for pipe syntax for nested structures."""
|
11
|
+
|
10
12
|
SPACER_MARKER = "<!-- spacer -->"
|
11
|
-
|
12
|
-
|
13
|
-
TOGGLE_MARKER_PREFIX = "<!-- toggle_"
|
14
|
-
TOGGLE_MARKER_SUFFIX = " -->"
|
13
|
+
TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
|
14
|
+
PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
|
15
15
|
|
16
16
|
def __init__(self, block_registry: Optional[BlockElementRegistry] = None):
|
17
|
-
"""
|
18
|
-
Initialize the MarkdownToNotionConverter.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
block_registry: Optional registry of Notion block elements
|
22
|
-
"""
|
17
|
+
"""Initialize the converter with an optional custom block registry."""
|
23
18
|
self._block_registry = (
|
24
19
|
block_registry or BlockElementRegistryBuilder().create_full_registry()
|
25
20
|
)
|
26
21
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
def convert(self, markdown_text: str) -> List[Dict[str, Any]]:
|
23
|
+
"""Convert markdown text to Notion API block format."""
|
24
|
+
if not markdown_text:
|
25
|
+
return []
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
# Collect all blocks with their positions in the text
|
28
|
+
all_blocks_with_positions = self._collect_all_blocks_with_positions(
|
29
|
+
markdown_text
|
30
|
+
)
|
35
31
|
|
36
|
-
|
37
|
-
|
38
|
-
Convert markdown text to Notion API block format.
|
32
|
+
# Sort all blocks by their position in the text
|
33
|
+
all_blocks_with_positions.sort(key=lambda x: x[0])
|
39
34
|
|
40
|
-
|
41
|
-
|
35
|
+
# Extract just the blocks without position information
|
36
|
+
blocks = [block for _, _, block in all_blocks_with_positions]
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
"""
|
46
|
-
if not markdown_text:
|
47
|
-
return []
|
38
|
+
# Process spacing between blocks
|
39
|
+
return self._process_block_spacing(blocks)
|
48
40
|
|
49
|
-
|
41
|
+
def _collect_all_blocks_with_positions(
|
42
|
+
self, markdown_text: str
|
43
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
44
|
+
"""Collect all blocks with their positions in the text."""
|
50
45
|
all_blocks = []
|
51
46
|
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
# If we have toggles, process them and extract positions
|
56
|
-
if toggle_blocks:
|
57
|
-
all_blocks.extend(toggle_blocks)
|
47
|
+
# Process toggleable elements first (both Toggle and ToggleableHeading)
|
48
|
+
toggleable_blocks = self._identify_toggleable_blocks(markdown_text)
|
58
49
|
|
59
50
|
# Process other multiline elements
|
60
|
-
multiline_blocks = self._identify_multiline_blocks(
|
61
|
-
|
62
|
-
all_blocks.extend(multiline_blocks)
|
63
|
-
|
64
|
-
# Process remaining text line by line
|
65
|
-
line_blocks = self._process_text_lines(
|
66
|
-
markdown_text, toggle_blocks + multiline_blocks
|
51
|
+
multiline_blocks = self._identify_multiline_blocks(
|
52
|
+
markdown_text, toggleable_blocks
|
67
53
|
)
|
68
|
-
if line_blocks:
|
69
|
-
all_blocks.extend(line_blocks)
|
70
54
|
|
71
|
-
#
|
72
|
-
|
55
|
+
# Process remaining text line by line
|
56
|
+
processed_blocks = toggleable_blocks + multiline_blocks
|
57
|
+
line_blocks = self._process_text_lines(markdown_text, processed_blocks)
|
73
58
|
|
74
|
-
#
|
75
|
-
|
59
|
+
# Combine all blocks
|
60
|
+
all_blocks.extend(toggleable_blocks)
|
61
|
+
all_blocks.extend(multiline_blocks)
|
62
|
+
all_blocks.extend(line_blocks)
|
76
63
|
|
77
|
-
|
78
|
-
return self._process_block_spacing(blocks)
|
64
|
+
return all_blocks
|
79
65
|
|
80
|
-
def
|
66
|
+
def _identify_toggleable_blocks(
|
81
67
|
self, text: str
|
82
68
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
83
|
-
"""
|
84
|
-
|
69
|
+
"""Identify all toggleable blocks (Toggle and ToggleableHeading) in the text."""
|
70
|
+
toggleable_blocks = []
|
71
|
+
|
72
|
+
# Find all toggleable elements
|
73
|
+
toggleable_elements = self._get_toggleable_elements()
|
85
74
|
|
86
|
-
|
87
|
-
|
75
|
+
if not toggleable_elements:
|
76
|
+
return []
|
77
|
+
|
78
|
+
# Process each toggleable element type
|
79
|
+
for element in toggleable_elements:
|
80
|
+
if hasattr(element, "find_matches"):
|
81
|
+
# Find matches with context awareness
|
82
|
+
matches = element.find_matches(text, self.convert, context_aware=True)
|
83
|
+
if matches:
|
84
|
+
toggleable_blocks.extend(matches)
|
88
85
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
86
|
+
return toggleable_blocks
|
87
|
+
|
88
|
+
def _get_toggleable_elements(self):
|
89
|
+
"""Return all toggleable elements from the registry."""
|
90
|
+
toggleable_elements = []
|
94
91
|
for element in self._block_registry.get_elements():
|
95
92
|
if (
|
96
93
|
element.is_multiline()
|
97
94
|
and hasattr(element, "match_markdown")
|
98
|
-
and element.__name__
|
95
|
+
and element.__name__ in self.TOGGLE_ELEMENT_TYPES
|
99
96
|
):
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
if not toggle_element:
|
104
|
-
return []
|
105
|
-
|
106
|
-
# Use the find_matches method with context awareness
|
107
|
-
# Pass the converter's convert method as a callback to process nested content
|
108
|
-
toggle_blocks = toggle_element.find_matches(
|
109
|
-
text, self.convert, context_aware=True
|
110
|
-
)
|
111
|
-
return toggle_blocks
|
97
|
+
toggleable_elements.append(element)
|
98
|
+
return toggleable_elements
|
112
99
|
|
113
100
|
def _identify_multiline_blocks(
|
114
101
|
self, text: str, exclude_blocks: List[Tuple[int, int, Dict[str, Any]]]
|
115
102
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
116
|
-
"""
|
117
|
-
|
118
|
-
|
119
|
-
Args:
|
120
|
-
text: The text to process
|
121
|
-
exclude_blocks: Blocks to exclude (e.g., already identified toggle blocks)
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
List of (start_pos, end_pos, block) tuples
|
125
|
-
"""
|
126
|
-
# Get all multiline elements except ToggleElement
|
127
|
-
multiline_elements = [
|
128
|
-
element
|
129
|
-
for element in self._block_registry.get_multiline_elements()
|
130
|
-
if element.__name__ != "ToggleElement"
|
131
|
-
]
|
103
|
+
"""Identify all multiline blocks (except toggleable blocks)."""
|
104
|
+
# Get all multiline elements except toggleable ones
|
105
|
+
multiline_elements = self._get_non_toggleable_multiline_elements()
|
132
106
|
|
133
107
|
if not multiline_elements:
|
134
108
|
return []
|
135
109
|
|
136
|
-
# Create
|
137
|
-
|
138
|
-
for start, end, _ in exclude_blocks:
|
139
|
-
exclude_ranges.update(range(start, end + 1))
|
110
|
+
# Create set of positions to exclude
|
111
|
+
excluded_ranges = self._create_excluded_position_set(exclude_blocks)
|
140
112
|
|
141
113
|
multiline_blocks = []
|
142
114
|
for element in multiline_elements:
|
143
115
|
if not hasattr(element, "find_matches"):
|
144
116
|
continue
|
145
117
|
|
146
|
-
|
147
|
-
if hasattr(element, "set_converter_callback"):
|
148
|
-
matches = element.find_matches(text, self.convert)
|
149
|
-
else:
|
150
|
-
matches = element.find_matches(text)
|
118
|
+
matches = element.find_matches(text)
|
151
119
|
|
152
120
|
if not matches:
|
153
121
|
continue
|
154
122
|
|
155
|
-
# Add
|
156
|
-
for
|
157
|
-
|
158
|
-
|
123
|
+
# Add blocks that don't overlap with excluded positions
|
124
|
+
for start_pos, end_pos, block in matches:
|
125
|
+
if self._overlaps_with_excluded_positions(
|
126
|
+
start_pos, end_pos, excluded_ranges
|
127
|
+
):
|
159
128
|
continue
|
160
|
-
multiline_blocks.append((
|
129
|
+
multiline_blocks.append((start_pos, end_pos, block))
|
161
130
|
|
162
131
|
return multiline_blocks
|
163
132
|
|
133
|
+
def _get_non_toggleable_multiline_elements(self):
|
134
|
+
"""Get multiline elements that are not toggleable elements."""
|
135
|
+
return [
|
136
|
+
element
|
137
|
+
for element in self._block_registry.get_multiline_elements()
|
138
|
+
if element.__name__ not in self.TOGGLE_ELEMENT_TYPES
|
139
|
+
]
|
140
|
+
|
141
|
+
def _create_excluded_position_set(self, exclude_blocks):
|
142
|
+
"""Create a set of positions to exclude based on block ranges."""
|
143
|
+
excluded_positions = set()
|
144
|
+
for start_pos, end_pos, _ in exclude_blocks:
|
145
|
+
excluded_positions.update(range(start_pos, end_pos + 1))
|
146
|
+
return excluded_positions
|
147
|
+
|
148
|
+
def _overlaps_with_excluded_positions(self, start_pos, end_pos, excluded_positions):
|
149
|
+
"""Check if a range overlaps with any excluded positions."""
|
150
|
+
return any(pos in excluded_positions for pos in range(start_pos, end_pos + 1))
|
151
|
+
|
164
152
|
def _process_text_lines(
|
165
153
|
self, text: str, exclude_blocks: List[Tuple[int, int, Dict[str, Any]]]
|
166
154
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
167
|
-
"""
|
168
|
-
Process text line by line, excluding ranges already processed.
|
169
|
-
|
170
|
-
Args:
|
171
|
-
text: The text to process
|
172
|
-
exclude_blocks: Blocks to exclude (e.g., already identified toggle and multiline blocks)
|
173
|
-
|
174
|
-
Returns:
|
175
|
-
List of (start_pos, end_pos, block) tuples
|
176
|
-
"""
|
155
|
+
"""Process text line by line, excluding already processed ranges and handling pipe syntax lines."""
|
177
156
|
if not text:
|
178
157
|
return []
|
179
158
|
|
180
|
-
# Create
|
181
|
-
|
182
|
-
for start, end, _ in exclude_blocks:
|
183
|
-
exclude_positions.update(range(start, end + 1))
|
159
|
+
# Create set of excluded positions
|
160
|
+
excluded_positions = self._create_excluded_position_set(exclude_blocks)
|
184
161
|
|
185
162
|
line_blocks = []
|
186
163
|
lines = text.split("\n")
|
@@ -194,210 +171,245 @@ class MarkdownToNotionConverter:
|
|
194
171
|
line_length = len(line) + 1 # +1 for newline
|
195
172
|
line_end = current_pos + line_length - 1
|
196
173
|
|
197
|
-
# Skip lines
|
198
|
-
if
|
174
|
+
# Skip excluded lines and pipe syntax lines (they're part of toggleable content)
|
175
|
+
if self._overlaps_with_excluded_positions(
|
176
|
+
current_pos, line_end, excluded_positions
|
177
|
+
) or self._is_pipe_syntax_line(line):
|
199
178
|
current_pos += line_length
|
200
179
|
continue
|
201
180
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
current_pos += line_length
|
212
|
-
continue
|
181
|
+
processed = self._process_line(
|
182
|
+
line,
|
183
|
+
current_pos,
|
184
|
+
line_end,
|
185
|
+
line_blocks,
|
186
|
+
current_paragraph,
|
187
|
+
paragraph_start,
|
188
|
+
in_todo_sequence,
|
189
|
+
)
|
213
190
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
todo_block,
|
219
|
-
line_length,
|
220
|
-
current_pos,
|
221
|
-
current_paragraph,
|
222
|
-
paragraph_start,
|
223
|
-
line_blocks,
|
224
|
-
in_todo_sequence,
|
225
|
-
)
|
226
|
-
in_todo_sequence = True
|
227
|
-
current_pos += line_length
|
228
|
-
continue
|
191
|
+
current_pos = processed["current_pos"]
|
192
|
+
current_paragraph = processed["current_paragraph"]
|
193
|
+
paragraph_start = processed["paragraph_start"]
|
194
|
+
in_todo_sequence = processed["in_todo_sequence"]
|
229
195
|
|
230
|
-
|
231
|
-
|
196
|
+
# Process remaining paragraph
|
197
|
+
self._process_paragraph(
|
198
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
199
|
+
)
|
232
200
|
|
233
|
-
|
234
|
-
self._process_paragraph_if_present(
|
235
|
-
current_paragraph, paragraph_start, current_pos, line_blocks
|
236
|
-
)
|
237
|
-
current_paragraph = []
|
238
|
-
current_pos += line_length
|
239
|
-
continue
|
201
|
+
return line_blocks
|
240
202
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
current_paragraph, paragraph_start, current_pos, line_blocks
|
245
|
-
)
|
246
|
-
line_blocks.append(
|
247
|
-
(current_pos, current_pos + line_length - 1, special_block)
|
248
|
-
)
|
249
|
-
current_paragraph = []
|
250
|
-
current_pos += line_length
|
251
|
-
continue
|
203
|
+
def _is_pipe_syntax_line(self, line: str) -> bool:
|
204
|
+
"""Check if a line uses pipe syntax (for nested content)."""
|
205
|
+
import re
|
252
206
|
|
253
|
-
|
254
|
-
if not current_paragraph:
|
255
|
-
paragraph_start = current_pos
|
256
|
-
current_paragraph.append(line)
|
257
|
-
current_pos += line_length
|
207
|
+
return bool(re.match(self.PIPE_CONTENT_PATTERN, line))
|
258
208
|
|
259
|
-
|
260
|
-
self
|
261
|
-
|
262
|
-
|
209
|
+
def _process_line(
|
210
|
+
self,
|
211
|
+
line: str,
|
212
|
+
current_pos: int,
|
213
|
+
line_end: int,
|
214
|
+
line_blocks: List[Tuple[int, int, Dict[str, Any]]],
|
215
|
+
current_paragraph: List[str],
|
216
|
+
paragraph_start: int,
|
217
|
+
in_todo_sequence: bool,
|
218
|
+
) -> Dict[str, Any]:
|
219
|
+
"""Process a single line of text."""
|
220
|
+
line_length = len(line) + 1 # +1 for newline
|
221
|
+
|
222
|
+
# Check for spacer
|
223
|
+
if self._is_spacer_line(line):
|
224
|
+
line_blocks.append((current_pos, line_end, self._create_empty_paragraph()))
|
225
|
+
return self._update_line_state(
|
226
|
+
current_pos + line_length,
|
227
|
+
current_paragraph,
|
228
|
+
paragraph_start,
|
229
|
+
in_todo_sequence,
|
230
|
+
)
|
263
231
|
|
264
|
-
|
232
|
+
# Handle todo items
|
233
|
+
todo_block = self._extract_todo_item(line)
|
234
|
+
if todo_block:
|
235
|
+
return self._process_todo_line(
|
236
|
+
todo_block,
|
237
|
+
current_pos,
|
238
|
+
line_end,
|
239
|
+
line_blocks,
|
240
|
+
current_paragraph,
|
241
|
+
paragraph_start,
|
242
|
+
in_todo_sequence,
|
243
|
+
line_length,
|
244
|
+
)
|
265
245
|
|
266
|
-
|
267
|
-
|
268
|
-
return line.strip() == self.SPACER_MARKER
|
246
|
+
if in_todo_sequence:
|
247
|
+
in_todo_sequence = False
|
269
248
|
|
270
|
-
|
271
|
-
|
272
|
-
|
249
|
+
# Handle empty lines
|
250
|
+
if not line.strip():
|
251
|
+
self._process_paragraph(
|
252
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
253
|
+
)
|
254
|
+
return self._update_line_state(
|
255
|
+
current_pos + line_length, [], paragraph_start, False
|
256
|
+
)
|
273
257
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
)
|
284
|
-
|
285
|
-
|
258
|
+
# Handle special blocks
|
259
|
+
special_block = self._extract_special_block(line)
|
260
|
+
if special_block:
|
261
|
+
self._process_paragraph(
|
262
|
+
current_paragraph, paragraph_start, current_pos, line_blocks
|
263
|
+
)
|
264
|
+
line_blocks.append((current_pos, line_end, special_block))
|
265
|
+
return self._update_line_state(
|
266
|
+
current_pos + line_length, [], paragraph_start, False
|
267
|
+
)
|
268
|
+
|
269
|
+
# Handle as paragraph
|
270
|
+
if not current_paragraph:
|
271
|
+
paragraph_start = current_pos
|
272
|
+
current_paragraph.append(line)
|
286
273
|
|
287
|
-
|
274
|
+
return self._update_line_state(
|
275
|
+
current_pos + line_length,
|
276
|
+
current_paragraph,
|
277
|
+
paragraph_start,
|
278
|
+
in_todo_sequence,
|
279
|
+
)
|
280
|
+
|
281
|
+
def _is_spacer_line(self, line: str) -> bool:
|
282
|
+
"""Check if a line is a spacer marker."""
|
283
|
+
return line.strip() == self.SPACER_MARKER
|
284
|
+
|
285
|
+
def _process_todo_line(
|
288
286
|
self,
|
289
287
|
todo_block: Dict[str, Any],
|
290
|
-
line_length: int,
|
291
288
|
current_pos: int,
|
289
|
+
line_end: int,
|
290
|
+
line_blocks: List[Tuple[int, int, Dict[str, Any]]],
|
292
291
|
current_paragraph: List[str],
|
293
292
|
paragraph_start: int,
|
294
|
-
line_blocks: List[Tuple[int, int, Dict[str, Any]]],
|
295
293
|
in_todo_sequence: bool,
|
296
|
-
|
297
|
-
|
298
|
-
|
294
|
+
line_length: int,
|
295
|
+
) -> Dict[str, Any]:
|
296
|
+
"""Process a line that contains a todo item."""
|
297
|
+
# Finish paragraph if needed
|
299
298
|
if not in_todo_sequence and current_paragraph:
|
300
|
-
self.
|
299
|
+
self._process_paragraph(
|
301
300
|
current_paragraph, paragraph_start, current_pos, line_blocks
|
302
301
|
)
|
303
|
-
current_paragraph.clear()
|
304
302
|
|
305
|
-
line_blocks.append((current_pos,
|
303
|
+
line_blocks.append((current_pos, line_end, todo_block))
|
304
|
+
|
305
|
+
return self._update_line_state(
|
306
|
+
current_pos + line_length, [], paragraph_start, True
|
307
|
+
)
|
308
|
+
|
309
|
+
def _update_line_state(
|
310
|
+
self,
|
311
|
+
current_pos: int,
|
312
|
+
current_paragraph: List[str],
|
313
|
+
paragraph_start: int,
|
314
|
+
in_todo_sequence: bool,
|
315
|
+
) -> Dict[str, Any]:
|
316
|
+
"""Update and return the state after processing a line."""
|
317
|
+
return {
|
318
|
+
"current_pos": current_pos,
|
319
|
+
"current_paragraph": current_paragraph,
|
320
|
+
"paragraph_start": paragraph_start,
|
321
|
+
"in_todo_sequence": in_todo_sequence,
|
322
|
+
}
|
323
|
+
|
324
|
+
def _extract_todo_item(self, line: str) -> Optional[Dict[str, Any]]:
|
325
|
+
"""Extract a todo item from a line if possible."""
|
326
|
+
todo_elements = [
|
327
|
+
element
|
328
|
+
for element in self._block_registry.get_elements()
|
329
|
+
if not element.is_multiline() and element.__name__ == "TodoElement"
|
330
|
+
]
|
331
|
+
|
332
|
+
for element in todo_elements:
|
333
|
+
if element.match_markdown(line):
|
334
|
+
return element.markdown_to_notion(line)
|
335
|
+
return None
|
306
336
|
|
307
337
|
def _extract_special_block(self, line: str) -> Optional[Dict[str, Any]]:
|
308
|
-
"""
|
309
|
-
|
338
|
+
"""Extract a special block (not paragraph) from a line if possible."""
|
339
|
+
non_multiline_elements = [
|
340
|
+
element
|
341
|
+
for element in self._block_registry.get_elements()
|
342
|
+
if not element.is_multiline()
|
343
|
+
]
|
310
344
|
|
311
|
-
|
312
|
-
|
313
|
-
"""
|
314
|
-
for element in self._block_registry.get_elements():
|
315
|
-
if (
|
316
|
-
not element.is_multiline()
|
317
|
-
and hasattr(element, "match_markdown")
|
318
|
-
and element.match_markdown(line)
|
319
|
-
):
|
345
|
+
for element in non_multiline_elements:
|
346
|
+
if element.match_markdown(line):
|
320
347
|
block = element.markdown_to_notion(line)
|
321
348
|
if block and block.get("type") != "paragraph":
|
322
349
|
return block
|
323
350
|
return None
|
324
351
|
|
325
|
-
def
|
352
|
+
def _process_paragraph(
|
326
353
|
self,
|
327
354
|
paragraph_lines: List[str],
|
328
355
|
start_pos: int,
|
329
356
|
end_pos: int,
|
330
357
|
blocks: List[Tuple[int, int, Dict[str, Any]]],
|
331
358
|
) -> None:
|
332
|
-
"""
|
333
|
-
Process a paragraph and add it to the blocks list if valid.
|
334
|
-
|
335
|
-
Args:
|
336
|
-
paragraph_lines: Lines that make up the paragraph
|
337
|
-
start_pos: Starting position of the paragraph
|
338
|
-
end_pos: Ending position of the paragraph
|
339
|
-
blocks: List to add the processed paragraph block to
|
340
|
-
"""
|
359
|
+
"""Process a paragraph and add it to blocks if valid."""
|
341
360
|
if not paragraph_lines:
|
342
361
|
return
|
343
362
|
|
344
363
|
paragraph_text = "\n".join(paragraph_lines)
|
345
364
|
block = self._block_registry.markdown_to_notion(paragraph_text)
|
346
365
|
|
347
|
-
if
|
348
|
-
|
349
|
-
|
350
|
-
blocks.append((start_pos, end_pos, block))
|
366
|
+
if block:
|
367
|
+
blocks.append((start_pos, end_pos, block))
|
351
368
|
|
352
369
|
def _process_block_spacing(
|
353
370
|
self, blocks: List[Dict[str, Any]]
|
354
371
|
) -> List[Dict[str, Any]]:
|
355
|
-
"""
|
356
|
-
Process blocks and add spacing only where no explicit spacer is present.
|
357
|
-
|
358
|
-
Args:
|
359
|
-
blocks: List of Notion blocks
|
360
|
-
|
361
|
-
Returns:
|
362
|
-
List of Notion blocks with processed spacing
|
363
|
-
"""
|
372
|
+
"""Add spacing between blocks where needed."""
|
364
373
|
if not blocks:
|
365
374
|
return blocks
|
366
375
|
|
367
376
|
final_blocks = []
|
368
|
-
i = 0
|
369
377
|
|
370
|
-
|
371
|
-
current_block = blocks[i]
|
378
|
+
for block_index, current_block in enumerate(blocks):
|
372
379
|
final_blocks.append(current_block)
|
373
380
|
|
374
|
-
#
|
381
|
+
# Only add spacing after multiline blocks
|
375
382
|
if not self._is_multiline_block_type(current_block.get("type")):
|
376
|
-
i += 1
|
377
383
|
continue
|
378
384
|
|
379
|
-
# Check if
|
380
|
-
if
|
381
|
-
# Next block is already a spacer, don't add another
|
382
|
-
pass
|
383
|
-
else:
|
384
|
-
# No explicit spacer found, add one automatically
|
385
|
+
# Check if we need to add a spacer
|
386
|
+
if self._needs_spacer_after_block(blocks, block_index):
|
385
387
|
final_blocks.append(self._create_empty_paragraph())
|
386
388
|
|
387
|
-
i += 1
|
388
|
-
|
389
389
|
return final_blocks
|
390
390
|
|
391
|
-
def
|
392
|
-
|
393
|
-
|
391
|
+
def _needs_spacer_after_block(
|
392
|
+
self, blocks: List[Dict[str, Any]], block_index: int
|
393
|
+
) -> bool:
|
394
|
+
"""Determine if we need to add a spacer after the current block."""
|
395
|
+
# Check if this is the last block (no need for spacer)
|
396
|
+
if block_index + 1 >= len(blocks):
|
397
|
+
return False
|
398
|
+
|
399
|
+
# Check if next block is already a spacer
|
400
|
+
next_block = blocks[block_index + 1]
|
401
|
+
if self._is_empty_paragraph(next_block):
|
402
|
+
return False
|
394
403
|
|
395
|
-
|
396
|
-
|
404
|
+
# No spacer needed
|
405
|
+
return True
|
397
406
|
|
398
|
-
|
399
|
-
|
400
|
-
"""
|
407
|
+
def _create_empty_paragraph(self):
|
408
|
+
"""Create an empty paragraph block."""
|
409
|
+
return {"type": "paragraph", "paragraph": {"rich_text": []}}
|
410
|
+
|
411
|
+
def _is_multiline_block_type(self, block_type: str) -> bool:
|
412
|
+
"""Check if a block type corresponds to a multiline element."""
|
401
413
|
if not block_type:
|
402
414
|
return False
|
403
415
|
|
@@ -416,26 +428,9 @@ class MarkdownToNotionConverter:
|
|
416
428
|
return False
|
417
429
|
|
418
430
|
def _is_empty_paragraph(self, block: Dict[str, Any]) -> bool:
|
419
|
-
"""
|
420
|
-
Check if a block is an empty paragraph.
|
421
|
-
|
422
|
-
Args:
|
423
|
-
block: The block to check
|
424
|
-
|
425
|
-
Returns:
|
426
|
-
True if it's an empty paragraph, False otherwise
|
427
|
-
"""
|
431
|
+
"""Check if a block is an empty paragraph."""
|
428
432
|
if block.get("type") != "paragraph":
|
429
433
|
return False
|
430
434
|
|
431
435
|
rich_text = block.get("paragraph", {}).get("rich_text", [])
|
432
436
|
return not rich_text or len(rich_text) == 0
|
433
|
-
|
434
|
-
def _create_empty_paragraph(self) -> Dict[str, Any]:
|
435
|
-
"""
|
436
|
-
Create an empty paragraph block.
|
437
|
-
|
438
|
-
Returns:
|
439
|
-
Empty paragraph block dictionary
|
440
|
-
"""
|
441
|
-
return {"type": "paragraph", "paragraph": {"rich_text": []}}
|