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
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class FileMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class HeadingMarkdownNode(
|
|
8
|
-
|
|
8
|
+
class HeadingMarkdownNode(ContainerNode):
|
|
9
|
+
MIN_LEVEL = 1
|
|
10
|
+
MAX_LEVEL = 3
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
text: str,
|
|
15
|
+
level: int = 1,
|
|
16
|
+
children: list[MarkdownNode] | None = None,
|
|
17
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
18
|
+
) -> None:
|
|
9
19
|
super().__init__(syntax_registry=syntax_registry)
|
|
10
20
|
self.text = text
|
|
11
|
-
self.level =
|
|
21
|
+
self.level = self._validate_level(level)
|
|
22
|
+
self.children = children or []
|
|
12
23
|
|
|
13
24
|
@override
|
|
14
25
|
def to_markdown(self) -> str:
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
heading_prefix = self._get_heading_prefix()
|
|
27
|
+
result = f"{heading_prefix} {self.text}"
|
|
28
|
+
result += self.render_children()
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
def _validate_level(self, level: int) -> int:
|
|
32
|
+
return max(self.MIN_LEVEL, min(self.MAX_LEVEL, level))
|
|
33
|
+
|
|
34
|
+
def _get_heading_prefix(self) -> str:
|
|
35
|
+
delimiter = self._syntax_registry.get_heading_syntax().start_delimiter
|
|
36
|
+
return delimiter * self.level
|
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ImageMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class NumberedListMarkdownNode(
|
|
8
|
-
def __init__(
|
|
8
|
+
class NumberedListMarkdownNode(ContainerNode):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
texts: list[str],
|
|
12
|
+
children: list[MarkdownNode | None] | None = None,
|
|
13
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
14
|
+
):
|
|
9
15
|
super().__init__(syntax_registry=syntax_registry)
|
|
10
16
|
self.texts = texts
|
|
17
|
+
self.children = children or []
|
|
11
18
|
|
|
12
19
|
@override
|
|
13
20
|
def to_markdown(self) -> str:
|
|
14
|
-
|
|
15
|
-
return "\n".join(
|
|
21
|
+
list_items = [self._render_list_item(index, text) for index, text in enumerate(self.texts)]
|
|
22
|
+
return "\n".join(list_items)
|
|
23
|
+
|
|
24
|
+
def _render_list_item(self, index: int, text: str) -> str:
|
|
25
|
+
item_number = index + 1
|
|
26
|
+
item_line = f"{item_number}. {text}"
|
|
27
|
+
|
|
28
|
+
child = self._get_child_for_item(index)
|
|
29
|
+
if child:
|
|
30
|
+
child_content = self.render_child(child)
|
|
31
|
+
return f"{item_line}\n{child_content}"
|
|
32
|
+
|
|
33
|
+
return item_line
|
|
34
|
+
|
|
35
|
+
def _get_child_for_item(self, index: int) -> MarkdownNode | None:
|
|
36
|
+
if not self.children or index >= len(self.children):
|
|
37
|
+
return None
|
|
38
|
+
return self.children[index]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class ParagraphMarkdownNode(MarkdownNode):
|
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class PdfMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class QuoteMarkdownNode(
|
|
8
|
-
def __init__(
|
|
8
|
+
class QuoteMarkdownNode(ContainerNode):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
text: str,
|
|
12
|
+
children: list[MarkdownNode] | None = None,
|
|
13
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
14
|
+
) -> None:
|
|
9
15
|
super().__init__(syntax_registry=syntax_registry)
|
|
10
16
|
self.text = text
|
|
17
|
+
self.children = children or []
|
|
11
18
|
|
|
12
19
|
@override
|
|
13
20
|
def to_markdown(self) -> str:
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
quote_delimiter = self._get_quote_delimiter()
|
|
22
|
+
result = f"{quote_delimiter}{self.text}"
|
|
23
|
+
result += self.render_children()
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
def _get_quote_delimiter(self) -> str:
|
|
27
|
+
return self._syntax_registry.get_quote_syntax().start_delimiter
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class SpaceMarkdownNode(MarkdownNode):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TableMarkdownNode(MarkdownNode):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.syntax
|
|
4
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TableOfContentsMarkdownNode(MarkdownNode):
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class TodoMarkdownNode(
|
|
8
|
+
class TodoMarkdownNode(ContainerNode):
|
|
9
|
+
VALID_MARKER = "-"
|
|
10
|
+
|
|
8
11
|
def __init__(
|
|
9
|
-
self,
|
|
12
|
+
self,
|
|
13
|
+
text: str,
|
|
14
|
+
checked: bool = False,
|
|
15
|
+
marker: str = "-",
|
|
16
|
+
children: list[MarkdownNode] | None = None,
|
|
17
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
10
18
|
):
|
|
11
19
|
super().__init__(syntax_registry=syntax_registry)
|
|
12
20
|
self.text = text
|
|
13
21
|
self.checked = checked
|
|
14
22
|
self.marker = marker
|
|
23
|
+
self.children = children or []
|
|
15
24
|
|
|
16
25
|
@override
|
|
17
26
|
def to_markdown(self) -> str:
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
validated_marker = self._get_validated_marker()
|
|
28
|
+
checkbox_state = self._get_checkbox_state()
|
|
29
|
+
result = f"{validated_marker}{checkbox_state} {self.text}"
|
|
30
|
+
result += self.render_children()
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
def _get_validated_marker(self) -> str:
|
|
34
|
+
return self.marker if self.marker == self.VALID_MARKER else self.VALID_MARKER
|
|
35
|
+
|
|
36
|
+
def _get_checkbox_state(self) -> str:
|
|
20
37
|
todo_syntax = self._syntax_registry.get_todo_syntax()
|
|
21
|
-
|
|
22
|
-
return f"{valid_marker} {checkbox_state} {self.text}"
|
|
38
|
+
return todo_syntax.end_delimiter if self.checked else todo_syntax.start_delimiter
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
|
-
from notionary.page.content.
|
|
4
|
+
from notionary.page.content.markdown.nodes.container import ContainerNode
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class ToggleMarkdownNode(
|
|
8
|
+
class ToggleMarkdownNode(ContainerNode):
|
|
8
9
|
def __init__(
|
|
9
|
-
self,
|
|
10
|
+
self,
|
|
11
|
+
title: str,
|
|
12
|
+
children: list[MarkdownNode] | None = None,
|
|
13
|
+
syntax_registry: SyntaxRegistry | None = None,
|
|
10
14
|
):
|
|
11
15
|
super().__init__(syntax_registry=syntax_registry)
|
|
12
16
|
self.title = title
|
|
@@ -14,15 +18,10 @@ class ToggleMarkdownNode(MarkdownNode):
|
|
|
14
18
|
|
|
15
19
|
@override
|
|
16
20
|
def to_markdown(self) -> str:
|
|
17
|
-
|
|
18
|
-
result = f"{
|
|
21
|
+
toggle_delimiter = self._get_toggle_delimiter()
|
|
22
|
+
result = f"{toggle_delimiter} {self.title}"
|
|
23
|
+
result += self.render_children()
|
|
24
|
+
return result
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return result
|
|
23
|
-
|
|
24
|
-
# Convert children to markdown
|
|
25
|
-
content_parts = [child.to_markdown() for child in self.children]
|
|
26
|
-
content_text = "\n\n".join(content_parts)
|
|
27
|
-
|
|
28
|
-
return result + "\n" + content_text + f"\n{toggle_syntax.end_delimiter}"
|
|
26
|
+
def _get_toggle_delimiter(self) -> str:
|
|
27
|
+
return self._syntax_registry.get_toggle_syntax().start_delimiter
|
|
@@ -2,7 +2,7 @@ from typing import override
|
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.markdown.nodes.base import MarkdownNode
|
|
4
4
|
from notionary.page.content.markdown.nodes.mixins.caption import CaptionMarkdownNodeMixin
|
|
5
|
-
from notionary.page.content.syntax
|
|
5
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class VideoMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Awaitable, Callable
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
4
|
|
|
6
5
|
from notionary.blocks.schemas import BlockCreatePayload
|
|
6
|
+
from notionary.page.content.syntax.grammar import MarkdownGrammar
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@dataclass
|
|
10
9
|
class ParentBlockContext:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
block: BlockCreatePayload,
|
|
13
|
+
child_lines: list[str],
|
|
14
|
+
child_blocks: list[BlockCreatePayload] | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.block = block
|
|
17
|
+
self.child_lines = child_lines
|
|
18
|
+
self.child_blocks = child_blocks if child_blocks is not None else []
|
|
14
19
|
|
|
15
20
|
def add_child_line(self, content: str) -> None:
|
|
16
21
|
self.child_lines.append(content)
|
|
@@ -19,31 +24,103 @@ class ParentBlockContext:
|
|
|
19
24
|
self.child_blocks.append(block)
|
|
20
25
|
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
_ParseChildrenCallback = Callable[[str], Awaitable[list[BlockCreatePayload]]]
|
|
23
28
|
|
|
24
29
|
|
|
25
|
-
@dataclass
|
|
26
30
|
class BlockParsingContext:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
line: str,
|
|
34
|
+
result_blocks: list[BlockCreatePayload],
|
|
35
|
+
parent_stack: list[ParentBlockContext],
|
|
36
|
+
parse_children_callback: _ParseChildrenCallback | None = None,
|
|
37
|
+
all_lines: list[str] | None = None,
|
|
38
|
+
current_line_index: int | None = None,
|
|
39
|
+
lines_consumed: int = 0,
|
|
40
|
+
is_previous_line_empty: bool = False,
|
|
41
|
+
markdown_grammar: MarkdownGrammar | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.line = line
|
|
44
|
+
self.result_blocks = result_blocks
|
|
45
|
+
self.parent_stack = parent_stack
|
|
46
|
+
self.parse_children_callback = parse_children_callback
|
|
47
|
+
self.all_lines = all_lines
|
|
48
|
+
self.current_line_index = current_line_index
|
|
49
|
+
self.lines_consumed = lines_consumed
|
|
50
|
+
self.is_previous_line_empty = is_previous_line_empty
|
|
51
|
+
markdown_grammar = markdown_grammar or MarkdownGrammar()
|
|
52
|
+
self._spaces_per_nesting_level = markdown_grammar.spaces_per_nesting_level
|
|
53
|
+
|
|
54
|
+
async def parse_nested_markdown(self, text: str) -> list[BlockCreatePayload]:
|
|
55
|
+
if not self._can_parse_children(text):
|
|
39
56
|
return []
|
|
40
57
|
|
|
41
|
-
return await self.parse_children_callback(
|
|
58
|
+
return await self.parse_children_callback(text)
|
|
59
|
+
|
|
60
|
+
def _can_parse_children(self, text: str) -> bool:
|
|
61
|
+
return self.parse_children_callback is not None and bool(text)
|
|
42
62
|
|
|
43
63
|
def get_remaining_lines(self) -> list[str]:
|
|
44
|
-
if
|
|
64
|
+
if not self._has_remaining_lines():
|
|
45
65
|
return []
|
|
46
66
|
return self.all_lines[self.current_line_index + 1 :]
|
|
47
67
|
|
|
68
|
+
def _has_remaining_lines(self) -> bool:
|
|
69
|
+
return self.all_lines is not None and self.current_line_index is not None
|
|
70
|
+
|
|
48
71
|
def is_inside_parent_context(self) -> bool:
|
|
49
72
|
return len(self.parent_stack) > 0
|
|
73
|
+
|
|
74
|
+
def get_line_indentation_level(self, line: str | None = None) -> int:
|
|
75
|
+
target_line = self._get_target_line(line)
|
|
76
|
+
leading_spaces = self._count_leading_spaces(target_line)
|
|
77
|
+
return self._calculate_indentation_level(leading_spaces)
|
|
78
|
+
|
|
79
|
+
def _get_target_line(self, line: str | None) -> str:
|
|
80
|
+
return line if line is not None else self.line
|
|
81
|
+
|
|
82
|
+
def _count_leading_spaces(self, line: str) -> int:
|
|
83
|
+
return len(line) - len(line.lstrip())
|
|
84
|
+
|
|
85
|
+
def _calculate_indentation_level(self, leading_spaces: int) -> int:
|
|
86
|
+
return leading_spaces // self._spaces_per_nesting_level
|
|
87
|
+
|
|
88
|
+
def collect_indented_child_lines(self, parent_indent_level: int) -> list[str]:
|
|
89
|
+
child_lines = []
|
|
90
|
+
expected_child_indent = parent_indent_level + 1
|
|
91
|
+
|
|
92
|
+
for line in self.get_remaining_lines():
|
|
93
|
+
if self._should_include_line_as_child(line, expected_child_indent):
|
|
94
|
+
child_lines.append(line)
|
|
95
|
+
else:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
return child_lines
|
|
99
|
+
|
|
100
|
+
def _should_include_line_as_child(self, line: str, expected_indent: int) -> bool:
|
|
101
|
+
if self._is_empty_line(line):
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
line_indent = self.get_line_indentation_level(line)
|
|
105
|
+
return line_indent >= expected_indent
|
|
106
|
+
|
|
107
|
+
def _is_empty_line(self, line: str) -> bool:
|
|
108
|
+
return not line.strip()
|
|
109
|
+
|
|
110
|
+
def strip_indentation_level(self, lines: list[str], levels: int = 1) -> list[str]:
|
|
111
|
+
return [self._strip_line_indentation(line, levels) for line in lines]
|
|
112
|
+
|
|
113
|
+
def _strip_line_indentation(self, line: str, levels: int) -> str:
|
|
114
|
+
if self._is_empty_line(line):
|
|
115
|
+
return line
|
|
116
|
+
|
|
117
|
+
spaces_to_remove = self._calculate_spaces_to_remove(levels)
|
|
118
|
+
return self._remove_leading_spaces(line, spaces_to_remove)
|
|
119
|
+
|
|
120
|
+
def _calculate_spaces_to_remove(self, levels: int) -> int:
|
|
121
|
+
return self._spaces_per_nesting_level * levels
|
|
122
|
+
|
|
123
|
+
def _remove_leading_spaces(self, line: str, spaces: int) -> str:
|
|
124
|
+
if len(line) < spaces:
|
|
125
|
+
return line
|
|
126
|
+
return line[spaces:]
|
|
@@ -26,11 +26,10 @@ from notionary.page.content.parser.parsers import (
|
|
|
26
26
|
TableOfContentsParser,
|
|
27
27
|
TableParser,
|
|
28
28
|
TodoParser,
|
|
29
|
-
ToggleableHeadingParser,
|
|
30
29
|
ToggleParser,
|
|
31
30
|
VideoParser,
|
|
32
31
|
)
|
|
33
|
-
from notionary.page.content.syntax
|
|
32
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
class ConverterChainFactory:
|
|
@@ -49,7 +48,6 @@ class ConverterChainFactory:
|
|
|
49
48
|
table_parser = self._create_table_parser()
|
|
50
49
|
column_parser = self._create_column_parser()
|
|
51
50
|
column_list_parser = self._create_column_list_parser()
|
|
52
|
-
toggleable_heading_parser = self._create_toggleable_heading_parser()
|
|
53
51
|
toggle_parser = self._create_toggle_parser()
|
|
54
52
|
|
|
55
53
|
# Single-line blocks
|
|
@@ -82,7 +80,6 @@ class ConverterChainFactory:
|
|
|
82
80
|
.set_next(table_parser)
|
|
83
81
|
.set_next(column_parser)
|
|
84
82
|
.set_next(column_list_parser)
|
|
85
|
-
.set_next(toggleable_heading_parser)
|
|
86
83
|
.set_next(toggle_parser)
|
|
87
84
|
.set_next(divider_parser)
|
|
88
85
|
.set_next(breadcrumb_parser)
|
|
@@ -128,12 +125,6 @@ class ConverterChainFactory:
|
|
|
128
125
|
def _create_column_list_parser(self) -> ColumnListParser:
|
|
129
126
|
return ColumnListParser(syntax_registry=self._syntax_registry)
|
|
130
127
|
|
|
131
|
-
def _create_toggleable_heading_parser(self) -> ToggleableHeadingParser:
|
|
132
|
-
return ToggleableHeadingParser(
|
|
133
|
-
syntax_registry=self._syntax_registry,
|
|
134
|
-
rich_text_converter=self._rich_text_converter,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
128
|
def _create_toggle_parser(self) -> ToggleParser:
|
|
138
129
|
return ToggleParser(
|
|
139
130
|
syntax_registry=self._syntax_registry,
|
|
@@ -24,7 +24,6 @@ from .table import TableParser
|
|
|
24
24
|
from .table_of_contents import TableOfContentsParser
|
|
25
25
|
from .todo import TodoParser
|
|
26
26
|
from .toggle import ToggleParser
|
|
27
|
-
from .toggleable_heading import ToggleableHeadingParser
|
|
28
27
|
from .video import VideoParser
|
|
29
28
|
|
|
30
29
|
__all__ = [
|
|
@@ -55,6 +54,5 @@ __all__ = [
|
|
|
55
54
|
"TableParser",
|
|
56
55
|
"TodoParser",
|
|
57
56
|
"ToggleParser",
|
|
58
|
-
"ToggleableHeadingParser",
|
|
59
57
|
"VideoParser",
|
|
60
58
|
]
|
|
@@ -7,7 +7,7 @@ from notionary.blocks.schemas import (
|
|
|
7
7
|
FileType,
|
|
8
8
|
)
|
|
9
9
|
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
10
|
-
from notionary.page.content.syntax
|
|
10
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class AudioParser(LineParser):
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
|
|
5
5
|
from notionary.page.content.parser.context import BlockParsingContext
|
|
6
|
-
from notionary.page.content.syntax
|
|
6
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class LineParser(ABC):
|
|
@@ -4,7 +4,7 @@ from typing import override
|
|
|
4
4
|
|
|
5
5
|
from notionary.blocks.schemas import BookmarkData, CreateBookmarkBlock
|
|
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 BookmarkParser(LineParser):
|
|
@@ -5,7 +5,7 @@ from notionary.page.content.parser.parsers.base import (
|
|
|
5
5
|
BlockParsingContext,
|
|
6
6
|
LineParser,
|
|
7
7
|
)
|
|
8
|
-
from notionary.page.content.syntax
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BreadcrumbParser(LineParser):
|
|
@@ -3,12 +3,12 @@ from typing import override
|
|
|
3
3
|
from notionary.blocks.rich_text.markdown_rich_text_converter import (
|
|
4
4
|
MarkdownRichTextConverter,
|
|
5
5
|
)
|
|
6
|
-
from notionary.blocks.schemas import
|
|
6
|
+
from notionary.blocks.schemas import CreateBulletedListItemBlock, CreateBulletedListItemData
|
|
7
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 BulletedListParser(LineParser):
|
|
@@ -21,21 +21,65 @@ class BulletedListParser(LineParser):
|
|
|
21
21
|
def _can_handle(self, context: BlockParsingContext) -> bool:
|
|
22
22
|
if context.is_inside_parent_context():
|
|
23
23
|
return False
|
|
24
|
-
return self.
|
|
24
|
+
return self._is_bulleted_list_line(context.line)
|
|
25
|
+
|
|
26
|
+
def _is_bulleted_list_line(self, line: str) -> bool:
|
|
27
|
+
return self._syntax.regex_pattern.match(line) is not None
|
|
25
28
|
|
|
26
29
|
@override
|
|
27
30
|
async def _process(self, context: BlockParsingContext) -> None:
|
|
28
31
|
block = await self._create_bulleted_list_block(context.line)
|
|
29
|
-
if block:
|
|
30
|
-
|
|
32
|
+
if not block:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
await self._process_nested_children(block, context)
|
|
36
|
+
context.result_blocks.append(block)
|
|
37
|
+
|
|
38
|
+
async def _process_nested_children(self, block: CreateBulletedListItemBlock, context: BlockParsingContext) -> None:
|
|
39
|
+
child_lines = self._collect_child_lines(context)
|
|
40
|
+
if not child_lines:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
child_blocks = await self._parse_child_blocks(child_lines, context)
|
|
44
|
+
if child_blocks:
|
|
45
|
+
block.bulleted_list_item.children = child_blocks
|
|
46
|
+
|
|
47
|
+
context.lines_consumed = len(child_lines)
|
|
48
|
+
|
|
49
|
+
def _collect_child_lines(self, context: BlockParsingContext) -> list[str]:
|
|
50
|
+
parent_indent_level = context.get_line_indentation_level()
|
|
51
|
+
return context.collect_indented_child_lines(parent_indent_level)
|
|
52
|
+
|
|
53
|
+
async def _parse_child_blocks(
|
|
54
|
+
self, child_lines: list[str], context: BlockParsingContext
|
|
55
|
+
) -> list[CreateBulletedListItemBlock]:
|
|
56
|
+
stripped_lines = self._remove_parent_indentation(child_lines, context)
|
|
57
|
+
children_text = self._convert_lines_to_text(stripped_lines)
|
|
58
|
+
return await context.parse_nested_markdown(children_text)
|
|
59
|
+
|
|
60
|
+
def _remove_parent_indentation(self, lines: list[str], context: BlockParsingContext) -> list[str]:
|
|
61
|
+
return context.strip_indentation_level(lines, levels=1)
|
|
62
|
+
|
|
63
|
+
def _convert_lines_to_text(self, lines: list[str]) -> str:
|
|
64
|
+
return "\n".join(lines)
|
|
31
65
|
|
|
32
66
|
async def _create_bulleted_list_block(self, text: str) -> CreateBulletedListItemBlock | None:
|
|
67
|
+
content = self._extract_list_content(text)
|
|
68
|
+
if content is None:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
rich_text = await self._convert_to_rich_text(content)
|
|
72
|
+
return self._build_block(rich_text)
|
|
73
|
+
|
|
74
|
+
def _extract_list_content(self, text: str) -> str | None:
|
|
33
75
|
match = self._syntax.regex_pattern.match(text)
|
|
34
76
|
if not match:
|
|
35
77
|
return None
|
|
78
|
+
return match.group(2)
|
|
36
79
|
|
|
37
|
-
|
|
38
|
-
|
|
80
|
+
async def _convert_to_rich_text(self, content: str):
|
|
81
|
+
return await self._rich_text_converter.to_rich_text(content)
|
|
39
82
|
|
|
40
|
-
|
|
83
|
+
def _build_block(self, rich_text) -> CreateBulletedListItemBlock:
|
|
84
|
+
bulleted_list_content = CreateBulletedListItemData(rich_text=rich_text)
|
|
41
85
|
return CreateBulletedListItemBlock(bulleted_list_item=bulleted_list_content)
|