notionary 0.2.28__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +9 -2
- notionary/blocks/__init__.py +5 -0
- notionary/blocks/client.py +6 -4
- notionary/blocks/enums.py +28 -1
- notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
- notionary/blocks/rich_text/models.py +14 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
- notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
- notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
- notionary/blocks/rich_text/rich_text_patterns.py +3 -0
- notionary/blocks/schemas.py +42 -10
- notionary/comments/__init__.py +5 -0
- notionary/comments/client.py +7 -10
- notionary/comments/factory.py +4 -6
- notionary/data_source/http/data_source_instance_client.py +14 -4
- notionary/data_source/properties/{models.py → schemas.py} +4 -8
- notionary/data_source/query/__init__.py +9 -0
- notionary/data_source/query/builder.py +38 -10
- notionary/data_source/query/schema.py +13 -10
- notionary/data_source/query/validator.py +11 -11
- notionary/data_source/schema/registry.py +104 -0
- notionary/data_source/schema/service.py +136 -0
- notionary/data_source/schemas.py +1 -1
- notionary/data_source/service.py +29 -103
- notionary/database/service.py +17 -60
- notionary/exceptions/__init__.py +5 -1
- notionary/exceptions/block_parsing.py +21 -0
- notionary/exceptions/search.py +24 -0
- notionary/http/client.py +9 -10
- notionary/http/models.py +5 -4
- notionary/page/content/factory.py +10 -3
- notionary/page/content/markdown/builder.py +76 -154
- notionary/page/content/markdown/nodes/__init__.py +0 -2
- notionary/page/content/markdown/nodes/audio.py +1 -1
- notionary/page/content/markdown/nodes/base.py +1 -1
- notionary/page/content/markdown/nodes/bookmark.py +1 -1
- notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
- notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
- notionary/page/content/markdown/nodes/callout.py +12 -10
- notionary/page/content/markdown/nodes/code.py +3 -5
- notionary/page/content/markdown/nodes/columns.py +39 -21
- notionary/page/content/markdown/nodes/container.py +64 -0
- notionary/page/content/markdown/nodes/divider.py +1 -1
- notionary/page/content/markdown/nodes/embed.py +1 -1
- notionary/page/content/markdown/nodes/equation.py +1 -1
- notionary/page/content/markdown/nodes/file.py +1 -1
- notionary/page/content/markdown/nodes/heading.py +26 -6
- notionary/page/content/markdown/nodes/image.py +1 -1
- notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
- notionary/page/content/markdown/nodes/numbered_list.py +28 -5
- notionary/page/content/markdown/nodes/paragraph.py +1 -1
- notionary/page/content/markdown/nodes/pdf.py +1 -1
- notionary/page/content/markdown/nodes/quote.py +17 -5
- notionary/page/content/markdown/nodes/space.py +1 -1
- notionary/page/content/markdown/nodes/table.py +1 -1
- notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
- notionary/page/content/markdown/nodes/todo.py +23 -7
- notionary/page/content/markdown/nodes/toggle.py +13 -14
- notionary/page/content/markdown/nodes/video.py +1 -1
- notionary/page/content/parser/context.py +98 -21
- notionary/page/content/parser/factory.py +1 -10
- notionary/page/content/parser/parsers/__init__.py +0 -2
- notionary/page/content/parser/parsers/audio.py +1 -1
- notionary/page/content/parser/parsers/base.py +1 -1
- notionary/page/content/parser/parsers/bookmark.py +1 -1
- notionary/page/content/parser/parsers/breadcrumb.py +1 -1
- notionary/page/content/parser/parsers/bulleted_list.py +52 -8
- notionary/page/content/parser/parsers/callout.py +55 -84
- notionary/page/content/parser/parsers/caption.py +1 -1
- notionary/page/content/parser/parsers/code.py +5 -5
- notionary/page/content/parser/parsers/column.py +23 -64
- notionary/page/content/parser/parsers/column_list.py +45 -45
- notionary/page/content/parser/parsers/divider.py +1 -1
- notionary/page/content/parser/parsers/embed.py +1 -1
- notionary/page/content/parser/parsers/equation.py +1 -1
- notionary/page/content/parser/parsers/file.py +1 -1
- notionary/page/content/parser/parsers/heading.py +65 -8
- notionary/page/content/parser/parsers/image.py +1 -1
- notionary/page/content/parser/parsers/numbered_list.py +52 -8
- notionary/page/content/parser/parsers/paragraph.py +3 -2
- notionary/page/content/parser/parsers/pdf.py +1 -1
- notionary/page/content/parser/parsers/quote.py +75 -15
- notionary/page/content/parser/parsers/space.py +14 -8
- notionary/page/content/parser/parsers/table.py +1 -1
- notionary/page/content/parser/parsers/table_of_contents.py +1 -1
- notionary/page/content/parser/parsers/todo.py +57 -19
- notionary/page/content/parser/parsers/toggle.py +17 -74
- notionary/page/content/parser/parsers/video.py +1 -1
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
- notionary/page/content/parser/service.py +9 -0
- notionary/page/content/renderer/context.py +5 -2
- notionary/page/content/renderer/factory.py +2 -11
- notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
- notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
- notionary/page/content/renderer/renderers/__init__.py +0 -2
- notionary/page/content/renderer/renderers/base.py +1 -1
- notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
- notionary/page/content/renderer/renderers/callout.py +6 -21
- notionary/page/content/renderer/renderers/captioned_block.py +1 -1
- notionary/page/content/renderer/renderers/column.py +28 -19
- notionary/page/content/renderer/renderers/column_list.py +24 -11
- notionary/page/content/renderer/renderers/heading.py +53 -27
- notionary/page/content/renderer/renderers/numbered_list.py +6 -5
- notionary/page/content/renderer/renderers/quote.py +1 -1
- notionary/page/content/renderer/renderers/todo.py +1 -1
- notionary/page/content/renderer/renderers/toggle.py +6 -7
- notionary/page/content/service.py +4 -1
- notionary/page/content/syntax/__init__.py +4 -0
- notionary/page/content/syntax/grammar.py +10 -0
- notionary/page/content/syntax/models.py +0 -2
- notionary/page/content/syntax/{service.py → registry.py} +31 -91
- notionary/page/properties/client.py +3 -3
- notionary/page/properties/models.py +3 -2
- notionary/page/properties/service.py +18 -3
- notionary/page/service.py +22 -80
- notionary/shared/entity/service.py +94 -36
- notionary/shared/models/cover.py +1 -1
- notionary/shared/typings.py +3 -0
- notionary/user/base.py +60 -11
- notionary/user/factory.py +0 -0
- notionary/utils/decorators.py +122 -0
- notionary/utils/fuzzy.py +18 -6
- notionary/utils/mixins/logging.py +38 -27
- notionary/utils/pagination.py +70 -16
- notionary/workspace/__init__.py +2 -1
- notionary/workspace/client.py +4 -2
- notionary/workspace/query/__init__.py +3 -0
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +12 -3
- notionary/workspace/query/service.py +57 -32
- notionary/workspace/service.py +31 -21
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
- notionary-0.3.1.dist-info/RECORD +211 -0
- notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
- notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
- notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
- notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
- notionary/utils/async_retry.py +0 -39
- notionary/utils/singleton.py +0 -13
- notionary-0.2.28.dist-info/RECORD +0 -200
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from typing import override
|
|
2
3
|
|
|
3
4
|
from notionary.blocks.rich_text.markdown_rich_text_converter import MarkdownRichTextConverter
|
|
4
5
|
from notionary.blocks.schemas import CreateCalloutBlock, CreateCalloutData
|
|
5
|
-
from notionary.page.content.parser.context import ParentBlockContext
|
|
6
6
|
from notionary.page.content.parser.parsers.base import (
|
|
7
7
|
BlockParsingContext,
|
|
8
8
|
LineParser,
|
|
9
9
|
)
|
|
10
|
-
from notionary.page.content.syntax
|
|
10
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
11
11
|
from notionary.shared.models.icon import EmojiIcon
|
|
12
12
|
|
|
13
13
|
|
|
@@ -17,113 +17,84 @@ class CalloutParser(LineParser):
|
|
|
17
17
|
def __init__(self, syntax_registry: SyntaxRegistry, rich_text_converter: MarkdownRichTextConverter) -> None:
|
|
18
18
|
super().__init__(syntax_registry)
|
|
19
19
|
self._syntax = syntax_registry.get_callout_syntax()
|
|
20
|
-
self.
|
|
21
|
-
self._end_pattern = self._syntax.end_regex_pattern
|
|
20
|
+
self._pattern = self._syntax.regex_pattern
|
|
22
21
|
self._rich_text_converter = rich_text_converter
|
|
23
22
|
|
|
24
23
|
@override
|
|
25
24
|
def _can_handle(self, context: BlockParsingContext) -> bool:
|
|
26
|
-
return self.
|
|
25
|
+
return self._pattern.search(context.line) is not None
|
|
27
26
|
|
|
28
27
|
@override
|
|
29
28
|
async def _process(self, context: BlockParsingContext) -> None:
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
block = await self._create_callout_block(context.line)
|
|
30
|
+
if not block:
|
|
31
|
+
return
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
await self._finalize_callout(context)
|
|
33
|
+
await self._process_nested_children(block, context)
|
|
35
34
|
|
|
36
|
-
if self.
|
|
37
|
-
|
|
35
|
+
if self._is_nested_in_parent_context(context):
|
|
36
|
+
context.parent_stack[-1].add_child_block(block)
|
|
37
|
+
else:
|
|
38
|
+
context.result_blocks.append(block)
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
-
|
|
40
|
+
async def _process_nested_children(self, block: CreateCalloutBlock, context: BlockParsingContext) -> None:
|
|
41
|
+
child_lines = self._collect_child_lines(context)
|
|
42
|
+
if not child_lines:
|
|
43
|
+
return
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
+
child_blocks = await self._parse_child_blocks(child_lines, context)
|
|
46
|
+
if child_blocks:
|
|
47
|
+
block.callout.children = child_blocks
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
return False
|
|
49
|
+
context.lines_consumed = len(child_lines)
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
def _collect_child_lines(self, context: BlockParsingContext) -> list[str]:
|
|
52
|
+
parent_indent_level = context.get_line_indentation_level()
|
|
53
|
+
return context.collect_indented_child_lines(parent_indent_level)
|
|
51
54
|
|
|
52
|
-
async def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
async def _parse_child_blocks(
|
|
56
|
+
self, child_lines: list[str], context: BlockParsingContext
|
|
57
|
+
) -> list[CreateCalloutBlock]:
|
|
58
|
+
stripped_lines = self._remove_parent_indentation(child_lines, context)
|
|
59
|
+
children_text = self._convert_lines_to_text(stripped_lines)
|
|
60
|
+
return await context.parse_nested_markdown(children_text)
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
def _remove_parent_indentation(self, lines: list[str], context: BlockParsingContext) -> list[str]:
|
|
63
|
+
return context.strip_indentation_level(lines, levels=1)
|
|
64
|
+
|
|
65
|
+
def _convert_lines_to_text(self, lines: list[str]) -> str:
|
|
66
|
+
return "\n".join(lines)
|
|
62
67
|
|
|
63
68
|
async def _create_callout_block(self, line: str) -> CreateCalloutBlock | None:
|
|
64
|
-
match = self.
|
|
69
|
+
match = self._pattern.search(line)
|
|
65
70
|
if not match:
|
|
66
71
|
return None
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
content, emoji = self._extract_content_and_emoji(match)
|
|
74
|
+
rich_text = await self._convert_to_rich_text(content)
|
|
75
|
+
return self._build_block(rich_text, emoji)
|
|
76
|
+
|
|
77
|
+
def _extract_content_and_emoji(self, match: re.Match[str]) -> tuple[str, str]:
|
|
78
|
+
inline_content = match.group(1)
|
|
79
|
+
if inline_content:
|
|
80
|
+
return inline_content.strip(), match.group(2) or self.DEFAULT_EMOJI
|
|
81
|
+
|
|
82
|
+
block_content = match.group(3)
|
|
83
|
+
if block_content:
|
|
84
|
+
return block_content.strip(), match.group(4) or self.DEFAULT_EMOJI
|
|
85
|
+
|
|
86
|
+
return "", self.DEFAULT_EMOJI
|
|
87
|
+
|
|
88
|
+
async def _convert_to_rich_text(self, content: str):
|
|
89
|
+
return await self._rich_text_converter.to_rich_text(content)
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
# The actual content will be added as children
|
|
91
|
+
def _build_block(self, rich_text, emoji: str) -> CreateCalloutBlock:
|
|
73
92
|
callout_data = CreateCalloutData(
|
|
74
|
-
rich_text=
|
|
93
|
+
rich_text=rich_text,
|
|
75
94
|
icon=EmojiIcon(emoji=emoji),
|
|
76
95
|
children=[],
|
|
77
96
|
)
|
|
78
97
|
return CreateCalloutBlock(callout=callout_data)
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
callout_context = context.parent_stack.pop()
|
|
82
|
-
await self._assign_callout_children_if_any(callout_context, context)
|
|
83
|
-
|
|
84
|
-
if self._is_nested_in_other_parent_context(context):
|
|
85
|
-
self._assign_to_parent_context(context, callout_context)
|
|
86
|
-
else:
|
|
87
|
-
context.result_blocks.append(callout_context.block)
|
|
88
|
-
|
|
89
|
-
def _is_nested_in_other_parent_context(self, context: BlockParsingContext) -> bool:
|
|
99
|
+
def _is_nested_in_parent_context(self, context: BlockParsingContext) -> bool:
|
|
90
100
|
return bool(context.parent_stack)
|
|
91
|
-
|
|
92
|
-
def _assign_to_parent_context(self, context: BlockParsingContext, callout_context: ParentBlockContext) -> None:
|
|
93
|
-
parent_context = context.parent_stack[-1]
|
|
94
|
-
parent_context.add_child_block(callout_context.block)
|
|
95
|
-
|
|
96
|
-
async def _assign_callout_children_if_any(
|
|
97
|
-
self, callout_context: ParentBlockContext, context: BlockParsingContext
|
|
98
|
-
) -> None:
|
|
99
|
-
all_children = []
|
|
100
|
-
|
|
101
|
-
if callout_context.child_lines:
|
|
102
|
-
children_text = "\n".join(callout_context.child_lines)
|
|
103
|
-
text_blocks = await self._parse_nested_content(children_text, context)
|
|
104
|
-
all_children.extend(text_blocks)
|
|
105
|
-
|
|
106
|
-
# Add any child blocks
|
|
107
|
-
if callout_context.child_blocks:
|
|
108
|
-
all_children.extend(callout_context.child_blocks)
|
|
109
|
-
|
|
110
|
-
callout_context.block.callout.children = all_children
|
|
111
|
-
|
|
112
|
-
def _is_callout_content(self, context: BlockParsingContext) -> bool:
|
|
113
|
-
if not context.parent_stack:
|
|
114
|
-
return False
|
|
115
|
-
|
|
116
|
-
current_parent = context.parent_stack[-1]
|
|
117
|
-
if not isinstance(current_parent.block, CreateCalloutBlock):
|
|
118
|
-
return False
|
|
119
|
-
|
|
120
|
-
return not (self._start_pattern.match(context.line) or self._end_pattern.match(context.line))
|
|
121
|
-
|
|
122
|
-
def _add_callout_content(self, context: BlockParsingContext) -> None:
|
|
123
|
-
context.parent_stack[-1].add_child_line(context.line)
|
|
124
|
-
|
|
125
|
-
async def _parse_nested_content(self, text: str, context: BlockParsingContext) -> list:
|
|
126
|
-
if not text.strip():
|
|
127
|
-
return []
|
|
128
|
-
|
|
129
|
-
return await context.parse_nested_content(text)
|
|
@@ -8,7 +8,7 @@ from notionary.page.content.parser.parsers.base import (
|
|
|
8
8
|
BlockParsingContext,
|
|
9
9
|
LineParser,
|
|
10
10
|
)
|
|
11
|
-
from notionary.page.content.syntax
|
|
11
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class CaptionParser(LineParser):
|
|
@@ -3,13 +3,13 @@ from typing import override
|
|
|
3
3
|
|
|
4
4
|
from notionary.blocks.rich_text.markdown_rich_text_converter import MarkdownRichTextConverter
|
|
5
5
|
from notionary.blocks.rich_text.models import RichText
|
|
6
|
-
from notionary.blocks.schemas import CodeData,
|
|
6
|
+
from notionary.blocks.schemas import CodeData, CodingLanguage, CreateCodeBlock
|
|
7
7
|
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
8
|
-
from notionary.page.content.syntax
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class CodeParser(LineParser):
|
|
12
|
-
DEFAULT_LANGUAGE =
|
|
12
|
+
DEFAULT_LANGUAGE = CodingLanguage.PLAIN_TEXT
|
|
13
13
|
|
|
14
14
|
def __init__(self, syntax_registry: SyntaxRegistry, rich_text_converter: MarkdownRichTextConverter) -> None:
|
|
15
15
|
super().__init__(syntax_registry)
|
|
@@ -67,8 +67,8 @@ class CodeParser(LineParser):
|
|
|
67
67
|
code_data = CodeData(rich_text=rich_text, language=language, caption=[])
|
|
68
68
|
return CreateCodeBlock(code=code_data)
|
|
69
69
|
|
|
70
|
-
def _parse_language(self, language_str: str | None) ->
|
|
71
|
-
return
|
|
70
|
+
def _parse_language(self, language_str: str | None) -> CodingLanguage:
|
|
71
|
+
return CodingLanguage.from_string(language_str, default=self.DEFAULT_LANGUAGE)
|
|
72
72
|
|
|
73
73
|
async def _create_rich_text_from_code(self, code_lines: list[str]) -> list[RichText]:
|
|
74
74
|
content = "\n".join(code_lines) if code_lines else ""
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
|
-
from notionary.blocks.schemas import CreateColumnBlock, CreateColumnData
|
|
4
|
-
from notionary.page.content.parser.context import ParentBlockContext
|
|
3
|
+
from notionary.blocks.schemas import CreateColumnBlock, CreateColumnData
|
|
5
4
|
from notionary.page.content.parser.parsers.base import (
|
|
6
5
|
BlockParsingContext,
|
|
7
6
|
LineParser,
|
|
8
7
|
)
|
|
9
|
-
from notionary.page.content.syntax
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class ColumnParser(LineParser):
|
|
@@ -19,54 +18,23 @@ class ColumnParser(LineParser):
|
|
|
19
18
|
|
|
20
19
|
@override
|
|
21
20
|
def _can_handle(self, context: BlockParsingContext) -> bool:
|
|
22
|
-
return self._is_column_start(context)
|
|
21
|
+
return self._is_column_start(context)
|
|
23
22
|
|
|
24
23
|
@override
|
|
25
24
|
async def _process(self, context: BlockParsingContext) -> None:
|
|
26
25
|
if self._is_column_start(context):
|
|
27
|
-
await self.
|
|
28
|
-
elif self._is_column_end(context):
|
|
29
|
-
await self._finalize_column(context)
|
|
30
|
-
elif self._is_column_content(context):
|
|
31
|
-
await self._add_column_content(context)
|
|
26
|
+
await self._process_column(context)
|
|
32
27
|
|
|
33
28
|
def _is_column_start(self, context: BlockParsingContext) -> bool:
|
|
34
29
|
return self._syntax.regex_pattern.match(context.line) is not None
|
|
35
30
|
|
|
36
|
-
def
|
|
37
|
-
if not self._syntax.end_regex_pattern.match(context.line):
|
|
38
|
-
return False
|
|
39
|
-
|
|
40
|
-
if not context.parent_stack:
|
|
41
|
-
return False
|
|
42
|
-
|
|
43
|
-
current_parent = context.parent_stack[-1]
|
|
44
|
-
return isinstance(current_parent.block, CreateColumnBlock)
|
|
45
|
-
|
|
46
|
-
def _is_column_content(self, context: BlockParsingContext) -> bool:
|
|
47
|
-
if not context.parent_stack:
|
|
48
|
-
return False
|
|
49
|
-
|
|
50
|
-
current_parent = context.parent_stack[-1]
|
|
51
|
-
if not isinstance(current_parent.block, CreateColumnBlock):
|
|
52
|
-
return False
|
|
53
|
-
|
|
54
|
-
line = context.line.strip()
|
|
55
|
-
return not (self._syntax.regex_pattern.match(line) or self._syntax.end_regex_pattern.match(line))
|
|
56
|
-
|
|
57
|
-
async def _add_column_content(self, context: BlockParsingContext) -> None:
|
|
58
|
-
context.parent_stack[-1].add_child_line(context.line)
|
|
59
|
-
|
|
60
|
-
async def _start_column(self, context: BlockParsingContext) -> None:
|
|
31
|
+
async def _process_column(self, context: BlockParsingContext) -> None:
|
|
61
32
|
block = self._create_column_block(context.line)
|
|
62
33
|
if not block:
|
|
63
34
|
return
|
|
64
35
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
child_lines=[],
|
|
68
|
-
)
|
|
69
|
-
context.parent_stack.append(parent_context)
|
|
36
|
+
await self._populate_children(block, context)
|
|
37
|
+
context.result_blocks.append(block)
|
|
70
38
|
|
|
71
39
|
def _create_column_block(self, line: str) -> CreateColumnBlock | None:
|
|
72
40
|
match = self._syntax.regex_pattern.match(line)
|
|
@@ -74,7 +42,7 @@ class ColumnParser(LineParser):
|
|
|
74
42
|
return None
|
|
75
43
|
|
|
76
44
|
width_ratio = self._parse_width_ratio(match.group(1))
|
|
77
|
-
column_data = CreateColumnData(width_ratio=width_ratio)
|
|
45
|
+
column_data = CreateColumnData(width_ratio=width_ratio, children=[])
|
|
78
46
|
|
|
79
47
|
return CreateColumnBlock(column=column_data)
|
|
80
48
|
|
|
@@ -84,34 +52,25 @@ class ColumnParser(LineParser):
|
|
|
84
52
|
|
|
85
53
|
try:
|
|
86
54
|
width_ratio = float(ratio_str)
|
|
87
|
-
return width_ratio if self.
|
|
55
|
+
return width_ratio if self._is_valid_width_ratio(width_ratio) else None
|
|
88
56
|
except ValueError:
|
|
89
57
|
return None
|
|
90
58
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await self._assign_column_children(column_context, context)
|
|
94
|
-
|
|
95
|
-
if self._has_column_list_parent(context):
|
|
96
|
-
parent = context.parent_stack[-1]
|
|
97
|
-
parent.add_child_block(column_context.block)
|
|
98
|
-
else:
|
|
99
|
-
context.result_blocks.append(column_context.block)
|
|
59
|
+
def _is_valid_width_ratio(self, width_ratio: float) -> bool:
|
|
60
|
+
return self.MIN_WIDTH_RATIO < width_ratio <= self.MAX_WIDTH_RATIO
|
|
100
61
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return isinstance(context.parent_stack[-1].block, CreateColumnListBlock)
|
|
62
|
+
async def _populate_children(self, block: CreateColumnBlock, context: BlockParsingContext) -> None:
|
|
63
|
+
parent_indent_level = context.get_line_indentation_level()
|
|
64
|
+
child_lines = context.collect_indented_child_lines(parent_indent_level)
|
|
105
65
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if column_context.child_lines:
|
|
110
|
-
children_text = "\n".join(column_context.child_lines)
|
|
111
|
-
text_blocks = await context.parse_nested_content(children_text)
|
|
112
|
-
all_children.extend(text_blocks)
|
|
66
|
+
if not child_lines:
|
|
67
|
+
return
|
|
113
68
|
|
|
114
|
-
|
|
115
|
-
|
|
69
|
+
child_blocks = await self._parse_indented_children(child_lines, context)
|
|
70
|
+
block.column.children = child_blocks
|
|
71
|
+
context.lines_consumed = len(child_lines)
|
|
116
72
|
|
|
117
|
-
|
|
73
|
+
async def _parse_indented_children(self, child_lines: list[str], context: BlockParsingContext) -> list:
|
|
74
|
+
stripped_lines = context.strip_indentation_level(child_lines, levels=1)
|
|
75
|
+
child_markdown = "\n".join(stripped_lines)
|
|
76
|
+
return await context.parse_nested_markdown(child_markdown)
|
|
@@ -2,12 +2,11 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.blocks.enums import BlockType
|
|
4
4
|
from notionary.blocks.schemas import BlockCreatePayload, CreateColumnListBlock, CreateColumnListData
|
|
5
|
-
from notionary.page.content.parser.context import ParentBlockContext
|
|
6
5
|
from notionary.page.content.parser.parsers.base import (
|
|
7
6
|
BlockParsingContext,
|
|
8
7
|
LineParser,
|
|
9
8
|
)
|
|
10
|
-
from notionary.page.content.syntax
|
|
9
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class ColumnListParser(LineParser):
|
|
@@ -17,65 +16,66 @@ class ColumnListParser(LineParser):
|
|
|
17
16
|
|
|
18
17
|
@override
|
|
19
18
|
def _can_handle(self, context: BlockParsingContext) -> bool:
|
|
20
|
-
return self._is_column_list_start(context)
|
|
19
|
+
return self._is_column_list_start(context)
|
|
21
20
|
|
|
22
21
|
@override
|
|
23
22
|
async def _process(self, context: BlockParsingContext) -> None:
|
|
24
23
|
if self._is_column_list_start(context):
|
|
25
|
-
await self.
|
|
26
|
-
elif self._is_column_list_end(context):
|
|
27
|
-
await self._finalize_column_list(context)
|
|
24
|
+
await self._process_column_list(context)
|
|
28
25
|
|
|
29
26
|
def _is_column_list_start(self, context: BlockParsingContext) -> bool:
|
|
30
27
|
return self._syntax.regex_pattern.match(context.line) is not None
|
|
31
28
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
async def _process_column_list(self, context: BlockParsingContext) -> None:
|
|
30
|
+
block = self._create_column_list_block()
|
|
31
|
+
await self._populate_columns(block, context)
|
|
32
|
+
context.result_blocks.append(block)
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
def _create_column_list_block(self) -> CreateColumnListBlock:
|
|
35
|
+
column_list_data = CreateColumnListData(children=[])
|
|
36
|
+
return CreateColumnListBlock(column_list=column_list_data)
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
async def _populate_columns(self, block: CreateColumnListBlock, context: BlockParsingContext) -> None:
|
|
39
|
+
parent_indent_level = context.get_line_indentation_level()
|
|
40
|
+
child_lines = self._collect_children_allowing_empty_lines(context, parent_indent_level)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
block = CreateColumnListBlock(column_list=column_list_data)
|
|
42
|
+
if not child_lines:
|
|
43
|
+
return
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
context.parent_stack.append(parent_context)
|
|
45
|
+
column_blocks = await self._parse_column_children(child_lines, context)
|
|
46
|
+
block.column_list.children = column_blocks
|
|
47
|
+
context.lines_consumed = len(child_lines)
|
|
51
48
|
|
|
52
|
-
async def
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
async def _parse_column_children(self, child_lines: list[str], context: BlockParsingContext) -> list:
|
|
50
|
+
stripped_lines = context.strip_indentation_level(child_lines, levels=1)
|
|
51
|
+
child_markdown = "\n".join(stripped_lines)
|
|
52
|
+
parsed_blocks = await context.parse_nested_markdown(child_markdown)
|
|
53
|
+
return self._extract_column_blocks(parsed_blocks)
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
def _collect_children_allowing_empty_lines(
|
|
56
|
+
self, context: BlockParsingContext, parent_indent_level: int
|
|
57
|
+
) -> list[str]:
|
|
58
|
+
child_lines = []
|
|
59
|
+
expected_child_indent = parent_indent_level + 1
|
|
60
|
+
remaining_lines = context.get_remaining_lines()
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
for line in remaining_lines:
|
|
63
|
+
if self._should_include_as_child(line, expected_child_indent, context):
|
|
64
|
+
child_lines.append(line)
|
|
65
|
+
else:
|
|
66
|
+
break
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
children_text = "\n".join(column_list_context.child_lines)
|
|
69
|
-
text_blocks = await context.parse_nested_content(children_text)
|
|
70
|
-
all_children.extend(text_blocks)
|
|
68
|
+
return child_lines
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
def _should_include_as_child(self, line: str, expected_indent: int, context: BlockParsingContext) -> bool:
|
|
71
|
+
if not line.strip():
|
|
72
|
+
return True
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
line_indent = context.get_line_indentation_level(line)
|
|
75
|
+
return line_indent >= expected_indent
|
|
77
76
|
|
|
78
|
-
def
|
|
79
|
-
return [
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
def _extract_column_blocks(self, blocks: list[BlockCreatePayload]) -> list:
|
|
78
|
+
return [block for block in blocks if self._is_valid_column_block(block)]
|
|
79
|
+
|
|
80
|
+
def _is_valid_column_block(self, block: BlockCreatePayload) -> bool:
|
|
81
|
+
return block.type == BlockType.COLUMN and block.column is not None
|
|
@@ -4,7 +4,7 @@ from typing import override
|
|
|
4
4
|
|
|
5
5
|
from notionary.blocks.schemas import CreateEmbedBlock, EmbedData
|
|
6
6
|
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
7
|
-
from notionary.page.content.syntax
|
|
7
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class EmbedParser(LineParser):
|
|
@@ -9,7 +9,7 @@ from notionary.blocks.schemas import (
|
|
|
9
9
|
FileType,
|
|
10
10
|
)
|
|
11
11
|
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
12
|
-
from notionary.page.content.syntax
|
|
12
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class FileParser(LineParser):
|
|
@@ -3,6 +3,8 @@ from typing import override
|
|
|
3
3
|
from notionary.blocks.rich_text.markdown_rich_text_converter import MarkdownRichTextConverter
|
|
4
4
|
from notionary.blocks.schemas import (
|
|
5
5
|
BlockColor,
|
|
6
|
+
BlockCreatePayload,
|
|
7
|
+
BlockType,
|
|
6
8
|
CreateHeading1Block,
|
|
7
9
|
CreateHeading2Block,
|
|
8
10
|
CreateHeading3Block,
|
|
@@ -13,10 +15,13 @@ from notionary.page.content.parser.parsers.base import (
|
|
|
13
15
|
BlockParsingContext,
|
|
14
16
|
LineParser,
|
|
15
17
|
)
|
|
16
|
-
from notionary.page.content.syntax
|
|
18
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class HeadingParser(LineParser):
|
|
22
|
+
MIN_HEADING_LEVEL = 1
|
|
23
|
+
MAX_HEADING_LEVEL = 3
|
|
24
|
+
|
|
20
25
|
def __init__(self, syntax_registry: SyntaxRegistry, rich_text_converter: MarkdownRichTextConverter) -> None:
|
|
21
26
|
super().__init__(syntax_registry)
|
|
22
27
|
self._syntax = syntax_registry.get_heading_syntax()
|
|
@@ -31,8 +36,54 @@ class HeadingParser(LineParser):
|
|
|
31
36
|
@override
|
|
32
37
|
async def _process(self, context: BlockParsingContext) -> None:
|
|
33
38
|
block = await self._create_heading_block(context.line)
|
|
34
|
-
if block:
|
|
35
|
-
|
|
39
|
+
if not block:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
await self._process_nested_children(block, context)
|
|
43
|
+
context.result_blocks.append(block)
|
|
44
|
+
|
|
45
|
+
async def _process_nested_children(self, block: CreateHeadingBlock, context: BlockParsingContext) -> None:
|
|
46
|
+
parent_indent_level = context.get_line_indentation_level()
|
|
47
|
+
child_lines = context.collect_indented_child_lines(parent_indent_level)
|
|
48
|
+
|
|
49
|
+
if not child_lines:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
child_lines = self._remove_trailing_empty_lines(child_lines)
|
|
53
|
+
|
|
54
|
+
if not child_lines:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
self._set_heading_toggleable(block, True)
|
|
58
|
+
|
|
59
|
+
stripped_lines = context.strip_indentation_level(child_lines, levels=1)
|
|
60
|
+
child_markdown = "\n".join(stripped_lines)
|
|
61
|
+
|
|
62
|
+
child_blocks = await context.parse_nested_markdown(child_markdown)
|
|
63
|
+
self._set_heading_children(block, child_blocks)
|
|
64
|
+
|
|
65
|
+
context.lines_consumed = len(child_lines)
|
|
66
|
+
|
|
67
|
+
def _set_heading_toggleable(self, block: CreateHeadingBlock, is_toggleable: bool) -> None:
|
|
68
|
+
if block.type == BlockType.HEADING_1:
|
|
69
|
+
block.heading_1.is_toggleable = is_toggleable
|
|
70
|
+
elif block.type == BlockType.HEADING_2:
|
|
71
|
+
block.heading_2.is_toggleable = is_toggleable
|
|
72
|
+
elif block.type == BlockType.HEADING_3:
|
|
73
|
+
block.heading_3.is_toggleable = is_toggleable
|
|
74
|
+
|
|
75
|
+
def _set_heading_children(self, block: CreateHeadingBlock, children: list[BlockCreatePayload]) -> None:
|
|
76
|
+
if block.type == BlockType.HEADING_1:
|
|
77
|
+
block.heading_1.children = children
|
|
78
|
+
elif block.type == BlockType.HEADING_2:
|
|
79
|
+
block.heading_2.children = children
|
|
80
|
+
elif block.type == BlockType.HEADING_3:
|
|
81
|
+
block.heading_3.children = children
|
|
82
|
+
|
|
83
|
+
def _remove_trailing_empty_lines(self, lines: list[str]) -> list[str]:
|
|
84
|
+
while lines and not lines[-1].strip():
|
|
85
|
+
lines.pop()
|
|
86
|
+
return lines
|
|
36
87
|
|
|
37
88
|
async def _create_heading_block(self, line: str) -> CreateHeadingBlock | None:
|
|
38
89
|
match = self._syntax.regex_pattern.match(line)
|
|
@@ -40,16 +91,22 @@ class HeadingParser(LineParser):
|
|
|
40
91
|
return None
|
|
41
92
|
|
|
42
93
|
level = len(match.group(1))
|
|
43
|
-
if level < 1 or level > 3:
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
94
|
content = match.group(2).strip()
|
|
47
|
-
|
|
95
|
+
|
|
96
|
+
if not self._is_valid_heading(level, content):
|
|
48
97
|
return None
|
|
49
98
|
|
|
99
|
+
heading_data = await self._build_heading_data(content)
|
|
100
|
+
return self._create_heading_block_by_level(level, heading_data)
|
|
101
|
+
|
|
102
|
+
def _is_valid_heading(self, level: int, content: str) -> bool:
|
|
103
|
+
return self.MIN_HEADING_LEVEL <= level <= self.MAX_HEADING_LEVEL and bool(content)
|
|
104
|
+
|
|
105
|
+
async def _build_heading_data(self, content: str) -> CreateHeadingData:
|
|
50
106
|
rich_text = await self._rich_text_converter.to_rich_text(content)
|
|
51
|
-
|
|
107
|
+
return CreateHeadingData(rich_text=rich_text, color=BlockColor.DEFAULT, is_toggleable=False, children=[])
|
|
52
108
|
|
|
109
|
+
def _create_heading_block_by_level(self, level: int, heading_data: CreateHeadingData) -> CreateHeadingBlock:
|
|
53
110
|
if level == 1:
|
|
54
111
|
return CreateHeading1Block(heading_1=heading_data)
|
|
55
112
|
elif level == 2:
|
|
@@ -9,7 +9,7 @@ from notionary.blocks.schemas import (
|
|
|
9
9
|
FileType,
|
|
10
10
|
)
|
|
11
11
|
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
12
|
-
from notionary.page.content.syntax
|
|
12
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ImageParser(LineParser):
|