notionary 0.2.21__py3-none-any.whl → 0.2.23__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/blocks/_bootstrap.py +9 -1
- notionary/blocks/audio/audio_element.py +53 -28
- notionary/blocks/audio/audio_markdown_node.py +10 -4
- notionary/blocks/base_block_element.py +15 -3
- notionary/blocks/bookmark/bookmark_element.py +39 -36
- notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
- notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
- notionary/blocks/callout/callout_element.py +20 -4
- notionary/blocks/child_database/__init__.py +11 -4
- notionary/blocks/child_database/child_database_element.py +59 -0
- notionary/blocks/child_database/child_database_models.py +7 -14
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/client.py +0 -1
- notionary/blocks/code/code_element.py +51 -2
- notionary/blocks/code/code_markdown_node.py +52 -1
- notionary/blocks/column/column_element.py +9 -3
- notionary/blocks/column/column_list_element.py +18 -3
- notionary/blocks/divider/divider_element.py +3 -11
- notionary/blocks/embed/embed_element.py +27 -6
- notionary/blocks/equation/equation_element.py +94 -41
- notionary/blocks/equation/equation_element_markdown_node.py +8 -9
- notionary/blocks/file/file_element.py +56 -37
- notionary/blocks/file/file_element_markdown_node.py +9 -7
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/heading_element.py +23 -4
- notionary/blocks/image_block/image_element.py +43 -38
- notionary/blocks/image_block/image_markdown_node.py +10 -5
- notionary/blocks/mixins/captions/__init__.py +4 -0
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
- notionary/blocks/mixins/captions/caption_mixin.py +92 -0
- notionary/blocks/models.py +3 -1
- notionary/blocks/numbered_list/numbered_list_element.py +21 -4
- notionary/blocks/paragraph/paragraph_element.py +21 -5
- notionary/blocks/pdf/pdf_element.py +47 -41
- notionary/blocks/pdf/pdf_markdown_node.py +9 -7
- notionary/blocks/quote/quote_element.py +26 -9
- notionary/blocks/quote/quote_markdown_node.py +2 -2
- notionary/blocks/registry/block_registry.py +1 -46
- notionary/blocks/registry/block_registry_builder.py +8 -0
- notionary/blocks/rich_text/rich_text_models.py +62 -29
- notionary/blocks/rich_text/text_inline_formatter.py +432 -101
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/table_element.py +110 -9
- notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
- notionary/blocks/todo/todo_element.py +21 -4
- notionary/blocks/toggle/toggle_element.py +19 -3
- notionary/blocks/toggle/toggle_markdown_node.py +1 -1
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
- notionary/blocks/types.py +69 -0
- notionary/blocks/video/video_element.py +44 -39
- notionary/blocks/video/video_markdown_node.py +10 -5
- notionary/comments/__init__.py +26 -0
- notionary/comments/client.py +211 -0
- notionary/comments/models.py +129 -0
- notionary/database/client.py +23 -0
- notionary/file_upload/models.py +2 -2
- notionary/markdown/markdown_builder.py +34 -27
- notionary/page/client.py +21 -6
- notionary/page/notion_page.py +77 -2
- notionary/page/page_content_deleting_service.py +117 -0
- notionary/page/page_content_writer.py +89 -113
- notionary/page/page_context.py +64 -0
- notionary/page/reader/handler/__init__.py +2 -0
- notionary/page/reader/handler/base_block_renderer.py +4 -4
- notionary/page/reader/handler/block_rendering_context.py +5 -0
- notionary/page/reader/handler/line_renderer.py +16 -3
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/page_content_retriever.py +17 -5
- notionary/page/writer/handler/__init__.py +2 -0
- notionary/page/writer/handler/code_handler.py +12 -40
- notionary/page/writer/handler/column_handler.py +12 -12
- notionary/page/writer/handler/column_list_handler.py +13 -13
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +4 -4
- notionary/page/writer/handler/regular_line_handler.py +31 -37
- notionary/page/writer/handler/table_handler.py +8 -72
- notionary/page/writer/handler/toggle_handler.py +14 -12
- notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
- notionary/page/writer/markdown_to_notion_converter.py +28 -9
- notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
- notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +150 -0
- notionary/shared/__init__.py +5 -0
- notionary/shared/name_to_id_resolver.py +203 -0
- notionary/telemetry/service.py +0 -1
- notionary/user/notion_user_manager.py +22 -95
- notionary/util/concurrency_limiter.py +0 -0
- notionary/workspace.py +4 -4
- notionary-0.2.23.dist-info/METADATA +235 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
- notionary/page/markdown_whitespace_processor.py +0 -80
- notionary/page/notion_text_length_utils.py +0 -119
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.21.dist-info/METADATA +0 -229
- /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
@@ -1,3 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
1
5
|
from notionary.page.reader.handler import BlockHandler, BlockRenderingContext
|
2
6
|
|
3
7
|
|
@@ -8,9 +12,9 @@ class LineRenderer(BlockHandler):
|
|
8
12
|
# Always can handle - this is the fallback handler
|
9
13
|
return True
|
10
14
|
|
11
|
-
def _process(self, context: BlockRenderingContext) -> None:
|
12
|
-
# Convert the block itself
|
13
|
-
block_markdown =
|
15
|
+
async def _process(self, context: BlockRenderingContext) -> None:
|
16
|
+
# Convert the block itself using direct element iteration
|
17
|
+
block_markdown = await self._convert_block_to_markdown(context)
|
14
18
|
|
15
19
|
# If block has no direct markdown, either return empty or process children
|
16
20
|
if not block_markdown:
|
@@ -58,3 +62,12 @@ class LineRenderer(BlockHandler):
|
|
58
62
|
else block_markdown
|
59
63
|
)
|
60
64
|
context.was_processed = True
|
65
|
+
|
66
|
+
async def _convert_block_to_markdown(
|
67
|
+
self, context: BlockRenderingContext
|
68
|
+
) -> Optional[str]:
|
69
|
+
"""Convert a Notion block to markdown using registered elements."""
|
70
|
+
for element in context.block_registry.get_elements():
|
71
|
+
if element.match_notion(context.block):
|
72
|
+
return await element.notion_to_markdown(context.block)
|
73
|
+
return None
|
@@ -0,0 +1,85 @@
|
|
1
|
+
from notionary.blocks.models import Block, BlockType
|
2
|
+
from notionary.blocks.registry.block_registry import BlockRegistry
|
3
|
+
from notionary.page.reader.handler.base_block_renderer import BlockHandler
|
4
|
+
from notionary.page.reader.handler.block_rendering_context import BlockRenderingContext
|
5
|
+
|
6
|
+
|
7
|
+
class NumberedListRenderer(BlockHandler):
|
8
|
+
"""Handles numbered list items with sequential numbering."""
|
9
|
+
|
10
|
+
def _can_handle(self, context: BlockRenderingContext) -> bool:
|
11
|
+
"""Check if this is a numbered list item."""
|
12
|
+
return (
|
13
|
+
context.block.type == BlockType.NUMBERED_LIST_ITEM
|
14
|
+
and context.block.numbered_list_item is not None
|
15
|
+
)
|
16
|
+
|
17
|
+
async def _process(self, context: BlockRenderingContext) -> None:
|
18
|
+
"""Process numbered list item with sequential numbering."""
|
19
|
+
if context.all_blocks is None or context.current_block_index is None:
|
20
|
+
await self._process_single_item(context, 1)
|
21
|
+
return
|
22
|
+
|
23
|
+
items, blocks_to_skip = self._collect_numbered_list_items(context)
|
24
|
+
|
25
|
+
markdown_parts = []
|
26
|
+
for i, item_context in enumerate(items, 1):
|
27
|
+
item_markdown = await self._process_single_item(item_context, i)
|
28
|
+
if item_markdown:
|
29
|
+
markdown_parts.append(item_markdown)
|
30
|
+
|
31
|
+
# Set result and mark how many blocks to skip
|
32
|
+
if markdown_parts:
|
33
|
+
context.markdown_result = "\n".join(markdown_parts)
|
34
|
+
context.was_processed = True
|
35
|
+
context.blocks_consumed = blocks_to_skip
|
36
|
+
|
37
|
+
def _collect_numbered_list_items(
|
38
|
+
self, context: BlockRenderingContext
|
39
|
+
) -> tuple[list[BlockRenderingContext], int]:
|
40
|
+
"""Collect all consecutive numbered list items starting from current position."""
|
41
|
+
items = []
|
42
|
+
current_index = context.current_block_index
|
43
|
+
all_blocks = context.all_blocks
|
44
|
+
|
45
|
+
# Start with current block
|
46
|
+
items.append(context)
|
47
|
+
blocks_processed = 1
|
48
|
+
|
49
|
+
# Look ahead for more numbered list items
|
50
|
+
for i in range(current_index + 1, len(all_blocks)):
|
51
|
+
block = all_blocks[i]
|
52
|
+
|
53
|
+
# Check if it's a numbered list item
|
54
|
+
if (
|
55
|
+
block.type == BlockType.NUMBERED_LIST_ITEM
|
56
|
+
and block.numbered_list_item is not None
|
57
|
+
):
|
58
|
+
|
59
|
+
# Create context for this item
|
60
|
+
item_context = BlockRenderingContext(
|
61
|
+
block=block,
|
62
|
+
indent_level=context.indent_level,
|
63
|
+
block_registry=context.block_registry,
|
64
|
+
convert_children_callback=context.convert_children_callback,
|
65
|
+
)
|
66
|
+
items.append(item_context)
|
67
|
+
blocks_processed += 1
|
68
|
+
else:
|
69
|
+
# Not a numbered list item - stop collecting
|
70
|
+
break
|
71
|
+
|
72
|
+
return items, blocks_processed
|
73
|
+
|
74
|
+
async def _process_single_item(
|
75
|
+
self, context: BlockRenderingContext, number: int
|
76
|
+
) -> str:
|
77
|
+
"""Process a single numbered list item with the given number."""
|
78
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
79
|
+
|
80
|
+
rich_text = context.block.numbered_list_item.rich_text
|
81
|
+
content = await TextInlineFormatter.extract_text_with_formatting(rich_text)
|
82
|
+
|
83
|
+
# Apply indentation
|
84
|
+
indent = " " * context.indent_level
|
85
|
+
return f"{indent}{number}. {content}"
|
@@ -5,6 +5,7 @@ from notionary.page.reader.handler import (
|
|
5
5
|
ColumnListRenderer,
|
6
6
|
ColumnRenderer,
|
7
7
|
LineRenderer,
|
8
|
+
NumberedListRenderer,
|
8
9
|
ToggleableHeadingRenderer,
|
9
10
|
ToggleRenderer,
|
10
11
|
)
|
@@ -27,7 +28,7 @@ class PageContentRetriever(LoggingMixin):
|
|
27
28
|
Retrieve page content and convert it to Markdown.
|
28
29
|
Uses the chain of responsibility pattern for scalable block processing.
|
29
30
|
"""
|
30
|
-
return self._convert_blocks_to_markdown(blocks, indent_level=0)
|
31
|
+
return await self._convert_blocks_to_markdown(blocks, indent_level=0)
|
31
32
|
|
32
33
|
def _setup_handler_chain(self) -> None:
|
33
34
|
"""Setup the chain of handlers in priority order."""
|
@@ -35,16 +36,19 @@ class PageContentRetriever(LoggingMixin):
|
|
35
36
|
toggleable_heading_handler = ToggleableHeadingRenderer()
|
36
37
|
column_list_handler = ColumnListRenderer()
|
37
38
|
column_handler = ColumnRenderer()
|
39
|
+
numbered_list_handler = NumberedListRenderer()
|
38
40
|
regular_handler = LineRenderer()
|
39
41
|
|
40
42
|
# Chain handlers - most specific first
|
41
43
|
toggle_handler.set_next(toggleable_heading_handler).set_next(
|
42
44
|
column_list_handler
|
43
|
-
).set_next(column_handler).set_next(
|
45
|
+
).set_next(column_handler).set_next(numbered_list_handler).set_next(
|
46
|
+
regular_handler
|
47
|
+
)
|
44
48
|
|
45
49
|
self._handler_chain = toggle_handler
|
46
50
|
|
47
|
-
def _convert_blocks_to_markdown(
|
51
|
+
async def _convert_blocks_to_markdown(
|
48
52
|
self, blocks: list[Block], indent_level: int = 0
|
49
53
|
) -> str:
|
50
54
|
"""Convert blocks to Markdown using the handler chain."""
|
@@ -52,18 +56,26 @@ class PageContentRetriever(LoggingMixin):
|
|
52
56
|
return ""
|
53
57
|
|
54
58
|
markdown_parts = []
|
59
|
+
i = 0
|
55
60
|
|
56
|
-
|
61
|
+
while i < len(blocks):
|
62
|
+
block = blocks[i]
|
57
63
|
context = BlockRenderingContext(
|
58
64
|
block=block,
|
59
65
|
indent_level=indent_level,
|
60
66
|
block_registry=self._block_registry,
|
67
|
+
all_blocks=blocks,
|
68
|
+
current_block_index=i,
|
69
|
+
convert_children_callback=self._convert_blocks_to_markdown,
|
61
70
|
)
|
62
71
|
|
63
|
-
self._handler_chain.handle(context)
|
72
|
+
await self._handler_chain.handle(context)
|
64
73
|
|
65
74
|
if context.was_processed and context.markdown_result:
|
66
75
|
markdown_parts.append(context.markdown_result)
|
67
76
|
|
77
|
+
# Skip additional blocks if they were consumed by batch processing
|
78
|
+
i += max(1, context.blocks_consumed)
|
79
|
+
|
68
80
|
separator = "\n\n" if indent_level == 0 else "\n"
|
69
81
|
return separator.join(markdown_parts)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from .code_handler import CodeHandler
|
2
2
|
from .column_handler import ColumnHandler
|
3
3
|
from .column_list_handler import ColumnListHandler
|
4
|
+
from .equation_handler import EquationHandler
|
4
5
|
from .line_handler import LineHandler
|
5
6
|
from .line_processing_context import LineProcessingContext, ParentBlockContext
|
6
7
|
from .regular_line_handler import RegularLineHandler
|
@@ -19,4 +20,5 @@ __all__ = [
|
|
19
20
|
"TableHandler",
|
20
21
|
"RegularLineHandler",
|
21
22
|
"CodeHandler",
|
23
|
+
"EquationHandler",
|
22
24
|
]
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import re
|
2
2
|
|
3
3
|
from notionary.blocks.code.code_element import CodeElement
|
4
|
-
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
5
4
|
from notionary.page.writer.handler.line_handler import (
|
6
5
|
LineHandler,
|
7
6
|
LineProcessingContext,
|
@@ -27,9 +26,9 @@ class CodeHandler(LineHandler):
|
|
27
26
|
return False
|
28
27
|
return self._is_code_start(context)
|
29
28
|
|
30
|
-
def _process(self, context: LineProcessingContext) -> None:
|
29
|
+
async def _process(self, context: LineProcessingContext) -> None:
|
31
30
|
if self._is_code_start(context):
|
32
|
-
self._process_complete_code_block(context)
|
31
|
+
await self._process_complete_code_block(context)
|
33
32
|
self._mark_processed(context)
|
34
33
|
|
35
34
|
def _is_code_start(self, context: LineProcessingContext) -> bool:
|
@@ -40,33 +39,19 @@ class CodeHandler(LineHandler):
|
|
40
39
|
"""Check if we're currently inside any parent context (toggle, heading, etc.)."""
|
41
40
|
return len(context.parent_stack) > 0
|
42
41
|
|
43
|
-
def _process_complete_code_block(
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# Create base code block
|
49
|
-
result = CodeElement.markdown_to_notion(f"```{language}")
|
50
|
-
if not result:
|
51
|
-
return
|
52
|
-
|
53
|
-
block = result[0] if isinstance(result, list) else result
|
54
|
-
|
42
|
+
async def _process_complete_code_block(
|
43
|
+
self, context: LineProcessingContext
|
44
|
+
) -> None:
|
45
|
+
"""Process the entire code block in one go using CodeElement."""
|
55
46
|
code_lines, lines_to_consume = self._collect_code_lines(context)
|
56
47
|
|
57
|
-
|
48
|
+
block = CodeElement.create_from_markdown_block(
|
49
|
+
opening_line=context.line, code_lines=code_lines
|
50
|
+
)
|
58
51
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
context.result_blocks.append(block)
|
63
|
-
|
64
|
-
def _extract_fence_info(self, line: str) -> tuple[str, str]:
|
65
|
-
"""Extract the language and optional caption from a code fence."""
|
66
|
-
match = self._code_start_pattern.match(line.strip())
|
67
|
-
lang = match.group(1) if match and match.group(1) else ""
|
68
|
-
cap = match.group(2) if match and match.group(2) else ""
|
69
|
-
return lang, cap
|
52
|
+
if block:
|
53
|
+
context.lines_consumed = lines_to_consume
|
54
|
+
context.result_blocks.append(block)
|
70
55
|
|
71
56
|
def _collect_code_lines(
|
72
57
|
self, context: LineProcessingContext
|
@@ -85,16 +70,3 @@ class CodeHandler(LineHandler):
|
|
85
70
|
"""Mark context as processed and continue."""
|
86
71
|
context.was_processed = True
|
87
72
|
context.should_continue = True
|
88
|
-
|
89
|
-
def _set_block_content(self, block, code_lines: list[str]) -> None:
|
90
|
-
"""Set the code rich_text content on the block."""
|
91
|
-
if not code_lines:
|
92
|
-
return
|
93
|
-
content = "\n".join(code_lines)
|
94
|
-
block.code.rich_text = [RichTextObject.for_code_block(content)]
|
95
|
-
|
96
|
-
def _set_block_caption(self, block, caption: str) -> None:
|
97
|
-
"""Append caption to the code block if provided."""
|
98
|
-
if not caption:
|
99
|
-
return
|
100
|
-
block.code.caption.append(RichTextObject.for_code_block(caption))
|
@@ -26,14 +26,14 @@ class ColumnHandler(LineHandler):
|
|
26
26
|
def _can_handle(self, context: LineProcessingContext) -> bool:
|
27
27
|
return self._is_column_start(context) or self._is_column_end(context)
|
28
28
|
|
29
|
-
def _process(self, context: LineProcessingContext) -> None:
|
29
|
+
async def _process(self, context: LineProcessingContext) -> None:
|
30
30
|
if self._is_column_start(context):
|
31
|
-
self._start_column(context)
|
31
|
+
await self._start_column(context)
|
32
32
|
self._mark_processed(context)
|
33
33
|
return
|
34
34
|
|
35
35
|
if self._is_column_end(context):
|
36
|
-
self._finalize_column(context)
|
36
|
+
await self._finalize_column(context)
|
37
37
|
self._mark_processed(context)
|
38
38
|
|
39
39
|
def _is_column_start(self, context: LineProcessingContext) -> bool:
|
@@ -52,15 +52,15 @@ class ColumnHandler(LineHandler):
|
|
52
52
|
current_parent = context.parent_stack[-1]
|
53
53
|
return issubclass(current_parent.element_type, ColumnElement)
|
54
54
|
|
55
|
-
def _start_column(self, context: LineProcessingContext) -> None:
|
55
|
+
async def _start_column(self, context: LineProcessingContext) -> None:
|
56
56
|
"""Start a new column."""
|
57
57
|
# Create Column block directly - much more efficient!
|
58
58
|
column_element = ColumnElement()
|
59
|
-
result = column_element.markdown_to_notion(context.line)
|
59
|
+
result = await column_element.markdown_to_notion(context.line)
|
60
60
|
if not result:
|
61
61
|
return
|
62
62
|
|
63
|
-
block = result
|
63
|
+
block = result
|
64
64
|
|
65
65
|
# Push to parent stack
|
66
66
|
parent_context = ParentBlockContext(
|
@@ -70,10 +70,10 @@ class ColumnHandler(LineHandler):
|
|
70
70
|
)
|
71
71
|
context.parent_stack.append(parent_context)
|
72
72
|
|
73
|
-
def _finalize_column(self, context: LineProcessingContext) -> None:
|
73
|
+
async def _finalize_column(self, context: LineProcessingContext) -> None:
|
74
74
|
"""Finalize a single column and add it to the column list or result."""
|
75
75
|
column_context = context.parent_stack.pop()
|
76
|
-
self._assign_column_children_if_any(column_context, context)
|
76
|
+
await self._assign_column_children_if_any(column_context, context)
|
77
77
|
|
78
78
|
if context.parent_stack:
|
79
79
|
parent = context.parent_stack[-1]
|
@@ -87,7 +87,7 @@ class ColumnHandler(LineHandler):
|
|
87
87
|
# Fallback: no parent or parent is not ColumnList
|
88
88
|
context.result_blocks.append(column_context.block)
|
89
89
|
|
90
|
-
def _assign_column_children_if_any(
|
90
|
+
async def _assign_column_children_if_any(
|
91
91
|
self, column_context: ParentBlockContext, context: LineProcessingContext
|
92
92
|
) -> None:
|
93
93
|
"""Collect and assign any children blocks inside this column."""
|
@@ -96,7 +96,7 @@ class ColumnHandler(LineHandler):
|
|
96
96
|
# Process text lines
|
97
97
|
if column_context.child_lines:
|
98
98
|
children_text = "\n".join(column_context.child_lines)
|
99
|
-
text_blocks = self._convert_children_text(
|
99
|
+
text_blocks = await self._convert_children_text(
|
100
100
|
children_text, context.block_registry
|
101
101
|
)
|
102
102
|
all_children.extend(text_blocks)
|
@@ -123,7 +123,7 @@ class ColumnHandler(LineHandler):
|
|
123
123
|
parent.block.column_list.children.append(column_context.block)
|
124
124
|
return True
|
125
125
|
|
126
|
-
def _convert_children_text(self, text: str, block_registry) -> list:
|
126
|
+
async def _convert_children_text(self, text: str, block_registry) -> list:
|
127
127
|
"""Convert children text to blocks."""
|
128
128
|
from notionary.page.writer.markdown_to_notion_converter import (
|
129
129
|
MarkdownToNotionConverter,
|
@@ -133,7 +133,7 @@ class ColumnHandler(LineHandler):
|
|
133
133
|
return []
|
134
134
|
|
135
135
|
child_converter = MarkdownToNotionConverter(block_registry)
|
136
|
-
return child_converter.
|
136
|
+
return await child_converter.process_lines(text)
|
137
137
|
|
138
138
|
def _mark_processed(self, context: LineProcessingContext) -> None:
|
139
139
|
"""Mark context as processed and signal to continue."""
|
@@ -7,9 +7,9 @@ from notionary.page.writer.handler.line_handler import (
|
|
7
7
|
LineHandler,
|
8
8
|
LineProcessingContext,
|
9
9
|
)
|
10
|
-
|
11
10
|
from notionary.page.writer.handler.line_processing_context import ParentBlockContext
|
12
11
|
|
12
|
+
|
13
13
|
class ColumnListHandler(LineHandler):
|
14
14
|
"""Handles column list elements - both start and end.
|
15
15
|
Syntax:
|
@@ -31,15 +31,15 @@ class ColumnListHandler(LineHandler):
|
|
31
31
|
def _can_handle(self, context: LineProcessingContext) -> bool:
|
32
32
|
return self._is_column_list_start(context) or self._is_column_list_end(context)
|
33
33
|
|
34
|
-
def _process(self, context: LineProcessingContext) -> None:
|
34
|
+
async def _process(self, context: LineProcessingContext) -> None:
|
35
35
|
if self._is_column_list_start(context):
|
36
|
-
self._start_column_list(context)
|
36
|
+
await self._start_column_list(context)
|
37
37
|
context.was_processed = True
|
38
38
|
context.should_continue = True
|
39
39
|
return
|
40
40
|
|
41
41
|
if self._is_column_list_end(context):
|
42
|
-
self._finalize_column_list(context)
|
42
|
+
await self._finalize_column_list(context)
|
43
43
|
context.was_processed = True
|
44
44
|
context.should_continue = True
|
45
45
|
|
@@ -59,7 +59,7 @@ class ColumnListHandler(LineHandler):
|
|
59
59
|
current_parent = context.parent_stack[-1]
|
60
60
|
return issubclass(current_parent.element_type, ColumnListElement)
|
61
61
|
|
62
|
-
def _start_column_list(self, context: LineProcessingContext) -> None:
|
62
|
+
async def _start_column_list(self, context: LineProcessingContext) -> None:
|
63
63
|
"""Start a new column list."""
|
64
64
|
# Create ColumnList block using the element from registry
|
65
65
|
column_list_element = None
|
@@ -72,11 +72,11 @@ class ColumnListHandler(LineHandler):
|
|
72
72
|
return
|
73
73
|
|
74
74
|
# Create the block
|
75
|
-
result = column_list_element.markdown_to_notion(context.line)
|
75
|
+
result = await column_list_element.markdown_to_notion(context.line)
|
76
76
|
if not result:
|
77
77
|
return
|
78
78
|
|
79
|
-
block = result
|
79
|
+
block = result
|
80
80
|
|
81
81
|
# Push to parent stack
|
82
82
|
parent_context = ParentBlockContext(
|
@@ -86,10 +86,10 @@ class ColumnListHandler(LineHandler):
|
|
86
86
|
)
|
87
87
|
context.parent_stack.append(parent_context)
|
88
88
|
|
89
|
-
def _finalize_column_list(self, context: LineProcessingContext) -> None:
|
89
|
+
async def _finalize_column_list(self, context: LineProcessingContext) -> None:
|
90
90
|
"""Finalize a column list and add it to result_blocks."""
|
91
91
|
column_list_context = context.parent_stack.pop()
|
92
|
-
self._assign_column_list_children_if_any(column_list_context, context)
|
92
|
+
await self._assign_column_list_children_if_any(column_list_context, context)
|
93
93
|
|
94
94
|
# Check if we have a parent context to add this column_list to
|
95
95
|
if context.parent_stack:
|
@@ -101,7 +101,7 @@ class ColumnListHandler(LineHandler):
|
|
101
101
|
# No parent, add to top level
|
102
102
|
context.result_blocks.append(column_list_context.block)
|
103
103
|
|
104
|
-
def _assign_column_list_children_if_any(
|
104
|
+
async def _assign_column_list_children_if_any(
|
105
105
|
self, column_list_context: ParentBlockContext, context: LineProcessingContext
|
106
106
|
) -> None:
|
107
107
|
"""Collect and assign any column children blocks inside this column list."""
|
@@ -110,7 +110,7 @@ class ColumnListHandler(LineHandler):
|
|
110
110
|
# Process text lines
|
111
111
|
if column_list_context.child_lines:
|
112
112
|
children_text = "\n".join(column_list_context.child_lines)
|
113
|
-
children_blocks = self._convert_children_text(
|
113
|
+
children_blocks = await self._convert_children_text(
|
114
114
|
children_text, context.block_registry
|
115
115
|
)
|
116
116
|
all_children.extend(children_blocks)
|
@@ -126,7 +126,7 @@ class ColumnListHandler(LineHandler):
|
|
126
126
|
]
|
127
127
|
column_list_context.block.column_list.children = column_children
|
128
128
|
|
129
|
-
def _convert_children_text(self, text: str, block_registry) -> list:
|
129
|
+
async def _convert_children_text(self, text: str, block_registry) -> list:
|
130
130
|
"""Convert children text to blocks."""
|
131
131
|
from notionary.page.writer.markdown_to_notion_converter import (
|
132
132
|
MarkdownToNotionConverter,
|
@@ -136,4 +136,4 @@ class ColumnListHandler(LineHandler):
|
|
136
136
|
return []
|
137
137
|
|
138
138
|
child_converter = MarkdownToNotionConverter(block_registry)
|
139
|
-
return child_converter.
|
139
|
+
return await child_converter.process_lines(text)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
from notionary.blocks.equation.equation_element import EquationElement
|
4
|
+
from notionary.page.writer.handler.line_handler import (
|
5
|
+
LineHandler,
|
6
|
+
LineProcessingContext,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
class EquationHandler(LineHandler):
|
11
|
+
"""Handles equation block specific logic with batching.
|
12
|
+
|
13
|
+
Markdown syntax:
|
14
|
+
$$
|
15
|
+
\sum_{i=1}^n i = \frac{n(n+1)}{2} \\
|
16
|
+
\sum_{i=1}^n i^2 = \frac{n(n+1)(2n+1)}{6} \\
|
17
|
+
\sum_{i=1}^n i^3 = \left(\frac{n(n+1)}{2}\right)^2
|
18
|
+
$$
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self):
|
22
|
+
super().__init__()
|
23
|
+
self._equation_start_pattern = re.compile(r"^\$\$\s*$")
|
24
|
+
self._equation_end_pattern = re.compile(r"^\$\$\s*$")
|
25
|
+
|
26
|
+
def _can_handle(self, context: LineProcessingContext) -> bool:
|
27
|
+
if self._is_inside_parent_context(context):
|
28
|
+
return False
|
29
|
+
return self._is_equation_start(context)
|
30
|
+
|
31
|
+
async def _process(self, context: LineProcessingContext) -> None:
|
32
|
+
if self._is_equation_start(context):
|
33
|
+
await self._process_complete_equation_block(context)
|
34
|
+
self._mark_processed(context)
|
35
|
+
|
36
|
+
def _is_equation_start(self, context: LineProcessingContext) -> bool:
|
37
|
+
"""Check if this line starts an equation block."""
|
38
|
+
return self._equation_start_pattern.match(context.line.strip()) is not None
|
39
|
+
|
40
|
+
def _is_inside_parent_context(self, context: LineProcessingContext) -> bool:
|
41
|
+
"""Check if we're currently inside any parent context (toggle, heading, etc.)."""
|
42
|
+
return len(context.parent_stack) > 0
|
43
|
+
|
44
|
+
async def _process_complete_equation_block(
|
45
|
+
self, context: LineProcessingContext
|
46
|
+
) -> None:
|
47
|
+
"""Process the entire equation block in one go using EquationElement."""
|
48
|
+
equation_lines, lines_to_consume = self._collect_equation_lines(context)
|
49
|
+
|
50
|
+
block = EquationElement.create_from_markdown_block(
|
51
|
+
opening_line=context.line, equation_lines=equation_lines
|
52
|
+
)
|
53
|
+
|
54
|
+
if block:
|
55
|
+
context.lines_consumed = lines_to_consume
|
56
|
+
context.result_blocks.append(block)
|
57
|
+
|
58
|
+
def _collect_equation_lines(
|
59
|
+
self, context: LineProcessingContext
|
60
|
+
) -> tuple[list[str], int]:
|
61
|
+
"""Collect lines until closing $$ fence and return (lines, count_to_consume)."""
|
62
|
+
lines = []
|
63
|
+
for idx, ln in enumerate(context.get_remaining_lines()):
|
64
|
+
if self._equation_end_pattern.match(ln.strip()):
|
65
|
+
return lines, idx + 1
|
66
|
+
lines.append(ln)
|
67
|
+
# No closing fence: consume all remaining
|
68
|
+
rem = context.get_remaining_lines()
|
69
|
+
return rem, len(rem)
|
70
|
+
|
71
|
+
def _mark_processed(self, context: LineProcessingContext) -> None:
|
72
|
+
"""Mark context as processed and continue."""
|
73
|
+
context.was_processed = True
|
74
|
+
context.should_continue = True
|
@@ -17,12 +17,12 @@ class LineHandler(ABC):
|
|
17
17
|
self._next_handler = handler
|
18
18
|
return handler
|
19
19
|
|
20
|
-
def handle(self, context: LineProcessingContext) -> None:
|
20
|
+
async def handle(self, context: LineProcessingContext) -> None:
|
21
21
|
"""Handle the line or pass to next handler."""
|
22
22
|
if self._can_handle(context):
|
23
|
-
self._process(context)
|
23
|
+
await self._process(context)
|
24
24
|
elif self._next_handler:
|
25
|
-
self._next_handler.handle(context)
|
25
|
+
await self._next_handler.handle(context)
|
26
26
|
|
27
27
|
@abstractmethod
|
28
28
|
def _can_handle(self, context: LineProcessingContext) -> bool:
|
@@ -30,6 +30,6 @@ class LineHandler(ABC):
|
|
30
30
|
pass
|
31
31
|
|
32
32
|
@abstractmethod
|
33
|
-
def _process(self, context: LineProcessingContext) -> None:
|
33
|
+
async def _process(self, context: LineProcessingContext) -> None:
|
34
34
|
"""Process the line and update context."""
|
35
35
|
pass
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.column.column_element import ColumnElement
|
2
2
|
from notionary.blocks.column.column_list_element import ColumnListElement
|
3
|
-
from notionary.blocks.models import BlockCreateRequest, BlockCreateResult
|
4
3
|
from notionary.page.writer.handler import LineHandler, LineProcessingContext
|
5
4
|
|
6
5
|
|
@@ -10,16 +9,16 @@ class RegularLineHandler(LineHandler):
|
|
10
9
|
def _can_handle(self, context: LineProcessingContext) -> bool:
|
11
10
|
return context.line.strip()
|
12
11
|
|
13
|
-
def _process(self, context: LineProcessingContext) -> None:
|
12
|
+
async def _process(self, context: LineProcessingContext) -> None:
|
14
13
|
if self._is_in_column_context(context):
|
15
14
|
self._add_to_column_context(context)
|
16
15
|
context.was_processed = True
|
17
16
|
context.should_continue = True
|
18
17
|
return
|
19
18
|
|
20
|
-
block_created = self._process_single_line_content(context)
|
19
|
+
block_created = await self._process_single_line_content(context)
|
21
20
|
if not block_created:
|
22
|
-
self._process_as_paragraph(context)
|
21
|
+
await self._process_as_paragraph(context)
|
23
22
|
|
24
23
|
context.was_processed = True
|
25
24
|
|
@@ -37,56 +36,51 @@ class RegularLineHandler(LineHandler):
|
|
37
36
|
"""Add line as child to the current Column context."""
|
38
37
|
context.parent_stack[-1].add_child_line(context.line)
|
39
38
|
|
40
|
-
def _process_single_line_content(
|
39
|
+
async def _process_single_line_content(
|
40
|
+
self, context: LineProcessingContext
|
41
|
+
) -> bool:
|
41
42
|
"""Process a regular line for simple elements (lists, etc.)."""
|
43
|
+
specialized_elements = self._get_specialized_elements()
|
44
|
+
|
42
45
|
for element in context.block_registry.get_elements():
|
43
|
-
# Skip all elements that have specialized handlers
|
44
|
-
from notionary.blocks.code import CodeElement
|
45
|
-
from notionary.blocks.paragraph import ParagraphElement
|
46
|
-
from notionary.blocks.table import TableElement
|
47
|
-
from notionary.blocks.toggle import ToggleElement
|
48
|
-
from notionary.blocks.toggleable_heading import ToggleableHeadingElement
|
49
|
-
|
50
|
-
specialized_elements = (
|
51
|
-
ColumnListElement,
|
52
|
-
ColumnElement,
|
53
|
-
ToggleElement,
|
54
|
-
ToggleableHeadingElement,
|
55
|
-
TableElement,
|
56
|
-
CodeElement,
|
57
|
-
ParagraphElement, # Skip paragraph to ensure equations are processed first
|
58
|
-
)
|
59
46
|
|
60
47
|
if issubclass(element, specialized_elements):
|
61
48
|
continue
|
62
49
|
|
63
|
-
result = element.markdown_to_notion(context.line)
|
50
|
+
result = await element.markdown_to_notion(context.line)
|
64
51
|
if not result:
|
65
52
|
continue
|
66
53
|
|
67
|
-
|
68
|
-
for block in blocks:
|
69
|
-
context.result_blocks.append(block)
|
54
|
+
context.result_blocks.append(result)
|
70
55
|
|
71
56
|
return True
|
72
57
|
|
73
58
|
return False
|
74
59
|
|
75
|
-
def _process_as_paragraph(self, context: LineProcessingContext) -> None:
|
60
|
+
async def _process_as_paragraph(self, context: LineProcessingContext) -> None:
|
76
61
|
"""Process a line as a paragraph."""
|
77
62
|
from notionary.blocks.paragraph.paragraph_element import ParagraphElement
|
78
63
|
|
79
64
|
paragraph_element = ParagraphElement()
|
80
|
-
result = paragraph_element.markdown_to_notion(context.line)
|
65
|
+
result = await paragraph_element.markdown_to_notion(context.line)
|
81
66
|
|
82
67
|
if result:
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
68
|
+
context.result_blocks.append(result)
|
69
|
+
|
70
|
+
def _get_specialized_elements(self):
|
71
|
+
"""Get tuple of elements that have specialized handlers."""
|
72
|
+
from notionary.blocks.code import CodeElement
|
73
|
+
from notionary.blocks.paragraph import ParagraphElement
|
74
|
+
from notionary.blocks.table import TableElement
|
75
|
+
from notionary.blocks.toggle import ToggleElement
|
76
|
+
from notionary.blocks.toggleable_heading import ToggleableHeadingElement
|
77
|
+
|
78
|
+
return (
|
79
|
+
ColumnListElement,
|
80
|
+
ColumnElement,
|
81
|
+
ToggleElement,
|
82
|
+
ToggleableHeadingElement,
|
83
|
+
TableElement,
|
84
|
+
CodeElement,
|
85
|
+
ParagraphElement,
|
86
|
+
)
|